diff --git a/documentation/schema.json b/documentation/schema.json index 8e825d0aa7a..0d3fabe6421 100644 --- a/documentation/schema.json +++ b/documentation/schema.json @@ -1722,6 +1722,17 @@ "type": "string", "description": "The name of the egress provider to which the trace is egressed.", "minLength": 1 + }, + "StoppingEvent": { + "description": "The event to watch for while collecting the trace, and once observed the trace will be stopped.", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/definitions/TraceEventFilter" + } + ] } } }, @@ -1793,6 +1804,36 @@ "Verbose" ] }, + "TraceEventFilter": { + "type": "object", + "additionalProperties": false, + "required": [ + "ProviderName", + "EventName" + ], + "properties": { + "ProviderName": { + "type": "string", + "description": "The event provider that will produce the specified event.", + "minLength": 1 + }, + "EventName": { + "type": "string", + "description": "The name of the event, which is a concatenation of the task name and opcode name, if any. The task and opcode names are separated by a '/'. If the event has no opcode, then the event name is just the task name.", + "minLength": 1 + }, + "PayloadFilter": { + "type": [ + "null", + "object" + ], + "description": "A mapping of event payload field names to their expected value. A subset of the payload fields may be specified.", + "additionalProperties": { + "type": "string" + } + } + } + }, "ExecuteOptions": { "type": "object", "additionalProperties": false, diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs index c8c368690c4..e4bffb6763a 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs +++ b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs @@ -715,6 +715,15 @@ public static string DisplayAttributeDescription_CollectTraceOptions_RequestRund } } + /// + /// Looks up a localized string similar to The event to watch for while collecting the trace, and once observed the trace will be stopped.. + /// + public static string DisplayAttributeDescription_CollectTraceOptions_StoppingEvent { + get { + return ResourceManager.GetString("DisplayAttributeDescription_CollectTraceOptions_StoppingEvent", resourceCulture); + } + } + /// /// Looks up a localized string similar to Buffer size used when copying data from an egress callback returning a stream to the egress callback that is provided a stream to which data is written.. /// @@ -1391,6 +1400,33 @@ public static string DisplayAttributeDescription_ThreadpoolQueueLengthOptions_Le } } + /// + /// Looks up a localized string similar to The name of the event, which is a concatenation of the task name and opcode name, if any. The task and opcode names are separated by a '/'. If the event has no opcode, then the event name is just the task name.. + /// + public static string DisplayAttributeDescription_TraceEventFilter_EventName { + get { + return ResourceManager.GetString("DisplayAttributeDescription_TraceEventFilter_EventName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A mapping of event payload field names to their expected value. A subset of the payload fields may be specified.. + /// + public static string DisplayAttributeDescription_TraceEventFilter_PayloadFilter { + get { + return ResourceManager.GetString("DisplayAttributeDescription_TraceEventFilter_PayloadFilter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The event provider that will produce the specified event.. + /// + public static string DisplayAttributeDescription_TraceEventFilter_ProviderName { + get { + return ResourceManager.GetString("DisplayAttributeDescription_TraceEventFilter_ProviderName", resourceCulture); + } + } + /// /// Looks up a localized string similar to The {0} field, {1} field, or {2} field is required.. /// diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx index 68cba82be5f..c5c6e2fcd41 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx +++ b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx @@ -729,4 +729,20 @@ The name of the egress provider to which the call stacks are egressed. + + The event to watch for while collecting the trace, and once observed the trace will be stopped. + The description provided for the StoppingEvent parameter on CollectTraceOptions. + + + The name of the event, which is a concatenation of the task name and opcode name, if any. The task and opcode names are separated by a '/'. If the event has no opcode, then the event name is just the task name. + The description provided for the EventName parameter on TraceEventFilter. + + + The event provider that will produce the specified event. + The description provided for the ProviderName parameter on TraceEventFilter. + + + A mapping of event payload field names to their expected value. A subset of the payload fields may be specified. + The description provided for the PayloadFilter parameter on TraceEventFilter. + \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs new file mode 100644 index 00000000000..7312162b2cb --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs @@ -0,0 +1,260 @@ +// 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.Globalization; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + /// + /// A stream that can monitor an event stream which is compatible with + /// for a specific event while also passing along the event data to a destination stream. + /// + public sealed class EventMonitoringPassthroughStream : Stream + { + private readonly Action _onPayloadFilterMismatch; + private readonly Action _onEvent; + private readonly bool _callOnEventOnlyOnce; + + private readonly Stream _sourceStream; + private readonly Stream _destinationStream; + private EventPipeEventSource _eventSource; + + private readonly string _providerName; + private readonly string _eventName; + + // The original payload filter of fieldName->fieldValue specified by the user. It will only be used to hydrate _payloadFilterIndexCache. + private readonly IDictionary _payloadFilter; + + // This tracks the exact indices into the provided event's payload to check for the expected values instead + // of repeatedly searching the payload for the field names in _payloadFilter. + private Dictionary _payloadFilterIndexCache; + + + /// + /// A stream that can monitor an event stream which is compatible with + /// for a specific event while also passing along the event data to a destination stream. + /// + /// The stopping event provider name. + /// The stopping event name, which is the concatenation of the task name and opcode name, if set. for more information about the format. + /// A mapping of the stopping event payload field names to their expected values. A subset of the payload fields may be specified. + /// A callback that will be invoked each time the requested event has been observed. + /// A callback that will be invoked if the field names specified in do not match those in the event's manifest. + /// The source event stream which is compatible with . + /// The destination stream to write events. It must either be full duplex or be write-only. + /// The size of the buffer to use when writing to the . + /// If true, the provided will only be called for the first matching event. + /// If true, the provided will not be automatically closed when this class is. + public EventMonitoringPassthroughStream( + string providerName, + string eventName, + IDictionary payloadFilter, + Action onEvent, + Action onPayloadFilterMismatch, + Stream sourceStream, + Stream destinationStream, + int bufferSize, + bool callOnEventOnlyOnce, + bool leaveDestinationStreamOpen) : base() + { + _providerName = providerName; + _eventName = eventName; + _onEvent = onEvent; + _onPayloadFilterMismatch = onPayloadFilterMismatch; + _sourceStream = sourceStream; + _payloadFilter = payloadFilter; + _callOnEventOnlyOnce = callOnEventOnlyOnce; + + // Wrap a buffered stream around the destination stream + // to avoid slowing down the event processing with the data + // passthrough unless there is significant pressure. + _destinationStream = new BufferedStream( + leaveDestinationStreamOpen + ? new StreamLeaveOpenWrapper(destinationStream) + : destinationStream, + bufferSize); + } + + /// + /// Start processing the event stream, monitoring it for the requested event and transferring its data to the specified destination stream. + /// This will continue to run until the event stream is complete or a stop is requested, regardless of if the requested event has been observed. + /// + /// The cancellation token. + /// + public Task ProcessAsync(CancellationToken token) + { + return Task.Run(() => + { + _eventSource = new EventPipeEventSource(this); + token.ThrowIfCancellationRequested(); + using IDisposable registration = token.Register(() => _eventSource.Dispose()); + + _eventSource.Dynamic.AddCallbackForProviderEvent(_providerName, _eventName, TraceEventCallback); + + // The EventPipeEventSource will drive the transferring of data to the destination stream as it processes events. + _eventSource.Process(); + token.ThrowIfCancellationRequested(); + }, token); + } + + /// + /// Stops monitoring for the specified stopping event. Data will continue to be written to the provided destination stream. + /// + private void StopMonitoringForEvent() + { + _eventSource?.Dynamic.RemoveCallback(TraceEventCallback); + } + + private void TraceEventCallback(TraceEvent obj) + { + if (_payloadFilterIndexCache == null && !HydratePayloadFilterCache(obj)) + { + // The payload filter doesn't map onto the actual data, + // we'll never match the event so stop checking it + // and proceed with just transferring the data to the destination stream. + StopMonitoringForEvent(); + _onPayloadFilterMismatch(obj); + return; + } + + if (!DoesPayloadMatch(obj)) + { + return; + } + + if (_callOnEventOnlyOnce) + { + StopMonitoringForEvent(); + } + + _onEvent(obj); + } + + /// + /// Hydrates the payload filter cache. + /// + /// An instance of the stopping event (matching provider, task name, and opcode), but without checking the payload yet. + /// + private bool HydratePayloadFilterCache(TraceEvent obj) + { + if (_payloadFilterIndexCache != null) + { + return true; + } + + // If there's no payload filter, there's nothing to do. + if (_payloadFilter == null || _payloadFilter.Count == 0) + { + _payloadFilterIndexCache = new Dictionary(capacity: 0); + return true; + } + + // If the payload has fewer fields than the requested filter, we can never match it. + // NOTE: this function will only ever be called with an instance of the stopping event + // (matching provider, task name, and opcode) but without checking the payload yet. + if (obj.PayloadNames.Length < _payloadFilter.Count) + { + return false; + } + + Dictionary payloadFilterCache = new(capacity: _payloadFilter.Count); + for (int i = 0; (i < obj.PayloadNames.Length) && (payloadFilterCache.Count < _payloadFilter.Count); i++) + { + if (_payloadFilter.TryGetValue(obj.PayloadNames[i], out string payloadValue)) + { + payloadFilterCache.Add(i, payloadValue); + } + } + + // Check if one or more of the requested filter field names did not exist on the actual payload. + if (_payloadFilter.Count != payloadFilterCache.Count) + { + return false; + } + + _payloadFilterIndexCache = payloadFilterCache; + + return true; + } + + private bool DoesPayloadMatch(TraceEvent obj) + { + foreach (var (fieldIndex, expectedValue) in _payloadFilterIndexCache) + { + string fieldValue = Convert.ToString(obj.PayloadValue(fieldIndex), CultureInfo.InvariantCulture) ?? string.Empty; + if (!string.Equals(fieldValue, expectedValue, StringComparison.Ordinal)) + { + return false; + } + } + + return true; + } + + public override int Read(byte[] buffer, int offset, int count) + { + return Read(buffer.AsSpan(offset, count)); + } + + public override int Read(Span buffer) + { + int bytesRead = _sourceStream.Read(buffer); + if (bytesRead != 0) + { + _destinationStream.Write(buffer[..bytesRead]); + } + + return bytesRead; + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + } + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + int bytesRead = await _sourceStream.ReadAsync(buffer, cancellationToken); + if (bytesRead != 0) + { + await _destinationStream.WriteAsync(buffer[..bytesRead], cancellationToken); + } + + return bytesRead; + } + + public override bool CanSeek => false; + public override bool CanWrite => false; + + public override bool CanTimeout => _sourceStream.CanRead; + public override bool CanRead => _sourceStream.CanRead; + public override long Length => _sourceStream.Length; + + public override long Position { get => _sourceStream.Position; set => _sourceStream.Position = value; } + public override int ReadTimeout { get => _sourceStream.ReadTimeout; set => _sourceStream.ReadTimeout = value; } + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + public override void CopyTo(Stream destination, int bufferSize) => throw new NotSupportedException(); + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => throw new NotSupportedException(); + + public override void Flush() => _destinationStream.Flush(); + public override Task FlushAsync(CancellationToken cancellationToken) => _destinationStream.FlushAsync(cancellationToken); + + public override async ValueTask DisposeAsync() + { + _eventSource?.Dispose(); + await _sourceStream.DisposeAsync(); + await _destinationStream.DisposeAsync(); + await base.DisposeAsync(); + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/LoggingExtensions.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/LoggingExtensions.cs index aeb48aca370..d4e936fe6fd 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/LoggingExtensions.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/LoggingExtensions.cs @@ -2,6 +2,7 @@ // 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 Microsoft.Extensions.Logging; using System; @@ -51,6 +52,18 @@ internal static class LoggingExtensions logLevel: LogLevel.Warning, formatString: Strings.LogFormatString_DefaultProcessUnexpectedFailure); + private static readonly Action _stoppingTraceEventHit = + LoggerMessage.Define( + eventId: new EventId(8, "StoppingTraceEventHit"), + logLevel: LogLevel.Debug, + formatString: Strings.LogFormatString_StoppingTraceEventHit); + + private static readonly Action _stoppingTraceEventPayloadFilterMismatch = + LoggerMessage.Define( + eventId: new EventId(9, "StoppingTraceEventPayloadFilterMismatch"), + logLevel: LogLevel.Warning, + formatString: Strings.LogFormatString_StoppingTraceEventPayloadFilterMismatch); + private static readonly Action _diagnosticRequestFailed = LoggerMessage.Define( eventId: new EventId(10, "DiagnosticRequestFailed"), @@ -98,6 +111,16 @@ public static void DefaultProcessUnexpectedFailure(this ILogger logger, Exceptio _defaultProcessUnexpectedFailure(logger, ex); } + public static void StoppingTraceEventHit(this ILogger logger, TraceEvent traceEvent) + { + _stoppingTraceEventHit(logger, traceEvent.ProviderName, traceEvent.EventName, null); + } + + public static void StoppingTraceEventPayloadFilterMismatch(this ILogger logger, TraceEvent traceEvent) + { + _stoppingTraceEventPayloadFilterMismatch(logger, traceEvent.ProviderName, traceEvent.EventName, string.Join(' ', traceEvent.PayloadNames), null); + } + public static void DiagnosticRequestFailed(this ILogger logger, int processId, Exception ex) { _diagnosticRequestFailed(logger, processId, ex); diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/StreamLeaveOpenWrapper.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/StreamLeaveOpenWrapper.cs new file mode 100644 index 00000000000..635d87bbf2d --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/StreamLeaveOpenWrapper.cs @@ -0,0 +1,74 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + /// + /// Wraps a given stream but leaves it open on Dispose. + /// + public sealed class StreamLeaveOpenWrapper : Stream + { + private readonly Stream _baseStream; + + public StreamLeaveOpenWrapper(Stream baseStream) : base() + { + _baseStream = baseStream; + } + + public override bool CanSeek => _baseStream.CanSeek; + + public override bool CanTimeout => _baseStream.CanTimeout; + + public override bool CanRead => _baseStream.CanRead; + + public override bool CanWrite => _baseStream.CanWrite; + + public override long Length => _baseStream.Length; + + public override long Position { get => _baseStream.Position; set => _baseStream.Position = value; } + + public override int ReadTimeout { get => _baseStream.ReadTimeout; set => _baseStream.ReadTimeout = value; } + + public override int WriteTimeout { get => _baseStream.WriteTimeout; set => _baseStream.WriteTimeout = value; } + + public override long Seek(long offset, SeekOrigin origin) => _baseStream.Seek(offset, origin); + + public override int Read(Span buffer) => _baseStream.Read(buffer); + + public override int Read(byte[] buffer, int offset, int count) => _baseStream.Read(buffer, offset, count); + + public override int ReadByte() => _baseStream.ReadByte(); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _baseStream.ReadAsync(buffer, offset, count, cancellationToken); + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) => _baseStream.ReadAsync(buffer, cancellationToken); + + public override void Flush() => _baseStream.Flush(); + + public override void SetLength(long value) => _baseStream.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) => _baseStream.Write(buffer, offset, count); + + public override void Write(ReadOnlySpan buffer) => _baseStream.Write(buffer); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _baseStream.WriteAsync(buffer, offset, count, cancellationToken); + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => _baseStream.WriteAsync(buffer, cancellationToken); + + public override void WriteByte(byte value) => _baseStream.WriteByte(value); + + public override Task FlushAsync(CancellationToken cancellationToken) => _baseStream.FlushAsync(cancellationToken); + + public override void CopyTo(Stream destination, int bufferSize) => _baseStream.CopyTo(destination, bufferSize); + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => _baseStream.CopyToAsync(destination, bufferSize, cancellationToken); + + public override async ValueTask DisposeAsync() => await base.DisposeAsync(); + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs index 69e7294d0c4..abee5032299 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs @@ -14,6 +14,14 @@ namespace Microsoft.Diagnostics.Monitoring.WebApi { internal static class TraceUtilities { + // Buffer size matches FileStreamResult + private const int DefaultBufferSize = 0x10000; + + public static string GenerateTraceFileName(IEndpointInfo endpointInfo) + { + return FormattableString.Invariant($"{Utilities.GetFileNameTimeStampUtcNow()}_{endpointInfo.ProcessId}.nettrace"); + } + public static MonitoringSourceConfiguration GetTraceConfiguration(Models.TraceProfile profile, float metricsIntervalSeconds) { var configurations = new List(); @@ -65,5 +73,73 @@ public static MonitoringSourceConfiguration GetTraceConfiguration(Models.EventPi requestRundown: requestRundown, bufferSizeInMB: bufferSizeInMB); } + + public static async Task CaptureTraceAsync(TaskCompletionSource startCompletionSource, IEndpointInfo endpointInfo, MonitoringSourceConfiguration configuration, TimeSpan duration, Stream outputStream, CancellationToken token) + { + Func streamAvailable = async (Stream eventStream, CancellationToken token) => + { + if (null != startCompletionSource) + { + startCompletionSource.TrySetResult(null); + } + //CONSIDER Should we allow client to change the buffer size? + await eventStream.CopyToAsync(outputStream, DefaultBufferSize, token); + }; + + var client = new DiagnosticsClient(endpointInfo.Endpoint); + + await using EventTracePipeline pipeProcessor = new EventTracePipeline(client, new EventTracePipelineSettings + { + Configuration = configuration, + Duration = duration, + }, streamAvailable); + + await pipeProcessor.RunAsync(token); + } + + public static async Task CaptureTraceUntilEventAsync(TaskCompletionSource startCompletionSource, IEndpointInfo endpointInfo, MonitoringSourceConfiguration configuration, TimeSpan timeout, Stream outputStream, string providerName, string eventName, IDictionary payloadFilter, ILogger logger, CancellationToken token) + { + DiagnosticsClient client = new(endpointInfo.Endpoint); + TaskCompletionSource stoppingEventHitSource = new(TaskCreationOptions.RunContinuationsAsynchronously); + + using IDisposable registration = token.Register( + () => stoppingEventHitSource.TrySetCanceled(token)); + + await using EventTracePipeline pipeProcessor = new(client, new EventTracePipelineSettings + { + Configuration = configuration, + Duration = timeout, + }, + async (eventStream, token) => + { + startCompletionSource?.TrySetResult(null); + await using EventMonitoringPassthroughStream eventMonitoringStream = new( + providerName, + eventName, + payloadFilter, + onEvent: (traceEvent) => + { + logger.StoppingTraceEventHit(traceEvent); + stoppingEventHitSource.TrySetResult(null); + }, + onPayloadFilterMismatch: logger.StoppingTraceEventPayloadFilterMismatch, + eventStream, + outputStream, + DefaultBufferSize, + callOnEventOnlyOnce: true, + leaveDestinationStreamOpen: true /* We do not have ownership of the outputStream */); + + await eventMonitoringStream.ProcessAsync(token); + }); + + Task pipelineRunTask = pipeProcessor.RunAsync(token); + await Task.WhenAny(pipelineRunTask, stoppingEventHitSource.Task).Unwrap(); + + if (stoppingEventHitSource.Task.IsCompleted) + { + await pipeProcessor.StopAsync(token); + await pipelineRunTask; + } + } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.csproj b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.csproj index be259cd1cb3..f4e45fe8ec9 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.csproj +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.csproj @@ -27,6 +27,7 @@ + diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TestAppScenarios.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TestAppScenarios.cs index 11f44963b21..1ca2d0f8a9e 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TestAppScenarios.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TestAppScenarios.cs @@ -2,6 +2,8 @@ // 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; + namespace Microsoft.Diagnostics.Monitoring.TestCommon { public static class TestAppScenarios @@ -90,5 +92,22 @@ public static class Commands public const string StopSpin = nameof(StopSpin); } } + + public static class TraceEvents + { + public const string Name = nameof(TraceEvents); + public const string UniqueEventName = "UniqueEvent"; + public const string EventProviderName = "TestScenario"; + public const string UniqueEventMessage = "FooBar"; + public const string UniqueEventPayloadField = "message"; + + public const TraceEventOpcode UniqueEventOpcode = TraceEventOpcode.Reply; + + public static class Commands + { + public const string EmitUniqueEvent = nameof(EmitUniqueEvent); + public const string ShutdownScenario = nameof(ShutdownScenario); + } + } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs new file mode 100644 index 00000000000..f1cead807a5 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs @@ -0,0 +1,190 @@ +// 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.TestCommon; +using Microsoft.Diagnostics.Monitoring.TestCommon.Options; +using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; +using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Fixtures; +using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Runners; +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Diagnostics.Monitoring.WebApi.Models; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions; +using Microsoft.Diagnostics.Tracing; +using Microsoft.Diagnostics.Tracing.Parsers.Clr; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests +{ + [TargetFrameworkMonikerTrait(TargetFrameworkMonikerExtensions.CurrentTargetFrameworkMoniker)] + [Collection(DefaultCollectionFixture.Name)] + public class CollectTraceTests + { + private readonly IHttpClientFactory _httpClientFactory; + private readonly ITestOutputHelper _outputHelper; + + public CollectTraceTests(ITestOutputHelper outputHelper, ServiceProviderFixture serviceProviderFixture) + { + _httpClientFactory = serviceProviderFixture.ServiceProvider.GetService(); + _outputHelper = outputHelper; + } + +#if NET5_0_OR_GREATER + [Fact] + public Task StopOnEvent_Succeeds_WithMatchingOpcode() + { + return StopOnEventTestCore(expectStoppingEvent: true); + } + + [Fact] + public Task StopOnEvent_Succeeds_WithMatchingOpcodeAndNoRundown() + { + return StopOnEventTestCore(expectStoppingEvent: true, collectRundown: false); + } + + [Fact] + public Task StopOnEvent_Succeeds_WithMatchingPayload() + { + return StopOnEventTestCore(expectStoppingEvent: true, payloadFilter: new Dictionary() + { + { TestAppScenarios.TraceEvents.UniqueEventPayloadField, TestAppScenarios.TraceEvents.UniqueEventMessage } + }); + } + + [Fact] + public Task StopOnEvent_DoesNotStop_WhenOpcodeDoesNotMatch() + { + return StopOnEventTestCore(expectStoppingEvent: false, opcode: TraceEventOpcode.Resume); + } + + [Fact] + public Task StopOnEvent_DoesNotStop_WhenPayloadFieldNamesMismatch() + { + return StopOnEventTestCore(expectStoppingEvent: false, payloadFilter: new Dictionary() + { + { TestAppScenarios.TraceEvents.UniqueEventPayloadField, TestAppScenarios.TraceEvents.UniqueEventMessage }, + { "foobar", "baz" } + }); + } + + [Fact] + public Task StopOnEvent_DoesNotStop_WhenPayloadFieldValueMismatch() + { + return StopOnEventTestCore(expectStoppingEvent: false, payloadFilter: new Dictionary() + { + { TestAppScenarios.TraceEvents.UniqueEventPayloadField, TestAppScenarios.TraceEvents.UniqueEventMessage.ToUpperInvariant() } + }); + } + + private string ConstructQualifiedEventName(string eventName, TraceEventOpcode opcode) + { + return (opcode == TraceEventOpcode.Info) + ? eventName + : FormattableString.Invariant($"{eventName}/{opcode}"); + } + + private async Task StopOnEventTestCore(bool expectStoppingEvent, TraceEventOpcode opcode = TestAppScenarios.TraceEvents.UniqueEventOpcode, bool collectRundown = true, IDictionary payloadFilter = null, TimeSpan? duration = null) + { + TimeSpan DefaultCollectTraceTimeout = TimeSpan.FromSeconds(10); + const string DefaultRuleName = "FunctionalTestRule"; + const string EgressProvider = "TmpEgressProvider"; + + using TemporaryDirectory tempDirectory = new(_outputHelper); + + Task ruleCompletedTask = null; + + TraceEventFilter traceEventFilter = new() + { + ProviderName = TestAppScenarios.TraceEvents.EventProviderName, + EventName = ConstructQualifiedEventName(TestAppScenarios.TraceEvents.UniqueEventName, opcode), + PayloadFilter = payloadFilter + }; + + await ScenarioRunner.SingleTarget(_outputHelper, + _httpClientFactory, + DiagnosticPortConnectionMode.Listen, + TestAppScenarios.TraceEvents.Name, + appValidate: async (appRunner, apiClient) => + { + await appRunner.SendCommandAsync(TestAppScenarios.TraceEvents.Commands.EmitUniqueEvent); + await ruleCompletedTask; + await appRunner.SendCommandAsync(TestAppScenarios.TraceEvents.Commands.ShutdownScenario); + }, + configureTool: runner => + { + runner.ConfigurationFromEnvironment.AddFileSystemEgress(EgressProvider, tempDirectory.FullName); + runner.ConfigurationFromEnvironment.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectTraceAction( + new EventPipeProvider[] { + new EventPipeProvider() + { + Name = TestAppScenarios.TraceEvents.EventProviderName, + Keywords = "-1" + } + }, + EgressProvider, options => + { + options.Duration = duration ?? DefaultCollectTraceTimeout; + options.StoppingEvent = traceEventFilter; + options.RequestRundown = collectRundown; + }); + + ruleCompletedTask = runner.WaitForCollectionRuleCompleteAsync(DefaultRuleName); + }); + + string[] files = Directory.GetFiles(tempDirectory.FullName, "*.nettrace", SearchOption.TopDirectoryOnly); + string traceFile = Assert.Single(files); + + var (hasStoppingEvent, hasRundown) = await ValidateNettraceFile(traceFile, traceEventFilter); + Assert.Equal(expectStoppingEvent, hasStoppingEvent); + Assert.Equal(collectRundown, hasRundown); + } + + private Task<(bool hasStoppingEvent, bool hasRundown)> ValidateNettraceFile(string filePath, TraceEventFilter eventFilter) + { + return Task.Run(() => + { + using FileStream fs = File.OpenRead(filePath); + using EventPipeEventSource eventSource = new(fs); + + bool didSeeRundownEvents = false; + bool didSeeStoppingEvent = false; + + eventSource.Dynamic.AddCallbackForProviderEvent(eventFilter.ProviderName, eventFilter.EventName, (obj) => + { + if (eventFilter.PayloadFilter != null) + { + foreach (var (fieldName, fieldValue) in eventFilter.PayloadFilter) + { + object payloadValue = obj.PayloadByName(fieldName); + if (!string.Equals(fieldValue, payloadValue?.ToString(), StringComparison.Ordinal)) + { + return; + } + } + } + + didSeeStoppingEvent = true; + }); + + ClrRundownTraceEventParser rundown = new(eventSource); + rundown.RuntimeStart += (data) => + { + didSeeRundownEvents = true; + }; + + eventSource.Process(); + return (didSeeStoppingEvent, didSeeRundownEvents); + }); + } +#endif // NET5_0_OR_GREATER + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.csproj b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.csproj index 7ff5f2778fb..c31186be018 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.csproj +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.csproj @@ -27,6 +27,7 @@ + diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs index 56d2d827087..95d45b0fc64 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs @@ -1132,6 +1132,111 @@ public Task CollectionRuleOptions_CollectTraceAction_ProviderPropertyValidation( }); } + [Fact] + public Task CollectionRuleOptions_CollectTraceAction_StopOnEvent() + { + const string ExpectedEgressProvider = "TmpEgressProvider"; + const string ExpectedEventProviderName = "Microsoft-Extensions-Logging"; + List ExpectedProviders = new() + { + new() { Name = ExpectedEventProviderName } + }; + + TraceEventFilter expectedStoppingEvent = new() + { + EventName = "CustomEvent", + ProviderName = ExpectedEventProviderName + }; + + return ValidateSuccess( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectTraceAction(ExpectedProviders, ExpectedEgressProvider, (options) => + { + options.StoppingEvent = expectedStoppingEvent; + }); + rootOptions.AddFileSystemEgress(ExpectedEgressProvider, "/tmp"); + }, + ruleOptions => + { + ruleOptions.VerifyCollectTraceAction(0, ExpectedProviders, ExpectedEgressProvider, expectedStoppingEvent); + }); + } + + [Fact] + public Task CollectionRuleOptions_CollectTraceAction_StopOnEvent_MissingProviderConfig() + { + const string ExpectedEgressProvider = "TmpEgressProvider"; + const string ExpectedMissingEventProviderName = "Non-Existent-Provider"; + + List ExpectedProviders = new() + { + new() { Name = "Microsoft-Extensions-Logging" } + }; + + return ValidateFailure( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectTraceAction(ExpectedProviders, ExpectedEgressProvider, (options) => + { + options.StoppingEvent = new TraceEventFilter() + { + EventName = "CustomEvent", + ProviderName = ExpectedMissingEventProviderName + }; + }); + rootOptions.AddFileSystemEgress(ExpectedEgressProvider, "/tmp"); + }, + ex => + { + string[] failures = ex.Failures.ToArray(); + Assert.Single(failures); + VerifyMissingStoppingEventProviderMessage( + failures, + 0, + nameof(CollectTraceOptions.StoppingEvent), + ExpectedMissingEventProviderName, + nameof(CollectTraceOptions.Providers)); + }); + } + + [Fact] + public Task CollectionRuleOptions_CollectTraceAction_BothProfileAndStoppingEvent() + { + const string ExpectedEgressProvider = "TmpEgressProvider"; + + return ValidateFailure( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectTraceAction(TraceProfile.Metrics, ExpectedEgressProvider, options => + { + options.StoppingEvent = new TraceEventFilter() + { + EventName = "CustomEvent", + ProviderName = "CustomProvider" + }; + }); + + rootOptions.AddFileSystemEgress(ExpectedEgressProvider, "/tmp"); + }, + ex => + { + string[] failures = ex.Failures.ToArray(); + Assert.NotEmpty(failures); + VerifyBothCannotBeSpecifiedMessage( + failures, + 0, + nameof(CollectTraceOptions.Profile), + nameof(CollectTraceOptions.StoppingEvent)); + }); + } + [Fact] public Task CollectionRuleOptions_CollectLiveMetricsAction_RoundTrip() { @@ -1751,5 +1856,17 @@ private static void VerifyFeatureDisabled(string[] failures, int index) Assert.Equal(expectedMessage, failures[index]); } + + private static void VerifyMissingStoppingEventProviderMessage(string[] failures, int index, string fieldName, string providerName, string providerFieldName) + { + string message = string.Format( + CultureInfo.InvariantCulture, + Strings.ErrorMessage_MissingStoppingEventProvider, + fieldName, + providerName, + providerFieldName); + + Assert.Equal(message, failures[index]); + } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Options/CollectionRuleOptionsExtensions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Options/CollectionRuleOptionsExtensions.cs index 7026b21cb62..6574caa9bb7 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Options/CollectionRuleOptionsExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Options/CollectionRuleOptionsExtensions.cs @@ -497,7 +497,7 @@ public static CollectTraceOptions VerifyCollectTraceAction(this CollectionRuleOp return collectTraceOptions; } - public static CollectTraceOptions VerifyCollectTraceAction(this CollectionRuleOptions ruleOptions, int actionIndex, IEnumerable providers, string expectedEgress) + public static CollectTraceOptions VerifyCollectTraceAction(this CollectionRuleOptions ruleOptions, int actionIndex, IEnumerable providers, string expectedEgress, TraceEventFilter expectedStoppingEvent = null) { CollectTraceOptions collectTraceOptions = ruleOptions.VerifyAction( actionIndex, KnownCollectionRuleActions.CollectTrace); @@ -505,6 +505,7 @@ public static CollectTraceOptions VerifyCollectTraceAction(this CollectionRuleOp Assert.Equal(expectedEgress, collectTraceOptions.Egress); Assert.NotNull(collectTraceOptions.Providers); Assert.Equal(providers.Count(), collectTraceOptions.Providers.Count); + Assert.Equal(expectedStoppingEvent, collectTraceOptions.StoppingEvent); int index = 0; foreach (EventPipeProvider expectedProvider in providers) diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Program.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Program.cs index eda6c15b7c2..1f1ef774c4c 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Program.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Program.cs @@ -20,7 +20,8 @@ public static Task Main(string[] args) LoggerScenario.Command(), SpinWaitScenario.Command(), EnvironmentVariablesScenario.Command(), - StacksScenario.Command() + StacksScenario.Command(), + TraceEventsScenario.Command() }) .UseDefaults() .Build() diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs new file mode 100644 index 00000000000..48d249c863a --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs @@ -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.Monitoring.TestCommon; +using System; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.Diagnostics.Tracing; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.UnitTestApp.Scenarios +{ + /// + /// Continously emits trace events and a unique one on request. + /// Only stops once an exit request is received. + /// + internal class TraceEventsScenario + { + [EventSource(Name = "TestScenario")] + class TestScenarioEventSource : EventSource + { + public static TestScenarioEventSource Log { get; } = new TestScenarioEventSource(); + + [Event(1)] + public void RandomNumberGenerated(int number) => WriteEvent(1, number); + + [Event(2, Opcode = EventOpcode.Reply)] + public void UniqueEvent(string message) => WriteEvent(2, message); + } + + public static Command Command() + { + Command command = new(TestAppScenarios.TraceEvents.Name); + command.SetHandler(ExecuteAsync); + return command; + } + + public static async Task ExecuteAsync(InvocationContext context) + { + string[] acceptableCommands = new string[] + { + TestAppScenarios.TraceEvents.Commands.EmitUniqueEvent, + TestAppScenarios.TraceEvents.Commands.ShutdownScenario + }; + + context.ExitCode = await ScenarioHelpers.RunScenarioAsync(async logger => + { + using ManualResetEventSlim stopGeneratingEvents = new(initialState: false); + + Task eventEmitterTask = Task.Run(async () => + { + Random random = new(); + while (!stopGeneratingEvents.IsSet) + { + TestScenarioEventSource.Log.RandomNumberGenerated(random.Next()); + await Task.Delay(TimeSpan.FromMilliseconds(100), context.GetCancellationToken()); + } + }, context.GetCancellationToken()); + + while (true) + { + switch (await ScenarioHelpers.WaitForCommandAsync(acceptableCommands, logger)) + { + case TestAppScenarios.TraceEvents.Commands.EmitUniqueEvent: + TestScenarioEventSource.Log.UniqueEvent(TestAppScenarios.TraceEvents.UniqueEventMessage); + break; + case TestAppScenarios.TraceEvents.Commands.ShutdownScenario: + stopGeneratingEvents.Set(); + eventEmitterTask.Wait(context.GetCancellationToken()); + return 0; + } + } + }, context.GetCancellationToken()); + } + } +} diff --git a/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs b/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs index a874a78a1a0..2d5ad37c4ac 100644 --- a/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs +++ b/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs @@ -8,6 +8,7 @@ using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Exceptions; using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; using System.Collections.Generic; @@ -66,6 +67,8 @@ protected override async Task ExecuteCoreAsync( MonitoringSourceConfiguration configuration; + TraceEventFilter stoppingEvent = null; + if (Options.Profile.HasValue) { TraceProfile profile = Options.Profile.Value; @@ -78,8 +81,9 @@ protected override async Task ExecuteCoreAsync( EventPipeProvider[] optionsProviders = Options.Providers.ToArray(); bool requestRundown = Options.RequestRundown.GetValueOrDefault(CollectTraceOptionsDefaults.RequestRundown); int bufferSizeMegabytes = Options.BufferSizeMegabytes.GetValueOrDefault(CollectTraceOptionsDefaults.BufferSizeMegabytes); - configuration = TraceUtilities.GetTraceConfiguration(optionsProviders, requestRundown, bufferSizeMegabytes); + + stoppingEvent = Options.StoppingEvent; } KeyValueLogScope scope = Utils.CreateArtifactScope(Utils.ArtifactType_Trace, EndpointInfo); @@ -108,7 +112,18 @@ protected override async Task ExecuteCoreAsync( async (outputStream, token) => { using IDisposable operationRegistration = _operationTrackerService.Register(EndpointInfo); - await operation.ExecuteAsync(outputStream, startCompletionSource, token); + if (null != stoppingEvent) + { + ILogger logger = _serviceProvider + .GetRequiredService() + .CreateLogger(); + + await TraceUtilities.CaptureTraceUntilEventAsync(startCompletionSource, EndpointInfo, configuration, duration, outputStream, stoppingEvent.ProviderName, stoppingEvent.EventName, stoppingEvent.PayloadFilter, logger, token); + } + else + { + await TraceUtilities.CaptureTraceAsync(startCompletionSource, EndpointInfo, configuration, duration, outputStream, token); + } }, egressProvider, operation.GenerateFileName(), diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.Validate.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.Validate.cs index 81cc26804a8..49463d3e6dc 100644 --- a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.Validate.cs +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.Validate.cs @@ -27,9 +27,20 @@ IEnumerable IValidatableObject.Validate(ValidationContext vali bool hasProfile = Profile.HasValue; bool hasProviders = null != Providers && Providers.Any(); + bool hasStoppingEvent = null != StoppingEvent; if (hasProfile) { + if (hasStoppingEvent) + { + results.Add(new ValidationResult( + string.Format( + CultureInfo.InvariantCulture, + Strings.ErrorMessage_TwoFieldsCannotBeSpecified, + nameof(Profile), + nameof(StoppingEvent)))); + } + if (hasProviders) { // Both Profile and Providers cannot be specified at the same time, otherwise @@ -75,6 +86,23 @@ IEnumerable IValidatableObject.Validate(ValidationContext vali nameof(Providers)))); } + if (hasStoppingEvent) + { + bool hasMatchingStoppingProvider = hasProviders + && null != Providers.Find(x => string.Equals(x.Name, StoppingEvent.ProviderName, System.StringComparison.Ordinal)); + + if (!hasMatchingStoppingProvider) + { + results.Add(new ValidationResult( + string.Format( + CultureInfo.InvariantCulture, + Strings.ErrorMessage_MissingStoppingEventProvider, + nameof(StoppingEvent), + StoppingEvent.ProviderName, + nameof(Providers)))); + } + } + return results; } } diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.cs index 72c752bb9a8..51b213a9fd2 100644 --- a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.cs +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.cs @@ -63,5 +63,10 @@ internal sealed partial record class CollectTraceOptions : BaseRecordOptions, IE [ValidateEgressProvider] #endif public string Egress { get; set; } + + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_CollectTraceOptions_StoppingEvent))] + public TraceEventFilter StoppingEvent { get; set; } } } diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/TraceEventFilter.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/TraceEventFilter.cs new file mode 100644 index 00000000000..357ceda20c7 --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/TraceEventFilter.cs @@ -0,0 +1,38 @@ +// 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.WebApi; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; + +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions +{ + /// + /// A trace event filter. + /// + [DebuggerDisplay("TraceEventFilter")] +#if SCHEMAGEN + [NJsonSchema.Annotations.JsonSchemaFlatten] +#endif + internal sealed record class TraceEventFilter : BaseRecordOptions + { + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_TraceEventFilter_ProviderName))] + [Required] + public string ProviderName { get; set; } + + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_TraceEventFilter_EventName))] + [Required] + public string EventName { get; set; } + + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_TraceEventFilter_PayloadFilter))] + public IDictionary PayloadFilter { get; set; } + } +} diff --git a/src/Tools/dotnet-monitor/Strings.Designer.cs b/src/Tools/dotnet-monitor/Strings.Designer.cs index 1b9c6e3e060..39dbcd180e7 100644 --- a/src/Tools/dotnet-monitor/Strings.Designer.cs +++ b/src/Tools/dotnet-monitor/Strings.Designer.cs @@ -303,6 +303,15 @@ internal static string ErrorMessage_MaxConnections { } } + /// + /// Looks up a localized string similar to The {0} field specified the provider '{1}' but it was not found in the {2} field.. + /// + internal static string ErrorMessage_MissingStoppingEventProvider { + get { + return ResourceManager.GetString("ErrorMessage_MissingStoppingEventProvider", resourceCulture); + } + } + /// /// Looks up a localized string similar to The environment block does not contain the '{0}' variable.. /// diff --git a/src/Tools/dotnet-monitor/Strings.resx b/src/Tools/dotnet-monitor/Strings.resx index f79d4cb08b7..c56e6ca4a43 100644 --- a/src/Tools/dotnet-monitor/Strings.resx +++ b/src/Tools/dotnet-monitor/Strings.resx @@ -245,6 +245,14 @@ Value must be between 1 and 254, or -1 (to obtain the maximum number allowed by system resources). + + The {0} field specified the provider '{1}' but it was not found in the {2} field. + Gets the format string for CollectTraceOptions type failure due to missing the needed provider for StoppingEventOptions. +3 Format Parameter: +1. stoppingEventField: The field that specified the StoppingEventOptions that contains the failure. +2. missingProviderName: The specified provider that is missing. +3. providersField: the field that the provider should be defined in. + The environment block does not contain the '{0}' variable. Gets the format string for an error when the given environment variable does not exist.