From f475bc5c54181e996cfac3e1952b26de7adbac41 Mon Sep 17 00:00:00 2001 From: Joe Schmitt <1146681+schmittjoseph@users.noreply.github.com> Date: Thu, 10 Nov 2022 10:53:40 -0800 Subject: [PATCH] Merge --- documentation/openapi.json | 28 +++- .../Controllers/DiagController.Metrics.cs | 2 - .../Controllers/DiagController.cs | 112 +++++++------- .../Controllers/OperationsController.cs | 23 ++- .../ITraceOperationFactory.cs | 35 +++++ .../LoggingExtensions.cs | 11 ++ .../Models/EgressOperationStatus.cs | 9 +- .../Operation/EgressOperation.cs | 37 ++--- .../Operation/EgressOperationService.cs | 22 ++- .../Operation/EgressOperationStore.cs | 52 ++++++- .../Operation/EgressProcessInfo.cs | 22 +++ .../Operation/HttpResponseEgressOperation.cs | 65 ++++++++ .../Operation/IEgressOperation.cs | 6 + .../RequestLimitAttribute.cs | 14 -- .../Strings.Designer.cs | 36 +++++ .../Strings.resx | 24 +++ .../Utilities/TraceUtilities.cs | 32 ---- .../TraceTestUtilities.cs | 52 +++++++ .../EgressTests.cs | 142 +++++++++++++++++- .../HttpApi/ApiClient.cs | 50 ++++++ .../HttpApi/ApiClientExtensions.cs | 20 ++- .../HttpApi/ResponseStreamHolder.cs | 8 +- .../CollectTraceActionTests.cs | 2 - .../TestHostHelper.cs | 1 + .../Actions/CollectTraceAction.cs | 28 +++- .../Commands/CollectCommandHandler.cs | 1 + .../PipelineArtifactOperation.cs | 7 +- .../dotnet-monitor/RequestLimitMiddleware.cs | 71 --------- src/Tools/dotnet-monitor/Startup.cs | 3 - .../Trace/AbstractTraceOperation.cs | 33 ++++ .../dotnet-monitor/Trace/TraceOperation.cs | 40 +++++ .../Trace/TraceOperationFactory.cs | 44 ++++++ .../Trace/TraceUntilEventOperation.cs | 84 +++++++++++ 33 files changed, 893 insertions(+), 223 deletions(-) create mode 100644 src/Microsoft.Diagnostics.Monitoring.WebApi/ITraceOperationFactory.cs create mode 100644 src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressProcessInfo.cs create mode 100644 src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/HttpResponseEgressOperation.cs delete mode 100644 src/Microsoft.Diagnostics.Monitoring.WebApi/RequestThrottling/RequestLimitAttribute.cs create mode 100644 src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TraceTestUtilities.cs delete mode 100644 src/Tools/dotnet-monitor/RequestLimitMiddleware.cs create mode 100644 src/Tools/dotnet-monitor/Trace/AbstractTraceOperation.cs create mode 100644 src/Tools/dotnet-monitor/Trace/TraceOperation.cs create mode 100644 src/Tools/dotnet-monitor/Trace/TraceOperationFactory.cs create mode 100644 src/Tools/dotnet-monitor/Trace/TraceUntilEventOperation.cs diff --git a/documentation/openapi.json b/documentation/openapi.json index fa32bedb132..53b5b96b0e5 100644 --- a/documentation/openapi.json +++ b/documentation/openapi.json @@ -1207,6 +1207,14 @@ "type": "string", "format": "uuid" } + }, + { + "name": "stop", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } } ], "responses": { @@ -1215,6 +1223,9 @@ }, "200": { "description": "Success" + }, + "202": { + "description": "Accepted" } } } @@ -1496,7 +1507,8 @@ "Running", "Succeeded", "Failed", - "Cancelled" + "Cancelled", + "Stopping" ], "type": "string" }, @@ -1517,6 +1529,13 @@ "process": { "$ref": "#/components/schemas/OperationProcessInfo" }, + "egressProviderName": { + "type": "string", + "nullable": true + }, + "isStoppable": { + "type": "boolean" + }, "resourceLocation": { "type": "string", "nullable": true @@ -1544,6 +1563,13 @@ }, "process": { "$ref": "#/components/schemas/OperationProcessInfo" + }, + "egressProviderName": { + "type": "string", + "nullable": true + }, + "isStoppable": { + "type": "boolean" } }, "additionalProperties": false, diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.Metrics.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.Metrics.cs index 7137822e1b4..9e8bfb9b4e3 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.Metrics.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.Metrics.cs @@ -25,7 +25,6 @@ partial class DiagController [ProducesWithProblemDetails(ContentTypes.ApplicationJsonSequence)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status429TooManyRequests)] [ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)] - [RequestLimit(LimitKey = Utilities.ArtifactType_Metrics)] [EgressValidation] public Task CaptureMetrics( [FromQuery] @@ -75,7 +74,6 @@ public Task CaptureMetrics( [ProducesWithProblemDetails(ContentTypes.ApplicationJsonSequence)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status429TooManyRequests)] [ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)] - [RequestLimit(LimitKey = Utilities.ArtifactType_Metrics)] [EgressValidation] public Task CaptureMetricsCustom( [FromBody][Required] diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.cs index 18c48f8282e..3406f99e826 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.cs @@ -52,6 +52,7 @@ public partial class DiagController : ControllerBase private readonly ICollectionRuleService _collectionRuleService; private readonly ProfilerChannel _profilerChannel; private readonly ILogsOperationFactory _logsOperationFactory; + private readonly ITraceOperationFactory _traceOperationFactory; public DiagController(ILogger logger, IServiceProvider serviceProvider) @@ -67,6 +68,7 @@ public DiagController(ILogger logger, _collectionRuleService = serviceProvider.GetRequiredService(); _profilerChannel = serviceProvider.GetRequiredService(); _logsOperationFactory = serviceProvider.GetRequiredService(); + _traceOperationFactory = serviceProvider.GetRequiredService(); } /// @@ -207,7 +209,6 @@ public Task>> GetProcessEnvironment( [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status429TooManyRequests)] [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)] - [RequestLimit(LimitKey = Utilities.ArtifactType_Dump)] [EgressValidation] public Task CaptureDump( [FromQuery] @@ -221,6 +222,8 @@ public Task CaptureDump( [FromQuery] string egressProvider = null) { + const string artifactType = Utilities.ArtifactType_Dump; + ProcessKey? processKey = Utilities.GetProcessKey(pid, uid, name); return InvokeForProcess(async processInfo => @@ -229,6 +232,7 @@ public Task CaptureDump( if (string.IsNullOrEmpty(egressProvider)) { + await RegisterCurrentHttpResponseAsOperation(processInfo, artifactType); Stream dumpStream = await _dumpService.DumpAsync(processInfo.EndpointInfo, type, HttpContext.RequestAborted); _logger.WrittenToHttpStream(); @@ -238,7 +242,7 @@ public Task CaptureDump( } else { - KeyValueLogScope scope = Utilities.CreateArtifactScope(Utilities.ArtifactType_Dump, processInfo.EndpointInfo); + KeyValueLogScope scope = Utilities.CreateArtifactScope(artifactType, processInfo.EndpointInfo); return await SendToEgress(new EgressOperation( token => _dumpService.DumpAsync(processInfo.EndpointInfo, type, token), @@ -246,9 +250,9 @@ public Task CaptureDump( dumpFileName, processInfo, ContentTypes.ApplicationOctetStream, - scope), limitKey: Utilities.ArtifactType_Dump); + scope), limitKey: artifactType); } - }, processKey, Utilities.ArtifactType_Dump); + }, processKey, artifactType); } /// @@ -266,7 +270,6 @@ public Task CaptureDump( [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status429TooManyRequests)] [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)] - [RequestLimit(LimitKey = Utilities.ArtifactType_GCDump)] [EgressValidation] public Task CaptureGcDump( [FromQuery] @@ -325,7 +328,6 @@ public Task CaptureGcDump( [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status429TooManyRequests)] [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)] - [RequestLimit(LimitKey = Utilities.ArtifactType_Trace)] [EgressValidation] public Task CaptureTrace( [FromQuery] @@ -369,7 +371,6 @@ public Task CaptureTrace( [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status429TooManyRequests)] [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)] - [RequestLimit(LimitKey = Utilities.ArtifactType_Trace)] [EgressValidation] public Task CaptureTraceCustom( [FromBody][Required] @@ -420,7 +421,6 @@ public Task CaptureTraceCustom( [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status429TooManyRequests)] [ProducesResponseType(typeof(string), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)] - [RequestLimit(LimitKey = Utilities.ArtifactType_Logs)] [EgressValidation] public Task CaptureLogs( [FromQuery] @@ -476,7 +476,6 @@ public Task CaptureLogs( [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status429TooManyRequests)] [ProducesResponseType(typeof(string), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)] - [RequestLimit(LimitKey = Utilities.ArtifactType_Logs)] [EgressValidation] public Task CaptureLogsCustom( [FromBody] @@ -593,7 +592,6 @@ public Task> GetCollectionRuleDe [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status429TooManyRequests)] [ProducesResponseType(typeof(string), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)] - [RequestLimit(LimitKey = Utilities.ArtifactType_Stacks)] [EgressValidation] public async Task CaptureStacks( [FromQuery] @@ -636,29 +634,21 @@ private Task StartTrace( TimeSpan duration, string egressProvider) { - string fileName = TraceUtilities.GenerateTraceFileName(processInfo.EndpointInfo); + IArtifactOperation traceOperation = _traceOperationFactory.Create( + processInfo.EndpointInfo, + configuration, + duration); + + if (_diagnosticPortOptions.Value.ConnectionMode == DiagnosticPortConnectionMode.Listen) + { + IDisposable operationRegistration = _operationTrackerService.Register(processInfo.EndpointInfo); + HttpContext.Response.RegisterForDispose(operationRegistration); + } return Result( Utilities.ArtifactType_Trace, egressProvider, - async (outputStream, token) => - { - IDisposable operationRegistration = null; - try - { - if (_diagnosticPortOptions.Value.ConnectionMode == DiagnosticPortConnectionMode.Listen) - { - operationRegistration = _operationTrackerService.Register(processInfo.EndpointInfo); - } - await TraceUtilities.CaptureTraceAsync(null, processInfo.EndpointInfo, configuration, duration, outputStream, token); - } - finally - { - operationRegistration?.Dispose(); - } - }, - fileName, - ContentTypes.ApplicationOctetStream, + traceOperation, processInfo); } @@ -723,12 +713,10 @@ private Task StartLogs( return null; } - private Task Result( + private async Task Result( string artifactType, string providerName, - Func action, - string fileName, - string contentType, + IArtifactOperation operation, IProcessInfo processInfo, bool asAttachment = true) { @@ -736,29 +724,29 @@ private Task Result( if (string.IsNullOrEmpty(providerName)) { - return Task.FromResult(new OutputStreamResult( - action, - contentType, - asAttachment ? fileName : null, - scope)); + await RegisterCurrentHttpResponseAsOperation(processInfo, artifactType, operation); + return new OutputStreamResult( + operation, + asAttachment ? operation.GenerateFileName() : null, + scope); } else { - return SendToEgress(new EgressOperation( - action, + return await SendToEgress(new EgressOperation( + operation, providerName, - fileName, processInfo, - contentType, scope), limitKey: artifactType); } } - private Task Result( + private async Task Result( string artifactType, string providerName, - IArtifactOperation operation, + Func action, + string fileName, + string contentType, IProcessInfo processInfo, bool asAttachment = true) { @@ -766,32 +754,48 @@ private Task Result( if (string.IsNullOrEmpty(providerName)) { - return Task.FromResult(new OutputStreamResult( - operation, - asAttachment ? operation.GenerateFileName() : null, - scope)); + await RegisterCurrentHttpResponseAsOperation(processInfo, artifactType); + return new OutputStreamResult( + action, + contentType, + asAttachment ? fileName : null, + scope); } else { - return SendToEgress(new EgressOperation( - operation, + return await SendToEgress(new EgressOperation( + action, providerName, + fileName, processInfo, + contentType, scope), limitKey: artifactType); } } - private async Task SendToEgress(EgressOperation egressStreamResult, string limitKey) + private async Task RegisterCurrentHttpResponseAsOperation(IProcessInfo processInfo, string artifactType, IArtifactOperation operation = null) + { + // While not strictly a Location redirect, use the same header as externally egressed operations for consistency. + HttpContext.Response.Headers["Location"] = await RegisterOperation( + new HttpResponseEgressOperation(HttpContext, processInfo, operation), + limitKey: artifactType); + } + + private async Task RegisterOperation(IEgressOperation egressOperation, string limitKey) { // Will throw TooManyRequestsException if there are too many concurrent operations. - Guid operationId = await _operationsStore.AddOperation(egressStreamResult, limitKey); - string newUrl = this.Url.Action( + Guid operationId = await _operationsStore.AddOperation(egressOperation, limitKey); + return this.Url.Action( action: nameof(OperationsController.GetOperationStatus), controller: OperationsController.ControllerName, new { operationId = operationId }, protocol: this.HttpContext.Request.Scheme, this.HttpContext.Request.Host.ToString()); + } - return Accepted(newUrl); + private async Task SendToEgress(IEgressOperation egressOperation, string limitKey) + { + string operationUrl = await RegisterOperation(egressOperation, limitKey); + return Accepted(operationUrl); } private Task InvokeForProcess(Func func, ProcessKey? processKey, string artifactType = null) diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/OperationsController.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/OperationsController.cs index 7eb26f5680b..1c420885038 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/OperationsController.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/OperationsController.cs @@ -72,14 +72,31 @@ public IActionResult GetOperationStatus(Guid operationId) [HttpDelete("{operationId}")] [ProducesWithProblemDetails(ContentTypes.ApplicationJson)] [ProducesResponseType(typeof(void), StatusCodes.Status200OK)] - public IActionResult CancelOperation(Guid operationId) + [ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)] + public IActionResult CancelOperation( + Guid operationId, + [FromQuery] + bool stop = false) { return this.InvokeService(() => { //Note that if the operation is not found, it will throw an InvalidOperationException and //return an error code. - _operationsStore.CancelOperation(operationId); - return Ok(); + if (stop) + { + // If stopping an operation fails, it's undefined behavior. + // Leave the operation in the "Stopping" state and it'll either complete on its own + // or the user will cancel it. + _operationsStore.StopOperation(operationId, (ex) => _logger.StopOperationFailed(operationId, ex)); + + // Stop operations are not instant, they are instead queued and can take an indeterminate amount of time. + return Accepted(); + } + else + { + _operationsStore.CancelOperation(operationId); + return Ok(); + } }, _logger); } } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/ITraceOperationFactory.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/ITraceOperationFactory.cs new file mode 100644 index 00000000000..b82e6c04a99 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/ITraceOperationFactory.cs @@ -0,0 +1,35 @@ +// 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.EventPipe; +using System; +using System.Collections.Generic; + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + /// + /// Factory for creating operations that produce trace artifacts. + /// + internal interface ITraceOperationFactory + { + /// + /// Creates an operation that produces a trace artifact. + /// + IArtifactOperation Create( + IEndpointInfo endpointInfo, + MonitoringSourceConfiguration configuration, + TimeSpan duration); + + /// + /// Creates an operation that produces a trace artifact and supports a stopping event. + /// + IArtifactOperation Create( + IEndpointInfo endpointInfo, + MonitoringSourceConfiguration configuration, + TimeSpan duration, + string providerName, + string eventName, + IDictionary payloadFilter); + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/LoggingExtensions.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/LoggingExtensions.cs index 0b30e71b9d7..aeb48aca370 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/LoggingExtensions.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/LoggingExtensions.cs @@ -57,6 +57,12 @@ internal static class LoggingExtensions logLevel: LogLevel.Debug, formatString: Strings.LogFormatString_DiagnosticRequestFailed); + private static readonly Action _stopOperationFailed = + LoggerMessage.Define( + eventId: new EventId(11, "StopOperationFailed"), + logLevel: LogLevel.Warning, + formatString: Strings.LogFormatString_StopOperationFailed); + public static void RequestFailed(this ILogger logger, Exception ex) { _requestFailed(logger, ex); @@ -96,5 +102,10 @@ public static void DiagnosticRequestFailed(this ILogger logger, int processId, E { _diagnosticRequestFailed(logger, processId, ex); } + + public static void StopOperationFailed(this ILogger logger, Guid operationId, Exception ex) + { + _stopOperationFailed(logger, operationId, ex); + } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/EgressOperationStatus.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/EgressOperationStatus.cs index 54edfca8e24..7490ae760ee 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/EgressOperationStatus.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/EgressOperationStatus.cs @@ -23,6 +23,12 @@ public class OperationSummary [JsonPropertyName("process")] public OperationProcessInfo Process { get; set; } + + [JsonPropertyName("egressProviderName")] + public string EgressProviderName { get; set; } + + [JsonPropertyName("isStoppable")] + public bool IsStoppable { get; set; } } /// @@ -62,7 +68,8 @@ public enum OperationState Running, Succeeded, Failed, - Cancelled + Cancelled, + Stopping } public class OperationError diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperation.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperation.cs index fbce334eb74..93542e90d29 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperation.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperation.cs @@ -14,23 +14,27 @@ namespace Microsoft.Diagnostics.Monitoring.WebApi internal class EgressOperation : IEgressOperation { private readonly Func> _egress; - private readonly string _egressProvider; private readonly KeyValueLogScope _scope; public EgressProcessInfo ProcessInfo { get; private set; } + public string EgressProviderName { get; private set; } + public bool IsStoppable { get { return _operation?.IsStoppable ?? false; } } + + private readonly IArtifactOperation _operation; + public EgressOperation(Func> action, string endpointName, string artifactName, IProcessInfo processInfo, string contentType, KeyValueLogScope scope, CollectionRuleMetadata collectionRuleMetadata = null) { _egress = (service, token) => service.EgressAsync(endpointName, action, artifactName, contentType, processInfo.EndpointInfo, collectionRuleMetadata, token); - _egressProvider = endpointName; _scope = scope; + EgressProviderName = endpointName; ProcessInfo = new EgressProcessInfo(processInfo.ProcessName, processInfo.EndpointInfo.ProcessId, processInfo.EndpointInfo.RuntimeInstanceCookie); } public EgressOperation(Func action, string endpointName, string artifactName, IProcessInfo processInfo, string contentType, KeyValueLogScope scope, CollectionRuleMetadata collectionRuleMetadata = null) { _egress = (service, token) => service.EgressAsync(endpointName, action, artifactName, contentType, processInfo.EndpointInfo, collectionRuleMetadata, token); - _egressProvider = endpointName; + EgressProviderName = endpointName; _scope = scope; ProcessInfo = new EgressProcessInfo(processInfo.ProcessName, processInfo.EndpointInfo.ProcessId, processInfo.EndpointInfo.RuntimeInstanceCookie); @@ -39,20 +43,21 @@ public EgressOperation(Func action, string endp public EgressOperation(IArtifactOperation operation, string endpointName, IProcessInfo processInfo, KeyValueLogScope scope, CollectionRuleMetadata collectionRuleMetadata = null) : this(operation.ExecuteAsync, endpointName, operation.GenerateFileName(), processInfo, operation.ContentType, scope, collectionRuleMetadata) { + _operation = operation; } // The below constructors don't need EgressProcessInfo as their callers don't store to the operations table. public EgressOperation(Func action, string endpointName, string artifactName, IEndpointInfo source, string contentType, KeyValueLogScope scope, CollectionRuleMetadata collectionRuleMetadata) { _egress = (service, token) => service.EgressAsync(endpointName, action, artifactName, contentType, source, collectionRuleMetadata, token); - _egressProvider = endpointName; + EgressProviderName = endpointName; _scope = scope; } public EgressOperation(Func> action, string endpointName, string artifactName, IEndpointInfo source, string contentType, KeyValueLogScope scope, CollectionRuleMetadata collectionRuleMetadata) { _egress = (service, token) => service.EgressAsync(endpointName, action, artifactName, contentType, source, collectionRuleMetadata, token); - _egressProvider = endpointName; + EgressProviderName = endpointName; _scope = scope; } @@ -81,26 +86,22 @@ public async Task> ExecuteAsync(IServiceProvider s return ExecutionResult.Succeeded(egressResult); }, logger, token); } - + public void Validate(IServiceProvider serviceProvider) { serviceProvider .GetRequiredService() - .ValidateProvider(_egressProvider); + .ValidateProvider(EgressProviderName); } - } - internal class EgressProcessInfo - { - public string ProcessName { get; } - public int ProcessId { get; } - public Guid RuntimeInstanceCookie { get; } - - public EgressProcessInfo(string processName, int processId, Guid runtimeInstanceCookie) + public Task StopAsync(CancellationToken token) { - this.ProcessName = processName; - this.ProcessId = processId; - this.RuntimeInstanceCookie = runtimeInstanceCookie; + if (_operation == null) + { + throw new InvalidOperationException(); + } + + return _operation.StopAsync(token); } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperationService.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperationService.cs index 5c59eaaf5f3..73cd48d5609 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperationService.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperationService.cs @@ -47,13 +47,29 @@ private async Task ExecuteEgressOperation(EgressRequest egressRequest, Cancellat try { - var result = await egressRequest.EgressOperation.ExecuteAsync(_serviceProvider, token); + ExecutionResult result = await egressRequest.EgressOperation.ExecuteAsync(_serviceProvider, token); //It is possible that this operation never completes, due to infinite duration operations. _operationsStore.CompleteOperation(egressRequest.OperationId, result); } - //This is unexpected, but an unhandled exception should still fail the operation. - catch (Exception e) when (!(e is OperationCanceledException)) + catch (OperationCanceledException) + { + try + { + // Mirror the state in the operations store incase the operation was cancelled via another means besides + // the operations API. + _operationsStore.CancelOperation(egressRequest.OperationId); + } + // Expected if the state already reflects the cancellation. + catch (InvalidOperationException) + { + + } + + throw; + } + // This is unexpected, but an unhandled exception should still fail the operation. + catch (Exception e) { _operationsStore.CompleteOperation(egressRequest.OperationId, ExecutionResult.Failed(e)); } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperationStore.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperationStore.cs index 4408ab7f569..a516a18e3fe 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperationStore.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperationStore.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.Diagnostics.Monitoring.WebApi @@ -14,6 +15,14 @@ internal sealed class EgressOperationStore { private sealed class EgressEntry { + public bool IsStoppable + { + get + { + return State == Models.OperationState.Running && EgressRequest.EgressOperation.IsStoppable; + } + } + public ExecutionResult ExecutionResult { get; set; } public Models.OperationState State { get; set; } @@ -67,14 +76,12 @@ public async Task AddOperation(IEgressOperation egressOperation, string li OperationId = operationId }); } - - //Kick off work to attempt egress await _taskQueue.EnqueueAsync(request); return operationId; } - public void CancelOperation(Guid operationId) + public void StopOperation(Guid operationId, Action onStopException) { lock (_requests) { @@ -88,6 +95,37 @@ public void CancelOperation(Guid operationId) throw new InvalidOperationException(Strings.ErrorMessage_OperationNotRunning); } + if (!entry.EgressRequest.EgressOperation.IsStoppable) + { + throw new InvalidOperationException(Strings.ErrorMessage_OperationDoesNotSupportBeingStopped); + } + + entry.State = Models.OperationState.Stopping; + + CancellationToken token = entry.EgressRequest.CancellationTokenSource.Token; + _ = Task.Run(() => entry.EgressRequest.EgressOperation.StopAsync(token), token) + .ContinueWith(task => onStopException(task.Exception), + token, + TaskContinuationOptions.OnlyOnFaulted, + TaskScheduler.Default); + } + } + + public void CancelOperation(Guid operationId) + { + lock (_requests) + { + if (!_requests.TryGetValue(operationId, out EgressEntry entry)) + { + throw new InvalidOperationException(Strings.ErrorMessage_OperationNotFound); + } + + if (entry.State != Models.OperationState.Running && + entry.State != Models.OperationState.Stopping) + { + throw new InvalidOperationException(Strings.ErrorMessage_OperationNotRunning); + } + entry.State = Models.OperationState.Cancelled; entry.EgressRequest.CancellationTokenSource.Cancel(); entry.EgressRequest.Dispose(); @@ -102,7 +140,9 @@ public void CompleteOperation(Guid operationId, ExecutionResult re { throw new InvalidOperationException(Strings.ErrorMessage_OperationNotFound); } - if (entry.State != Models.OperationState.Running) + + if (entry.State != Models.OperationState.Running && + entry.State != Models.OperationState.Stopping) { throw new InvalidOperationException(Strings.ErrorMessage_OperationNotRunning); } @@ -164,6 +204,8 @@ public void CompleteOperation(Guid operationId, ExecutionResult re OperationId = kvp.Key, CreatedDateTime = kvp.Value.CreatedDateTime, Status = kvp.Value.State, + EgressProviderName = kvp.Value.EgressRequest.EgressOperation.EgressProviderName, + IsStoppable = kvp.Value.IsStoppable, Process = processInfo != null ? new Models.OperationProcessInfo { @@ -191,6 +233,8 @@ public Models.OperationStatus GetOperationStatus(Guid operationId) OperationId = entry.EgressRequest.OperationId, Status = entry.State, CreatedDateTime = entry.CreatedDateTime, + EgressProviderName = entry.EgressRequest.EgressOperation.EgressProviderName, + IsStoppable = entry.IsStoppable, Process = processInfo != null ? new Models.OperationProcessInfo { diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressProcessInfo.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressProcessInfo.cs new file mode 100644 index 00000000000..049f1f1f305 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressProcessInfo.cs @@ -0,0 +1,22 @@ +// 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; + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + internal class EgressProcessInfo + { + public string ProcessName { get; } + public int ProcessId { get; } + public Guid RuntimeInstanceCookie { get; } + + public EgressProcessInfo(string processName, int processId, Guid runtimeInstanceCookie) + { + this.ProcessName = processName; + this.ProcessId = processId; + this.RuntimeInstanceCookie = runtimeInstanceCookie; + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/HttpResponseEgressOperation.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/HttpResponseEgressOperation.cs new file mode 100644 index 00000000000..62716ce2db2 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/HttpResponseEgressOperation.cs @@ -0,0 +1,65 @@ +// 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.AspNetCore.Http; +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + internal sealed class HttpResponseEgressOperation : IEgressOperation + { + private readonly HttpContext _httpContext; + private readonly TaskCompletionSource _responseFinishedCompletionSource = new(); + + public EgressProcessInfo ProcessInfo { get; private set; } + public string EgressProviderName { get { return null; } } + public bool IsStoppable { get { return _operation?.IsStoppable ?? false; } } + + private readonly IArtifactOperation _operation; + + public HttpResponseEgressOperation(HttpContext context, IProcessInfo processInfo, IArtifactOperation operation = null) + { + _httpContext = context; + _httpContext.Response.OnCompleted(() => + { + _responseFinishedCompletionSource.TrySetResult(_httpContext.Response.StatusCode); + return Task.CompletedTask; + }); + + _operation = operation; + + ProcessInfo = new EgressProcessInfo(processInfo.ProcessName, processInfo.EndpointInfo.ProcessId, processInfo.EndpointInfo.RuntimeInstanceCookie); + } + + public async Task> ExecuteAsync(IServiceProvider serviceProvider, CancellationToken token) + { + using CancellationTokenSource cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, _httpContext.RequestAborted); + using IDisposable registration = token.Register(_httpContext.Abort); + + int statusCode = await _responseFinishedCompletionSource.Task.WaitAsync(cancellationTokenSource.Token); + + return statusCode >= (int)HttpStatusCode.OK && statusCode < (int)HttpStatusCode.Ambiguous + ? ExecutionResult.Empty() + : ExecutionResult.Failed(new Exception($"HTTP request failed with status code: ${statusCode}")); + } + + public void Validate(IServiceProvider serviceProvider) + { + // noop + } + + public Task StopAsync(CancellationToken token) + { + if (_operation == null) + { + throw new InvalidOperationException(); + } + + return _operation.StopAsync(token); + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/IEgressOperation.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/IEgressOperation.cs index 057fec61971..2ad9e935482 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/IEgressOperation.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/IEgressOperation.cs @@ -10,10 +10,16 @@ namespace Microsoft.Diagnostics.Monitoring.WebApi { internal interface IEgressOperation { + public bool IsStoppable { get; } + + public string EgressProviderName { get; } + public EgressProcessInfo ProcessInfo { get; } Task> ExecuteAsync(IServiceProvider serviceProvider, CancellationToken token); + Task StopAsync(CancellationToken token); + void Validate(IServiceProvider serviceProvider); } } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/RequestThrottling/RequestLimitAttribute.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/RequestThrottling/RequestLimitAttribute.cs deleted file mode 100644 index a6347efed62..00000000000 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/RequestThrottling/RequestLimitAttribute.cs +++ /dev/null @@ -1,14 +0,0 @@ -// 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; - -namespace Microsoft.Diagnostics.Monitoring.WebApi -{ - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] - internal sealed class RequestLimitAttribute : Attribute - { - public string LimitKey { get; set; } - } -} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.Designer.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.Designer.cs index 0d1ed3ff523..027c1cc34b6 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.Designer.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.Designer.cs @@ -141,6 +141,15 @@ internal static string ErrorMessage_NoTargetProcess { } } + /// + /// Looks up a localized string similar to Operation does not support being stopped.. + /// + internal static string ErrorMessage_OperationDoesNotSupportBeingStopped { + get { + return ResourceManager.GetString("ErrorMessage_OperationDoesNotSupportBeingStopped", resourceCulture); + } + } + /// /// Looks up a localized string similar to Operation not found.. /// @@ -294,6 +303,33 @@ internal static string LogFormatString_ResolvedTargetProcess { } } + /// + /// Looks up a localized string similar to Failed to stop the '{operationId}' operation.. + /// + internal static string LogFormatString_StopOperationFailed { + get { + return ResourceManager.GetString("LogFormatString_StopOperationFailed", resourceCulture); + } + } + + /// + /// 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 0ee6886e96d..9843b3f059c 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.resx +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.resx @@ -151,6 +151,9 @@ Unable to discover a target process. Gets a string similar to "Unable to discover a target process.". + + Operation does not support being stopped. + Operation not found. @@ -228,6 +231,27 @@ Resolved target process. Gets the format string that is printed in the 3:ResolvedTargetProcess event. 0 Format Parameters + + + Failed to stop the '{operationId}' operation. + Gets the format string that is printed in the 11:StopOperationFailed event. +1 Format Parameter: +1. operationId: The id of the operation that failed to stop. + + + 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 0a60cf5948a..69e7294d0c4 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs @@ -8,20 +8,12 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Threading; -using System.Threading.Tasks; namespace Microsoft.Diagnostics.Monitoring.WebApi { internal static class TraceUtilities { - public static string GenerateTraceFileName(IEndpointInfo endpointInfo) - { - return FormattableString.Invariant($"{Utilities.GetFileNameTimeStampUtcNow()}_{endpointInfo.ProcessId}.nettrace"); - } - public static MonitoringSourceConfiguration GetTraceConfiguration(Models.TraceProfile profile, float metricsIntervalSeconds) { var configurations = new List(); @@ -73,29 +65,5 @@ public static MonitoringSourceConfiguration GetTraceConfiguration(Models.EventPi requestRundown: requestRundown, bufferSizeInMB: bufferSizeInMB); } - - public static async Task CaptureTraceAsync(TaskCompletionSource startCompletionSource, IEndpointInfo endpointInfo, MonitoringSourceConfiguration configuration, TimeSpan duration, Stream outputStream, CancellationToken token) - { - Func streamAvailable = async (Stream eventStream, CancellationToken token) => - { - if (null != startCompletionSource) - { - startCompletionSource.TrySetResult(null); - } - //Buffer size matches FileStreamResult - //CONSIDER Should we allow client to change the buffer size? - await eventStream.CopyToAsync(outputStream, 0x10000, token); - }; - - var client = new DiagnosticsClient(endpointInfo.Endpoint); - - await using EventTracePipeline pipeProcessor = new EventTracePipeline(client, new EventTracePipelineSettings - { - Configuration = configuration, - Duration = duration, - }, streamAvailable); - - await pipeProcessor.RunAsync(token); - } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TraceTestUtilities.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TraceTestUtilities.cs new file mode 100644 index 00000000000..0c088a1ff01 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TraceTestUtilities.cs @@ -0,0 +1,52 @@ +// 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 Microsoft.Diagnostics.Tracing.Parsers.Clr; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Diagnostics.Monitoring.TestCommon +{ + public static class TraceTestUtilities + { + public static async Task ValidateTrace(Stream traceStream, bool? expectRundown = null) + { + using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(CommonTestTimeouts.ValidateTraceTimeout); + + using var eventSource = new EventPipeEventSource(traceStream); + + // Dispose event source when cancelled. + using var _ = cancellationTokenSource.Token.Register(eventSource.Dispose); + + bool foundTraceObject = false; + bool foundRundown = false; + + eventSource.Dynamic.All += (TraceEvent obj) => + { + foundTraceObject = true; + }; + + if (expectRundown.HasValue) + { + var rundown = new ClrRundownTraceEventParser(eventSource); + rundown.RuntimeStart += (data) => + { + foundRundown = true; + }; + } + + await Task.Run(() => Assert.True(eventSource.Process()), cancellationTokenSource.Token); + + Assert.True(foundTraceObject); + + if (expectRundown.HasValue) + { + Assert.Equal(expectRundown.Value, foundRundown); + } + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/EgressTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/EgressTests.cs index d065f63b68a..3151120b1c9 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/EgressTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/EgressTests.cs @@ -21,7 +21,7 @@ using System.Linq; using System.Net; using System.Net.Http; -using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -101,6 +101,43 @@ await ScenarioRunner.SingleTarget( operationResult = await apiClient.GetOperationStatus(response.OperationUri); Assert.Equal(HttpStatusCode.OK, operationResult.StatusCode); Assert.Equal(OperationState.Cancelled, operationResult.OperationStatus.Status); + Assert.False(operationResult.OperationStatus.IsStoppable); + + await appRunner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); + }, + configureTool: (toolRunner) => + { + toolRunner.WriteKeyPerValueConfiguration(new RootOptions().AddFileSystemEgress(FileProviderName, _tempDirectory.FullName)); + }); + } + + [Fact] + public async Task EgressStopTest() + { + await ScenarioRunner.SingleTarget( + _outputHelper, + _httpClientFactory, + DiagnosticPortConnectionMode.Connect, + TestAppScenarios.AsyncWait.Name, + appValidate: async (appRunner, apiClient) => + { + int processId = await appRunner.ProcessIdTask; + + OperationResponse response = await apiClient.EgressTraceAsync(processId, durationSeconds: -1, FileProviderName); + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + + OperationStatusResponse operationResult = await apiClient.GetOperationStatus(response.OperationUri); + Assert.Equal(HttpStatusCode.OK, operationResult.StatusCode); + Assert.Equal(OperationState.Running, operationResult.OperationStatus.Status); + Assert.True(operationResult.OperationStatus.IsStoppable); + + HttpStatusCode deleteStatus = await apiClient.StopEgressOperation(response.OperationUri); + Assert.Equal(HttpStatusCode.Accepted, deleteStatus); + + OperationStatusResponse pollOperationResult = await apiClient.PollOperationToCompletion(response.OperationUri); + Assert.Equal(HttpStatusCode.Created, pollOperationResult.StatusCode); + Assert.Equal(OperationState.Succeeded, pollOperationResult.OperationStatus.Status); + Assert.False(pollOperationResult.OperationStatus.IsStoppable); await appRunner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); }, @@ -133,10 +170,14 @@ await ScenarioRunner.SingleTarget( OperationStatusResponse status1 = await apiClient.GetOperationStatus(response1.OperationUri); OperationSummary summary1 = result.First(os => os.OperationId == status1.OperationStatus.OperationId); ValidateOperation(status1.OperationStatus, summary1); + Assert.True(summary1.IsStoppable); + Assert.Equal(FileProviderName, summary1.EgressProviderName); OperationStatusResponse status2 = await apiClient.GetOperationStatus(response2.OperationUri); OperationSummary summary2 = result.First(os => os.OperationId == status2.OperationStatus.OperationId); ValidateOperation(status2.OperationStatus, summary2); + Assert.False(summary2.IsStoppable); + Assert.Equal(FileProviderName, summary2.EgressProviderName); await appRunner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); }, @@ -226,6 +267,103 @@ await ScenarioRunner.SingleTarget( }); } + [Fact] + public async Task HttpEgressCancelTest() + { + await ScenarioRunner.SingleTarget( + _outputHelper, + _httpClientFactory, + DiagnosticPortConnectionMode.Connect, + TestAppScenarios.AsyncWait.Name, + appValidate: async (appRunner, apiClient) => + { + int processId = await appRunner.ProcessIdTask; + + using ResponseStreamHolder responseBox = await apiClient.HttpEgressTraceAsync(processId, durationSeconds: -1); + + Uri operationUri = responseBox.Response.Headers.Location; + Assert.NotNull(operationUri); + + // Start consuming the stream + Task drainResponseTask = responseBox.Stream.CopyToAsync(Stream.Null); + + // Make sure the operation exists + OperationStatusResponse operationResult = await apiClient.GetOperationStatus(operationUri); + Assert.Equal(HttpStatusCode.OK, operationResult.StatusCode); + Assert.True(operationResult.OperationStatus.Status == OperationState.Running); + + // Cancel the trace operation + HttpStatusCode deleteStatus = await apiClient.CancelEgressOperation(operationUri); + Assert.Equal(HttpStatusCode.OK, deleteStatus); + + operationResult = await apiClient.GetOperationStatus(operationUri); + Assert.Equal(HttpStatusCode.OK, operationResult.StatusCode); + Assert.Equal(OperationState.Cancelled, operationResult.OperationStatus.Status); + + await Assert.ThrowsAsync(() => drainResponseTask); + + await appRunner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); + }, + configureTool: (toolRunner) => + { + toolRunner.WriteKeyPerValueConfiguration(new RootOptions().AddFileSystemEgress(FileProviderName, _tempDirectory.FullName)); + }); + } + + [Fact] + public async Task HttpEgressStopTest() + { + using TemporaryDirectory tempDir = new(_outputHelper); + + await ScenarioRunner.SingleTarget( + _outputHelper, + _httpClientFactory, + DiagnosticPortConnectionMode.Connect, + TestAppScenarios.AsyncWait.Name, + appValidate: async (appRunner, apiClient) => + { + int processId = await appRunner.ProcessIdTask; + + using ResponseStreamHolder responseBox = await apiClient.HttpEgressTraceAsync(processId, durationSeconds: -1); + + Uri operationUri = responseBox.Response.Headers.Location; + Assert.NotNull(operationUri); + + // Start saving the stream + string traceFile = Path.Combine(tempDir.FullName, "test.nettrace"); + using FileStream traceFileWriter = File.OpenWrite(traceFile); + + Task drainResponseTask = responseBox.Stream.CopyToAsync(traceFileWriter); + + // Make sure the operation exists + OperationStatusResponse operationResult = await apiClient.GetOperationStatus(operationUri); + Assert.Equal(HttpStatusCode.OK, operationResult.StatusCode); + Assert.True(operationResult.OperationStatus.Status == OperationState.Running); + + // Stop the trace operation + HttpStatusCode deleteStatus = await apiClient.StopEgressOperation(operationUri); + Assert.Equal(HttpStatusCode.Accepted, deleteStatus); + + using CancellationTokenSource timeoutCancellation = new(CommonTestTimeouts.TraceTimeout); + await drainResponseTask.WaitAsync(timeoutCancellation.Token); + await traceFileWriter.DisposeAsync(); + + operationResult = await apiClient.GetOperationStatus(operationUri); + Assert.Equal(HttpStatusCode.Created, operationResult.StatusCode); + Assert.Equal(OperationState.Succeeded, operationResult.OperationStatus.Status); + + // Verify the resulting trace has rundown information + using FileStream traceStream = File.OpenRead(traceFile); + await TraceTestUtilities.ValidateTrace(traceStream, expectRundown: true); + + await appRunner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); + }, + configureTool: (toolRunner) => + { + toolRunner.WriteKeyPerValueConfiguration(new RootOptions().AddFileSystemEgress(FileProviderName, _tempDirectory.FullName)); + }); + } + /// /// Tests that turning off HTTP egress results in an error for dumps and logs (gcdumps and traces are currently not tested) /// @@ -340,6 +478,8 @@ private static void ValidateOperation(OperationStatus expected, OperationSummary Assert.Equal(expected.OperationId, summary.OperationId); Assert.Equal(expected.Status, summary.Status); Assert.Equal(expected.CreatedDateTime, summary.CreatedDateTime); + Assert.Equal(expected.EgressProviderName, summary.EgressProviderName); + Assert.Equal(expected.IsStoppable, summary.IsStoppable); } public void Dispose() diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ApiClient.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ApiClient.cs index fe165f03967..74bf32536cf 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ApiClient.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ApiClient.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Diagnostics.Monitoring.Options; using Microsoft.Diagnostics.Monitoring.WebApi.Models; using Microsoft.Extensions.Logging; @@ -514,6 +515,33 @@ public async Task ApiCall(string routeAndQuery, Cancellatio return response; } + public async Task HttpEgressTraceAsync(int processId, int durationSeconds, CancellationToken token) + { + string uri = FormattableString.Invariant($"/trace?pid={processId}&durationSeconds={durationSeconds}"); + using HttpRequestMessage request = new(HttpMethod.Get, uri); + + using DisposableBox responseBox = new( + await SendAndLogAsync( + request, + HttpCompletionOption.ResponseHeadersRead, + token).ConfigureAwait(false)); + + switch (responseBox.Value.StatusCode) + { + case HttpStatusCode.OK: + return await ResponseStreamHolder.CreateAsync(responseBox).ConfigureAwait(false); + case HttpStatusCode.BadRequest: + case HttpStatusCode.TooManyRequests: + ValidateContentType(responseBox.Value, ContentTypes.ApplicationProblemJson); + throw await CreateValidationProblemDetailsExceptionAsync(responseBox.Value).ConfigureAwait(false); + case HttpStatusCode.Unauthorized: + ThrowIfNotSuccess(responseBox.Value); + break; + } + + throw await CreateUnexpectedStatusCodeExceptionAsync(responseBox.Value).ConfigureAwait(false); + } + public async Task EgressTraceAsync(int processId, int durationSeconds, string egressProvider, CancellationToken token) { string uri = FormattableString.Invariant($"/trace?pid={processId}&egressProvider={egressProvider}&durationSeconds={durationSeconds}"); @@ -579,6 +607,28 @@ public async Task GetOperationStatus(Uri operation, Can throw await CreateUnexpectedStatusCodeExceptionAsync(response).ConfigureAwait(false); } + public async Task StopEgressOperation(Uri operation, CancellationToken token) + { + string operationUri = QueryHelpers.AddQueryString(operation.ToString(), "stop", "true"); + + using HttpRequestMessage request = new(HttpMethod.Delete, operationUri); + using HttpResponseMessage response = await SendAndLogAsync(request, HttpCompletionOption.ResponseContentRead, token).ConfigureAwait(false); + + switch (response.StatusCode) + { + case HttpStatusCode.Accepted: + return response.StatusCode; + case HttpStatusCode.BadRequest: + ValidateContentType(response, ContentTypes.ApplicationProblemJson); + throw await CreateValidationProblemDetailsExceptionAsync(response).ConfigureAwait(false); + case HttpStatusCode.Unauthorized: + ThrowIfNotSuccess(response); + break; + } + + throw await CreateUnexpectedStatusCodeExceptionAsync(response).ConfigureAwait(false); + } + public async Task CancelEgressOperation(Uri operation, CancellationToken token) { using HttpRequestMessage request = new(HttpMethod.Delete, operation.ToString()); diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ApiClientExtensions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ApiClientExtensions.cs index 544eda6c592..88836d6a692 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ApiClientExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ApiClientExtensions.cs @@ -394,6 +394,12 @@ public static async Task EgressTraceAsync(this ApiClient clie return await client.EgressTraceAsync(processId, durationSeconds, egressProvider, timeoutSource.Token).ConfigureAwait(false); } + public static async Task HttpEgressTraceAsync(this ApiClient client, int processId, int durationSeconds) + { + using CancellationTokenSource timeoutSource = new(TestTimeouts.HttpApi); + return await client.HttpEgressTraceAsync(processId, durationSeconds, timeoutSource.Token).ConfigureAwait(false); + } + public static async Task GetOperationStatus(this ApiClient client, Uri operation) { using CancellationTokenSource timeoutSource = new(TestTimeouts.HttpApi); @@ -406,6 +412,12 @@ public static async Task> GetOperations(this ApiClient cl return await client.GetOperations(timeoutSource.Token).ConfigureAwait(false); } + public static async Task StopEgressOperation(this ApiClient client, Uri operation) + { + using CancellationTokenSource timeoutSource = new(TestTimeouts.HttpApi); + return await client.StopEgressOperation(operation, timeoutSource.Token).ConfigureAwait(false); + } + public static async Task CancelEgressOperation(this ApiClient client, Uri operation) { using CancellationTokenSource timeoutSource = new(TestTimeouts.HttpApi); @@ -427,10 +439,14 @@ public static async Task PollOperationToCompletion(this { OperationStatusResponse operationResult = await apiClient.GetOperationStatus(operationUrl).ConfigureAwait(false); Assert.True(operationResult.StatusCode == HttpStatusCode.OK || operationResult.StatusCode == HttpStatusCode.Created); - Assert.True(operationResult.OperationStatus.Status == OperationState.Running || operationResult.OperationStatus.Status == OperationState.Succeeded); + Assert.True( + operationResult.OperationStatus.Status == OperationState.Running || + operationResult.OperationStatus.Status == OperationState.Succeeded || + operationResult.OperationStatus.Status == OperationState.Stopping); using CancellationTokenSource cancellationTokenSource = new(timeout); - while (operationResult.OperationStatus.Status == OperationState.Running) + while (operationResult.OperationStatus.Status == OperationState.Running || + operationResult.OperationStatus.Status == OperationState.Stopping) { cancellationTokenSource.Token.ThrowIfCancellationRequested(); await Task.Delay(TimeSpan.FromSeconds(1), cancellationTokenSource.Token).ConfigureAwait(false); diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ResponseStreamHolder.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ResponseStreamHolder.cs index ab6a1334947..52ba369868e 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ResponseStreamHolder.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ResponseStreamHolder.cs @@ -15,25 +15,25 @@ namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.HttpApi /// internal class ResponseStreamHolder : IDisposable { - private readonly HttpResponseMessage _response; + public HttpResponseMessage Response { get; } public Stream Stream { get; private set; } private ResponseStreamHolder(HttpResponseMessage response) { - _response = response ?? throw new ArgumentNullException(nameof(response)); + Response = response ?? throw new ArgumentNullException(nameof(response)); } public void Dispose() { // The response disposes the stream when disposed. - _response.Dispose(); + Response.Dispose(); } public static async Task CreateAsync(DisposableBox responseBox) { using DisposableBox holderBox = new(new(responseBox.Release())); - holderBox.Value.Stream = await holderBox.Value._response.Content.ReadAsStreamAsync().ConfigureAwait(false); + holderBox.Value.Stream = await holderBox.Value.Response.Content.ReadAsStreamAsync().ConfigureAwait(false); return holderBox.Release(); } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectTraceActionTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectTraceActionTests.cs index 889f6ac76b8..53a492aa8fc 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectTraceActionTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectTraceActionTests.cs @@ -10,13 +10,11 @@ using Microsoft.Diagnostics.Tools.Monitor.CollectionRules; using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Actions; using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions; -using Microsoft.Diagnostics.Tracing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; using System.Collections.Generic; using System.IO; -using System.Threading; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestHostHelper.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestHostHelper.cs index 546920f0a8a..a3b917e0917 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestHostHelper.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestHostHelper.cs @@ -109,6 +109,7 @@ public static IHost CreateHost( services.ConfigureInProcessFeatures(context.Configuration); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); servicesCallback?.Invoke(services); }) .Build(); diff --git a/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs b/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs index c5949086c9f..a874a78a1a0 100644 --- a/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs +++ b/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs @@ -82,20 +82,38 @@ protected override async Task ExecuteCoreAsync( configuration = TraceUtilities.GetTraceConfiguration(optionsProviders, requestRundown, bufferSizeMegabytes); } - string fileName = TraceUtilities.GenerateTraceFileName(EndpointInfo); - KeyValueLogScope scope = Utils.CreateArtifactScope(Utils.ArtifactType_Trace, EndpointInfo); + ITraceOperationFactory operationFactory = _serviceProvider.GetRequiredService(); + IArtifactOperation operation; + if (stoppingEvent == null) + { + operation = operationFactory.Create( + EndpointInfo, + configuration, + duration); + } + else + { + operation = operationFactory.Create( + EndpointInfo, + configuration, + duration, + stoppingEvent.ProviderName, + stoppingEvent.EventName, + stoppingEvent.PayloadFilter); + } + EgressOperation egressOperation = new EgressOperation( async (outputStream, token) => { using IDisposable operationRegistration = _operationTrackerService.Register(EndpointInfo); - await TraceUtilities.CaptureTraceAsync(startCompletionSource, EndpointInfo, configuration, duration, outputStream, token); + await operation.ExecuteAsync(outputStream, startCompletionSource, token); }, egressProvider, - fileName, + operation.GenerateFileName(), EndpointInfo, - ContentTypes.ApplicationOctetStream, + operation.ContentType, scope, collectionRuleMetadata); diff --git a/src/Tools/dotnet-monitor/Commands/CollectCommandHandler.cs b/src/Tools/dotnet-monitor/Commands/CollectCommandHandler.cs index 0d6b8b2b9ec..0eaf42d58db 100644 --- a/src/Tools/dotnet-monitor/Commands/CollectCommandHandler.cs +++ b/src/Tools/dotnet-monitor/Commands/CollectCommandHandler.cs @@ -168,6 +168,7 @@ private static IHostBuilder Configure(this IHostBuilder builder, AuthConfigurati services.ConfigureInProcessFeatures(context.Configuration); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); }) .ConfigureContainer((HostBuilderContext context, IServiceCollection services) => { diff --git a/src/Tools/dotnet-monitor/PipelineArtifactOperation.cs b/src/Tools/dotnet-monitor/PipelineArtifactOperation.cs index 0efe1f3e6af..615f44e7f31 100644 --- a/src/Tools/dotnet-monitor/PipelineArtifactOperation.cs +++ b/src/Tools/dotnet-monitor/PipelineArtifactOperation.cs @@ -17,15 +17,14 @@ internal abstract class PipelineArtifactOperation : where T : Pipeline { private readonly string _artifactType; - private readonly ILogger _logger; private Func _stopFunc; protected PipelineArtifactOperation(ILogger logger, string artifactType, IEndpointInfo endpointInfo, bool isStoppable = true) { _artifactType = artifactType; - _logger = logger; + Logger = logger; EndpointInfo = endpointInfo; IsStoppable = isStoppable; } @@ -38,7 +37,7 @@ public async Task ExecuteAsync(Stream outputStream, TaskCompletionSource Task runTask = await StartPipelineAsync(pipeline, token); - _logger.StartCollectArtifact(_artifactType); + Logger.StartCollectArtifact(_artifactType); // Signal that the logs operation has started startCompletionSource?.TrySetResult(null); @@ -78,5 +77,7 @@ public async Task StopAsync(CancellationToken token) protected abstract Task StartPipelineAsync(T pipeline, CancellationToken token); protected IEndpointInfo EndpointInfo { get; } + + protected ILogger Logger { get; } } } diff --git a/src/Tools/dotnet-monitor/RequestLimitMiddleware.cs b/src/Tools/dotnet-monitor/RequestLimitMiddleware.cs deleted file mode 100644 index c65d8e667dd..00000000000 --- a/src/Tools/dotnet-monitor/RequestLimitMiddleware.cs +++ /dev/null @@ -1,71 +0,0 @@ -// 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.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Diagnostics.Monitoring.WebApi; -using System; -using System.Text.Json; -using System.Threading.Tasks; - -namespace Microsoft.Diagnostics.Tools.Monitor -{ - /// - /// Limits the amount of requests that can be sent to the server. - /// - The current rate limits are based on concurrent requests to the server from any source on a per endpoint basis. - /// - Note we do not use Microsoft.AspNetCore.ConcurrencyLimiter because it works over the whole application instead of per endpoint. - /// - In the future, we may want to switch to https://github.com/dotnet/aspnetcore/issues/29933 - /// TODO For asp.net 2.1, this would be implemented as an ActionFilter. For 3.1+, we use an endpoints + middleware - /// - internal sealed class RequestLimitMiddleware - { - private readonly RequestDelegate _next; - - private readonly RequestLimitTracker _limitTracker; - private const string EgressQuery = "egressprovider"; - - public RequestLimitMiddleware(RequestDelegate next, RequestLimitTracker requestLimitTracker) - { - _next = next; - _limitTracker = requestLimitTracker; - } - - public async Task Invoke(HttpContext context) - { - var endpoint = context.GetEndpoint(); - - RequestLimitAttribute requestLimit = endpoint?.Metadata.GetMetadata(); - IDisposable incrementor = null; - - try - { - //Operations and middleware both share the same increment limits, but - //we don't want the middleware to increment the limit if the operation is doing it as well. - if ((requestLimit != null) && !context.Request.Query.ContainsKey(EgressQuery)) - { - incrementor = _limitTracker.Increment(requestLimit.LimitKey, out bool allowOperation); - if (!allowOperation) - { - - //We should report the same kind of error from Middleware and the Mvc layer. - context.Response.StatusCode = StatusCodes.Status429TooManyRequests; - context.Response.ContentType = ContentTypes.ApplicationProblemJson; - await context.Response.WriteAsync(JsonSerializer.Serialize(new ProblemDetails - { - Status = StatusCodes.Status429TooManyRequests, - Detail = Microsoft.Diagnostics.Monitoring.WebApi.Strings.ErrorMessage_TooManyRequests - }), context.RequestAborted); - return; - } - } - - await _next(context); - } - finally - { - incrementor?.Dispose(); - } - } - } -} diff --git a/src/Tools/dotnet-monitor/Startup.cs b/src/Tools/dotnet-monitor/Startup.cs index f91b19437ed..a45bc836cd0 100644 --- a/src/Tools/dotnet-monitor/Startup.cs +++ b/src/Tools/dotnet-monitor/Startup.cs @@ -95,9 +95,6 @@ public static void Configure(IApplicationBuilder app, IWebHostEnvironment env, I // https://github.com/dotnet/aspnetcore/issues/36960 //app.UseResponseCompression(); - //Note this must be after UseRouting but before UseEndpoints - app.UseMiddleware(); - app.UseEndpoints(builder => { builder.MapControllers(); diff --git a/src/Tools/dotnet-monitor/Trace/AbstractTraceOperation.cs b/src/Tools/dotnet-monitor/Trace/AbstractTraceOperation.cs new file mode 100644 index 00000000000..6f75ff5654b --- /dev/null +++ b/src/Tools/dotnet-monitor/Trace/AbstractTraceOperation.cs @@ -0,0 +1,33 @@ +// 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.EventPipe; +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Extensions.Logging; +using System; +using Utils = Microsoft.Diagnostics.Monitoring.WebApi.Utilities; + +namespace Microsoft.Diagnostics.Tools.Monitor +{ + internal abstract class AbstractTraceOperation : PipelineArtifactOperation + { + // Buffer size matches FileStreamResult + protected const int DefaultBufferSize = 0x10000; + + protected readonly EventTracePipelineSettings _settings; + + public AbstractTraceOperation(IEndpointInfo endpointInfo, EventTracePipelineSettings settings, ILogger logger) + : base(logger, Utils.ArtifactType_Trace, endpointInfo) + { + _settings = settings; + } + + public override string GenerateFileName() + { + return FormattableString.Invariant($"{Utils.GetFileNameTimeStampUtcNow()}_{EndpointInfo.ProcessId}.nettrace"); + } + + public override string ContentType => ContentTypes.ApplicationOctetStream; + } +} diff --git a/src/Tools/dotnet-monitor/Trace/TraceOperation.cs b/src/Tools/dotnet-monitor/Trace/TraceOperation.cs new file mode 100644 index 00000000000..153ea27bdf7 --- /dev/null +++ b/src/Tools/dotnet-monitor/Trace/TraceOperation.cs @@ -0,0 +1,40 @@ +// 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.EventPipe; +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Diagnostics.NETCore.Client; +using Microsoft.Extensions.Logging; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Tools.Monitor +{ + internal sealed class TraceOperation : AbstractTraceOperation + { + private readonly TaskCompletionSource _eventStreamAvailableCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); + + public TraceOperation(IEndpointInfo endpointInfo, EventTracePipelineSettings settings, ILogger logger) + : base(endpointInfo, settings, logger) { } + + protected override EventTracePipeline CreatePipeline(Stream outputStream) + { + DiagnosticsClient client = new(EndpointInfo.Endpoint); + return new EventTracePipeline(client, _settings, + async (eventStream, token) => + { + _eventStreamAvailableCompletionSource.TrySetResult(null); + await eventStream.CopyToAsync(outputStream, DefaultBufferSize, token); + }); + } + + protected override async Task StartPipelineAsync(EventTracePipeline pipeline, CancellationToken token) + { + Task pipelineRunTask = pipeline.RunAsync(token); + await _eventStreamAvailableCompletionSource.Task.WaitAsync(token); + return pipelineRunTask; + } + } +} diff --git a/src/Tools/dotnet-monitor/Trace/TraceOperationFactory.cs b/src/Tools/dotnet-monitor/Trace/TraceOperationFactory.cs new file mode 100644 index 00000000000..4e3c6d6b457 --- /dev/null +++ b/src/Tools/dotnet-monitor/Trace/TraceOperationFactory.cs @@ -0,0 +1,44 @@ +// 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.EventPipe; +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; + +namespace Microsoft.Diagnostics.Tools.Monitor +{ + internal sealed class TraceOperationFactory : ITraceOperationFactory + { + private readonly ILogger _logger; + + public TraceOperationFactory(ILogger logger) + { + _logger = logger; + } + + public IArtifactOperation Create(IEndpointInfo endpointInfo, MonitoringSourceConfiguration configuration, TimeSpan duration) + { + EventTracePipelineSettings settings = new() + { + Configuration = configuration, + Duration = duration + }; + + return new TraceOperation(endpointInfo, settings, _logger); + } + + public IArtifactOperation Create(IEndpointInfo endpointInfo, MonitoringSourceConfiguration configuration, TimeSpan duration, string providerName, string eventName, IDictionary payloadFilter) + { + EventTracePipelineSettings settings = new() + { + Configuration = configuration, + Duration = duration + }; + + return new TraceUntilEventOperation(endpointInfo, settings, providerName, eventName, payloadFilter, _logger); + } + } +} diff --git a/src/Tools/dotnet-monitor/Trace/TraceUntilEventOperation.cs b/src/Tools/dotnet-monitor/Trace/TraceUntilEventOperation.cs new file mode 100644 index 00000000000..57b4189f406 --- /dev/null +++ b/src/Tools/dotnet-monitor/Trace/TraceUntilEventOperation.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.EventPipe; +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Diagnostics.NETCore.Client; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Tools.Monitor +{ + internal sealed class TraceUntilEventOperation : AbstractTraceOperation + { + private readonly string _providerName; + private readonly string _eventName; + private readonly IDictionary _payloadFilter; + + private readonly TaskCompletionSource _eventStreamAvailableCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly TaskCompletionSource _stoppingEventHitSource = new(TaskCreationOptions.RunContinuationsAsynchronously); + + public TraceUntilEventOperation( + IEndpointInfo endpointInfo, + EventTracePipelineSettings settings, + string providerName, + string eventName, + IDictionary payloadFilter, + ILogger logger) + : base(endpointInfo, settings, logger) + { + _providerName = providerName; + _eventName = eventName; + _payloadFilter = payloadFilter; + } + + protected override EventTracePipeline CreatePipeline(Stream outputStream) + { + DiagnosticsClient client = new(EndpointInfo.Endpoint); + return new EventTracePipeline(client, _settings, + async (eventStream, token) => + { + _eventStreamAvailableCompletionSource?.TrySetResult(null); + + await using EventMonitoringPassthroughStream eventMonitoringStream = new( + _providerName, + _eventName, + _payloadFilter, + onEvent: (traceEvent) => + { + Logger.StoppingTraceEventHit(traceEvent); + _stoppingEventHitSource.TrySetResult(null); + }, + onPayloadFilterMismatch: Logger.StoppingTraceEventPayloadFilterMismatch, + eventStream, + outputStream, + DefaultBufferSize, + callOnEventOnlyOnce: true, + leaveDestinationStreamOpen: true /* We do not have ownership of the outputStream */); + + await eventMonitoringStream.ProcessAsync(token); + }); + } + + protected override async Task StartPipelineAsync(EventTracePipeline pipeline, CancellationToken token) + { + Task pipelineRunTask = pipeline.RunAsync(token); + await _eventStreamAvailableCompletionSource.Task.WaitAsync(token); + + return Task.Run(async () => + { + await Task.WhenAny(pipelineRunTask, _stoppingEventHitSource.Task).Unwrap(); + + if (_stoppingEventHitSource.Task.IsCompleted) + { + await pipeline.StopAsync(token); + await pipelineRunTask; + } + }, token); + } + } +}