From 36710dd7c65fe6d55fcc7a17c7c0b57fc4a09cde Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Tue, 13 Sep 2022 10:08:56 -0700 Subject: [PATCH 01/26] Add initial stop-on-event support --- documentation/schema.json | 72 ++++++++- .../OptionsDisplayStrings.Designer.cs | 36 +++++ .../OptionsDisplayStrings.resx | 16 ++ .../EventMonitoringPassthroughStream.cs | 153 ++++++++++++++++++ .../StreamLeaveOpenWrapper.cs | 73 +++++++++ .../Utilities/TraceUtilities.cs | 43 ++++- ...tics.Monitoring.ConfigurationSchema.csproj | 5 + .../TestAppScenarios.cs | 11 ++ .../CollectTraceTests.cs | 128 +++++++++++++++ ...ics.Monitoring.Tool.FunctionalTests.csproj | 1 + .../CollectionRuleOptionsTests.cs | 85 ++++++++++ .../CollectionRuleOptionsExtensions.cs | 3 +- .../Program.cs | 3 +- .../Scenarios/TraceEventsScenario.cs | 79 +++++++++ .../Actions/CollectTraceAction.cs | 14 +- .../Actions/CollectTraceOptions.Validate.cs | 18 +++ .../Options/Actions/CollectTraceOptions.cs | 5 + .../Options/Actions/TraceEventOptions.cs | 39 +++++ src/Tools/dotnet-monitor/Strings.Designer.cs | 9 ++ src/Tools/dotnet-monitor/Strings.resx | 8 + 20 files changed, 794 insertions(+), 7 deletions(-) create mode 100644 src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs create mode 100644 src/Microsoft.Diagnostics.Monitoring.WebApi/StreamLeaveOpenWrapper.cs create mode 100644 src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs create mode 100644 src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs create mode 100644 src/Tools/dotnet-monitor/CollectionRules/Options/Actions/TraceEventOptions.cs diff --git a/documentation/schema.json b/documentation/schema.json index d661e772684..c48bb71616f 100644 --- a/documentation/schema.json +++ b/documentation/schema.json @@ -1636,6 +1636,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/TraceEventOptions" + } + ] } } }, @@ -1707,6 +1718,65 @@ "Verbose" ] }, + "TraceEventOptions": { + "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.", + "minLength": 1 + }, + "Opcode": { + "description": "The opcode of the event, if empty the specified event's opcode will not be checked.", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/definitions/TraceEventOpcode" + } + ] + } + } + }, + "TraceEventOpcode": { + "type": "string", + "description": "", + "x-enumNames": [ + "Info", + "Start", + "Stop", + "DataCollectionStart", + "DataCollectionStop", + "Extension", + "Reply", + "Resume", + "Suspend", + "Transfer" + ], + "enum": [ + "Info", + "Start", + "Stop", + "DataCollectionStart", + "DataCollectionStop", + "Extension", + "Reply", + "Resume", + "Suspend", + "Transfer" + ] + }, "ExecuteOptions": { "type": "object", "additionalProperties": false, @@ -2236,4 +2306,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs index 102ad592287..3b4589a1550 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs +++ b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs @@ -697,6 +697,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.. /// @@ -1346,6 +1355,33 @@ public static string DisplayAttributeDescription_ThreadpoolQueueLengthOptions_Le } } + /// + /// Looks up a localized string similar to The name of the event.. + /// + public static string DisplayAttributeDescription_TraceEventOptions_EventName { + get { + return ResourceManager.GetString("DisplayAttributeDescription_TraceEventOptions_EventName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The opcode of the event, if empty the specified event's opcode will not be checked.. + /// + public static string DisplayAttributeDescription_TraceEventOptions_Opcode { + get { + return ResourceManager.GetString("DisplayAttributeDescription_TraceEventOptions_Opcode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The event provider that will produce the specified event.. + /// + public static string DisplayAttributeDescription_TraceEventOptions_ProviderName { + get { + return ResourceManager.GetString("DisplayAttributeDescription_TraceEventOptions_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 b11ceaa366c..fa79d994ff6 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx +++ b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx @@ -712,4 +712,20 @@ The name of the queue shared access signature (SAS) used to look up the value from the Egress options Properties map. The description provided for the QueueSharedAccessSignatureName parameter on AzureBlobEgressProviderOptions. + + 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. + The description provided for the EventName parameter on TraceEventOptions. + + + The opcode of the event, if empty the specified event's opcode will not be checked. + The description provided for the Opcode parameter on TraceEventOptions. + + + The event provider that will produce the specified event. + The description provided for the ProviderName parameter on TraceEventOptions. + \ 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..b8e79914846 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs @@ -0,0 +1,153 @@ +// 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.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 class EventMonitoringPassthroughStream : Stream + { + private readonly Action _onEvent; + private readonly string _providerName; + private readonly string _eventName; + private readonly TraceEventOpcode? _eventOpcode; + private readonly Stream _sourceStream; + private readonly Stream _destinationStream; + + private EventPipeEventSource _eventSource; + + // JSFIX: Add summary. + // Key takeaway is that onEvent will only be invoked once, and the source stream will continue to transfer to + // the destination stream even after the onEvent callback is invoked. + public EventMonitoringPassthroughStream( + string providerName, + string eventName, + TraceEventOpcode? eventOpcode, + Action onEvent, + Stream sourceStream, + Stream destinationStream, + int bufferSize, + bool leaveDestinationStreamOpen) : base() + { + _providerName = providerName; + _eventName = eventName; + _eventOpcode = eventOpcode; + _onEvent = onEvent; + _sourceStream = sourceStream; + + // 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. It can only be signaled before processing has been started. After that point or should be called to stop processing. + /// + public Task ProcessAsync(CancellationToken token) + { + return Task.Run(() => + { + _eventSource = new EventPipeEventSource(this); + _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); + } + + public void StopProcessing() + { + _eventSource?.StopProcessing(); + } + + private void TraceEventCallback(TraceEvent obj) + { + if (!_eventOpcode.HasValue || obj.Opcode == _eventOpcode) + { + // Once the specified event has been observed, stop watching for it. + // However, keep processing the data as to allow remaining trace event + // data, such as run down, to finish transferring to the destination stream. + _eventSource.Dynamic.RemoveCallback(TraceEventCallback); + + _onEvent(obj); + } + } + + 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() => _sourceStream.Flush(); + public override Task FlushAsync(CancellationToken cancellationToken) => _sourceStream.FlushAsync(cancellationToken); + + 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 async ValueTask DisposeAsync() + { + _eventSource?.Dispose(); + await _sourceStream.DisposeAsync(); + await _destinationStream.DisposeAsync(); + await base.DisposeAsync(); + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/StreamLeaveOpenWrapper.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/StreamLeaveOpenWrapper.cs new file mode 100644 index 00000000000..c74c1b4c621 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/StreamLeaveOpenWrapper.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.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + /// + /// Wraps a given stream but leaves it open on Dispose. + /// + public 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 0a60cf5948a..c25a79d73e9 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs @@ -5,6 +5,7 @@ using Microsoft.Diagnostics.Monitoring.EventPipe; using Microsoft.Diagnostics.Monitoring.WebApi.Validation; using Microsoft.Diagnostics.NETCore.Client; +using Microsoft.Diagnostics.Tracing; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -17,6 +18,9 @@ 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"); @@ -82,9 +86,8 @@ public static async Task CaptureTraceAsync(TaskCompletionSource startCom { startCompletionSource.TrySetResult(null); } - //Buffer size matches FileStreamResult //CONSIDER Should we allow client to change the buffer size? - await eventStream.CopyToAsync(outputStream, 0x10000, token); + await eventStream.CopyToAsync(outputStream, DefaultBufferSize, token); }; var client = new DiagnosticsClient(endpointInfo.Endpoint); @@ -97,5 +100,41 @@ public static async Task CaptureTraceAsync(TaskCompletionSource startCom await pipeProcessor.RunAsync(token); } + + public static async Task CaptureTraceUntilEventAsync(TaskCompletionSource startCompletionSource, IEndpointInfo endpointInfo, MonitoringSourceConfiguration configuration, TimeSpan timeout, Stream outputStream, string providerName, string eventName, TraceEventOpcode? opcode, CancellationToken token) + { + DiagnosticsClient client = new(endpointInfo.Endpoint); + EventTracePipeline pipeProcessor = null; + EventMonitoringPassthroughStream eventMonitoringStream = null; + + Func streamAvailable = async (Stream eventStream, CancellationToken token) => + { + startCompletionSource?.TrySetResult(null); + + eventMonitoringStream = new EventMonitoringPassthroughStream(providerName, eventName, opcode, async (_) => + { + // JSFIX: log + await pipeProcessor?.StopAsync(token); + }, eventStream, outputStream, DefaultBufferSize, leaveDestinationStreamOpen: true /* We do not have ownership of the outputStream */); + + await eventMonitoringStream.ProcessAsync(token); + }; + + pipeProcessor = new EventTracePipeline(client, new EventTracePipelineSettings + { + Configuration = configuration, + Duration = timeout, + }, streamAvailable); + + try + { + await pipeProcessor.RunAsync(token); + } + finally + { + await pipeProcessor.DisposeAsync(); + await eventMonitoringStream.DisposeAsync(); + } + } } } 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 124716acbfd..87b542d344a 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 @@ -25,6 +25,7 @@ + @@ -73,6 +74,10 @@ + + + + diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TestAppScenarios.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TestAppScenarios.cs index 36f7556494c..7e84fae9609 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TestAppScenarios.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TestAppScenarios.cs @@ -71,5 +71,16 @@ public static class Commands public const string StopSpin = nameof(StopSpin); } } + + public static class TraceEvents + { + public const string Name = nameof(TraceEvents); + + 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..6cb0ba36025 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs @@ -0,0 +1,128 @@ +// 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.Extensions.DependencyInjection; +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +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_WithNoOpcode() + { + return StopOnEventTestCore(opcode: null); + } + + [Fact] + public Task StopOnEvent_Succeeds_WithMatchingOpcode() + { + return StopOnEventTestCore(TraceEventOpcode.Info); + } + + [Fact] + public Task StopOnEvent_DoesNotStop_WhenOpcodeDoesNotMatch() + { + return Assert.ThrowsAsync(() => StopOnEventTestCore(TraceEventOpcode.Resume)); + } + + [Fact] + public Task StopOnEvent_UsesDuration_WhenNoEventMatchesInTime() + { + return StopOnEventTestCore(TraceEventOpcode.Resume, TimeSpan.FromSeconds(10)); + } + + private async Task StopOnEventTestCore(TraceEventOpcode? opcode, TimeSpan? duration = null) + { + const string DefaultRuleName = "FunctionalTestRule"; + const string EgressProvider = "TmpEgressProvider"; + const string EventProviderName = "TestScenario"; + const string StoppingEventName = "UniqueEvent"; + + using TemporaryDirectory tempDirectory = new(_outputHelper); + + Task ruleCompletedTask = null; + + 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 = EventProviderName, + Keywords = "-1" + } + }, + EgressProvider, options => + { + options.Duration = duration ?? TimeSpan.Parse(ActionOptionsConstants.Duration_MaxValue); + options.StoppingEvent = new TraceEventOptions() + { + ProviderName = EventProviderName, + EventName = StoppingEventName, + Opcode = opcode + }; + }); + + ruleCompletedTask = runner.WaitForCollectionRuleCompleteAsync(DefaultRuleName); + }); + + string[] files = Directory.GetFiles(tempDirectory.FullName, "*.nettrace", SearchOption.TopDirectoryOnly); + string traceFile = Assert.Single(files); + await ValidateNettraceFile(traceFile); + } + + private async Task ValidateNettraceFile(string filePath) + { + byte[] expectedMagicToken = Encoding.UTF8.GetBytes("Nettrace"); + byte[] actualMagicToken = new byte[8]; + + await using FileStream fs = File.OpenRead(filePath); + await fs.ReadAsync(actualMagicToken); + Assert.True(actualMagicToken.SequenceEqual(expectedMagicToken), $"{filePath} is not a Nettrace file!"); + } +#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 81783b8ab12..9aa57a66a64 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 @@ -26,6 +26,7 @@ + diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs index 2fb1941cca2..b35dfd80220 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs @@ -1132,6 +1132,79 @@ 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 } + }; + + TraceEventOptions expectedStoppingEvent = new() + { + EventName = "CustomEvent", + ProviderName = ExpectedEventProviderName, + Opcode = Tracing.TraceEventOpcode.Stop + }; + + 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 TraceEventOptions() + { + 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_CollectLiveMetricsAction_RoundTrip() { @@ -1662,5 +1735,17 @@ private static void VerifyEgressNotExistMessage(string[] failures, int index, st Assert.Equal(message, 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 3d8bffeea5d..08ae259bdc1 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Options/CollectionRuleOptionsExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Options/CollectionRuleOptionsExtensions.cs @@ -484,7 +484,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, TraceEventOptions expectedStoppingEvent = null) { CollectTraceOptions collectTraceOptions = ruleOptions.VerifyAction( actionIndex, KnownCollectionRuleActions.CollectTrace); @@ -492,6 +492,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 83b40ed88b0..3a013fa6e4f 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Program.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Program.cs @@ -18,7 +18,8 @@ public static Task Main(string[] args) AsyncWaitScenario.Command(), LoggerScenario.Command(), SpinWaitScenario.Command(), - EnvironmentVariablesScenario.Command() + EnvironmentVariablesScenario.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..af7d91eee3f --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs @@ -0,0 +1,79 @@ +// 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)] + public void UniqueEvent() => WriteEvent(2); + } + + 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(() => + { + Random random = new(); + while (!stopGeneratingEvents.IsSet) + { + TestScenarioEventSource.Log.RandomNumberGenerated(random.Next()); + Task.Delay(100, context.GetCancellationToken()); + } + }); + + while (true) + { + string command = await ScenarioHelpers.WaitForCommandAsync(acceptableCommands, logger); + + switch (await ScenarioHelpers.WaitForCommandAsync(acceptableCommands, logger)) + { + case TestAppScenarios.TraceEvents.Commands.EmitUniqueEvent: + TestScenarioEventSource.Log.UniqueEvent(); + 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 c5949086c9f..045e70df7de 100644 --- a/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs +++ b/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs @@ -66,6 +66,8 @@ protected override async Task ExecuteCoreAsync( MonitoringSourceConfiguration configuration; + TraceEventOptions stoppingEvent = null; + if (Options.Profile.HasValue) { TraceProfile profile = Options.Profile.Value; @@ -78,8 +80,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; } string fileName = TraceUtilities.GenerateTraceFileName(EndpointInfo); @@ -90,7 +93,14 @@ protected override async Task ExecuteCoreAsync( async (outputStream, token) => { using IDisposable operationRegistration = _operationTrackerService.Register(EndpointInfo); - await TraceUtilities.CaptureTraceAsync(startCompletionSource, EndpointInfo, configuration, duration, outputStream, token); + if (null != stoppingEvent) + { + await TraceUtilities.CaptureTraceUntilEventAsync(startCompletionSource, EndpointInfo, configuration, duration, outputStream, stoppingEvent.ProviderName, stoppingEvent.EventName, stoppingEvent.Opcode, token: token); + } + else + { + await TraceUtilities.CaptureTraceAsync(startCompletionSource, EndpointInfo, configuration, duration, outputStream, token); + } }, egressProvider, fileName, 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..3f6b701e0aa 100644 --- a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.Validate.cs +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.Validate.cs @@ -27,6 +27,7 @@ IEnumerable IValidatableObject.Validate(ValidationContext vali bool hasProfile = Profile.HasValue; bool hasProviders = null != Providers && Providers.Any(); + bool hasStoppingEvent = null != StoppingEvent; if (hasProfile) { @@ -75,6 +76,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..91fac39f1a7 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 TraceEventOptions StoppingEvent { get; set; } } } diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/TraceEventOptions.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/TraceEventOptions.cs new file mode 100644 index 00000000000..26c5417fd6a --- /dev/null +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/TraceEventOptions.cs @@ -0,0 +1,39 @@ +// 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 Microsoft.Diagnostics.Tracing; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; + +namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions +{ + /// + /// Options for the CollectTrace action. + /// + [DebuggerDisplay("TraceEvent")] +#if SCHEMAGEN + [NJsonSchema.Annotations.JsonSchemaFlatten] +#endif + internal sealed record class TraceEventOptions : BaseRecordOptions + { + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_TraceEventOptions_ProviderName))] + [Required] + public string ProviderName { get; set; } + + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_TraceEventOptions_EventName))] + [Required] + public string EventName { get; set; } + + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_TraceEventOptions_Opcode))] + [EnumDataType(typeof(TraceEventOpcode))] + public TraceEventOpcode? Opcode { get; set; } + } +} diff --git a/src/Tools/dotnet-monitor/Strings.Designer.cs b/src/Tools/dotnet-monitor/Strings.Designer.cs index e025f43d2af..45333a4f325 100644 --- a/src/Tools/dotnet-monitor/Strings.Designer.cs +++ b/src/Tools/dotnet-monitor/Strings.Designer.cs @@ -285,6 +285,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 007ecf6399d..98690b90c35 100644 --- a/src/Tools/dotnet-monitor/Strings.resx +++ b/src/Tools/dotnet-monitor/Strings.resx @@ -239,6 +239,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. +1 Format Parameter: +0. stoppingEventField: The field that specified the StoppingEventOptions that contains the failure. +1. missingProviderName: The specified provider that is missing. +2. 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. From ace9595f4afeb6862f51c69fced3a8abcb6d304f Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Tue, 13 Sep 2022 10:18:18 -0700 Subject: [PATCH 02/26] Add comment --- .../CollectionRules/Actions/CollectTraceAction.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs b/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs index 045e70df7de..37fdd6f2823 100644 --- a/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs +++ b/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs @@ -82,6 +82,13 @@ protected override async Task ExecuteCoreAsync( int bufferSizeMegabytes = Options.BufferSizeMegabytes.GetValueOrDefault(CollectTraceOptionsDefaults.BufferSizeMegabytes); configuration = TraceUtilities.GetTraceConfiguration(optionsProviders, requestRundown, bufferSizeMegabytes); + // JSFIX: Investigate what is desired behavior here is. + // We currently only honor StoppingEvent on providers config similair to the above request rundown + // and buffer size, but don't provide any insight to the user about this. + // + // We can update the StoppingEvent to work for both profile and provider configs, but would have to either: + // - add more initelligent config validation to ensure the stopping event provider is available via a profile + // - ignore the check altogether. stoppingEvent = Options.StoppingEvent; } From efd186c10c160aa2498fe6b24b57f9bd2899b27d Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Mon, 26 Sep 2022 09:53:15 -0700 Subject: [PATCH 03/26] Simplify opcode detection --- .../OptionsDisplayStrings.Designer.cs | 9 ---- .../OptionsDisplayStrings.resx | 4 -- .../EventMonitoringPassthroughStream.cs | 16 ++----- .../Utilities/TraceUtilities.cs | 48 +++++++++---------- .../CollectTraceTests.cs | 15 ++---- .../CollectionRuleOptionsTests.cs | 3 +- .../Scenarios/TraceEventsScenario.cs | 11 ++--- .../Actions/CollectTraceAction.cs | 9 +--- .../Actions/CollectTraceOptions.Validate.cs | 10 ++++ .../Options/Actions/TraceEventOptions.cs | 7 --- 10 files changed, 50 insertions(+), 82 deletions(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs index 3b4589a1550..7e1c0e4c882 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs +++ b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs @@ -1364,15 +1364,6 @@ public static string DisplayAttributeDescription_TraceEventOptions_EventName { } } - /// - /// Looks up a localized string similar to The opcode of the event, if empty the specified event's opcode will not be checked.. - /// - public static string DisplayAttributeDescription_TraceEventOptions_Opcode { - get { - return ResourceManager.GetString("DisplayAttributeDescription_TraceEventOptions_Opcode", resourceCulture); - } - } - /// /// Looks up a localized string similar to The event provider that will produce the specified event.. /// diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx index fa79d994ff6..039a2a41445 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx +++ b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx @@ -720,10 +720,6 @@ The name of the event. The description provided for the EventName parameter on TraceEventOptions. - - The opcode of the event, if empty the specified event's opcode will not be checked. - The description provided for the Opcode parameter on TraceEventOptions. - The event provider that will produce the specified event. The description provided for the ProviderName parameter on TraceEventOptions. diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs index b8e79914846..6b28edf97c9 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs @@ -19,7 +19,6 @@ public class EventMonitoringPassthroughStream : Stream private readonly Action _onEvent; private readonly string _providerName; private readonly string _eventName; - private readonly TraceEventOpcode? _eventOpcode; private readonly Stream _sourceStream; private readonly Stream _destinationStream; @@ -31,7 +30,6 @@ public class EventMonitoringPassthroughStream : Stream public EventMonitoringPassthroughStream( string providerName, string eventName, - TraceEventOpcode? eventOpcode, Action onEvent, Stream sourceStream, Stream destinationStream, @@ -40,7 +38,6 @@ public EventMonitoringPassthroughStream( { _providerName = providerName; _eventName = eventName; - _eventOpcode = eventOpcode; _onEvent = onEvent; _sourceStream = sourceStream; @@ -79,15 +76,12 @@ public void StopProcessing() private void TraceEventCallback(TraceEvent obj) { - if (!_eventOpcode.HasValue || obj.Opcode == _eventOpcode) - { - // Once the specified event has been observed, stop watching for it. - // However, keep processing the data as to allow remaining trace event - // data, such as run down, to finish transferring to the destination stream. - _eventSource.Dynamic.RemoveCallback(TraceEventCallback); + // Once the specified event has been observed, stop watching for it. + // However, keep processing the data as to allow remaining trace event + // data, such as run down, to finish transferring to the destination stream. + _eventSource.Dynamic.RemoveCallback(TraceEventCallback); - _onEvent(obj); - } + _onEvent(obj); } public override bool CanSeek => false; diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs index c25a79d73e9..1098863e71f 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs @@ -5,7 +5,6 @@ using Microsoft.Diagnostics.Monitoring.EventPipe; using Microsoft.Diagnostics.Monitoring.WebApi.Validation; using Microsoft.Diagnostics.NETCore.Client; -using Microsoft.Diagnostics.Tracing; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -101,40 +100,41 @@ public static async Task CaptureTraceAsync(TaskCompletionSource startCom await pipeProcessor.RunAsync(token); } - public static async Task CaptureTraceUntilEventAsync(TaskCompletionSource startCompletionSource, IEndpointInfo endpointInfo, MonitoringSourceConfiguration configuration, TimeSpan timeout, Stream outputStream, string providerName, string eventName, TraceEventOpcode? opcode, CancellationToken token) + public static async Task CaptureTraceUntilEventAsync(TaskCompletionSource startCompletionSource, IEndpointInfo endpointInfo, MonitoringSourceConfiguration configuration, TimeSpan timeout, Stream outputStream, string providerName, string eventName, CancellationToken token) { DiagnosticsClient client = new(endpointInfo.Endpoint); - EventTracePipeline pipeProcessor = null; - EventMonitoringPassthroughStream eventMonitoringStream = null; + TaskCompletionSource stoppingEventHitSource = new(TaskCreationOptions.RunContinuationsAsynchronously); - Func streamAvailable = async (Stream eventStream, CancellationToken token) => + await using EventTracePipeline pipeProcessor = new(client, new EventTracePipelineSettings + { + Configuration = configuration, + Duration = timeout, + }, + async (eventStream, token) => { startCompletionSource?.TrySetResult(null); - - eventMonitoringStream = new EventMonitoringPassthroughStream(providerName, eventName, opcode, async (_) => - { - // JSFIX: log - await pipeProcessor?.StopAsync(token); - }, eventStream, outputStream, DefaultBufferSize, leaveDestinationStreamOpen: true /* We do not have ownership of the outputStream */); + await using EventMonitoringPassthroughStream eventMonitoringStream = new( + providerName, + eventName, + onEvent: (_) => stoppingEventHitSource.TrySetResult(null), + eventStream, + outputStream, + DefaultBufferSize, + leaveDestinationStreamOpen: true /* We do not have ownership of the outputStream */); await eventMonitoringStream.ProcessAsync(token); - }; + }); - pipeProcessor = new EventTracePipeline(client, new EventTracePipelineSettings - { - Configuration = configuration, - Duration = timeout, - }, streamAvailable); + Task pipelineRunTask = pipeProcessor.RunAsync(token); + await Task.WhenAny(pipelineRunTask, stoppingEventHitSource.Task); - try - { - await pipeProcessor.RunAsync(token); - } - finally + + if (stoppingEventHitSource.Task.IsCompleted) { - await pipeProcessor.DisposeAsync(); - await eventMonitoringStream.DisposeAsync(); + await pipeProcessor.StopAsync(token); } + + await pipelineRunTask; } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs index 6cb0ba36025..6b7fd2c5f0d 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs @@ -37,16 +37,10 @@ public CollectTraceTests(ITestOutputHelper outputHelper, ServiceProviderFixture } #if NET5_0_OR_GREATER - [Fact] - public Task StopOnEvent_Succeeds_WithNoOpcode() - { - return StopOnEventTestCore(opcode: null); - } - [Fact] public Task StopOnEvent_Succeeds_WithMatchingOpcode() { - return StopOnEventTestCore(TraceEventOpcode.Info); + return StopOnEventTestCore(TraceEventOpcode.Reply); } [Fact] @@ -61,13 +55,15 @@ public Task StopOnEvent_UsesDuration_WhenNoEventMatchesInTime() return StopOnEventTestCore(TraceEventOpcode.Resume, TimeSpan.FromSeconds(10)); } - private async Task StopOnEventTestCore(TraceEventOpcode? opcode, TimeSpan? duration = null) + private async Task StopOnEventTestCore(TraceEventOpcode opcode = TraceEventOpcode.Info, TimeSpan? duration = null) { const string DefaultRuleName = "FunctionalTestRule"; const string EgressProvider = "TmpEgressProvider"; const string EventProviderName = "TestScenario"; const string StoppingEventName = "UniqueEvent"; + string qualifiedEventName = (opcode == TraceEventOpcode.Info) ? StoppingEventName : $"{StoppingEventName}/{opcode}"; + using TemporaryDirectory tempDirectory = new(_outputHelper); Task ruleCompletedTask = null; @@ -101,8 +97,7 @@ await ScenarioRunner.SingleTarget(_outputHelper, options.StoppingEvent = new TraceEventOptions() { ProviderName = EventProviderName, - EventName = StoppingEventName, - Opcode = opcode + EventName = qualifiedEventName, }; }); diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs index b35dfd80220..991383cd859 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs @@ -1145,8 +1145,7 @@ public Task CollectionRuleOptions_CollectTraceAction_StopOnEvent() TraceEventOptions expectedStoppingEvent = new() { EventName = "CustomEvent", - ProviderName = ExpectedEventProviderName, - Opcode = Tracing.TraceEventOpcode.Stop + ProviderName = ExpectedEventProviderName }; return ValidateSuccess( diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs index af7d91eee3f..0b57cef7bae 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs @@ -7,7 +7,6 @@ using System.CommandLine; using System.CommandLine.Invocation; using System.Diagnostics.Tracing; -using System.Threading; using System.Threading.Tasks; namespace Microsoft.Diagnostics.Monitoring.UnitTestApp.Scenarios @@ -26,7 +25,7 @@ class TestScenarioEventSource : EventSource [Event(1)] public void RandomNumberGenerated(int number) => WriteEvent(1, number); - [Event(2)] + [Event(2, Opcode = EventOpcode.Reply)] public void UniqueEvent() => WriteEvent(2); } @@ -47,11 +46,11 @@ public static async Task ExecuteAsync(InvocationContext context) context.ExitCode = await ScenarioHelpers.RunScenarioAsync(async logger => { - using ManualResetEventSlim stopGeneratingEvents = new(initialState: false); + TaskCompletionSource stopGeneratingEvents = new(); Task eventEmitterTask = Task.Run(() => { Random random = new(); - while (!stopGeneratingEvents.IsSet) + while (!stopGeneratingEvents.Task.IsCompleted) { TestScenarioEventSource.Log.RandomNumberGenerated(random.Next()); Task.Delay(100, context.GetCancellationToken()); @@ -60,15 +59,13 @@ public static async Task ExecuteAsync(InvocationContext context) while (true) { - string command = await ScenarioHelpers.WaitForCommandAsync(acceptableCommands, logger); - switch (await ScenarioHelpers.WaitForCommandAsync(acceptableCommands, logger)) { case TestAppScenarios.TraceEvents.Commands.EmitUniqueEvent: TestScenarioEventSource.Log.UniqueEvent(); break; case TestAppScenarios.TraceEvents.Commands.ShutdownScenario: - stopGeneratingEvents.Set(); + stopGeneratingEvents.TrySetResult(null); eventEmitterTask.Wait(context.GetCancellationToken()); return 0; } diff --git a/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs b/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs index 37fdd6f2823..0c17cbd9bfa 100644 --- a/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs +++ b/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs @@ -82,13 +82,6 @@ protected override async Task ExecuteCoreAsync( int bufferSizeMegabytes = Options.BufferSizeMegabytes.GetValueOrDefault(CollectTraceOptionsDefaults.BufferSizeMegabytes); configuration = TraceUtilities.GetTraceConfiguration(optionsProviders, requestRundown, bufferSizeMegabytes); - // JSFIX: Investigate what is desired behavior here is. - // We currently only honor StoppingEvent on providers config similair to the above request rundown - // and buffer size, but don't provide any insight to the user about this. - // - // We can update the StoppingEvent to work for both profile and provider configs, but would have to either: - // - add more initelligent config validation to ensure the stopping event provider is available via a profile - // - ignore the check altogether. stoppingEvent = Options.StoppingEvent; } @@ -102,7 +95,7 @@ protected override async Task ExecuteCoreAsync( using IDisposable operationRegistration = _operationTrackerService.Register(EndpointInfo); if (null != stoppingEvent) { - await TraceUtilities.CaptureTraceUntilEventAsync(startCompletionSource, EndpointInfo, configuration, duration, outputStream, stoppingEvent.ProviderName, stoppingEvent.EventName, stoppingEvent.Opcode, token: token); + await TraceUtilities.CaptureTraceUntilEventAsync(startCompletionSource, EndpointInfo, configuration, duration, outputStream, stoppingEvent.ProviderName, stoppingEvent.EventName, token: token); } else { 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 3f6b701e0aa..49463d3e6dc 100644 --- a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.Validate.cs +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.Validate.cs @@ -31,6 +31,16 @@ IEnumerable IValidatableObject.Validate(ValidationContext vali 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 diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/TraceEventOptions.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/TraceEventOptions.cs index 26c5417fd6a..ff9699ae1b8 100644 --- a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/TraceEventOptions.cs +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/TraceEventOptions.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using Microsoft.Diagnostics.Monitoring.WebApi; -using Microsoft.Diagnostics.Tracing; using System.ComponentModel.DataAnnotations; using System.Diagnostics; @@ -29,11 +28,5 @@ internal sealed record class TraceEventOptions : BaseRecordOptions Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_TraceEventOptions_EventName))] [Required] public string EventName { get; set; } - - [Display( - ResourceType = typeof(OptionsDisplayStrings), - Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_TraceEventOptions_Opcode))] - [EnumDataType(typeof(TraceEventOpcode))] - public TraceEventOpcode? Opcode { get; set; } } } From 5110bce141145149236ab841440d6c523d4c5c4b Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Mon, 26 Sep 2022 12:05:37 -0700 Subject: [PATCH 04/26] Add payload filtering --- .../OptionsDisplayStrings.Designer.cs | 9 ++ .../OptionsDisplayStrings.resx | 4 + .../EventMonitoringPassthroughStream.cs | 89 ++++++++++++++++++- .../Utilities/TraceUtilities.cs | 5 +- .../TestAppScenarios.cs | 1 + .../CollectTraceTests.cs | 42 +++++++-- .../CollectionRuleOptionsTests.cs | 33 +++++++ .../Scenarios/TraceEventsScenario.cs | 4 +- .../Actions/CollectTraceAction.cs | 2 +- .../Options/Actions/TraceEventOptions.cs | 6 ++ 10 files changed, 181 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs index 7e1c0e4c882..b352ebd84d2 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs +++ b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs @@ -1364,6 +1364,15 @@ public static string DisplayAttributeDescription_TraceEventOptions_EventName { } } + /// + /// Looks up a localized string similar to A mapping of event payload field names to their expected value.. + /// + public static string DisplayAttributeDescription_TraceEventOptions_PayloadFilter { + get { + return ResourceManager.GetString("DisplayAttributeDescription_TraceEventOptions_PayloadFilter", resourceCulture); + } + } + /// /// Looks up a localized string similar to The event provider that will produce the specified event.. /// diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx index 039a2a41445..07524de79ac 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx +++ b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx @@ -724,4 +724,8 @@ The event provider that will produce the specified event. The description provided for the ProviderName parameter on TraceEventOptions. + + A mapping of event payload field names to their expected value. + The description provided for the PayloadFilter parameter on TraceEventOptions. + \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs index 6b28edf97c9..9d19645a15a 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs @@ -4,6 +4,7 @@ using Microsoft.Diagnostics.Tracing; using System; +using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -17,20 +18,31 @@ namespace Microsoft.Diagnostics.Monitoring.WebApi public class EventMonitoringPassthroughStream : Stream { private readonly Action _onEvent; - private readonly string _providerName; - private readonly string _eventName; + private readonly Action> _onPayloadFilterFailure; + private readonly Stream _sourceStream; private readonly Stream _destinationStream; - private EventPipeEventSource _eventSource; + private readonly string _providerName; + private readonly string _eventName; + + // The original payload filter specified by the user. It will only be used to initialize _payloadFilterIndexCache. + private readonly IDictionary _payloadFilter; + + // A mapping of payload indexes to their expected value. + private object _payloadCacheLocker = new(); + private Dictionary _payloadFilterIndexCache; + // JSFIX: Add summary. // Key takeaway is that onEvent will only be invoked once, and the source stream will continue to transfer to // the destination stream even after the onEvent callback is invoked. public EventMonitoringPassthroughStream( string providerName, string eventName, + IDictionary payloadFilter, Action onEvent, + Action> onPayloadFilterFailure, Stream sourceStream, Stream destinationStream, int bufferSize, @@ -39,7 +51,9 @@ public EventMonitoringPassthroughStream( _providerName = providerName; _eventName = eventName; _onEvent = onEvent; + _onPayloadFilterFailure = onPayloadFilterFailure; _sourceStream = sourceStream; + _payloadFilter = payloadFilter; // Wrap a buffered stream around the destination stream // to avoid slowing down the event processing with the data @@ -76,14 +90,81 @@ public void StopProcessing() 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. + _eventSource.Dynamic.RemoveCallback(TraceEventCallback); + _onPayloadFilterFailure(obj.PayloadNames); + return; + } + + if (!DoesPayloadMatch(obj)) + { + return; + } + // Once the specified event has been observed, stop watching for it. // However, keep processing the data as to allow remaining trace event // data, such as run down, to finish transferring to the destination stream. _eventSource.Dynamic.RemoveCallback(TraceEventCallback); - _onEvent(obj); } + private bool HydratePayloadFilterCache(TraceEvent obj) + { + lock (_payloadCacheLocker) + { + if (_payloadFilterIndexCache != null) + { + return true; + } + + if (_payloadFilter == null || _payloadFilter.Count == 0) + { + _payloadFilterIndexCache = new(); + return true; + } + + if (obj.PayloadNames.Length < _payloadFilter.Count) + { + return false; + } + + Dictionary payloadFilterCache = new(); + for (int i = 0; i < obj.PayloadNames.Length; i++) + { + if (_payloadFilter.TryGetValue(obj.PayloadNames[i], out string payloadValue)) + { + payloadFilterCache.Add(i, payloadValue); + } + } + + if (_payloadFilter.Count != payloadFilterCache.Count) + { + return false; + } + + _payloadFilterIndexCache = payloadFilterCache; + } + + return true; + } + + private bool DoesPayloadMatch(TraceEvent obj) + { + foreach (var (payloadIndex, expectedValue) in _payloadFilterIndexCache) + { + if (!string.Equals(obj.PayloadString(payloadIndex), expectedValue, StringComparison.Ordinal)) + { + return false; + } + } + + return true; + } + public override bool CanSeek => false; public override bool CanWrite => false; diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs index 1098863e71f..83b6799402b 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs @@ -100,7 +100,7 @@ public static async Task CaptureTraceAsync(TaskCompletionSource startCom await pipeProcessor.RunAsync(token); } - public static async Task CaptureTraceUntilEventAsync(TaskCompletionSource startCompletionSource, IEndpointInfo endpointInfo, MonitoringSourceConfiguration configuration, TimeSpan timeout, Stream outputStream, string providerName, string eventName, CancellationToken token) + public static async Task CaptureTraceUntilEventAsync(TaskCompletionSource startCompletionSource, IEndpointInfo endpointInfo, MonitoringSourceConfiguration configuration, TimeSpan timeout, Stream outputStream, string providerName, string eventName, IDictionary payloadFilter, CancellationToken token) { DiagnosticsClient client = new(endpointInfo.Endpoint); TaskCompletionSource stoppingEventHitSource = new(TaskCreationOptions.RunContinuationsAsynchronously); @@ -116,7 +116,9 @@ public static async Task CaptureTraceUntilEventAsync(TaskCompletionSource stoppingEventHitSource.TrySetResult(null), + onPayloadFilterFailure: (_) => { }, eventStream, outputStream, DefaultBufferSize, @@ -128,7 +130,6 @@ public static async Task CaptureTraceUntilEventAsync(TaskCompletionSource(); _outputHelper = outputHelper; } - #if NET5_0_OR_GREATER + + private const TraceEventOpcode ExpectedEventOpcode = TraceEventOpcode.Reply; + [Fact] public Task StopOnEvent_Succeeds_WithMatchingOpcode() { - return StopOnEventTestCore(TraceEventOpcode.Reply); + return StopOnEventTestCore(); + } + + [Fact] + public Task StopOnEvent_Succeeds_WithMatchingPayload() + { + return StopOnEventTestCore(payloadFilter: new Dictionary() + { + { "message", TestAppScenarios.TraceEvents.UniqueEventMessage } + }); } [Fact] public Task StopOnEvent_DoesNotStop_WhenOpcodeDoesNotMatch() { - return Assert.ThrowsAsync(() => StopOnEventTestCore(TraceEventOpcode.Resume)); + return Assert.ThrowsAsync(() => StopOnEventTestCore(opcode: TraceEventOpcode.Resume)); + } + + [Fact] + public Task StopOnEvent_DoesNotStop_WhenPayloadFieldNamesMismatch() + { + return Assert.ThrowsAsync(() => StopOnEventTestCore(payloadFilter: new Dictionary() + { + { "message", TestAppScenarios.TraceEvents.UniqueEventMessage }, + { "foobar", "baz" } + })); + } + + [Fact] + public Task StopOnEvent_DoesNotStop_WhenPayloadFieldValueMismatch() + { + return Assert.ThrowsAsync(() => StopOnEventTestCore(payloadFilter: new Dictionary() + { + { "message", TestAppScenarios.TraceEvents.UniqueEventMessage.ToUpperInvariant() } + })); } [Fact] public Task StopOnEvent_UsesDuration_WhenNoEventMatchesInTime() { - return StopOnEventTestCore(TraceEventOpcode.Resume, TimeSpan.FromSeconds(10)); + return StopOnEventTestCore(opcode: TraceEventOpcode.Resume, duration: TimeSpan.FromSeconds(10)); } - private async Task StopOnEventTestCore(TraceEventOpcode opcode = TraceEventOpcode.Info, TimeSpan? duration = null) + private async Task StopOnEventTestCore(TraceEventOpcode opcode = ExpectedEventOpcode, IDictionary payloadFilter = null, TimeSpan? duration = null) { const string DefaultRuleName = "FunctionalTestRule"; const string EgressProvider = "TmpEgressProvider"; @@ -98,6 +129,7 @@ await ScenarioRunner.SingleTarget(_outputHelper, { ProviderName = EventProviderName, EventName = qualifiedEventName, + PayloadFilter = payloadFilter }; }); diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs index 991383cd859..c098a22109a 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs @@ -1204,6 +1204,39 @@ public Task CollectionRuleOptions_CollectTraceAction_StopOnEvent_MissingProvider }); } + [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 TraceEventOptions() + { + 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() { diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs index 0b57cef7bae..361584d6aff 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs @@ -26,7 +26,7 @@ class TestScenarioEventSource : EventSource public void RandomNumberGenerated(int number) => WriteEvent(1, number); [Event(2, Opcode = EventOpcode.Reply)] - public void UniqueEvent() => WriteEvent(2); + public void UniqueEvent(string message) => WriteEvent(2, message); } public static Command Command() @@ -62,7 +62,7 @@ public static async Task ExecuteAsync(InvocationContext context) switch (await ScenarioHelpers.WaitForCommandAsync(acceptableCommands, logger)) { case TestAppScenarios.TraceEvents.Commands.EmitUniqueEvent: - TestScenarioEventSource.Log.UniqueEvent(); + TestScenarioEventSource.Log.UniqueEvent(TestAppScenarios.TraceEvents.UniqueEventMessage); break; case TestAppScenarios.TraceEvents.Commands.ShutdownScenario: stopGeneratingEvents.TrySetResult(null); diff --git a/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs b/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs index 0c17cbd9bfa..c9c55d7894e 100644 --- a/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs +++ b/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs @@ -95,7 +95,7 @@ protected override async Task ExecuteCoreAsync( using IDisposable operationRegistration = _operationTrackerService.Register(EndpointInfo); if (null != stoppingEvent) { - await TraceUtilities.CaptureTraceUntilEventAsync(startCompletionSource, EndpointInfo, configuration, duration, outputStream, stoppingEvent.ProviderName, stoppingEvent.EventName, token: token); + await TraceUtilities.CaptureTraceUntilEventAsync(startCompletionSource, EndpointInfo, configuration, duration, outputStream, stoppingEvent.ProviderName, stoppingEvent.EventName, stoppingEvent.PayloadFilter, token: token); } else { diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/TraceEventOptions.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/TraceEventOptions.cs index ff9699ae1b8..124ba58b835 100644 --- a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/TraceEventOptions.cs +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/TraceEventOptions.cs @@ -3,6 +3,7 @@ // 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; @@ -28,5 +29,10 @@ internal sealed record class TraceEventOptions : BaseRecordOptions Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_TraceEventOptions_EventName))] [Required] public string EventName { get; set; } + + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_TraceEventOptions_PayloadFilter))] + public IDictionary PayloadFilter { get; set; } } } From d14f066357894bf97f9b0aeea492525b2c9820c6 Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Mon, 26 Sep 2022 14:58:19 -0700 Subject: [PATCH 05/26] Add logger statements --- .../EventMonitoringPassthroughStream.cs | 8 +++---- .../LoggingExtensions.cs | 23 +++++++++++++++++++ .../Strings.Designer.cs | 18 +++++++++++++++ .../Strings.resx | 15 ++++++++++++ .../Utilities/TraceUtilities.cs | 10 +++++--- .../Actions/CollectTraceAction.cs | 8 ++++++- 6 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs index 9d19645a15a..fa9cf5afb00 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs @@ -18,7 +18,7 @@ namespace Microsoft.Diagnostics.Monitoring.WebApi public class EventMonitoringPassthroughStream : Stream { private readonly Action _onEvent; - private readonly Action> _onPayloadFilterFailure; + private readonly Action _onPayloadFilterMismatch; private readonly Stream _sourceStream; private readonly Stream _destinationStream; @@ -42,7 +42,7 @@ public EventMonitoringPassthroughStream( string eventName, IDictionary payloadFilter, Action onEvent, - Action> onPayloadFilterFailure, + Action onPayloadFilterMismatch, Stream sourceStream, Stream destinationStream, int bufferSize, @@ -51,7 +51,7 @@ public EventMonitoringPassthroughStream( _providerName = providerName; _eventName = eventName; _onEvent = onEvent; - _onPayloadFilterFailure = onPayloadFilterFailure; + _onPayloadFilterMismatch = onPayloadFilterMismatch; _sourceStream = sourceStream; _payloadFilter = payloadFilter; @@ -96,7 +96,7 @@ private void TraceEventCallback(TraceEvent obj) // we'll never match the event so stop checking it // and proceed with just transferring the data to the destination stream. _eventSource.Dynamic.RemoveCallback(TraceEventCallback); - _onPayloadFilterFailure(obj.PayloadNames); + _onPayloadFilterMismatch(obj); return; } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/LoggingExtensions.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/LoggingExtensions.cs index 81ee488d273..3c94eb20458 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.Information, + formatString: Strings.LogFormatString_StoppingTraceEventHit); + + private static readonly Action _stoppingTraceEventPayloadFilterMismatch = + LoggerMessage.Define( + eventId: new EventId(9, "StoppingTraceEventPayloadFilterMismatch"), + logLevel: LogLevel.Warning, + formatString: Strings.LogFormatString_StoppingTraceEventPayloadFilterMismatch); + public static void RequestFailed(this ILogger logger, Exception ex) { _requestFailed(logger, ex); @@ -85,5 +98,15 @@ 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); + } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.Designer.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.Designer.cs index 43303c0bbb0..2f8fa02eddd 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.Designer.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.Designer.cs @@ -267,6 +267,24 @@ internal static string LogFormatString_ResolvedTargetProcess { } } + /// + /// Looks up a localized string similar to Hit stopping trace event '{providerName}/{eventName}'. + /// + internal static string LogFormatString_StoppingTraceEventHit { + get { + return ResourceManager.GetString("LogFormatString_StoppingTraceEventHit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to One or more field names specified in the payload filter for event '{providerName}/{eventName}' do not match any of the known field names: '{payloadFieldNames}'. As a result the requested stopping event is unreachable; will continue to collect the trace for the remaining specified duration.. + /// + internal static string LogFormatString_StoppingTraceEventPayloadFilterMismatch { + get { + return ResourceManager.GetString("LogFormatString_StoppingTraceEventPayloadFilterMismatch", resourceCulture); + } + } + /// /// Looks up a localized string similar to Request limit for endpoint reached. Limit: {limit}, oustanding requests: {requests}. /// diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.resx b/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.resx index 4d3069d257a..acacb5f471a 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.resx +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.resx @@ -219,6 +219,21 @@ Resolved target process. Gets the format string that is printed in the 3:ResolvedTargetProcess event. 0 Format Parameters + + + Hit stopping trace event '{providerName}/{eventName}' + Gets the format string that is printed in the 8:StoppingTraceEventHit event. +2 Format Parameter: +1. providerName: The stopping event provider name. +2. eventName: The stopping event name. + + + One or more field names specified in the payload filter for event '{providerName}/{eventName}' do not match any of the known field names: '{payloadFieldNames}'. As a result the requested stopping event is unreachable; will continue to collect the trace for the remaining specified duration. + Gets the format string that is printed in the 9:StoppingTraceEventPayloadFilterMismatch. +3 Format Parameter: +1. providerName: The stopping event provider name. +2. eventName: The stopping event name. +3. payloadFieldNames: The available payload field names. Request limit for endpoint reached. Limit: {limit}, oustanding requests: {requests} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs index 83b6799402b..183cc6ef3eb 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs @@ -100,7 +100,7 @@ public static async Task CaptureTraceAsync(TaskCompletionSource startCom 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, CancellationToken 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); @@ -117,8 +117,12 @@ public static async Task CaptureTraceUntilEventAsync(TaskCompletionSource stoppingEventHitSource.TrySetResult(null), - onPayloadFilterFailure: (_) => { }, + onEvent: (traceEvent) => + { + logger.StoppingTraceEventHit(traceEvent); + stoppingEventHitSource.TrySetResult(null); + }, + onPayloadFilterMismatch: logger.StoppingTraceEventPayloadFilterMismatch, eventStream, outputStream, DefaultBufferSize, diff --git a/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs b/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs index c9c55d7894e..0e7c5c140cc 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; @@ -95,7 +96,12 @@ protected override async Task ExecuteCoreAsync( using IDisposable operationRegistration = _operationTrackerService.Register(EndpointInfo); if (null != stoppingEvent) { - await TraceUtilities.CaptureTraceUntilEventAsync(startCompletionSource, EndpointInfo, configuration, duration, outputStream, stoppingEvent.ProviderName, stoppingEvent.EventName, stoppingEvent.PayloadFilter, token: token); + ILogger logger = _serviceProvider + .GetRequiredService() + .CreateLogger(); + using var _ = logger.BeginScope(scope); + + await TraceUtilities.CaptureTraceUntilEventAsync(startCompletionSource, EndpointInfo, configuration, duration, outputStream, stoppingEvent.ProviderName, stoppingEvent.EventName, stoppingEvent.PayloadFilter, logger, token); } else { From f212cd0e3a08c70749c186ca3b90aa6939f15276 Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Mon, 26 Sep 2022 15:01:16 -0700 Subject: [PATCH 06/26] Fix resx help comment --- src/Tools/dotnet-monitor/Strings.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tools/dotnet-monitor/Strings.resx b/src/Tools/dotnet-monitor/Strings.resx index 98690b90c35..f4583060cf6 100644 --- a/src/Tools/dotnet-monitor/Strings.resx +++ b/src/Tools/dotnet-monitor/Strings.resx @@ -242,7 +242,7 @@ 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. -1 Format Parameter: +3 Format Parameter: 0. stoppingEventField: The field that specified the StoppingEventOptions that contains the failure. 1. missingProviderName: The specified provider that is missing. 2. providersField: the field that the provider should be defined in. From 623e868d1dbded5a18bdf0025efdda98f0c40efc Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Mon, 26 Sep 2022 15:50:26 -0700 Subject: [PATCH 07/26] Support calling onEvent only once --- documentation/schema.json | 47 ++-------- .../EventMonitoringPassthroughStream.cs | 92 ++++++++++++------- .../Utilities/TraceUtilities.cs | 1 + ...tics.Monitoring.ConfigurationSchema.csproj | 4 - .../CollectTraceTests.cs | 2 +- 5 files changed, 72 insertions(+), 74 deletions(-) diff --git a/documentation/schema.json b/documentation/schema.json index 966fb09107a..61143656138 100644 --- a/documentation/schema.json +++ b/documentation/schema.json @@ -1776,47 +1776,18 @@ "description": "The name of the event.", "minLength": 1 }, - "Opcode": { - "description": "The opcode of the event, if empty the specified event's opcode will not be checked.", - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/definitions/TraceEventOpcode" - } - ] + "PayloadFilter": { + "type": [ + "null", + "object" + ], + "description": "A mapping of event payload field names to their expected value.", + "additionalProperties": { + "type": "string" + } } } }, - "TraceEventOpcode": { - "type": "string", - "description": "", - "x-enumNames": [ - "Info", - "Start", - "Stop", - "DataCollectionStart", - "DataCollectionStop", - "Extension", - "Reply", - "Resume", - "Suspend", - "Transfer" - ], - "enum": [ - "Info", - "Start", - "Stop", - "DataCollectionStart", - "DataCollectionStop", - "Extension", - "Reply", - "Resume", - "Suspend", - "Transfer" - ] - }, "ExecuteOptions": { "type": "object", "additionalProperties": false, diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs index fa9cf5afb00..eb1f9d26bab 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs @@ -17,8 +17,9 @@ namespace Microsoft.Diagnostics.Monitoring.WebApi /// public class EventMonitoringPassthroughStream : Stream { - private readonly Action _onEvent; private readonly Action _onPayloadFilterMismatch; + private readonly Action _onEvent; + private readonly bool _callOnEventOnlyOnce; private readonly Stream _sourceStream; private readonly Stream _destinationStream; @@ -27,16 +28,30 @@ public class EventMonitoringPassthroughStream : Stream private readonly string _providerName; private readonly string _eventName; - // The original payload filter specified by the user. It will only be used to initialize _payloadFilterIndexCache. + // The original payload filter of fieldName->fieldValue specified by the user. It will only be used to hydrate _payloadFilterIndexCache. private readonly IDictionary _payloadFilter; - // A mapping of payload indexes to their expected value. + // Guards _payloadFilterIndexCache. private object _payloadCacheLocker = new(); + // A cache of the mapping of payload indexes to their expected value. + // Unlike _payloadFilter, this tracks the exact indices into the provided event's payload to check for the expected values. private Dictionary _payloadFilterIndexCache; - // JSFIX: Add summary. - // Key takeaway is that onEvent will only be invoked once, and the source stream will continue to transfer to - // the destination stream even after the onEvent callback is invoked. + + /// + /// 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 possible 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. + /// 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, @@ -46,6 +61,7 @@ public EventMonitoringPassthroughStream( Stream sourceStream, Stream destinationStream, int bufferSize, + bool callOnEventOnlyOnce, bool leaveDestinationStreamOpen) : base() { _providerName = providerName; @@ -54,6 +70,7 @@ public EventMonitoringPassthroughStream( _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 @@ -83,6 +100,18 @@ public Task ProcessAsync(CancellationToken token) }, token); } + /// + /// Stops monitoring for the specified stopping event. Data will continue to be written to the provided destination stream. + /// + public void StopMonitoringForEvent() + { + _eventSource.Dynamic.RemoveCallback(TraceEventCallback); + + } + + /// + /// Stops processing the event data, data will no longer be written to the provided destination stream. + /// public void StopProcessing() { _eventSource?.StopProcessing(); @@ -95,7 +124,7 @@ private void TraceEventCallback(TraceEvent 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. - _eventSource.Dynamic.RemoveCallback(TraceEventCallback); + StopMonitoringForEvent(); _onPayloadFilterMismatch(obj); return; } @@ -105,10 +134,11 @@ private void TraceEventCallback(TraceEvent obj) return; } - // Once the specified event has been observed, stop watching for it. - // However, keep processing the data as to allow remaining trace event - // data, such as run down, to finish transferring to the destination stream. - _eventSource.Dynamic.RemoveCallback(TraceEventCallback); + if (_callOnEventOnlyOnce) + { + StopMonitoringForEvent(); + } + _onEvent(obj); } @@ -165,26 +195,6 @@ private bool DoesPayloadMatch(TraceEvent obj) return true; } - 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() => _sourceStream.Flush(); - public override Task FlushAsync(CancellationToken cancellationToken) => _sourceStream.FlushAsync(cancellationToken); - public override int Read(byte[] buffer, int offset, int count) { return Read(buffer.AsSpan(offset, count)); @@ -217,6 +227,26 @@ public override async ValueTask ReadAsync(Memory buffer, Cancellation 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() => _sourceStream.Flush(); + public override Task FlushAsync(CancellationToken cancellationToken) => _sourceStream.FlushAsync(cancellationToken); + public override async ValueTask DisposeAsync() { _eventSource?.Dispose(); diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs index 183cc6ef3eb..c1a09ad59bd 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs @@ -126,6 +126,7 @@ public static async Task CaptureTraceUntilEventAsync(TaskCompletionSource - - - - diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs index 8c252b5c6c4..c61eddaa84e 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs @@ -93,7 +93,7 @@ private async Task StopOnEventTestCore(TraceEventOpcode opcode = ExpectedEventOp const string EventProviderName = "TestScenario"; const string StoppingEventName = "UniqueEvent"; - string qualifiedEventName = (opcode == TraceEventOpcode.Info) ? StoppingEventName : $"{StoppingEventName}/{opcode}"; + string qualifiedEventName = (opcode == TraceEventOpcode.Info) ? StoppingEventName : FormattableString.Invariant($"{StoppingEventName}/{opcode}"); using TemporaryDirectory tempDirectory = new(_outputHelper); From 60aa45b40877eedfbd9af5a044331ce9e2ba91b4 Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Mon, 26 Sep 2022 15:53:11 -0700 Subject: [PATCH 08/26] Update resx comment --- src/Tools/dotnet-monitor/Strings.resx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Tools/dotnet-monitor/Strings.resx b/src/Tools/dotnet-monitor/Strings.resx index 63e86983de8..f00db5117c1 100644 --- a/src/Tools/dotnet-monitor/Strings.resx +++ b/src/Tools/dotnet-monitor/Strings.resx @@ -246,9 +246,9 @@ 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: -0. stoppingEventField: The field that specified the StoppingEventOptions that contains the failure. -1. missingProviderName: The specified provider that is missing. -2. providersField: the field that the provider should be defined in. +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. From 4fb8a28b556ce3ae072ea24958202377e59bd070 Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Mon, 26 Sep 2022 15:56:16 -0700 Subject: [PATCH 09/26] Improve schema docs --- documentation/schema.json | 2 +- .../OptionsDisplayStrings.Designer.cs | 2 +- .../OptionsDisplayStrings.resx | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/documentation/schema.json b/documentation/schema.json index 61143656138..60185069255 100644 --- a/documentation/schema.json +++ b/documentation/schema.json @@ -1773,7 +1773,7 @@ }, "EventName": { "type": "string", - "description": "The name of the event.", + "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 seperated by a '/'. If the event has no opcode, then the event name is just the task name.", "minLength": 1 }, "PayloadFilter": { diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs index 36e7882137e..58ac028f27c 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs +++ b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs @@ -1383,7 +1383,7 @@ public static string DisplayAttributeDescription_ThreadpoolQueueLengthOptions_Le } /// - /// Looks up a localized string similar to The name of the event.. + /// 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 seperated by a '/'. If the event has no opcode, then the event name is just the task name.. /// public static string DisplayAttributeDescription_TraceEventOptions_EventName { get { diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx index 3bb6c4b6b71..8a7184c7ba6 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx +++ b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx @@ -723,12 +723,12 @@ Allows features that require diagnostic components to be loaded into target processes to be enabled. These features may have minimal performance impact on target processes. - + 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. + The name of the event, which is a concatenation of the task name and opcode name, if any. The task and opcode names are seperated 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 TraceEventOptions. From eef1097463b49d86c3ecb6b173c1d755be6aab60 Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Mon, 26 Sep 2022 15:58:49 -0700 Subject: [PATCH 10/26] Update schema docs for payload filtering --- documentation/schema.json | 2 +- .../OptionsDisplayStrings.Designer.cs | 2 +- .../OptionsDisplayStrings.resx | 2 +- .../EventMonitoringPassthroughStream.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/documentation/schema.json b/documentation/schema.json index 60185069255..ace7f039a43 100644 --- a/documentation/schema.json +++ b/documentation/schema.json @@ -1781,7 +1781,7 @@ "null", "object" ], - "description": "A mapping of event payload field names to their expected value.", + "description": "A mapping of event payload field names to their expected value. A subset of the payload fields may be specified", "additionalProperties": { "type": "string" } diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs index 58ac028f27c..0008337989c 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs +++ b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs @@ -1392,7 +1392,7 @@ public static string DisplayAttributeDescription_TraceEventOptions_EventName { } /// - /// Looks up a localized string similar to A mapping of event payload field names to their expected value.. + /// 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_TraceEventOptions_PayloadFilter { get { diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx index 8a7184c7ba6..18986c6cc7d 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx +++ b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx @@ -736,7 +736,7 @@ The description provided for the ProviderName parameter on TraceEventOptions. - A mapping of event payload field names to their expected value. + 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 TraceEventOptions. \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs index eb1f9d26bab..d9517f15655 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs @@ -44,7 +44,7 @@ public class EventMonitoringPassthroughStream : 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 possible fields may be specified. + /// 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 . From fdee6f2949045ba561555fe28dfbae54991f5848 Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Mon, 26 Sep 2022 16:00:21 -0700 Subject: [PATCH 11/26] Handle null invoke --- .../EventMonitoringPassthroughStream.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs index d9517f15655..ada6cb66747 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs @@ -105,7 +105,7 @@ public Task ProcessAsync(CancellationToken token) /// public void StopMonitoringForEvent() { - _eventSource.Dynamic.RemoveCallback(TraceEventCallback); + _eventSource?.Dynamic.RemoveCallback(TraceEventCallback); } From 7a709e3c6f453726d470eeb946a6fade37ac9ceb Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Mon, 26 Sep 2022 16:00:39 -0700 Subject: [PATCH 12/26] Run formatter --- .../EventMonitoringPassthroughStream.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs index ada6cb66747..56d4be51249 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs @@ -106,7 +106,6 @@ public Task ProcessAsync(CancellationToken token) public void StopMonitoringForEvent() { _eventSource?.Dynamic.RemoveCallback(TraceEventCallback); - } /// From f3060411234ed6a004219f87cbd91aaa6f2b9def Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Mon, 26 Sep 2022 16:02:51 -0700 Subject: [PATCH 13/26] Run formatter --- .../StreamLeaveOpenWrapper.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/StreamLeaveOpenWrapper.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/StreamLeaveOpenWrapper.cs index c74c1b4c621..539b4a7912a 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/StreamLeaveOpenWrapper.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/StreamLeaveOpenWrapper.cs @@ -22,6 +22,7 @@ public StreamLeaveOpenWrapper(Stream baseStream) : base() } public override bool CanSeek => _baseStream.CanSeek; + public override bool CanTimeout => _baseStream.CanTimeout; public override bool CanRead => _baseStream.CanRead; From 0856edb6ab9a10da3c2ae2115590315aea8e2258 Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Mon, 26 Sep 2022 16:08:54 -0700 Subject: [PATCH 14/26] Fix comment --- .../EventMonitoringPassthroughStream.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs index 56d4be51249..7f4d7903fe6 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs @@ -33,8 +33,8 @@ public class EventMonitoringPassthroughStream : Stream // Guards _payloadFilterIndexCache. private object _payloadCacheLocker = new(); - // A cache of the mapping of payload indexes to their expected value. - // Unlike _payloadFilter, this tracks the exact indices into the provided event's payload to check for the expected values. + // 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; From 602dfba6c0a4ead08120c682c2a18cc57c01a12a Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Mon, 26 Sep 2022 16:26:11 -0700 Subject: [PATCH 15/26] Update docs --- documentation/api/definitions.md | 10 ++++++++++ documentation/configuration.md | 1 + documentation/schema.json | 8 ++++---- .../OptionsDisplayStrings.Designer.cs | 16 ++++++++-------- .../OptionsDisplayStrings.resx | 18 +++++++++--------- ...stics.Monitoring.ConfigurationSchema.csproj | 2 +- .../CollectTraceTests.cs | 2 +- ...tics.Monitoring.Tool.FunctionalTests.csproj | 2 +- .../CollectionRuleOptionsTests.cs | 6 +++--- .../Options/CollectionRuleOptionsExtensions.cs | 2 +- .../Actions/CollectTraceAction.cs | 2 +- .../Options/Actions/CollectTraceOptions.cs | 2 +- ...raceEventOptions.cs => TraceEventFilter.cs} | 12 ++++++------ 13 files changed, 47 insertions(+), 36 deletions(-) rename src/Tools/dotnet-monitor/CollectionRules/Options/Actions/{TraceEventOptions.cs => TraceEventFilter.cs} (79%) diff --git a/documentation/api/definitions.md b/documentation/api/definitions.md index 88c0001b9ab..f9d89c130f0 100644 --- a/documentation/api/definitions.md +++ b/documentation/api/definitions.md @@ -342,6 +342,16 @@ The `uid` property is useful for uniquely identifying a process when it is runni "processArchitecture": "x64" } ``` +## TraceEventFilter + +Object describing a filter for trace events. + +| Name | Type | Description | +|---|---|---| +| `ProviderName` | string | The event provider that will produce the specified event. | +| `EventName` | string | 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. | +| `PayloadFilter` | map (of string) | (Optional) A mapping of event payload field names to their expected value. A subset of the payload fields may be specified. | + ## TraceProfile diff --git a/documentation/configuration.md b/documentation/configuration.md index 9365605dea2..7fb39f30590 100644 --- a/documentation/configuration.md +++ b/documentation/configuration.md @@ -1421,6 +1421,7 @@ An action that collects a trace of the process that the collection rule is targe | `BufferSizeMegabytes` | int | false | The size (in megabytes) of the event buffer used in the runtime. If the event buffer is filled, events produced by event providers may be dropped until the buffer is cleared. Increase the buffer size to mitigate this or pair down the list of event providers, keywords, and level to filter out extraneous events. Only applies when `Providers` is specified. | `256` | `1` | `1024` | | `Duration` | TimeSpan? | false | The duration of the trace operation. | `"00:00:30"` (30 seconds) | `"00:00:01"` (1 second) | `"1.00:00:00"` (1 day) | | `Egress` | string | true | The named [egress provider](egress.md) for egressing the collected trace. | | | | +| `StoppingEvent` | [TraceEventFilter](api/definitions.md#traceeventfilter)? | false | The event to watch for while collecting the trace, and once either the event is hit or the `Duration` is reached the trace will be stopped. This can only be specified if `Providers` is set. | `null` | | | ##### Outputs diff --git a/documentation/schema.json b/documentation/schema.json index ace7f039a43..a41b3c4e268 100644 --- a/documentation/schema.json +++ b/documentation/schema.json @@ -1684,7 +1684,7 @@ "type": "null" }, { - "$ref": "#/definitions/TraceEventOptions" + "$ref": "#/definitions/TraceEventFilter" } ] } @@ -1758,7 +1758,7 @@ "Verbose" ] }, - "TraceEventOptions": { + "TraceEventFilter": { "type": "object", "additionalProperties": false, "required": [ @@ -1773,7 +1773,7 @@ }, "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 seperated by a '/'. If the event has no opcode, then the event name is just the task name.", + "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": { @@ -1781,7 +1781,7 @@ "null", "object" ], - "description": "A mapping of event payload field names to their expected value. A subset of the payload fields may be specified", + "description": "A mapping of event payload field names to their expected value. A subset of the payload fields may be specified.", "additionalProperties": { "type": "string" } diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs index 0008337989c..72142011972 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs +++ b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs @@ -1383,29 +1383,29 @@ 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 seperated by a '/'. If the event has no opcode, then the event name is just the task name.. + /// 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_TraceEventOptions_EventName { + public static string DisplayAttributeDescription_TraceEventFilter_EventName { get { - return ResourceManager.GetString("DisplayAttributeDescription_TraceEventOptions_EventName", resourceCulture); + 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. + /// 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_TraceEventOptions_PayloadFilter { + public static string DisplayAttributeDescription_TraceEventFilter_PayloadFilter { get { - return ResourceManager.GetString("DisplayAttributeDescription_TraceEventOptions_PayloadFilter", resourceCulture); + 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_TraceEventOptions_ProviderName { + public static string DisplayAttributeDescription_TraceEventFilter_ProviderName { get { - return ResourceManager.GetString("DisplayAttributeDescription_TraceEventOptions_ProviderName", resourceCulture); + return ResourceManager.GetString("DisplayAttributeDescription_TraceEventFilter_ProviderName", resourceCulture); } } diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx index 18986c6cc7d..3b350e8a700 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx +++ b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx @@ -727,16 +727,16 @@ 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 seperated 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 TraceEventOptions. + + 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 TraceEventOptions. + 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 TraceEventOptions. + + 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/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.csproj b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.csproj index b47c73ca419..2446f66146e 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 @@ -25,7 +25,7 @@ - + diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs index c61eddaa84e..a69d4b2b5c7 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs @@ -125,7 +125,7 @@ await ScenarioRunner.SingleTarget(_outputHelper, EgressProvider, options => { options.Duration = duration ?? TimeSpan.Parse(ActionOptionsConstants.Duration_MaxValue); - options.StoppingEvent = new TraceEventOptions() + options.StoppingEvent = new TraceEventFilter() { ProviderName = EventProviderName, EventName = qualifiedEventName, 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 9aa57a66a64..f02301d3bd1 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 @@ -26,7 +26,7 @@ - + diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs index c098a22109a..fb6f998dae9 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs @@ -1142,7 +1142,7 @@ public Task CollectionRuleOptions_CollectTraceAction_StopOnEvent() new() { Name = ExpectedEventProviderName } }; - TraceEventOptions expectedStoppingEvent = new() + TraceEventFilter expectedStoppingEvent = new() { EventName = "CustomEvent", ProviderName = ExpectedEventProviderName @@ -1183,7 +1183,7 @@ public Task CollectionRuleOptions_CollectTraceAction_StopOnEvent_MissingProvider .SetStartupTrigger() .AddCollectTraceAction(ExpectedProviders, ExpectedEgressProvider, (options) => { - options.StoppingEvent = new TraceEventOptions() + options.StoppingEvent = new TraceEventFilter() { EventName = "CustomEvent", ProviderName = ExpectedMissingEventProviderName @@ -1216,7 +1216,7 @@ public Task CollectionRuleOptions_CollectTraceAction_BothProfileAndStoppingEvent .SetStartupTrigger() .AddCollectTraceAction(TraceProfile.Metrics, ExpectedEgressProvider, options => { - options.StoppingEvent = new TraceEventOptions() + options.StoppingEvent = new TraceEventFilter() { EventName = "CustomEvent", ProviderName = "CustomProvider" 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 e969f263b41..245e29a7e50 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Options/CollectionRuleOptionsExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Options/CollectionRuleOptionsExtensions.cs @@ -484,7 +484,7 @@ public static CollectTraceOptions VerifyCollectTraceAction(this CollectionRuleOp return collectTraceOptions; } - public static CollectTraceOptions VerifyCollectTraceAction(this CollectionRuleOptions ruleOptions, int actionIndex, IEnumerable providers, string expectedEgress, TraceEventOptions expectedStoppingEvent = null) + public static CollectTraceOptions VerifyCollectTraceAction(this CollectionRuleOptions ruleOptions, int actionIndex, IEnumerable providers, string expectedEgress, TraceEventFilter expectedStoppingEvent = null) { CollectTraceOptions collectTraceOptions = ruleOptions.VerifyAction( actionIndex, KnownCollectionRuleActions.CollectTrace); diff --git a/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs b/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs index 0e7c5c140cc..625ab66803f 100644 --- a/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs +++ b/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs @@ -67,7 +67,7 @@ protected override async Task ExecuteCoreAsync( MonitoringSourceConfiguration configuration; - TraceEventOptions stoppingEvent = null; + TraceEventFilter stoppingEvent = null; if (Options.Profile.HasValue) { diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.cs index 91fac39f1a7..51b213a9fd2 100644 --- a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.cs +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/CollectTraceOptions.cs @@ -67,6 +67,6 @@ internal sealed partial record class CollectTraceOptions : BaseRecordOptions, IE [Display( ResourceType = typeof(OptionsDisplayStrings), Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_CollectTraceOptions_StoppingEvent))] - public TraceEventOptions StoppingEvent { get; set; } + public TraceEventFilter StoppingEvent { get; set; } } } diff --git a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/TraceEventOptions.cs b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/TraceEventFilter.cs similarity index 79% rename from src/Tools/dotnet-monitor/CollectionRules/Options/Actions/TraceEventOptions.cs rename to src/Tools/dotnet-monitor/CollectionRules/Options/Actions/TraceEventFilter.cs index 124ba58b835..357ceda20c7 100644 --- a/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/TraceEventOptions.cs +++ b/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/TraceEventFilter.cs @@ -10,29 +10,29 @@ namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions { /// - /// Options for the CollectTrace action. + /// A trace event filter. /// - [DebuggerDisplay("TraceEvent")] + [DebuggerDisplay("TraceEventFilter")] #if SCHEMAGEN [NJsonSchema.Annotations.JsonSchemaFlatten] #endif - internal sealed record class TraceEventOptions : BaseRecordOptions + internal sealed record class TraceEventFilter : BaseRecordOptions { [Display( ResourceType = typeof(OptionsDisplayStrings), - Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_TraceEventOptions_ProviderName))] + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_TraceEventFilter_ProviderName))] [Required] public string ProviderName { get; set; } [Display( ResourceType = typeof(OptionsDisplayStrings), - Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_TraceEventOptions_EventName))] + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_TraceEventFilter_EventName))] [Required] public string EventName { get; set; } [Display( ResourceType = typeof(OptionsDisplayStrings), - Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_TraceEventOptions_PayloadFilter))] + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_TraceEventFilter_PayloadFilter))] public IDictionary PayloadFilter { get; set; } } } From 2bcf5c266659952ec5044f2f4bace67593f9888c Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Tue, 27 Sep 2022 09:15:05 -0700 Subject: [PATCH 16/26] Make payload comparison invariant --- .../EventMonitoringPassthroughStream.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs index 7f4d7903fe6..309f6211c63 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs @@ -5,6 +5,7 @@ using Microsoft.Diagnostics.Tracing; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -183,9 +184,10 @@ private bool HydratePayloadFilterCache(TraceEvent obj) private bool DoesPayloadMatch(TraceEvent obj) { - foreach (var (payloadIndex, expectedValue) in _payloadFilterIndexCache) + foreach (var (fieldIndex, expectedValue) in _payloadFilterIndexCache) { - if (!string.Equals(obj.PayloadString(payloadIndex), expectedValue, StringComparison.Ordinal)) + string fieldValue = Convert.ToString(obj.PayloadValue(fieldIndex), CultureInfo.InvariantCulture) ?? string.Empty; + if (!string.Equals(fieldValue, expectedValue, StringComparison.Ordinal)) { return false; } From 61ecd54b29e51ff241ae8ab480f97df02860ee64 Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Tue, 27 Sep 2022 09:29:04 -0700 Subject: [PATCH 17/26] Set cache capacity --- .../EventMonitoringPassthroughStream.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs index 309f6211c63..faeecd75c41 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs @@ -153,7 +153,7 @@ private bool HydratePayloadFilterCache(TraceEvent obj) if (_payloadFilter == null || _payloadFilter.Count == 0) { - _payloadFilterIndexCache = new(); + _payloadFilterIndexCache = new(capacity: 0); return true; } @@ -162,7 +162,7 @@ private bool HydratePayloadFilterCache(TraceEvent obj) return false; } - Dictionary payloadFilterCache = new(); + Dictionary payloadFilterCache = new(capacity: _payloadFilter.Count); for (int i = 0; i < obj.PayloadNames.Length; i++) { if (_payloadFilter.TryGetValue(obj.PayloadNames[i], out string payloadValue)) From 2435072ab2d925cdf11100ee0e2cc27b26dfc696 Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Tue, 27 Sep 2022 09:41:30 -0700 Subject: [PATCH 18/26] Avoid extra payload field iterations during cache hydration --- .../EventMonitoringPassthroughStream.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs index faeecd75c41..a595134ea31 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs @@ -163,7 +163,7 @@ private bool HydratePayloadFilterCache(TraceEvent obj) } Dictionary payloadFilterCache = new(capacity: _payloadFilter.Count); - for (int i = 0; i < obj.PayloadNames.Length; i++) + for (int i = 0; (i < obj.PayloadNames.Length) && (payloadFilterCache.Count < _payloadFilter.Count); i++) { if (_payloadFilter.TryGetValue(obj.PayloadNames[i], out string payloadValue)) { From 6a157a5b2229d55da81aaba3a22f487d5c5f6048 Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Tue, 27 Sep 2022 15:18:54 -0700 Subject: [PATCH 19/26] Remove unneeded lock --- .../EventMonitoringPassthroughStream.cs | 51 +++++++++---------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs index a595134ea31..415b4040d40 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs @@ -32,8 +32,6 @@ public class EventMonitoringPassthroughStream : Stream // The original payload filter of fieldName->fieldValue specified by the user. It will only be used to hydrate _payloadFilterIndexCache. private readonly IDictionary _payloadFilter; - // Guards _payloadFilterIndexCache. - private object _payloadCacheLocker = new(); // 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; @@ -144,41 +142,38 @@ private void TraceEventCallback(TraceEvent obj) private bool HydratePayloadFilterCache(TraceEvent obj) { - lock (_payloadCacheLocker) + if (_payloadFilterIndexCache != null) { - if (_payloadFilterIndexCache != null) - { - return true; - } - - if (_payloadFilter == null || _payloadFilter.Count == 0) - { - _payloadFilterIndexCache = new(capacity: 0); - return true; - } + return true; + } - if (obj.PayloadNames.Length < _payloadFilter.Count) - { - return false; - } + if (_payloadFilter == null || _payloadFilter.Count == 0) + { + _payloadFilterIndexCache = new(capacity: 0); + return true; + } - 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); - } - } + if (obj.PayloadNames.Length < _payloadFilter.Count) + { + return false; + } - if (_payloadFilter.Count != payloadFilterCache.Count) + 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)) { - return false; + payloadFilterCache.Add(i, payloadValue); } + } - _payloadFilterIndexCache = payloadFilterCache; + if (_payloadFilter.Count != payloadFilterCache.Count) + { + return false; } + _payloadFilterIndexCache = payloadFilterCache; + return true; } From 2dc0d100eb2922b76767c2ad5fd0c3afa9aab7d4 Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Fri, 30 Sep 2022 13:28:46 -0700 Subject: [PATCH 20/26] Address PR feedback --- .../Utilities/TraceUtilities.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs index c1a09ad59bd..8000fbe04ee 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs @@ -105,6 +105,9 @@ public static async Task CaptureTraceUntilEventAsync(TaskCompletionSource stoppingEventHitSource = new(TaskCreationOptions.RunContinuationsAsynchronously); + using IDisposable registration = token.Register( + () => stoppingEventHitSource.TrySetCanceled(token)); + await using EventTracePipeline pipeProcessor = new(client, new EventTracePipelineSettings { Configuration = configuration, @@ -133,14 +136,12 @@ public static async Task CaptureTraceUntilEventAsync(TaskCompletionSource Date: Mon, 3 Oct 2022 11:04:42 -0700 Subject: [PATCH 21/26] Apply suggestions from code review Co-authored-by: Justin Anderson --- .../LoggingExtensions.cs | 2 +- .../Scenarios/TraceEventsScenario.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/LoggingExtensions.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/LoggingExtensions.cs index 3c94eb20458..7bd8da75bf9 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/LoggingExtensions.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/LoggingExtensions.cs @@ -55,7 +55,7 @@ internal static class LoggingExtensions private static readonly Action _stoppingTraceEventHit = LoggerMessage.Define( eventId: new EventId(8, "StoppingTraceEventHit"), - logLevel: LogLevel.Information, + logLevel: LogLevel.Debug, formatString: Strings.LogFormatString_StoppingTraceEventHit); private static readonly Action _stoppingTraceEventPayloadFilterMismatch = diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs index 361584d6aff..e91d6e51ebb 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs @@ -47,15 +47,15 @@ public static async Task ExecuteAsync(InvocationContext context) context.ExitCode = await ScenarioHelpers.RunScenarioAsync(async logger => { TaskCompletionSource stopGeneratingEvents = new(); - Task eventEmitterTask = Task.Run(() => + Task eventEmitterTask = Task.Run(async () => { Random random = new(); while (!stopGeneratingEvents.Task.IsCompleted) { TestScenarioEventSource.Log.RandomNumberGenerated(random.Next()); - Task.Delay(100, context.GetCancellationToken()); + await Task.Delay(TimeSpan.FromMilliseconds(100), context.GetCancellationToken()); } - }); + }, context.GetCancellationToken()); while (true) { From 076ec197515cb9947931217ff820db3a02abed69 Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Mon, 3 Oct 2022 11:08:24 -0700 Subject: [PATCH 22/26] Address PR feedback --- .../Scenarios/TraceEventsScenario.cs | 8 +++++--- .../CollectionRules/Actions/CollectTraceAction.cs | 1 - 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs index e91d6e51ebb..b0a8f2b65ba 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs @@ -7,6 +7,7 @@ using System.CommandLine; using System.CommandLine.Invocation; using System.Diagnostics.Tracing; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.Diagnostics.Monitoring.UnitTestApp.Scenarios @@ -46,11 +47,12 @@ public static async Task ExecuteAsync(InvocationContext context) context.ExitCode = await ScenarioHelpers.RunScenarioAsync(async logger => { - TaskCompletionSource stopGeneratingEvents = new(); + ManualResetEventSlim stopGeneratingEvents = new(initialState: false); + Task eventEmitterTask = Task.Run(async () => { Random random = new(); - while (!stopGeneratingEvents.Task.IsCompleted) + while (!stopGeneratingEvents.IsSet) { TestScenarioEventSource.Log.RandomNumberGenerated(random.Next()); await Task.Delay(TimeSpan.FromMilliseconds(100), context.GetCancellationToken()); @@ -65,7 +67,7 @@ public static async Task ExecuteAsync(InvocationContext context) TestScenarioEventSource.Log.UniqueEvent(TestAppScenarios.TraceEvents.UniqueEventMessage); break; case TestAppScenarios.TraceEvents.Commands.ShutdownScenario: - stopGeneratingEvents.TrySetResult(null); + stopGeneratingEvents.Set(); eventEmitterTask.Wait(context.GetCancellationToken()); return 0; } diff --git a/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs b/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs index 625ab66803f..b3d4dc8dd2e 100644 --- a/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs +++ b/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs @@ -99,7 +99,6 @@ protected override async Task ExecuteCoreAsync( ILogger logger = _serviceProvider .GetRequiredService() .CreateLogger(); - using var _ = logger.BeginScope(scope); await TraceUtilities.CaptureTraceUntilEventAsync(startCompletionSource, EndpointInfo, configuration, duration, outputStream, stoppingEvent.ProviderName, stoppingEvent.EventName, stoppingEvent.PayloadFilter, logger, token); } From ebc9c6341c483ebc1541255c80b495cefb4b9455 Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Mon, 3 Oct 2022 11:14:20 -0700 Subject: [PATCH 23/26] Add missing dispose --- .../Scenarios/TraceEventsScenario.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs index b0a8f2b65ba..48d249c863a 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs @@ -47,7 +47,7 @@ public static async Task ExecuteAsync(InvocationContext context) context.ExitCode = await ScenarioHelpers.RunScenarioAsync(async logger => { - ManualResetEventSlim stopGeneratingEvents = new(initialState: false); + using ManualResetEventSlim stopGeneratingEvents = new(initialState: false); Task eventEmitterTask = Task.Run(async () => { From 986ae7489264117a08e7d74dbabed4a58bd4f930 Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Mon, 10 Oct 2022 09:22:30 -0700 Subject: [PATCH 24/26] Address PR feedback --- .../EventMonitoringPassthroughStream.cs | 24 ++++++++++--------- .../StreamLeaveOpenWrapper.cs | 2 +- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs index 415b4040d40..4db65f8f419 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs @@ -16,7 +16,7 @@ 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 class EventMonitoringPassthroughStream : Stream + public sealed class EventMonitoringPassthroughStream : Stream { private readonly Action _onPayloadFilterMismatch; private readonly Action _onEvent; @@ -47,7 +47,7 @@ public class EventMonitoringPassthroughStream : Stream /// 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. + /// 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. @@ -102,19 +102,11 @@ public Task ProcessAsync(CancellationToken token) /// /// Stops monitoring for the specified stopping event. Data will continue to be written to the provided destination stream. /// - public void StopMonitoringForEvent() + private void StopMonitoringForEvent() { _eventSource?.Dynamic.RemoveCallback(TraceEventCallback); } - /// - /// Stops processing the event data, data will no longer be written to the provided destination stream. - /// - public void StopProcessing() - { - _eventSource?.StopProcessing(); - } - private void TraceEventCallback(TraceEvent obj) { if (_payloadFilterIndexCache == null && !HydratePayloadFilterCache(obj)) @@ -140,6 +132,11 @@ private void TraceEventCallback(TraceEvent obj) _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) @@ -147,12 +144,16 @@ private bool HydratePayloadFilterCache(TraceEvent obj) return true; } + // If there's no payload filter, there's nothing to do. if (_payloadFilter == null || _payloadFilter.Count == 0) { _payloadFilterIndexCache = new(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; @@ -167,6 +168,7 @@ private bool HydratePayloadFilterCache(TraceEvent obj) } } + // 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; diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/StreamLeaveOpenWrapper.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/StreamLeaveOpenWrapper.cs index 539b4a7912a..635d87bbf2d 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/StreamLeaveOpenWrapper.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/StreamLeaveOpenWrapper.cs @@ -12,7 +12,7 @@ namespace Microsoft.Diagnostics.Monitoring.WebApi /// /// Wraps a given stream but leaves it open on Dispose. /// - public class StreamLeaveOpenWrapper : Stream + public sealed class StreamLeaveOpenWrapper : Stream { private readonly Stream _baseStream; From 74ed0bcecad29a10f6950ca2c4554bb713158cb3 Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Mon, 10 Oct 2022 11:10:20 -0700 Subject: [PATCH 25/26] Address PR feedback --- .../EventMonitoringPassthroughStream.cs | 2 +- .../TestAppScenarios.cs | 7 ++ .../CollectTraceTests.cs | 111 ++++++++++++------ 3 files changed, 81 insertions(+), 39 deletions(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs index 4db65f8f419..e7002b4d2af 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs @@ -85,7 +85,7 @@ public EventMonitoringPassthroughStream( /// 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. It can only be signaled before processing has been started. After that point or should be called to stop processing. + /// The cancellation token. It can only be signaled before processing has been started. After that point should be called to stop processing. /// public Task ProcessAsync(CancellationToken token) { diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TestAppScenarios.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TestAppScenarios.cs index 7cf6d7e6096..b9cfa7b748f 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 @@ -85,7 +87,12 @@ public static class Commands 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 { diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs index a69d4b2b5c7..f1cead807a5 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs @@ -11,13 +11,12 @@ 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.Linq; using System.Net.Http; -using System.Text; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -36,69 +35,78 @@ public CollectTraceTests(ITestOutputHelper outputHelper, ServiceProviderFixture _httpClientFactory = serviceProviderFixture.ServiceProvider.GetService(); _outputHelper = outputHelper; } -#if NET5_0_OR_GREATER - - private const TraceEventOpcode ExpectedEventOpcode = TraceEventOpcode.Reply; +#if NET5_0_OR_GREATER [Fact] public Task StopOnEvent_Succeeds_WithMatchingOpcode() { - return StopOnEventTestCore(); + return StopOnEventTestCore(expectStoppingEvent: true); + } + + [Fact] + public Task StopOnEvent_Succeeds_WithMatchingOpcodeAndNoRundown() + { + return StopOnEventTestCore(expectStoppingEvent: true, collectRundown: false); } [Fact] public Task StopOnEvent_Succeeds_WithMatchingPayload() { - return StopOnEventTestCore(payloadFilter: new Dictionary() + return StopOnEventTestCore(expectStoppingEvent: true, payloadFilter: new Dictionary() { - { "message", TestAppScenarios.TraceEvents.UniqueEventMessage } + { TestAppScenarios.TraceEvents.UniqueEventPayloadField, TestAppScenarios.TraceEvents.UniqueEventMessage } }); } [Fact] public Task StopOnEvent_DoesNotStop_WhenOpcodeDoesNotMatch() { - return Assert.ThrowsAsync(() => StopOnEventTestCore(opcode: TraceEventOpcode.Resume)); + return StopOnEventTestCore(expectStoppingEvent: false, opcode: TraceEventOpcode.Resume); } [Fact] public Task StopOnEvent_DoesNotStop_WhenPayloadFieldNamesMismatch() { - return Assert.ThrowsAsync(() => StopOnEventTestCore(payloadFilter: new Dictionary() + return StopOnEventTestCore(expectStoppingEvent: false, payloadFilter: new Dictionary() { - { "message", TestAppScenarios.TraceEvents.UniqueEventMessage }, + { TestAppScenarios.TraceEvents.UniqueEventPayloadField, TestAppScenarios.TraceEvents.UniqueEventMessage }, { "foobar", "baz" } - })); + }); } [Fact] public Task StopOnEvent_DoesNotStop_WhenPayloadFieldValueMismatch() { - return Assert.ThrowsAsync(() => StopOnEventTestCore(payloadFilter: new Dictionary() + return StopOnEventTestCore(expectStoppingEvent: false, payloadFilter: new Dictionary() { - { "message", TestAppScenarios.TraceEvents.UniqueEventMessage.ToUpperInvariant() } - })); + { TestAppScenarios.TraceEvents.UniqueEventPayloadField, TestAppScenarios.TraceEvents.UniqueEventMessage.ToUpperInvariant() } + }); } - [Fact] - public Task StopOnEvent_UsesDuration_WhenNoEventMatchesInTime() + private string ConstructQualifiedEventName(string eventName, TraceEventOpcode opcode) { - return StopOnEventTestCore(opcode: TraceEventOpcode.Resume, duration: TimeSpan.FromSeconds(10)); + return (opcode == TraceEventOpcode.Info) + ? eventName + : FormattableString.Invariant($"{eventName}/{opcode}"); } - private async Task StopOnEventTestCore(TraceEventOpcode opcode = ExpectedEventOpcode, IDictionary payloadFilter = null, TimeSpan? duration = null) + 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"; - const string EventProviderName = "TestScenario"; - const string StoppingEventName = "UniqueEvent"; - - string qualifiedEventName = (opcode == TraceEventOpcode.Info) ? StoppingEventName : FormattableString.Invariant($"{StoppingEventName}/{opcode}"); 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, @@ -118,19 +126,15 @@ await ScenarioRunner.SingleTarget(_outputHelper, new EventPipeProvider[] { new EventPipeProvider() { - Name = EventProviderName, + Name = TestAppScenarios.TraceEvents.EventProviderName, Keywords = "-1" } }, EgressProvider, options => { - options.Duration = duration ?? TimeSpan.Parse(ActionOptionsConstants.Duration_MaxValue); - options.StoppingEvent = new TraceEventFilter() - { - ProviderName = EventProviderName, - EventName = qualifiedEventName, - PayloadFilter = payloadFilter - }; + options.Duration = duration ?? DefaultCollectTraceTimeout; + options.StoppingEvent = traceEventFilter; + options.RequestRundown = collectRundown; }); ruleCompletedTask = runner.WaitForCollectionRuleCompleteAsync(DefaultRuleName); @@ -138,17 +142,48 @@ await ScenarioRunner.SingleTarget(_outputHelper, string[] files = Directory.GetFiles(tempDirectory.FullName, "*.nettrace", SearchOption.TopDirectoryOnly); string traceFile = Assert.Single(files); - await ValidateNettraceFile(traceFile); + + var (hasStoppingEvent, hasRundown) = await ValidateNettraceFile(traceFile, traceEventFilter); + Assert.Equal(expectStoppingEvent, hasStoppingEvent); + Assert.Equal(collectRundown, hasRundown); } - private async Task ValidateNettraceFile(string filePath) + private Task<(bool hasStoppingEvent, bool hasRundown)> ValidateNettraceFile(string filePath, TraceEventFilter eventFilter) { - byte[] expectedMagicToken = Encoding.UTF8.GetBytes("Nettrace"); - byte[] actualMagicToken = new byte[8]; + 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; + }); - await using FileStream fs = File.OpenRead(filePath); - await fs.ReadAsync(actualMagicToken); - Assert.True(actualMagicToken.SequenceEqual(expectedMagicToken), $"{filePath} is not a Nettrace file!"); + ClrRundownTraceEventParser rundown = new(eventSource); + rundown.RuntimeStart += (data) => + { + didSeeRundownEvents = true; + }; + + eventSource.Process(); + return (didSeeStoppingEvent, didSeeRundownEvents); + }); } #endif // NET5_0_OR_GREATER } From c464779797a7f62d7b5678f7b438e6b1a02d913a Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Tue, 11 Oct 2022 10:58:45 -0700 Subject: [PATCH 26/26] Address PR feedback --- .../EventMonitoringPassthroughStream.cs | 12 ++++++++---- .../Utilities/TraceUtilities.cs | 1 + 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs index e7002b4d2af..7312162b2cb 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs @@ -85,17 +85,21 @@ public EventMonitoringPassthroughStream( /// 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. It can only be signaled before processing has been started. After that point should be called to stop processing. + /// 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); } @@ -147,7 +151,7 @@ private bool HydratePayloadFilterCache(TraceEvent obj) // If there's no payload filter, there's nothing to do. if (_payloadFilter == null || _payloadFilter.Count == 0) { - _payloadFilterIndexCache = new(capacity: 0); + _payloadFilterIndexCache = new Dictionary(capacity: 0); return true; } @@ -242,8 +246,8 @@ public override async ValueTask ReadAsync(Memory buffer, Cancellation 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() => _sourceStream.Flush(); - public override Task FlushAsync(CancellationToken cancellationToken) => _sourceStream.FlushAsync(cancellationToken); + public override void Flush() => _destinationStream.Flush(); + public override Task FlushAsync(CancellationToken cancellationToken) => _destinationStream.FlushAsync(cancellationToken); public override async ValueTask DisposeAsync() { diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs index 8000fbe04ee..050b6856186 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs @@ -141,6 +141,7 @@ public static async Task CaptureTraceUntilEventAsync(TaskCompletionSource