diff --git a/documentation/api/definitions.md b/documentation/api/definitions.md index 913a4072660..c136ab6e74e 100644 --- a/documentation/api/definitions.md +++ b/documentation/api/definitions.md @@ -267,6 +267,7 @@ Status of the egress operation. |---|---| | `Running` | Operation has been started. This is the initial state. | | `Cancelled` | The operation was cancelled by the user. | +| `Stopping` | The operation is in the process of stopping at the request of the user. | | `Succeeded` | Egress operation has been successful. Querying the operation will return the location of the egressed artifact. | | `Failed` | Egress operation failed. Querying the operation will return detailed error information. | @@ -281,6 +282,8 @@ Detailed information about an operation. | `operationId` | guid | Unique identifier for the operation. | | `createdDateTime` | datetime string | UTC DateTime string of when the operation was created. | | `status` | [OperationState](#operationstate) | The current status of operation. | +| `egressProviderName` | string | (8.0+) The name of the egress provider that the artifact is being sent to. This will be null if the artifact is being sent directly back to the user from an HTTP request. | +| `isStoppable` | bool | (8.0+) Whether this operation can be gracefully stopped using [Stop Operation](operations-stop.md). Not all operations support being stopped. | ### Example @@ -290,7 +293,9 @@ Detailed information about an operation. "error": null, "operationId": "67f07e40-5cca-4709-9062-26302c484f18", "createdDateTime": "2021-07-21T06:21:15.315861Z", - "status": "Succeeded" + "status": "Succeeded", + "egressProviderName": "monitorBlob", + "isStoppable": false, } ``` @@ -303,6 +308,8 @@ Summary state of an operation. | `operationId` | guid | Unique identifier for the operation. | | `createdDateTime` | datetime string | UTC DateTime string of when the operation was created. | | `status` | [OperationState](#operationstate) | The current status of operation. | +| `egressProviderName` | string | (8.0+) The name of the egress provider that the artifact is being sent to. This will be null if the artifact is being sent directly back to the user from an HTTP request. | +| `isStoppable` | bool | (8.0+) Whether this operation can be gracefully stopped using [Stop Operation](operations-stop.md). Not all operations support being stopped. | | `process` | [OperationProcessInfo](#operationprocessinfo) | (6.3+) The process on which the operation is performed. | ### Example @@ -312,6 +319,8 @@ Summary state of an operation. "operationId": "67f07e40-5cca-4709-9062-26302c484f18", "createdDateTime": "2021-07-21T06:21:15.315861Z", "status": "Succeeded", + "egressProviderName": null, + "isStoppable": false, "process": { "pid": 21632, "uid": "cd4da319-fa9e-4987-ac4e-e57b2aac248b", diff --git a/documentation/api/dump.md b/documentation/api/dump.md index ee6724c1292..df28864fde1 100644 --- a/documentation/api/dump.md +++ b/documentation/api/dump.md @@ -45,12 +45,14 @@ Allowed schemes: | Name | Type | Description | Content Type | |---|---|---|---| -| 200 OK | stream | A managed dump of the process. | `application/octet-stream` | -| 202 Accepted | | When an egress provider is specified, the Location header containers the URI of the operation for querying the egress status. | | +| 200 OK | stream | A managed dump of the process when no egress provider is specified. | `application/octet-stream` | +| 202 Accepted | | When an egress provider is specified, the artifact has begun being collected. | | | 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | | 429 Too Many Requests | | There are too many dump requests at this time. Try to request a dump at a later time. | | +> **NOTE: (8.0+)** Regardless if an egress provider is specified if the request was successful (response codes 200 or 202), the Location header contains the URI of the operation. This can be used to query the status of the operation or change its state. + ## Examples ### Sample Request @@ -77,6 +79,7 @@ The managed dump containing all memory of the process, chunk encoded, is returne HTTP/1.1 200 OK Content-Type: application/octet-stream Transfer-Encoding: chunked +Location: localhost:52323/operations/67f07e40-5cca-4709-9062-26302c484f18 ``` ## Supported Runtimes diff --git a/documentation/api/gcdump.md b/documentation/api/gcdump.md index 963ca7a2b1f..a6d63677160 100644 --- a/documentation/api/gcdump.md +++ b/documentation/api/gcdump.md @@ -48,12 +48,14 @@ Allowed schemes: | Name | Type | Description | Content Type | |---|---|---|---| -| 200 OK | stream | A GC dump of the process. | `application/octet-stream` | -| 202 Accepted | | When an egress provider is specified, the Location header containers the URI of the operation for querying the egress status. | | +| 200 OK | stream | A GC dump of the process when no egress provider is specified. | `application/octet-stream` | +| 202 Accepted | | When an egress provider is specified, the artifact has begun being collected. | | | 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | | 429 Too Many Requests | | There are too many GC dump requests at this time. Try to request a GC dump at a later time. | `application/problem+json` | +> **NOTE: (8.0+)** Regardless if an egress provider is specified if the request was successful (response codes 200 or 202), the Location header contains the URI of the operation. This can be used to query the status of the operation or change its state. + ## Examples ### Sample Request @@ -80,6 +82,7 @@ The GC dump, chunk encoded, is returned as the response body. HTTP/1.1 200 OK Content-Type: application/octet-stream Transfer-Encoding: chunked +Location: localhost:52323/operations/67f07e40-5cca-4709-9062-26302c484f18 ``` ## Supported Runtimes diff --git a/documentation/api/livemetrics-custom.md b/documentation/api/livemetrics-custom.md index ad404d9b48c..4f9f3aaca11 100644 --- a/documentation/api/livemetrics-custom.md +++ b/documentation/api/livemetrics-custom.md @@ -50,11 +50,13 @@ The expected content type is `application/json`. | Name | Type | Description | Content Type | |---|---|---|---| | 200 OK | [Metric](./definitions.md#metric) | The metrics from the process formatted as json sequence. Each JSON object is a [metrics object](./definitions.md#metric)| `application/json-seq` | -| 202 Accepted | | When an egress provider is specified, the Location header containers the URI of the operation for querying the egress status. | | +| 202 Accepted | | When an egress provider is specified, the artifact has begun being collected. | | | 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | | 429 Too Many Requests | | There are too many requests at this time. Try to request metrics at a later time. | `application/problem+json` | +> **NOTE: (8.0+)** Regardless if an egress provider is specified if the request was successful (response codes 200 or 202), the Location header contains the URI of the operation. This can be used to query the status of the operation or change its state. + ## Examples ### Sample Request @@ -83,6 +85,7 @@ Authorization: Bearer fffffffffffffffffffffffffffffffffffffffffff= ```http HTTP/1.1 200 OK Content-Type: application/json-seq +Location: localhost:52323/operations/67f07e40-5cca-4709-9062-26302c484f18 { "timestamp": "2021-08-31T16:58:39.7514031+00:00", diff --git a/documentation/api/livemetrics-get.md b/documentation/api/livemetrics-get.md index b7712403a9c..bedb17ab2ba 100644 --- a/documentation/api/livemetrics-get.md +++ b/documentation/api/livemetrics-get.md @@ -46,11 +46,13 @@ Allowed schemes: | Name | Type | Description | Content Type | |---|---|---|---| | 200 OK | [Metric](./definitions.md#metric) | The metrics from the process formatted as json sequence. | `application/json-seq` | -| 202 Accepted | | When an egress provider is specified, the Location header containers the URI of the operation for querying the egress status. | | +| 202 Accepted | | When an egress provider is specified, the artifact has begun being collected. | | | 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | | 429 Too Many Requests | | There are too many requests at this time. Try to request metrics at a later time. | `application/problem+json` | +> **NOTE: (8.0+)** Regardless if an egress provider is specified if the request was successful (response codes 200 or 202), the Location header contains the URI of the operation. This can be used to query the status of the operation or change its state. + ## Examples ### Sample Request @@ -66,6 +68,7 @@ Authorization: Bearer fffffffffffffffffffffffffffffffffffffffffff= ```http HTTP/1.1 200 OK Content-Type: application/json-seq +Location: localhost:52323/operations/67f07e40-5cca-4709-9062-26302c484f18 { "timestamp": "2021-08-31T16:58:39.7514031+00:00", diff --git a/documentation/api/logs-custom.md b/documentation/api/logs-custom.md index 654b400c2de..fb92e10923b 100644 --- a/documentation/api/logs-custom.md +++ b/documentation/api/logs-custom.md @@ -53,11 +53,13 @@ The expected content type is `application/json`. |---|---|---|---| | 200 OK | | The logs from the process formatted as [newline delimited JSON](https://github.com/ndjson/ndjson-spec). Each JSON object is a [LogEntry](definitions.md#logentry) | `application/x-ndjson` | | 200 OK | | The logs from the process formatted as plain text, similar to the output of the JSON console formatter. | `text/plain` | -| 202 Accepted | | When an egress provider is specified, the Location header containers the URI of the operation for querying the egress status. | | +| 202 Accepted | | When an egress provider is specified, the artifact has begun being collected. | | | 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | | 429 Too Many Requests | | There are too many logs requests at this time. Try to request logs at a later time. | `application/problem+json` | +> **NOTE: (8.0+)** Regardless if an egress provider is specified if the request was successful (response codes 200 or 202), the Location header contains the URI of the operation. This can be used to query the status of the operation or change its state. + ## Examples ### Sample Request @@ -98,6 +100,7 @@ The log statements logged at the Information level or higher for 1 minute is ret ```http HTTP/1.1 200 OK Content-Type: text/plain +Location: localhost:52323/operations/67f07e40-5cca-4709-9062-26302c484f18 2021-05-13 18:06:41Z info: Microsoft.AspNetCore.Hosting.Diagnostics[1] => RequestId:0HM8M726ENU3K:0000002B, RequestPath:/, SpanId:|4791a4a7-433aa59a9e362743., TraceId:4791a4a7-433aa59a9e362743, ParentId: diff --git a/documentation/api/logs-get.md b/documentation/api/logs-get.md index ed525f85875..01b9dd97199 100644 --- a/documentation/api/logs-get.md +++ b/documentation/api/logs-get.md @@ -48,11 +48,13 @@ Allowed schemes: |---|---|---|---| | 200 OK | | The logs from the process formatted as [newline delimited JSON](https://github.com/ndjson/ndjson-spec). Each JSON object is a [LogEntry](definitions.md#logentry) | `application/x-ndjson` | | 200 OK | | The logs from the process formatted as plain text, similar to the output of the JSON console formatter. | `text/plain` | -| 202 Accepted | | When an egress provider is specified, the Location header containers the URI of the operation for querying the egress status. | | +| 202 Accepted | | When an egress provider is specified,. | | | 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | | 429 Too Many Requests | | There are too many logs requests at this time. Try to request logs at a later time. | `application/problem+json` | +> **NOTE: (8.0+)** Regardless if an egress provider is specified if the request was successful (response codes 200 or 202), the Location header contains the URI of the operation. This can be used to query the status of the operation or change its state. + ## Examples ### Sample Request @@ -78,6 +80,7 @@ The log statements logged at the Information level or higher for 1 minute is ret ```http HTTP/1.1 200 OK Content-Type: text/plain +Location: localhost:52323/operations/67f07e40-5cca-4709-9062-26302c484f18 info: Agent.RequestProcessor[3][ProcessRequest] Processing request 353f398a-dc74-4adc-b107-ec35edd09968. diff --git a/documentation/api/operations-delete.md b/documentation/api/operations-delete.md index 50cddb21105..49c1f4558f6 100644 --- a/documentation/api/operations-delete.md +++ b/documentation/api/operations-delete.md @@ -3,7 +3,7 @@ # Operations - Delete -Cancel a running operation. Only valid against operations in the `Running` state. Transitions the operation to `Cancelled` state. +Cancel a running operation. Only valid against operations in the `Running` or `Stopping` state. Transitions the operation to `Cancelled` state. Cancelling an operation may result in an incomplete or unreadable artifact. To stop an operation early while still producing a valid artifact, use the [Stop Operation](operations-stop.md). ## HTTP Route @@ -45,6 +45,7 @@ Authorization: Bearer fffffffffffffffffffffffffffffffffffffffffff= ```http HTTP/1.1 200 OK +``` ## Supported Runtimes diff --git a/documentation/api/operations-get.md b/documentation/api/operations-get.md index 6f29c433829..203490accee 100644 --- a/documentation/api/operations-get.md +++ b/documentation/api/operations-get.md @@ -53,6 +53,8 @@ Content-Type: application/json "operationId": "67f07e40-5cca-4709-9062-26302c484f18", "createdDateTime": "2021-07-21T06:21:15.315861Z", "status": "Succeeded", + "egressProviderName": "monitorBlob", + "isStoppable": true, "process": { "pid":1, diff --git a/documentation/api/operations-list.md b/documentation/api/operations-list.md index aa5bc67cdcc..2dbde05537f 100644 --- a/documentation/api/operations-list.md +++ b/documentation/api/operations-list.md @@ -65,7 +65,22 @@ Content-Type: application/json { "operationId": "67f07e40-5cca-4709-9062-26302c484f18", "createdDateTime": "2021-07-21T06:21:15.315861Z", - "status": "Succeeded", + "status": "Succeeded", + "egressProviderName": "monitorBlob", + "isStoppable": false, + "process": + { + "pid":1, + "uid":"95b0202a-4ed3-44a6-98f1-767d270ec783", + "name":"dotnet-monitor-demo" + } + }, + { + "operationId": "06ac07e2-f7cd-45ad-80c6-e38160bc5881", + "createdDateTime": "2021-07-21T20:22:15.315861Z", + "status": "Stopping", + "egressProviderName": null, + "isStoppable": false, "process": { "pid":1, @@ -76,7 +91,9 @@ Content-Type: application/json { "operationId": "26e74e52-0a16-4e84-84bb-27f904bfaf85", "createdDateTime": "2021-07-21T23:30:22.3058272Z", - "status": "Failed", + "status": "Failed", + "egressProviderName": "monitorBlob", + "isStoppable": false, "process": { "pid":11782, @@ -105,7 +122,9 @@ Content-Type: application/json { "operationId": "67f07e40-5cca-4709-9062-26302c484f18", "createdDateTime": "2021-07-21T06:21:15.315861Z", - "status": "Succeeded", + "status": "Succeeded", + "egressProviderName": "monitorBlob", + "isStoppable": false, "process": { "pid":1, diff --git a/documentation/api/operations-stop.md b/documentation/api/operations-stop.md new file mode 100644 index 00000000000..6deaec64e34 --- /dev/null +++ b/documentation/api/operations-stop.md @@ -0,0 +1,58 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Foperations-stop) + +# Operations - Stop (8.0+) + +Gracefully stops a running operation. Only valid against operations with the `isStoppable` property set to `true`, not all operations support being gracefully stopped. Transitions the operation to `Succeeded` or `Failed` state depending on if the operation was successful. + +Stopping an operation may not happen immediately such as in the case of traces where stopping may collect rundown information. An operation in the `Stopping` state can still be cancelled using [Delete Operation](operations-delete.md). + +## HTTP Route + +```http +DELETE /operations/{operationId}?stop=true HTTP/1.1 +``` + +## Host Address + +The default host address for these routes is `https://localhost:52323`. This route is only available on the addresses configured via the `--urls` command line parameter and the `DOTNETMONITOR_URLS` environment variable. + +## Authentication + +Authentication is enforced for this route. See [Authentication](./../authentication.md) for further information. + +Allowed schemes: +- `Bearer` +- `Negotiate` (Windows only, running as unelevated) + +## Responses + +| Name | Type | Description | Content Type | +|---|---|---|---| +| 202 Accepted | | The operation was successfully queued to stop. | `application/json` | +| 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | +| 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | + +## Examples + +### Sample Request + +```http +DELETE /operations/67f07e40-5cca-4709-9062-26302c484f18?stop=true HTTP/1.1 +Host: localhost:52323 +Authorization: Bearer fffffffffffffffffffffffffffffffffffffffffff= +``` + +### Sample Response + +```http +HTTP/1.1 202 OK +``` + +## Supported Runtimes + +| Operating System | Runtime Version | +|---|---| +| Windows | .NET Core 3.1, .NET 5+ | +| Linux | .NET Core 3.1, .NET 5+ | +| MacOS | .NET Core 3.1, .NET 5+ | diff --git a/documentation/api/operations.md b/documentation/api/operations.md index 1241fb49662..6851da5eac0 100644 --- a/documentation/api/operations.md +++ b/documentation/api/operations.md @@ -10,3 +10,4 @@ Operations are used to track long running operations in dotnet-monitor, specific | [List Operations](operations-list.md) | Lists all the operations and their current state. | | [Get Operation](operations-get.md) | Get detailed information about an operation. | | [Delete Operation](operations-delete.md) | Cancels a running operation. | +| [Stop Operation](operations-stop.md) (8.0+) | Gracefully stops a running operation. | diff --git a/documentation/api/stacks.md b/documentation/api/stacks.md index 862f7056588..27bf84e753d 100644 --- a/documentation/api/stacks.md +++ b/documentation/api/stacks.md @@ -46,11 +46,13 @@ Allowed schemes: |---|---|---|---| | 200 OK | [CallStackResult](definitions.md#experimental-callstackresult-70) | Callstacks for all managed threads in the process. | `application/json` | | 200 OK | text | Text representation of callstacks in the process. | `text/plain` | -| 202 Accepted | | When an egress provider is specified, the Location header containers the URI of the operation for querying the egress status. | | +| 202 Accepted | | When an egress provider is specified, the artifact has begun being collected. | | | 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | | 429 Too Many Requests | | There are too many stack requests at this time. Try to request a stack at a later time. | `application/problem+json` | +> **NOTE: (8.0+)** Regardless if an egress provider is specified if the request was successful (response codes 200 or 202), the Location header contains the URI of the operation. This can be used to query the status of the operation or change its state. + ## Examples ### Sample Request @@ -67,6 +69,7 @@ Accept: application/json ```http HTTP/1.1 200 OK Content-Type: application/json +Location: localhost:52323/operations/67f07e40-5cca-4709-9062-26302c484f18 { "threadId": 30860, @@ -103,6 +106,7 @@ Accept: text/plain ```http HTTP/1.1 200 OK Content-Type: text/plain +Location: localhost:52323/operations/67f07e40-5cca-4709-9062-26302c484f18 Thread: (0x68C0) System.Private.CoreLib.dll!System.Threading.Monitor.Wait diff --git a/documentation/api/trace-custom.md b/documentation/api/trace-custom.md index 302215323e4..48cfa9e2c67 100644 --- a/documentation/api/trace-custom.md +++ b/documentation/api/trace-custom.md @@ -49,12 +49,14 @@ The expected content type is `application/json`. | Name | Type | Description | Content Type | |---|---|---|---| -| 200 OK | stream | A trace of the process. | `application/octet-stream` | -| 202 Accepted | | When an egress provider is specified, the Location header containers the URI of the operation for querying the egress status. | | +| 200 OK | stream | A trace of the process when no egress provider is specified. | `application/octet-stream` | +| 202 Accepted | | When an egress provider is specified, the artifact has begun being collected. | | | 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | | 429 Too Many Requests | | There are too many trace requests at this time. Try to request a trace at a later time. | `application/problem+json` | +> **NOTE: (8.0+)** Regardless if an egress provider is specified if the request was successful (response codes 200 or 202), the Location header contains the URI of the operation. This can be used to query the status of the operation or change its state. + > **NOTE:** After the expiration of the trace duration, completing the request may take a long time (up to several minutes) for large applications if `EventProvidersConfiguration.RequestRundown` is set to `true`. The runtime needs to send over the type cache for all managed code that was captured in the trace, known as rundown events. Thus, the length of time of the request may take significantly longer than the requested duration. ## Examples @@ -109,6 +111,7 @@ The 1 minute trace with CPU information, chunk encoded, is returned as the respo HTTP/1.1 200 OK Content-Type: application/octet-stream Transfer-Encoding: chunked +Location: localhost:52323/operations/67f07e40-5cca-4709-9062-26302c484f18 ``` ## Supported Runtimes diff --git a/documentation/api/trace-get.md b/documentation/api/trace-get.md index 40c68261e9c..04568dce052 100644 --- a/documentation/api/trace-get.md +++ b/documentation/api/trace-get.md @@ -44,12 +44,14 @@ Allowed schemes: | Name | Type | Description | Content Type | |---|---|---|---| -| 200 OK | stream | A trace of the process. | `application/octet-stream` | -| 202 Accepted | | When an egress provider is specified, the Location header containers the URI of the operation for querying the egress status. | | +| 200 OK | stream | A trace of the process when no egress provider is specified. | `application/octet-stream` | +| 202 Accepted | | When an egress provider is specified, the artifact has begun being collected. | | | 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | | 429 Too Many Requests | | There are too many trace requests at this time. Try to request a trace at a later time. | `application/problem+json` | +> **NOTE:** Regardless if an egress provider is specified if the request was successful (response codes 200 or 202), the Location header contains the URI of the operation. This can be used to query the status of the operation or change its state. + > **NOTE:** After the expiration of the trace duration, completing the request may take a long time (up to several minutes) for large applications. The runtime needs to send over the type cache for all managed code that was captured in the trace, known as rundown events. Thus, the length of time of the request may take significantly longer than the requested duration. ## Examples @@ -78,6 +80,7 @@ The 1 minute trace with http request handling and metric information, chunk enco HTTP/1.1 200 OK Content-Type: application/octet-stream Transfer-Encoding: chunked +Location: localhost:52323/operations/67f07e40-5cca-4709-9062-26302c484f18 ``` ## Supported Runtimes 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 a4d2c05a244..d4e936fe6fd 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/LoggingExtensions.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/LoggingExtensions.cs @@ -70,6 +70,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); @@ -119,5 +125,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 f2b469608bf..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,15 @@ 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}'. /// diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.resx b/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.resx index ba9688e00de..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,12 @@ 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}' diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs index 050b6856186..69e7294d0c4 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs @@ -8,23 +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 { - // Buffer size matches FileStreamResult - private const int DefaultBufferSize = 0x10000; - - public static string GenerateTraceFileName(IEndpointInfo endpointInfo) - { - return FormattableString.Invariant($"{Utilities.GetFileNameTimeStampUtcNow()}_{endpointInfo.ProcessId}.nettrace"); - } - public static MonitoringSourceConfiguration GetTraceConfiguration(Models.TraceProfile profile, float metricsIntervalSeconds) { var configurations = new List(); @@ -76,73 +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); - } - //CONSIDER Should we allow client to change the buffer size? - await eventStream.CopyToAsync(outputStream, DefaultBufferSize, token); - }; - - var client = new DiagnosticsClient(endpointInfo.Endpoint); - - await using EventTracePipeline pipeProcessor = new EventTracePipeline(client, new EventTracePipelineSettings - { - Configuration = configuration, - Duration = duration, - }, streamAvailable); - - await pipeProcessor.RunAsync(token); - } - - public static async Task CaptureTraceUntilEventAsync(TaskCompletionSource startCompletionSource, IEndpointInfo endpointInfo, MonitoringSourceConfiguration configuration, TimeSpan timeout, Stream outputStream, string providerName, string eventName, IDictionary payloadFilter, ILogger logger, CancellationToken token) - { - DiagnosticsClient client = new(endpointInfo.Endpoint); - TaskCompletionSource stoppingEventHitSource = new(TaskCreationOptions.RunContinuationsAsynchronously); - - using IDisposable registration = token.Register( - () => stoppingEventHitSource.TrySetCanceled(token)); - - await using EventTracePipeline pipeProcessor = new(client, new EventTracePipelineSettings - { - Configuration = configuration, - Duration = timeout, - }, - async (eventStream, token) => - { - startCompletionSource?.TrySetResult(null); - await using EventMonitoringPassthroughStream eventMonitoringStream = new( - providerName, - eventName, - payloadFilter, - onEvent: (traceEvent) => - { - logger.StoppingTraceEventHit(traceEvent); - stoppingEventHitSource.TrySetResult(null); - }, - onPayloadFilterMismatch: logger.StoppingTraceEventPayloadFilterMismatch, - eventStream, - outputStream, - DefaultBufferSize, - callOnEventOnlyOnce: true, - leaveDestinationStreamOpen: true /* We do not have ownership of the outputStream */); - - await eventMonitoringStream.ProcessAsync(token); - }); - - Task pipelineRunTask = pipeProcessor.RunAsync(token); - await Task.WhenAny(pipelineRunTask, stoppingEventHitSource.Task).Unwrap(); - - if (stoppingEventHitSource.Task.IsCompleted) - { - await pipeProcessor.StopAsync(token); - await pipelineRunTask; - } - } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TraceTestUtilities.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TraceTestUtilities.cs index 533962327cd..0c088a1ff01 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TraceTestUtilities.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TraceTestUtilities.cs @@ -3,6 +3,7 @@ // 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; @@ -12,25 +13,40 @@ namespace Microsoft.Diagnostics.Monitoring.TestCommon { public static class TraceTestUtilities { - public static async Task ValidateTrace(Stream traceStream) + 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()); + 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 c10e292225f..edf757c69ac 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; @@ -563,6 +564,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}"); @@ -628,6 +656,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 a353491ffb1..f3c1b200bbe 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ApiClientExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ApiClientExtensions.cs @@ -411,6 +411,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); @@ -423,6 +429,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); @@ -444,10 +456,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 49d33789a26..c9a540e8998 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 b3d4dc8dd2e..4bbe48ebd36 100644 --- a/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs +++ b/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectTraceAction.cs @@ -8,7 +8,6 @@ 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; @@ -86,31 +85,38 @@ protected override async Task ExecuteCoreAsync( stoppingEvent = Options.StoppingEvent; } - 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); - if (null != stoppingEvent) - { - ILogger logger = _serviceProvider - .GetRequiredService() - .CreateLogger(); - - await TraceUtilities.CaptureTraceUntilEventAsync(startCompletionSource, EndpointInfo, configuration, duration, outputStream, stoppingEvent.ProviderName, stoppingEvent.EventName, stoppingEvent.PayloadFilter, logger, token); - } - else - { - await TraceUtilities.CaptureTraceAsync(startCompletionSource, EndpointInfo, configuration, duration, outputStream, token); - } + 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); + } + } +}