From ed90ff95cd8043918f32127b8ad381533ba99ebc Mon Sep 17 00:00:00 2001 From: Junyi Ou Date: Mon, 19 Oct 2026 11:35:34 -0400 Subject: [PATCH 1/3] chore(openapi): add the recording search endpoint to the clients Regenerate `gateway-api.yaml`, the docs, the .NET client and the TS Angular client for `POST /jet/jrec/search`, its paging, and the `gateway.recordings.search` scope. Only the search parts are kept, because the generator also rewrites the `pullRecordingFile` content types, which belong to their own change. Issue: DGW-463 Co-Authored-By: Claude Opus 5.5 --- devolutions-gateway/openapi/doc/index.adoc | 283 ++++++++++++++++++ .../dotnet-client/.openapi-generator/FILES | 10 + .../openapi/dotnet-client/README.md | 6 + .../openapi/dotnet-client/docs/JrecApi.md | 102 +++++++ .../dotnet-client/docs/RecordingSearchHit.md | 16 + .../docs/RecordingSearchRequest.md | 15 + .../docs/RecordingSearchResponse.md | 12 + .../dotnet-client/docs/RecordingSelection.md | 10 + .../dotnet-client/docs/StartTimeRange.md | 12 + .../Devolutions.Gateway.Client/Api/JrecApi.cs | 173 +++++++++++ .../Model/AccessScope.cs | 10 +- .../Model/RecordingSearchHit.cs | 162 ++++++++++ .../Model/RecordingSearchRequest.cs | 156 ++++++++++ .../Model/RecordingSearchResponse.cs | 105 +++++++ .../Model/RecordingSelection.cs | 264 ++++++++++++++++ .../Model/StartTimeRange.cs | 95 ++++++ devolutions-gateway/openapi/gateway-api.yaml | 160 ++++++++++ .../.openapi-generator/FILES | 5 + .../ts-angular-client/api/jrec.service.ts | 86 ++++++ .../ts-angular-client/model/accessScope.ts | 5 +- .../openapi/ts-angular-client/model/models.ts | 5 + .../model/recordingSearchHit.ts | 41 +++ .../model/recordingSearchRequest.ts | 33 ++ .../model/recordingSearchResponse.ts | 26 ++ .../model/recordingSelection.ts | 21 ++ .../ts-angular-client/model/startTimeRange.ts | 25 ++ 26 files changed, 1835 insertions(+), 3 deletions(-) create mode 100644 devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchHit.md create mode 100644 devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchRequest.md create mode 100644 devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchResponse.md create mode 100644 devolutions-gateway/openapi/dotnet-client/docs/RecordingSelection.md create mode 100644 devolutions-gateway/openapi/dotnet-client/docs/StartTimeRange.md create mode 100644 devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchHit.cs create mode 100644 devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchRequest.cs create mode 100644 devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchResponse.cs create mode 100644 devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSelection.cs create mode 100644 devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/StartTimeRange.cs create mode 100644 devolutions-gateway/openapi/ts-angular-client/model/recordingSearchHit.ts create mode 100644 devolutions-gateway/openapi/ts-angular-client/model/recordingSearchRequest.ts create mode 100644 devolutions-gateway/openapi/ts-angular-client/model/recordingSearchResponse.ts create mode 100644 devolutions-gateway/openapi/ts-angular-client/model/recordingSelection.ts create mode 100644 devolutions-gateway/openapi/ts-angular-client/model/startTimeRange.ts diff --git a/devolutions-gateway/openapi/doc/index.adoc b/devolutions-gateway/openapi/doc/index.adoc index 04e0934d0f..4ba40c0cfc 100644 --- a/devolutions-gateway/openapi/doc/index.adoc +++ b/devolutions-gateway/openapi/doc/index.adoc @@ -1426,6 +1426,102 @@ ifdef::internal-generation[] endif::internal-generation[] +[.searchRecordings] +==== searchRecordings + +`POST /jet/jrec/search` + +Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance + +===== Description + +The query is a case-insensitive substring, matched against the raw text of each line. Gateway does not interpret the logs, so any part of a line can match, field names included. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, and a line longer than 256 KiB is matched on its first 256 KiB only. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + + +// markup not found, no include::{specDir}jet/jrec/search/POST/spec.adoc[opts=optional] + + + +===== Security + +[cols="2,1,1"] +|=== +| Name | Type | Scheme + +| `scope_token` +| http +| bearer +|=== + +===== Parameters + + +====== Body Parameter + +[cols="2,3,1,1,1"] +|=== +|Name| Description| Required| Default| Pattern + +| RecordingSearchRequest +| JSON-encoded search request <> +| X +| +| + +|=== + + + + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.HTTP Response Codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| Matching log lines +| <> + + +| 400 +| Bad request +| <<>> + + +| 401 +| Invalid or missing authorization token +| <<>> + + +| 403 +| Insufficient permissions +| <<>> + +|=== + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}jet/jrec/search/POST/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + [.Jrl] === Jrl @@ -3371,6 +3467,7 @@ endif::internal-generation[] | gateway.net.monitor.drain | gateway.agent.delete | gateway.agent.read +| gateway.recordings.search |=== @@ -5203,6 +5300,161 @@ Current auto-update schedule for Devolutions Agent. |=== +[#RecordingSearchHit] +=== _RecordingSearchHit_ + +A line that holds the search text. + + +[.fields-RecordingSearchHit] +[cols="2,1,1,2,4,1"] +|=== +| Field Name| Required| Nullable | Type| Description | Format + +| fileName +| X +| +| String +| Name of the `.slog` artifact holding the line, as listed in `recording.json`. +| + +| line +| X +| +| String +| The line's text without its line break; bytes that are not UTF-8 read as U+FFFD. +| + +| lineNumber +| X +| +| Long +| Number of the line in the file, counting from 1. +| int64 + +| offset +| X +| +| Long +| Byte offset of the line in the file. +| int64 + +| recordingId +| X +| +| UUID +| The recording whose artifact holds the line. +| uuid + +| truncated +| X +| +| Boolean +| The line is longer than 256 KiB, so `line` holds only its start, and the file holds the rest. +| + +|=== + + + +[#RecordingSearchRequest] +=== _RecordingSearchRequest_ + +What to search for, and in which recordings. + +Unknown fields are rejected, so that an option added later is never silently ignored by a Gateway that predates it. + + +[.fields-RecordingSearchRequest] +[cols="2,1,1,2,4,1"] +|=== +| Field Name| Required| Nullable | Type| Description | Format + +| cursor +| +| X +| String +| Where the page starts: the `nextCursor` of the previous page, sent unchanged with the same `query`, `recordings` and `startTime`. +| + +| maxHits +| +| X +| Integer +| Most hits in the page, from 1 to 1000; 1000 when missing. +| int32 + +| query +| X +| +| String +| Text to search for, ignoring case; leading and trailing spaces are part of it. +| + +| recordings +| X +| +| <> +| +| + +| startTime +| +| X +| <> +| +| + +|=== + + + +[#RecordingSearchResponse] +=== _RecordingSearchResponse_ + +The lines that hold the search text. + + +[.fields-RecordingSearchResponse] +[cols="2,1,1,2,4,1"] +|=== +| Field Name| Required| Nullable | Type| Description | Format + +| hits +| X +| +| List of <> +| Matching lines, newest recording first when searching all recordings, otherwise in the requested order. +| + +| nextCursor +| +| X +| String +| Where the next page starts; absent when every log has been read. The cursor is opaque, and its format may change between versions. A page with fewer hits than asked for may still be followed by more pages. +| + +|=== + + + +[#RecordingSelection] +=== _RecordingSelection_ + +`"all"`, or a list of recording IDs. + +A repeated ID is searched once, and an empty list searches nothing. + + +[.fields-RecordingSelection] +[cols="2,1,1,2,4,1"] +|=== +| Field Name| Required| Nullable | Type| Description | Format + +|=== + + + [#ScanOriginDto] === _ScanOriginDto_ @@ -5470,6 +5722,37 @@ Desired auto-update schedule to apply to Devolutions Agent. +[#StartTimeRange] +=== _StartTimeRange_ + +A range of recording start times, as written in `recording.json`, that includes `from` and excludes `to`. + +A missing end leaves that side of the range open. + + +[.fields-StartTimeRange] +[cols="2,1,1,2,4,1"] +|=== +| Field Name| Required| Nullable | Type| Description | Format + +| from +| +| X +| Date +| Earliest start time searched. +| date-time + +| to +| +| X +| Date +| Recordings that start at this time or later are not searched. +| date-time + +|=== + + + [#SubProvisionerKey] === _SubProvisionerKey_ diff --git a/devolutions-gateway/openapi/dotnet-client/.openapi-generator/FILES b/devolutions-gateway/openapi/dotnet-client/.openapi-generator/FILES index b49a091545..b2982cd9e2 100644 --- a/devolutions-gateway/openapi/dotnet-client/.openapi-generator/FILES +++ b/devolutions-gateway/openapi/dotnet-client/.openapi-generator/FILES @@ -59,6 +59,10 @@ docs/PreflightOperationKind.md docs/PreflightOutput.md docs/PreflightOutputKind.md docs/PubKeyFormat.md +docs/RecordingSearchHit.md +docs/RecordingSearchRequest.md +docs/RecordingSearchResponse.md +docs/RecordingSelection.md docs/ScanOriginDto.md docs/ScanResultSourceDto.md docs/SessionInfo.md @@ -67,6 +71,7 @@ docs/SessionTokenSignRequest.md docs/SessionsApi.md docs/SetConfigResponse.md docs/SetUpdateScheduleRequest.md +docs/StartTimeRange.md docs/SubProvisionerKey.md docs/Subscriber.md docs/TargetConnectionOptions.md @@ -158,6 +163,10 @@ src/Devolutions.Gateway.Client/Model/PreflightOperationKind.cs src/Devolutions.Gateway.Client/Model/PreflightOutput.cs src/Devolutions.Gateway.Client/Model/PreflightOutputKind.cs src/Devolutions.Gateway.Client/Model/PubKeyFormat.cs +src/Devolutions.Gateway.Client/Model/RecordingSearchHit.cs +src/Devolutions.Gateway.Client/Model/RecordingSearchRequest.cs +src/Devolutions.Gateway.Client/Model/RecordingSearchResponse.cs +src/Devolutions.Gateway.Client/Model/RecordingSelection.cs src/Devolutions.Gateway.Client/Model/ScanOriginDto.cs src/Devolutions.Gateway.Client/Model/ScanResultSourceDto.cs src/Devolutions.Gateway.Client/Model/SessionInfo.cs @@ -165,6 +174,7 @@ src/Devolutions.Gateway.Client/Model/SessionTokenContentType.cs src/Devolutions.Gateway.Client/Model/SessionTokenSignRequest.cs src/Devolutions.Gateway.Client/Model/SetConfigResponse.cs src/Devolutions.Gateway.Client/Model/SetUpdateScheduleRequest.cs +src/Devolutions.Gateway.Client/Model/StartTimeRange.cs src/Devolutions.Gateway.Client/Model/SubProvisionerKey.cs src/Devolutions.Gateway.Client/Model/Subscriber.cs src/Devolutions.Gateway.Client/Model/TargetConnectionOptions.cs diff --git a/devolutions-gateway/openapi/dotnet-client/README.md b/devolutions-gateway/openapi/dotnet-client/README.md index c60cd47e28..f08c67eb32 100644 --- a/devolutions-gateway/openapi/dotnet-client/README.md +++ b/devolutions-gateway/openapi/dotnet-client/README.md @@ -156,6 +156,7 @@ Class | Method | HTTP request | Description *JrecApi* | [**ListRecordings**](docs/JrecApi.md#listrecordings) | **GET** /jet/jrec/list | Lists all recordings stored on this instance *JrecApi* | [**PullRecordingFile**](docs/JrecApi.md#pullrecordingfile) | **GET** /jet/jrec/pull/{id}/{filename} | Retrieves a recording file for a given session *JrecApi* | [**PullRecordingSession**](docs/JrecApi.md#pullrecordingsession) | **GET** /jet/jrec/pull/{id} | Downloads an entire recorded session as a ZIP archive +*JrecApi* | [**SearchRecordings**](docs/JrecApi.md#searchrecordings) | **POST** /jet/jrec/search | Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance *JrlApi* | [**GetJrlInfo**](docs/JrlApi.md#getjrlinfo) | **GET** /jet/jrl/info | Retrieves current JRL (Json Revocation List) info *JrlApi* | [**UpdateJrl**](docs/JrlApi.md#updatejrl) | **POST** /jet/jrl | Updates JRL (Json Revocation List) using a JRL token *NetApi* | [**GetNetConfig**](docs/NetApi.md#getnetconfig) | **GET** /jet/net/config | Lists network interfaces @@ -227,6 +228,10 @@ Class | Method | HTTP request | Description - [Model.PreflightOutput](docs/PreflightOutput.md) - [Model.PreflightOutputKind](docs/PreflightOutputKind.md) - [Model.PubKeyFormat](docs/PubKeyFormat.md) + - [Model.RecordingSearchHit](docs/RecordingSearchHit.md) + - [Model.RecordingSearchRequest](docs/RecordingSearchRequest.md) + - [Model.RecordingSearchResponse](docs/RecordingSearchResponse.md) + - [Model.RecordingSelection](docs/RecordingSelection.md) - [Model.ScanOriginDto](docs/ScanOriginDto.md) - [Model.ScanResultSourceDto](docs/ScanResultSourceDto.md) - [Model.SessionInfo](docs/SessionInfo.md) @@ -234,6 +239,7 @@ Class | Method | HTTP request | Description - [Model.SessionTokenSignRequest](docs/SessionTokenSignRequest.md) - [Model.SetConfigResponse](docs/SetConfigResponse.md) - [Model.SetUpdateScheduleRequest](docs/SetUpdateScheduleRequest.md) + - [Model.StartTimeRange](docs/StartTimeRange.md) - [Model.SubProvisionerKey](docs/SubProvisionerKey.md) - [Model.Subscriber](docs/Subscriber.md) - [Model.TargetConnectionOptions](docs/TargetConnectionOptions.md) diff --git a/devolutions-gateway/openapi/dotnet-client/docs/JrecApi.md b/devolutions-gateway/openapi/dotnet-client/docs/JrecApi.md index 5777700acd..1bad0d5b48 100644 --- a/devolutions-gateway/openapi/dotnet-client/docs/JrecApi.md +++ b/devolutions-gateway/openapi/dotnet-client/docs/JrecApi.md @@ -9,6 +9,7 @@ All URIs are relative to *http://localhost* | [**ListRecordings**](JrecApi.md#listrecordings) | **GET** /jet/jrec/list | Lists all recordings stored on this instance | | [**PullRecordingFile**](JrecApi.md#pullrecordingfile) | **GET** /jet/jrec/pull/{id}/{filename} | Retrieves a recording file for a given session | | [**PullRecordingSession**](JrecApi.md#pullrecordingsession) | **GET** /jet/jrec/pull/{id} | Downloads an entire recorded session as a ZIP archive | +| [**SearchRecordings**](JrecApi.md#searchrecordings) | **POST** /jet/jrec/search | Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance | # **DeleteManyRecordings** @@ -513,3 +514,104 @@ catch (ApiException e) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **SearchRecordings** +> RecordingSearchResponse SearchRecordings (RecordingSearchRequest recordingSearchRequest) + +Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance + +The query is a case-insensitive substring, matched against the raw text of each line. Gateway does not interpret the logs, so any part of a line can match, field names included. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, and a line longer than 256 KiB is matched on its first 256 KiB only. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + +### Example +```csharp +using System.Collections.Generic; +using System.Diagnostics; +using System.Net.Http; +using Devolutions.Gateway.Client.Api; +using Devolutions.Gateway.Client.Client; +using Devolutions.Gateway.Client.Model; + +namespace Example +{ + public class SearchRecordingsExample + { + public static void Main() + { + Configuration config = new Configuration(); + config.BasePath = "http://localhost"; + // Configure Bearer token for authorization: scope_token + config.AccessToken = "YOUR_BEARER_TOKEN"; + + // create instances of HttpClient, HttpClientHandler to be reused later with different Api classes + HttpClient httpClient = new HttpClient(); + HttpClientHandler httpClientHandler = new HttpClientHandler(); + var apiInstance = new JrecApi(httpClient, config, httpClientHandler); + var recordingSearchRequest = new RecordingSearchRequest(); // RecordingSearchRequest | JSON-encoded search request + + try + { + // Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance + RecordingSearchResponse result = apiInstance.SearchRecordings(recordingSearchRequest); + Debug.WriteLine(result); + } + catch (ApiException e) + { + Debug.Print("Exception when calling JrecApi.SearchRecordings: " + e.Message); + Debug.Print("Status Code: " + e.ErrorCode); + Debug.Print(e.StackTrace); + } + } + } +} +``` + +#### Using the SearchRecordingsWithHttpInfo variant +This returns an ApiResponse object which contains the response data, status code and headers. + +```csharp +try +{ + // Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance + ApiResponse response = apiInstance.SearchRecordingsWithHttpInfo(recordingSearchRequest); + Debug.Write("Status Code: " + response.StatusCode); + Debug.Write("Response Headers: " + response.Headers); + Debug.Write("Response Body: " + response.Data); +} +catch (ApiException e) +{ + Debug.Print("Exception when calling JrecApi.SearchRecordingsWithHttpInfo: " + e.Message); + Debug.Print("Status Code: " + e.ErrorCode); + Debug.Print(e.StackTrace); +} +``` + +### Parameters + +| Name | Type | Description | Notes | +|------|------|-------------|-------| +| **recordingSearchRequest** | [**RecordingSearchRequest**](RecordingSearchRequest.md) | JSON-encoded search request | | + +### Return type + +[**RecordingSearchResponse**](RecordingSearchResponse.md) + +### Authorization + +[scope_token](../README.md#scope_token) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +| **200** | Matching log lines | - | +| **400** | Bad request | - | +| **401** | Invalid or missing authorization token | - | +| **403** | Insufficient permissions | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchHit.md b/devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchHit.md new file mode 100644 index 0000000000..48b77be83b --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchHit.md @@ -0,0 +1,16 @@ +# Devolutions.Gateway.Client.Model.RecordingSearchHit +A line that holds the search text. + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**FileName** | **string** | Name of the `.slog` artifact holding the line, as listed in `recording.json`. | +**Line** | **string** | The line's text without its line break; bytes that are not UTF-8 read as U+FFFD. | +**LineNumber** | **long** | Number of the line in the file, counting from 1. | +**Offset** | **long** | Byte offset of the line in the file. | +**RecordingId** | **Guid** | The recording whose artifact holds the line. | +**Truncated** | **bool** | The line is longer than 256 KiB, so `line` holds only its start, and the file holds the rest. | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchRequest.md b/devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchRequest.md new file mode 100644 index 0000000000..f97d24c465 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchRequest.md @@ -0,0 +1,15 @@ +# Devolutions.Gateway.Client.Model.RecordingSearchRequest +What to search for, and in which recordings. Unknown fields are rejected, so that an option added later is never silently ignored by a Gateway that predates it. + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Cursor** | **string** | Where the page starts: the `nextCursor` of the previous page, sent unchanged with the same `query`, `recordings` and `startTime`. | [optional] +**MaxHits** | **int?** | Most hits in the page, from 1 to 1000; 1000 when missing. | [optional] +**Query** | **string** | Text to search for, ignoring case; leading and trailing spaces are part of it. | +**Recordings** | [**RecordingSelection**](RecordingSelection.md) | | +**StartTime** | [**StartTimeRange**](StartTimeRange.md) | | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchResponse.md b/devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchResponse.md new file mode 100644 index 0000000000..7ae5974adf --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchResponse.md @@ -0,0 +1,12 @@ +# Devolutions.Gateway.Client.Model.RecordingSearchResponse +The lines that hold the search text. + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Hits** | [**List<RecordingSearchHit>**](RecordingSearchHit.md) | Matching lines, newest recording first when searching all recordings, otherwise in the requested order. | +**NextCursor** | **string** | Where the next page starts; absent when every log has been read. The cursor is opaque, and its format may change between versions. A page with fewer hits than asked for may still be followed by more pages. | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/RecordingSelection.md b/devolutions-gateway/openapi/dotnet-client/docs/RecordingSelection.md new file mode 100644 index 0000000000..167f6167ef --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/RecordingSelection.md @@ -0,0 +1,10 @@ +# Devolutions.Gateway.Client.Model.RecordingSelection +`\"all\"`, or a list of recording IDs. A repeated ID is searched once, and an empty list searches nothing. + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/StartTimeRange.md b/devolutions-gateway/openapi/dotnet-client/docs/StartTimeRange.md new file mode 100644 index 0000000000..961921e2ef --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/StartTimeRange.md @@ -0,0 +1,12 @@ +# Devolutions.Gateway.Client.Model.StartTimeRange +A range of recording start times, as written in `recording.json`, that includes `from` and excludes `to`. A missing end leaves that side of the range open. + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**From** | **DateTime?** | Earliest start time searched. | [optional] +**To** | **DateTime?** | Recordings that start at this time or later are not searched. | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/JrecApi.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/JrecApi.cs index 17a031fcd6..b64e2fb56d 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/JrecApi.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/JrecApi.cs @@ -126,6 +126,27 @@ public interface IJrecApiSync : IApiAccessor /// Recorded session ID /// ApiResponse of FileParameter ApiResponse PullRecordingSessionWithHttpInfo(Guid id); + /// + /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance + /// + /// + /// The query is a case-insensitive substring, matched against the raw text of each line. Gateway does not interpret the logs, so any part of a line can match, field names included. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, and a line longer than 256 KiB is matched on its first 256 KiB only. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// + /// Thrown when fails to make API call + /// JSON-encoded search request + /// RecordingSearchResponse + RecordingSearchResponse SearchRecordings(RecordingSearchRequest recordingSearchRequest); + + /// + /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance + /// + /// + /// The query is a case-insensitive substring, matched against the raw text of each line. Gateway does not interpret the logs, so any part of a line can match, field names included. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, and a line longer than 256 KiB is matched on its first 256 KiB only. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// + /// Thrown when fails to make API call + /// JSON-encoded search request + /// ApiResponse of RecordingSearchResponse + ApiResponse SearchRecordingsWithHttpInfo(RecordingSearchRequest recordingSearchRequest); #endregion Synchronous Operations } @@ -252,6 +273,29 @@ public interface IJrecApiAsync : IApiAccessor /// Cancellation Token to cancel the request. /// Task of ApiResponse (FileParameter) System.Threading.Tasks.Task> PullRecordingSessionWithHttpInfoAsync(Guid id, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); + /// + /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance + /// + /// + /// The query is a case-insensitive substring, matched against the raw text of each line. Gateway does not interpret the logs, so any part of a line can match, field names included. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, and a line longer than 256 KiB is matched on its first 256 KiB only. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// + /// Thrown when fails to make API call + /// JSON-encoded search request + /// Cancellation Token to cancel the request. + /// Task of RecordingSearchResponse + System.Threading.Tasks.Task SearchRecordingsAsync(RecordingSearchRequest recordingSearchRequest, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); + + /// + /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance + /// + /// + /// The query is a case-insensitive substring, matched against the raw text of each line. Gateway does not interpret the logs, so any part of a line can match, field names included. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, and a line longer than 256 KiB is matched on its first 256 KiB only. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// + /// Thrown when fails to make API call + /// JSON-encoded search request + /// Cancellation Token to cancel the request. + /// Task of ApiResponse (RecordingSearchResponse) + System.Threading.Tasks.Task> SearchRecordingsWithHttpInfoAsync(RecordingSearchRequest recordingSearchRequest, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); #endregion Asynchronous Operations } @@ -1080,5 +1124,134 @@ public Devolutions.Gateway.Client.Client.ApiResponse PullRecordin return localVarResponse; } + /// + /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance The query is a case-insensitive substring, matched against the raw text of each line. Gateway does not interpret the logs, so any part of a line can match, field names included. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, and a line longer than 256 KiB is matched on its first 256 KiB only. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// + /// Thrown when fails to make API call + /// JSON-encoded search request + /// RecordingSearchResponse + public RecordingSearchResponse SearchRecordings(RecordingSearchRequest recordingSearchRequest) + { + Devolutions.Gateway.Client.Client.ApiResponse localVarResponse = SearchRecordingsWithHttpInfo(recordingSearchRequest); + return localVarResponse.Data; + } + + /// + /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance The query is a case-insensitive substring, matched against the raw text of each line. Gateway does not interpret the logs, so any part of a line can match, field names included. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, and a line longer than 256 KiB is matched on its first 256 KiB only. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// + /// Thrown when fails to make API call + /// JSON-encoded search request + /// ApiResponse of RecordingSearchResponse + public Devolutions.Gateway.Client.Client.ApiResponse SearchRecordingsWithHttpInfo(RecordingSearchRequest recordingSearchRequest) + { + // verify the required parameter 'recordingSearchRequest' is set + if (recordingSearchRequest == null) + throw new Devolutions.Gateway.Client.Client.ApiException(400, "Missing required parameter 'recordingSearchRequest' when calling JrecApi->SearchRecordings"); + + Devolutions.Gateway.Client.Client.RequestOptions localVarRequestOptions = new Devolutions.Gateway.Client.Client.RequestOptions(); + + string[] _contentTypes = new string[] { + "application/json" + }; + + // to determine the Accept header + string[] _accepts = new string[] { + "application/json" + }; + + var localVarContentType = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderContentType(_contentTypes); + if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); + + var localVarAccept = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderAccept(_accepts); + if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); + + localVarRequestOptions.Data = recordingSearchRequest; + + // authentication (scope_token) required + // bearer authentication required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + + // make the HTTP request + var localVarResponse = this.Client.Post("/jet/jrec/search", localVarRequestOptions, this.Configuration); + + if (this.ExceptionFactory != null) + { + Exception _exception = this.ExceptionFactory("SearchRecordings", localVarResponse); + if (_exception != null) throw _exception; + } + + return localVarResponse; + } + + /// + /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance The query is a case-insensitive substring, matched against the raw text of each line. Gateway does not interpret the logs, so any part of a line can match, field names included. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, and a line longer than 256 KiB is matched on its first 256 KiB only. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// + /// Thrown when fails to make API call + /// JSON-encoded search request + /// Cancellation Token to cancel the request. + /// Task of RecordingSearchResponse + public async System.Threading.Tasks.Task SearchRecordingsAsync(RecordingSearchRequest recordingSearchRequest, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + Devolutions.Gateway.Client.Client.ApiResponse localVarResponse = await SearchRecordingsWithHttpInfoAsync(recordingSearchRequest, cancellationToken).ConfigureAwait(false); + return localVarResponse.Data; + } + + /// + /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance The query is a case-insensitive substring, matched against the raw text of each line. Gateway does not interpret the logs, so any part of a line can match, field names included. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, and a line longer than 256 KiB is matched on its first 256 KiB only. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// + /// Thrown when fails to make API call + /// JSON-encoded search request + /// Cancellation Token to cancel the request. + /// Task of ApiResponse (RecordingSearchResponse) + public async System.Threading.Tasks.Task> SearchRecordingsWithHttpInfoAsync(RecordingSearchRequest recordingSearchRequest, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + // verify the required parameter 'recordingSearchRequest' is set + if (recordingSearchRequest == null) + throw new Devolutions.Gateway.Client.Client.ApiException(400, "Missing required parameter 'recordingSearchRequest' when calling JrecApi->SearchRecordings"); + + + Devolutions.Gateway.Client.Client.RequestOptions localVarRequestOptions = new Devolutions.Gateway.Client.Client.RequestOptions(); + + string[] _contentTypes = new string[] { + "application/json" + }; + + // to determine the Accept header + string[] _accepts = new string[] { + "application/json" + }; + + + var localVarContentType = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderContentType(_contentTypes); + if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); + + var localVarAccept = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderAccept(_accepts); + if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); + + localVarRequestOptions.Data = recordingSearchRequest; + + // authentication (scope_token) required + // bearer authentication required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + + // make the HTTP request + + var localVarResponse = await this.AsynchronousClient.PostAsync("/jet/jrec/search", localVarRequestOptions, this.Configuration, cancellationToken).ConfigureAwait(false); + + if (this.ExceptionFactory != null) + { + Exception _exception = this.ExceptionFactory("SearchRecordings", localVarResponse); + if (_exception != null) throw _exception; + } + + return localVarResponse; + } + } } diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AccessScope.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AccessScope.cs index 98c7ac937d..03cf404273 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AccessScope.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AccessScope.cs @@ -145,7 +145,13 @@ public enum AccessScope /// Enum GatewayAgentRead for value: gateway.agent.read /// [EnumMember(Value = "gateway.agent.read")] - GatewayAgentRead = 19 + GatewayAgentRead = 19, + + /// + /// Enum GatewayRecordingsSearch for value: gateway.recordings.search + /// + [EnumMember(Value = "gateway.recordings.search")] + GatewayRecordingsSearch = 20 } public static class AccessScopeExtensions @@ -195,6 +201,8 @@ public static string ToValue(this AccessScope variant) return "gateway.agent.delete"; case AccessScope.GatewayAgentRead: return "gateway.agent.read"; + case AccessScope.GatewayRecordingsSearch: + return "gateway.recordings.search"; default: throw new ArgumentOutOfRangeException(nameof(variant), $"Unexpected variant: {variant}"); } diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchHit.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchHit.cs new file mode 100644 index 0000000000..e2ca5b4d5f --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchHit.cs @@ -0,0 +1,162 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// A line that holds the search text. + /// + [DataContract(Name = "RecordingSearchHit")] + public partial class RecordingSearchHit : IValidatableObject + { + /// + /// Initializes a new instance of the class. + /// + [JsonConstructorAttribute] + protected RecordingSearchHit() { } + /// + /// Initializes a new instance of the class. + /// + /// Name of the `.slog` artifact holding the line, as listed in `recording.json`. (required). + /// The line's text without its line break; bytes that are not UTF-8 read as U+FFFD. (required). + /// Number of the line in the file, counting from 1. (required). + /// Byte offset of the line in the file. (required). + /// The recording whose artifact holds the line. (required). + /// The line is longer than 256 KiB, so `line` holds only its start, and the file holds the rest. (required). + public RecordingSearchHit(string fileName = default(string), string line = default(string), long lineNumber = default(long), long offset = default(long), Guid recordingId = default(Guid), bool truncated = default(bool)) + { + // to ensure "fileName" is required (not null) + if (fileName == null) + { + throw new ArgumentNullException("fileName is a required property for RecordingSearchHit and cannot be null"); + } + this.FileName = fileName; + // to ensure "line" is required (not null) + if (line == null) + { + throw new ArgumentNullException("line is a required property for RecordingSearchHit and cannot be null"); + } + this.Line = line; + this.LineNumber = lineNumber; + this.Offset = offset; + this.RecordingId = recordingId; + this.Truncated = truncated; + } + + /// + /// Name of the `.slog` artifact holding the line, as listed in `recording.json`. + /// + /// Name of the `.slog` artifact holding the line, as listed in `recording.json`. + [DataMember(Name = "fileName", IsRequired = true, EmitDefaultValue = true)] + public string FileName { get; set; } + + /// + /// The line's text without its line break; bytes that are not UTF-8 read as U+FFFD. + /// + /// The line's text without its line break; bytes that are not UTF-8 read as U+FFFD. + [DataMember(Name = "line", IsRequired = true, EmitDefaultValue = true)] + public string Line { get; set; } + + /// + /// Number of the line in the file, counting from 1. + /// + /// Number of the line in the file, counting from 1. + [DataMember(Name = "lineNumber", IsRequired = true, EmitDefaultValue = true)] + public long LineNumber { get; set; } + + /// + /// Byte offset of the line in the file. + /// + /// Byte offset of the line in the file. + [DataMember(Name = "offset", IsRequired = true, EmitDefaultValue = true)] + public long Offset { get; set; } + + /// + /// The recording whose artifact holds the line. + /// + /// The recording whose artifact holds the line. + [DataMember(Name = "recordingId", IsRequired = true, EmitDefaultValue = true)] + public Guid RecordingId { get; set; } + + /// + /// The line is longer than 256 KiB, so `line` holds only its start, and the file holds the rest. + /// + /// The line is longer than 256 KiB, so `line` holds only its start, and the file holds the rest. + [DataMember(Name = "truncated", IsRequired = true, EmitDefaultValue = true)] + public bool Truncated { get; set; } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class RecordingSearchHit {\n"); + sb.Append(" FileName: ").Append(FileName).Append("\n"); + sb.Append(" Line: ").Append(Line).Append("\n"); + sb.Append(" LineNumber: ").Append(LineNumber).Append("\n"); + sb.Append(" Offset: ").Append(Offset).Append("\n"); + sb.Append(" RecordingId: ").Append(RecordingId).Append("\n"); + sb.Append(" Truncated: ").Append(Truncated).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public virtual string ToJson() + { + return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); + } + + /// + /// To validate all properties of the instance + /// + /// Validation context + /// Validation Result + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + // LineNumber (long) minimum + if (this.LineNumber < (long)1) + { + yield return new ValidationResult("Invalid value for LineNumber, must be a value greater than or equal to 1.", new [] { "LineNumber" }); + } + + // Offset (long) minimum + if (this.Offset < (long)0) + { + yield return new ValidationResult("Invalid value for Offset, must be a value greater than or equal to 0.", new [] { "Offset" }); + } + + yield break; + } + } + +} diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchRequest.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchRequest.cs new file mode 100644 index 0000000000..a74cdf6b41 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchRequest.cs @@ -0,0 +1,156 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// What to search for, and in which recordings. Unknown fields are rejected, so that an option added later is never silently ignored by a Gateway that predates it. + /// + [DataContract(Name = "RecordingSearchRequest")] + public partial class RecordingSearchRequest : IValidatableObject + { + /// + /// Initializes a new instance of the class. + /// + [JsonConstructorAttribute] + protected RecordingSearchRequest() { } + /// + /// Initializes a new instance of the class. + /// + /// Where the page starts: the `nextCursor` of the previous page, sent unchanged with the same `query`, `recordings` and `startTime`.. + /// Most hits in the page, from 1 to 1000; 1000 when missing.. + /// Text to search for, ignoring case; leading and trailing spaces are part of it. (required). + /// recordings (required). + /// startTime. + public RecordingSearchRequest(string cursor = default(string), int? maxHits = default(int?), string query = default(string), RecordingSelection recordings = default(RecordingSelection), StartTimeRange startTime = default(StartTimeRange)) + { + // to ensure "query" is required (not null) + if (query == null) + { + throw new ArgumentNullException("query is a required property for RecordingSearchRequest and cannot be null"); + } + this.Query = query; + // to ensure "recordings" is required (not null) + if (recordings == null) + { + throw new ArgumentNullException("recordings is a required property for RecordingSearchRequest and cannot be null"); + } + this.Recordings = recordings; + this.Cursor = cursor; + this.MaxHits = maxHits; + this.StartTime = startTime; + } + + /// + /// Where the page starts: the `nextCursor` of the previous page, sent unchanged with the same `query`, `recordings` and `startTime`. + /// + /// Where the page starts: the `nextCursor` of the previous page, sent unchanged with the same `query`, `recordings` and `startTime`. + [DataMember(Name = "cursor", EmitDefaultValue = true)] + public string Cursor { get; set; } + + /// + /// Most hits in the page, from 1 to 1000; 1000 when missing. + /// + /// Most hits in the page, from 1 to 1000; 1000 when missing. + [DataMember(Name = "maxHits", EmitDefaultValue = true)] + public int? MaxHits { get; set; } + + /// + /// Text to search for, ignoring case; leading and trailing spaces are part of it. + /// + /// Text to search for, ignoring case; leading and trailing spaces are part of it. + [DataMember(Name = "query", IsRequired = true, EmitDefaultValue = true)] + public string Query { get; set; } + + /// + /// Gets or Sets Recordings + /// + [DataMember(Name = "recordings", IsRequired = true, EmitDefaultValue = true)] + public RecordingSelection Recordings { get; set; } + + /// + /// Gets or Sets StartTime + /// + [DataMember(Name = "startTime", EmitDefaultValue = true)] + public StartTimeRange StartTime { get; set; } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class RecordingSearchRequest {\n"); + sb.Append(" Cursor: ").Append(Cursor).Append("\n"); + sb.Append(" MaxHits: ").Append(MaxHits).Append("\n"); + sb.Append(" Query: ").Append(Query).Append("\n"); + sb.Append(" Recordings: ").Append(Recordings).Append("\n"); + sb.Append(" StartTime: ").Append(StartTime).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public virtual string ToJson() + { + return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); + } + + /// + /// To validate all properties of the instance + /// + /// Validation context + /// Validation Result + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + // MaxHits (int?) maximum + if (this.MaxHits > (int?)1000) + { + yield return new ValidationResult("Invalid value for MaxHits, must be a value less than or equal to 1000.", new [] { "MaxHits" }); + } + + // MaxHits (int?) minimum + if (this.MaxHits < (int?)1) + { + yield return new ValidationResult("Invalid value for MaxHits, must be a value greater than or equal to 1.", new [] { "MaxHits" }); + } + + // Query (string) minLength + if (this.Query != null && this.Query.Length < 1) + { + yield return new ValidationResult("Invalid value for Query, length must be greater than 1.", new [] { "Query" }); + } + + yield break; + } + } + +} diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchResponse.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchResponse.cs new file mode 100644 index 0000000000..c32906bf2e --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchResponse.cs @@ -0,0 +1,105 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// The lines that hold the search text. + /// + [DataContract(Name = "RecordingSearchResponse")] + public partial class RecordingSearchResponse : IValidatableObject + { + /// + /// Initializes a new instance of the class. + /// + [JsonConstructorAttribute] + protected RecordingSearchResponse() { } + /// + /// Initializes a new instance of the class. + /// + /// Matching lines, newest recording first when searching all recordings, otherwise in the requested order. (required). + /// Where the next page starts; absent when every log has been read. The cursor is opaque, and its format may change between versions. A page with fewer hits than asked for may still be followed by more pages.. + public RecordingSearchResponse(List hits = default(List), string nextCursor = default(string)) + { + // to ensure "hits" is required (not null) + if (hits == null) + { + throw new ArgumentNullException("hits is a required property for RecordingSearchResponse and cannot be null"); + } + this.Hits = hits; + this.NextCursor = nextCursor; + } + + /// + /// Matching lines, newest recording first when searching all recordings, otherwise in the requested order. + /// + /// Matching lines, newest recording first when searching all recordings, otherwise in the requested order. + [DataMember(Name = "hits", IsRequired = true, EmitDefaultValue = true)] + public List Hits { get; set; } + + /// + /// Where the next page starts; absent when every log has been read. The cursor is opaque, and its format may change between versions. A page with fewer hits than asked for may still be followed by more pages. + /// + /// Where the next page starts; absent when every log has been read. The cursor is opaque, and its format may change between versions. A page with fewer hits than asked for may still be followed by more pages. + [DataMember(Name = "nextCursor", EmitDefaultValue = true)] + public string NextCursor { get; set; } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class RecordingSearchResponse {\n"); + sb.Append(" Hits: ").Append(Hits).Append("\n"); + sb.Append(" NextCursor: ").Append(NextCursor).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public virtual string ToJson() + { + return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); + } + + /// + /// To validate all properties of the instance + /// + /// Validation context + /// Validation Result + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + yield break; + } + } + +} diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSelection.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSelection.cs new file mode 100644 index 0000000000..c5953fc73e --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSelection.cs @@ -0,0 +1,264 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; +using System.Reflection; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// `\"all\"`, or a list of recording IDs. A repeated ID is searched once, and an empty list searches nothing. + /// + [JsonConverter(typeof(RecordingSelectionJsonConverter))] + [DataContract(Name = "RecordingSelection")] + public partial class RecordingSelection : AbstractOpenAPISchema, IValidatableObject + { + /// + /// Initializes a new instance of the class + /// with the class + /// + /// An instance of string. + public RecordingSelection(string actualInstance) + { + this.IsNullable = false; + this.SchemaType= "oneOf"; + this.ActualInstance = actualInstance ?? throw new ArgumentException("Invalid instance found. Must not be null."); + } + + /// + /// Initializes a new instance of the class + /// with the class + /// + /// An instance of List<Guid>. + public RecordingSelection(List actualInstance) + { + this.IsNullable = false; + this.SchemaType= "oneOf"; + this.ActualInstance = actualInstance ?? throw new ArgumentException("Invalid instance found. Must not be null."); + } + + + private Object _actualInstance; + + /// + /// Gets or Sets ActualInstance + /// + public override Object ActualInstance + { + get + { + return _actualInstance; + } + set + { + if (value.GetType() == typeof(List) || value is List) + { + this._actualInstance = value; + } + else if (value.GetType() == typeof(string) || value is string) + { + this._actualInstance = value; + } + else + { + throw new ArgumentException("Invalid instance found. Must be the following types: List, string"); + } + } + } + + /// + /// Get the actual instance of `string`. If the actual instance is not `string`, + /// the InvalidClassException will be thrown + /// + /// An instance of string + public string GetString() + { + return (string)this.ActualInstance; + } + + /// + /// Get the actual instance of `List<Guid>`. If the actual instance is not `List<Guid>`, + /// the InvalidClassException will be thrown + /// + /// An instance of List<Guid> + public List GetListUuid() + { + return (List)this.ActualInstance; + } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append("class RecordingSelection {\n"); + sb.Append(" ActualInstance: ").Append(this.ActualInstance).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public override string ToJson() + { + return JsonConvert.SerializeObject(this.ActualInstance, RecordingSelection.SerializerSettings); + } + + /// + /// Converts the JSON string into an instance of RecordingSelection + /// + /// JSON string + /// An instance of RecordingSelection + public static RecordingSelection FromJson(string jsonString) + { + RecordingSelection newRecordingSelection = null; + + if (string.IsNullOrEmpty(jsonString)) + { + return newRecordingSelection; + } + int match = 0; + List matchedTypes = new List(); + + try + { + // if it does not contains "AdditionalProperties", use SerializerSettings to deserialize + if (typeof(List).GetProperty("AdditionalProperties") == null) + { + newRecordingSelection = new RecordingSelection(JsonConvert.DeserializeObject>(jsonString, RecordingSelection.SerializerSettings)); + } + else + { + newRecordingSelection = new RecordingSelection(JsonConvert.DeserializeObject>(jsonString, RecordingSelection.AdditionalPropertiesSerializerSettings)); + } + matchedTypes.Add("List"); + match++; + } + catch (Exception exception) + { + // deserialization failed, try the next one + System.Diagnostics.Debug.WriteLine(string.Format("Failed to deserialize `{0}` into List: {1}", jsonString, exception.ToString())); + } + + try + { + // if it does not contains "AdditionalProperties", use SerializerSettings to deserialize + if (typeof(string).GetProperty("AdditionalProperties") == null) + { + newRecordingSelection = new RecordingSelection(JsonConvert.DeserializeObject(jsonString, RecordingSelection.SerializerSettings)); + } + else + { + newRecordingSelection = new RecordingSelection(JsonConvert.DeserializeObject(jsonString, RecordingSelection.AdditionalPropertiesSerializerSettings)); + } + matchedTypes.Add("string"); + match++; + } + catch (Exception exception) + { + // deserialization failed, try the next one + System.Diagnostics.Debug.WriteLine(string.Format("Failed to deserialize `{0}` into string: {1}", jsonString, exception.ToString())); + } + + if (match == 0) + { + throw new InvalidDataException("The JSON string `" + jsonString + "` cannot be deserialized into any schema defined."); + } + else if (match > 1) + { + throw new InvalidDataException("The JSON string `" + jsonString + "` incorrectly matches more than one schema (should be exactly one match): " + String.Join(",", matchedTypes)); + } + + // deserialization is considered successful at this point if no exception has been thrown. + return newRecordingSelection; + } + + + /// + /// To validate all properties of the instance + /// + /// Validation context + /// Validation Result + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + yield break; + } + } + + /// + /// Custom JSON converter for RecordingSelection + /// + public class RecordingSelectionJsonConverter : JsonConverter + { + /// + /// To write the JSON string + /// + /// JSON writer + /// Object to be converted into a JSON string + /// JSON Serializer + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteRawValue((string)(typeof(RecordingSelection).GetMethod("ToJson").Invoke(value, null))); + } + + /// + /// To convert a JSON string into an object + /// + /// JSON reader + /// Object type + /// Existing value + /// JSON Serializer + /// The object converted from the JSON string + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + switch(reader.TokenType) + { + case JsonToken.String: + return new RecordingSelection(Convert.ToString(reader.Value)); + case JsonToken.StartObject: + return RecordingSelection.FromJson(JObject.Load(reader).ToString(Formatting.None)); + case JsonToken.StartArray: + return RecordingSelection.FromJson(JArray.Load(reader).ToString(Formatting.None)); + default: + return null; + } + } + + /// + /// Check if the object can be converted + /// + /// Object type + /// True if the object can be converted + public override bool CanConvert(Type objectType) + { + return false; + } + } + +} diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/StartTimeRange.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/StartTimeRange.cs new file mode 100644 index 0000000000..1bc5d4e9b6 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/StartTimeRange.cs @@ -0,0 +1,95 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// A range of recording start times, as written in `recording.json`, that includes `from` and excludes `to`. A missing end leaves that side of the range open. + /// + [DataContract(Name = "StartTimeRange")] + public partial class StartTimeRange : IValidatableObject + { + /// + /// Initializes a new instance of the class. + /// + /// Earliest start time searched.. + /// Recordings that start at this time or later are not searched.. + public StartTimeRange(DateTime? from = default(DateTime?), DateTime? to = default(DateTime?)) + { + this.From = from; + this.To = to; + } + + /// + /// Earliest start time searched. + /// + /// Earliest start time searched. + [DataMember(Name = "from", EmitDefaultValue = true)] + public DateTime? From { get; set; } + + /// + /// Recordings that start at this time or later are not searched. + /// + /// Recordings that start at this time or later are not searched. + [DataMember(Name = "to", EmitDefaultValue = true)] + public DateTime? To { get; set; } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class StartTimeRange {\n"); + sb.Append(" From: ").Append(From).Append("\n"); + sb.Append(" To: ").Append(To).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public virtual string ToJson() + { + return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); + } + + /// + /// To validate all properties of the instance + /// + /// Validation context + /// Validation Result + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + yield break; + } + } + +} diff --git a/devolutions-gateway/openapi/gateway-api.yaml b/devolutions-gateway/openapi/gateway-api.yaml index a2c1b1b2b5..0219c3365e 100644 --- a/devolutions-gateway/openapi/gateway-api.yaml +++ b/devolutions-gateway/openapi/gateway-api.yaml @@ -346,6 +346,47 @@ paths: security: - jrec_token: - pull + /jet/jrec/search: + post: + tags: + - Jrec + summary: Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance + description: |- + The query is a case-insensitive substring, matched against the raw text of each line. + Gateway does not interpret the logs, so any part of a line can match, field names included. + Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, + and a line longer than 256 KiB is matched on its first 256 KiB only. + Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. + + Results come in pages. + To get the next page, send the same `query`, `recordings` and `startTime` again, + with the `nextCursor` of the previous page as `cursor`. + Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not + searched. + operationId: SearchRecordings + requestBody: + description: JSON-encoded search request + content: + application/json: + schema: + $ref: '#/components/schemas/RecordingSearchRequest' + required: true + responses: + '200': + description: Matching log lines + content: + application/json: + schema: + $ref: '#/components/schemas/RecordingSearchResponse' + '400': + description: Bad request + '401': + description: Invalid or missing authorization token + '403': + description: Insufficient permissions + security: + - scope_token: + - gateway.recordings.search /jet/jrl: post: tags: @@ -1326,6 +1367,7 @@ components: - gateway.net.monitor.drain - gateway.agent.delete - gateway.agent.read + - gateway.recordings.search AckRequest: type: object required: @@ -2196,6 +2238,106 @@ components: enum: - Spki - Pkcs1 + RecordingSearchHit: + type: object + description: A line that holds the search text. + required: + - recordingId + - fileName + - lineNumber + - offset + - line + - truncated + properties: + fileName: + type: string + description: Name of the `.slog` artifact holding the line, as listed in `recording.json`. + line: + type: string + description: The line's text without its line break; bytes that are not UTF-8 read as U+FFFD. + lineNumber: + type: integer + format: int64 + description: Number of the line in the file, counting from 1. + minimum: 1 + offset: + type: integer + format: int64 + description: Byte offset of the line in the file. + minimum: 0 + recordingId: + type: string + format: uuid + description: The recording whose artifact holds the line. + truncated: + type: boolean + description: The line is longer than 256 KiB, so `line` holds only its start, and the file holds the rest. + RecordingSearchRequest: + type: object + description: |- + What to search for, and in which recordings. + + Unknown fields are rejected, so that an option added later is never silently ignored by a Gateway that predates it. + required: + - query + - recordings + properties: + cursor: + type: string + description: |- + Where the page starts: the `nextCursor` of the previous page, sent unchanged with the same `query`, + `recordings` and `startTime`. + nullable: true + maxHits: + type: integer + format: int32 + description: Most hits in the page, from 1 to 1000; 1000 when missing. + nullable: true + maximum: 1000 + minimum: 1 + query: + type: string + description: Text to search for, ignoring case; leading and trailing spaces are part of it. + minLength: 1 + recordings: + $ref: '#/components/schemas/RecordingSelection' + startTime: + allOf: + - $ref: '#/components/schemas/StartTimeRange' + nullable: true + additionalProperties: false + RecordingSearchResponse: + type: object + description: The lines that hold the search text. + required: + - hits + properties: + hits: + type: array + items: + $ref: '#/components/schemas/RecordingSearchHit' + description: Matching lines, newest recording first when searching all recordings, otherwise in the requested order. + nextCursor: + type: string + description: |- + Where the next page starts; absent when every log has been read. + + The cursor is opaque, and its format may change between versions. + A page with fewer hits than asked for may still be followed by more pages. + nullable: true + RecordingSelection: + oneOf: + - type: string + enum: + - all + - type: array + items: + type: string + format: uuid + description: |- + `"all"`, or a list of recording IDs. + + A repeated ID is searched once, and an empty list searches nothing. ScanOriginDto: type: string enum: @@ -2349,6 +2491,24 @@ components: minimum: 0 SetUpdateScheduleResponse: type: object + StartTimeRange: + type: object + description: |- + A range of recording start times, as written in `recording.json`, that includes `from` and excludes `to`. + + A missing end leaves that side of the range open. + properties: + from: + type: string + format: date-time + description: Earliest start time searched. + nullable: true + to: + type: string + format: date-time + description: Recordings that start at this time or later are not searched. + nullable: true + additionalProperties: false SubProvisionerKey: type: object required: diff --git a/devolutions-gateway/openapi/ts-angular-client/.openapi-generator/FILES b/devolutions-gateway/openapi/ts-angular-client/.openapi-generator/FILES index 84eba61edd..a7c07e7165 100644 --- a/devolutions-gateway/openapi/ts-angular-client/.openapi-generator/FILES +++ b/devolutions-gateway/openapi/ts-angular-client/.openapi-generator/FILES @@ -68,6 +68,10 @@ model/preflightOperationKind.ts model/preflightOutput.ts model/preflightOutputKind.ts model/pubKeyFormat.ts +model/recordingSearchHit.ts +model/recordingSearchRequest.ts +model/recordingSearchResponse.ts +model/recordingSelection.ts model/scanOriginDto.ts model/scanResultSourceDto.ts model/sessionInfo.ts @@ -75,6 +79,7 @@ model/sessionTokenContentType.ts model/sessionTokenSignRequest.ts model/setConfigResponse.ts model/setUpdateScheduleRequest.ts +model/startTimeRange.ts model/subProvisionerKey.ts model/subscriber.ts model/targetConnectionOptions.ts diff --git a/devolutions-gateway/openapi/ts-angular-client/api/jrec.service.ts b/devolutions-gateway/openapi/ts-angular-client/api/jrec.service.ts index b1e8e0a18a..d5c5375815 100644 --- a/devolutions-gateway/openapi/ts-angular-client/api/jrec.service.ts +++ b/devolutions-gateway/openapi/ts-angular-client/api/jrec.service.ts @@ -18,6 +18,10 @@ import { Observable } from 'rxjs'; // @ts-ignore import { DeleteManyResult } from '../model/deleteManyResult'; +// @ts-ignore +import { RecordingSearchRequest } from '../model/recordingSearchRequest'; +// @ts-ignore +import { RecordingSearchResponse } from '../model/recordingSearchResponse'; // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; @@ -445,4 +449,86 @@ export class JrecService { ); } + /** + * Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance + * The query is a case-insensitive substring, matched against the raw text of each line. Gateway does not interpret the logs, so any part of a line can match, field names included. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, and a line longer than 256 KiB is matched on its first 256 KiB only. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + * @param recordingSearchRequest JSON-encoded search request + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public searchRecordings(recordingSearchRequest: RecordingSearchRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public searchRecordings(recordingSearchRequest: RecordingSearchRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public searchRecordings(recordingSearchRequest: RecordingSearchRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public searchRecordings(recordingSearchRequest: RecordingSearchRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (recordingSearchRequest === null || recordingSearchRequest === undefined) { + throw new Error('Required parameter recordingSearchRequest was null or undefined when calling searchRecordings.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // authentication (scope_token) required + localVarCredential = this.configuration.lookupCredential('scope_token'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', 'Bearer ' + localVarCredential); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/jet/jrec/search`; + return this.httpClient.request('post', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: recordingSearchRequest, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + } diff --git a/devolutions-gateway/openapi/ts-angular-client/model/accessScope.ts b/devolutions-gateway/openapi/ts-angular-client/model/accessScope.ts index 0827c35b77..07dc408fcb 100644 --- a/devolutions-gateway/openapi/ts-angular-client/model/accessScope.ts +++ b/devolutions-gateway/openapi/ts-angular-client/model/accessScope.ts @@ -9,7 +9,7 @@ */ -export type AccessScope = '*' | 'gateway.sessions.read' | 'gateway.session.terminate' | 'gateway.associations.read' | 'gateway.diagnostics.read' | 'gateway.jrl.read' | 'gateway.config.write' | 'gateway.heartbeat.read' | 'gateway.recording.delete' | 'gateway.recordings.read' | 'gateway.update' | 'gateway.update.read' | 'gateway.preflight' | 'gateway.traffic.claim' | 'gateway.traffic.ack' | 'gateway.net.monitor.config' | 'gateway.net.monitor.drain' | 'gateway.agent.delete' | 'gateway.agent.read'; +export type AccessScope = '*' | 'gateway.sessions.read' | 'gateway.session.terminate' | 'gateway.associations.read' | 'gateway.diagnostics.read' | 'gateway.jrl.read' | 'gateway.config.write' | 'gateway.heartbeat.read' | 'gateway.recording.delete' | 'gateway.recordings.read' | 'gateway.update' | 'gateway.update.read' | 'gateway.preflight' | 'gateway.traffic.claim' | 'gateway.traffic.ack' | 'gateway.net.monitor.config' | 'gateway.net.monitor.drain' | 'gateway.agent.delete' | 'gateway.agent.read' | 'gateway.recordings.search'; export const AccessScope = { Star: '*' as AccessScope, @@ -30,6 +30,7 @@ export const AccessScope = { GatewayNetMonitorConfig: 'gateway.net.monitor.config' as AccessScope, GatewayNetMonitorDrain: 'gateway.net.monitor.drain' as AccessScope, GatewayAgentDelete: 'gateway.agent.delete' as AccessScope, - GatewayAgentRead: 'gateway.agent.read' as AccessScope + GatewayAgentRead: 'gateway.agent.read' as AccessScope, + GatewayRecordingsSearch: 'gateway.recordings.search' as AccessScope }; diff --git a/devolutions-gateway/openapi/ts-angular-client/model/models.ts b/devolutions-gateway/openapi/ts-angular-client/model/models.ts index 7d7eb4f0fe..0c26c4654b 100644 --- a/devolutions-gateway/openapi/ts-angular-client/model/models.ts +++ b/devolutions-gateway/openapi/ts-angular-client/model/models.ts @@ -46,6 +46,10 @@ export * from './preflightOperationKind'; export * from './preflightOutput'; export * from './preflightOutputKind'; export * from './pubKeyFormat'; +export * from './recordingSearchHit'; +export * from './recordingSearchRequest'; +export * from './recordingSearchResponse'; +export * from './recordingSelection'; export * from './scanOriginDto'; export * from './scanResultSourceDto'; export * from './sessionInfo'; @@ -53,6 +57,7 @@ export * from './sessionTokenContentType'; export * from './sessionTokenSignRequest'; export * from './setConfigResponse'; export * from './setUpdateScheduleRequest'; +export * from './startTimeRange'; export * from './subProvisionerKey'; export * from './subscriber'; export * from './targetConnectionOptions'; diff --git a/devolutions-gateway/openapi/ts-angular-client/model/recordingSearchHit.ts b/devolutions-gateway/openapi/ts-angular-client/model/recordingSearchHit.ts new file mode 100644 index 0000000000..4dda6e3877 --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/recordingSearchHit.ts @@ -0,0 +1,41 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +/** + * A line that holds the search text. + */ +export interface RecordingSearchHit { + /** + * Name of the `.slog` artifact holding the line, as listed in `recording.json`. + */ + fileName: string; + /** + * The line\'s text without its line break; bytes that are not UTF-8 read as U+FFFD. + */ + line: string; + /** + * Number of the line in the file, counting from 1. + */ + lineNumber: number; + /** + * Byte offset of the line in the file. + */ + offset: number; + /** + * The recording whose artifact holds the line. + */ + recordingId: string; + /** + * The line is longer than 256 KiB, so `line` holds only its start, and the file holds the rest. + */ + truncated: boolean; +} + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/recordingSearchRequest.ts b/devolutions-gateway/openapi/ts-angular-client/model/recordingSearchRequest.ts new file mode 100644 index 0000000000..a7a7fb8202 --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/recordingSearchRequest.ts @@ -0,0 +1,33 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { StartTimeRange } from './startTimeRange'; +import { RecordingSelection } from './recordingSelection'; + + +/** + * What to search for, and in which recordings. Unknown fields are rejected, so that an option added later is never silently ignored by a Gateway that predates it. + */ +export interface RecordingSearchRequest { + /** + * Where the page starts: the `nextCursor` of the previous page, sent unchanged with the same `query`, `recordings` and `startTime`. + */ + cursor?: string | null; + /** + * Most hits in the page, from 1 to 1000; 1000 when missing. + */ + maxHits?: number | null; + /** + * Text to search for, ignoring case; leading and trailing spaces are part of it. + */ + query: string; + recordings: RecordingSelection; + startTime?: StartTimeRange | null; +} + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/recordingSearchResponse.ts b/devolutions-gateway/openapi/ts-angular-client/model/recordingSearchResponse.ts new file mode 100644 index 0000000000..142980d484 --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/recordingSearchResponse.ts @@ -0,0 +1,26 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { RecordingSearchHit } from './recordingSearchHit'; + + +/** + * The lines that hold the search text. + */ +export interface RecordingSearchResponse { + /** + * Matching lines, newest recording first when searching all recordings, otherwise in the requested order. + */ + hits: Array; + /** + * Where the next page starts; absent when every log has been read. The cursor is opaque, and its format may change between versions. A page with fewer hits than asked for may still be followed by more pages. + */ + nextCursor?: string | null; +} + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/recordingSelection.ts b/devolutions-gateway/openapi/ts-angular-client/model/recordingSelection.ts new file mode 100644 index 0000000000..985ada4cc4 --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/recordingSelection.ts @@ -0,0 +1,21 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +/** + * `\"all\"`, or a list of recording IDs. A repeated ID is searched once, and an empty list searches nothing. + */ +/** + * @type RecordingSelection + * `\"all\"`, or a list of recording IDs. A repeated ID is searched once, and an empty list searches nothing. + * @export + */ +export type RecordingSelection = Array | string; + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/startTimeRange.ts b/devolutions-gateway/openapi/ts-angular-client/model/startTimeRange.ts new file mode 100644 index 0000000000..c07bd4deda --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/startTimeRange.ts @@ -0,0 +1,25 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +/** + * A range of recording start times, as written in `recording.json`, that includes `from` and excludes `to`. A missing end leaves that side of the range open. + */ +export interface StartTimeRange { + /** + * Earliest start time searched. + */ + from?: string | null; + /** + * Recordings that start at this time or later are not searched. + */ + to?: string | null; +} + From 1643f9bc698ed35f25e535f4d197cb435f181b12 Mon Sep 17 00:00:00 2001 From: Junyi Ou Date: Wed, 21 Oct 2026 13:00:32 -0400 Subject: [PATCH 2/3] chore(openapi): describe recording search matching JSON string values Regenerated with openapi-generator 7.9.0 after the search endpoint started matching the string values of JSON log lines. Only descriptions change: the endpoint, the hit and the response. Issue: DGW-463 Co-Authored-By: Claude Opus 5.5 --- devolutions-gateway/openapi/doc/index.adoc | 6 +++--- .../openapi/dotnet-client/docs/JrecApi.md | 2 +- .../dotnet-client/docs/RecordingSearchHit.md | 2 +- .../docs/RecordingSearchResponse.md | 2 +- .../Devolutions.Gateway.Client/Api/JrecApi.cs | 16 ++++++++-------- .../Model/RecordingSearchHit.cs | 2 +- .../Model/RecordingSearchResponse.cs | 2 +- devolutions-gateway/openapi/gateway-api.yaml | 15 +++++++++------ .../ts-angular-client/api/jrec.service.ts | 2 +- .../model/recordingSearchHit.ts | 2 +- .../model/recordingSearchResponse.ts | 2 +- 11 files changed, 28 insertions(+), 25 deletions(-) diff --git a/devolutions-gateway/openapi/doc/index.adoc b/devolutions-gateway/openapi/doc/index.adoc index 4ba40c0cfc..146cec7c48 100644 --- a/devolutions-gateway/openapi/doc/index.adoc +++ b/devolutions-gateway/openapi/doc/index.adoc @@ -1435,7 +1435,7 @@ Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored o ===== Description -The query is a case-insensitive substring, matched against the raw text of each line. Gateway does not interpret the logs, so any part of a line can match, field names included. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, and a line longer than 256 KiB is matched on its first 256 KiB only. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. +The query is a case-insensitive substring. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. // markup not found, no include::{specDir}jet/jrec/search/POST/spec.adoc[opts=optional] @@ -5303,7 +5303,7 @@ Current auto-update schedule for Devolutions Agent. [#RecordingSearchHit] === _RecordingSearchHit_ -A line that holds the search text. +A line that matches the search text. [.fields-RecordingSearchHit] @@ -5412,7 +5412,7 @@ Unknown fields are rejected, so that an option added later is never silently ign [#RecordingSearchResponse] === _RecordingSearchResponse_ -The lines that hold the search text. +The lines that match the search text. [.fields-RecordingSearchResponse] diff --git a/devolutions-gateway/openapi/dotnet-client/docs/JrecApi.md b/devolutions-gateway/openapi/dotnet-client/docs/JrecApi.md index 1bad0d5b48..9aecad37a1 100644 --- a/devolutions-gateway/openapi/dotnet-client/docs/JrecApi.md +++ b/devolutions-gateway/openapi/dotnet-client/docs/JrecApi.md @@ -520,7 +520,7 @@ catch (ApiException e) Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance -The query is a case-insensitive substring, matched against the raw text of each line. Gateway does not interpret the logs, so any part of a line can match, field names included. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, and a line longer than 256 KiB is matched on its first 256 KiB only. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. +The query is a case-insensitive substring. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. ### Example ```csharp diff --git a/devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchHit.md b/devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchHit.md index 48b77be83b..bfab56ff4a 100644 --- a/devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchHit.md +++ b/devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchHit.md @@ -1,5 +1,5 @@ # Devolutions.Gateway.Client.Model.RecordingSearchHit -A line that holds the search text. +A line that matches the search text. ## Properties diff --git a/devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchResponse.md b/devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchResponse.md index 7ae5974adf..ecdca759e5 100644 --- a/devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchResponse.md +++ b/devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchResponse.md @@ -1,5 +1,5 @@ # Devolutions.Gateway.Client.Model.RecordingSearchResponse -The lines that hold the search text. +The lines that match the search text. ## Properties diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/JrecApi.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/JrecApi.cs index b64e2fb56d..94638efa23 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/JrecApi.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/JrecApi.cs @@ -130,7 +130,7 @@ public interface IJrecApiSync : IApiAccessor /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance /// /// - /// The query is a case-insensitive substring, matched against the raw text of each line. Gateway does not interpret the logs, so any part of a line can match, field names included. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, and a line longer than 256 KiB is matched on its first 256 KiB only. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// The query is a case-insensitive substring. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. /// /// Thrown when fails to make API call /// JSON-encoded search request @@ -141,7 +141,7 @@ public interface IJrecApiSync : IApiAccessor /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance /// /// - /// The query is a case-insensitive substring, matched against the raw text of each line. Gateway does not interpret the logs, so any part of a line can match, field names included. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, and a line longer than 256 KiB is matched on its first 256 KiB only. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// The query is a case-insensitive substring. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. /// /// Thrown when fails to make API call /// JSON-encoded search request @@ -277,7 +277,7 @@ public interface IJrecApiAsync : IApiAccessor /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance /// /// - /// The query is a case-insensitive substring, matched against the raw text of each line. Gateway does not interpret the logs, so any part of a line can match, field names included. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, and a line longer than 256 KiB is matched on its first 256 KiB only. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// The query is a case-insensitive substring. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. /// /// Thrown when fails to make API call /// JSON-encoded search request @@ -289,7 +289,7 @@ public interface IJrecApiAsync : IApiAccessor /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance /// /// - /// The query is a case-insensitive substring, matched against the raw text of each line. Gateway does not interpret the logs, so any part of a line can match, field names included. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, and a line longer than 256 KiB is matched on its first 256 KiB only. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// The query is a case-insensitive substring. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. /// /// Thrown when fails to make API call /// JSON-encoded search request @@ -1125,7 +1125,7 @@ public Devolutions.Gateway.Client.Client.ApiResponse PullRecordin } /// - /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance The query is a case-insensitive substring, matched against the raw text of each line. Gateway does not interpret the logs, so any part of a line can match, field names included. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, and a line longer than 256 KiB is matched on its first 256 KiB only. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance The query is a case-insensitive substring. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. /// /// Thrown when fails to make API call /// JSON-encoded search request @@ -1137,7 +1137,7 @@ public RecordingSearchResponse SearchRecordings(RecordingSearchRequest recording } /// - /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance The query is a case-insensitive substring, matched against the raw text of each line. Gateway does not interpret the logs, so any part of a line can match, field names included. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, and a line longer than 256 KiB is matched on its first 256 KiB only. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance The query is a case-insensitive substring. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. /// /// Thrown when fails to make API call /// JSON-encoded search request @@ -1187,7 +1187,7 @@ public Devolutions.Gateway.Client.Client.ApiResponse Se } /// - /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance The query is a case-insensitive substring, matched against the raw text of each line. Gateway does not interpret the logs, so any part of a line can match, field names included. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, and a line longer than 256 KiB is matched on its first 256 KiB only. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance The query is a case-insensitive substring. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. /// /// Thrown when fails to make API call /// JSON-encoded search request @@ -1200,7 +1200,7 @@ public Devolutions.Gateway.Client.Client.ApiResponse Se } /// - /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance The query is a case-insensitive substring, matched against the raw text of each line. Gateway does not interpret the logs, so any part of a line can match, field names included. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, and a line longer than 256 KiB is matched on its first 256 KiB only. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance The query is a case-insensitive substring. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. /// /// Thrown when fails to make API call /// JSON-encoded search request diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchHit.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchHit.cs index e2ca5b4d5f..4191f310b0 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchHit.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchHit.cs @@ -28,7 +28,7 @@ namespace Devolutions.Gateway.Client.Model { /// - /// A line that holds the search text. + /// A line that matches the search text. /// [DataContract(Name = "RecordingSearchHit")] public partial class RecordingSearchHit : IValidatableObject diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchResponse.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchResponse.cs index c32906bf2e..9119308fb5 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchResponse.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchResponse.cs @@ -28,7 +28,7 @@ namespace Devolutions.Gateway.Client.Model { /// - /// The lines that hold the search text. + /// The lines that match the search text. /// [DataContract(Name = "RecordingSearchResponse")] public partial class RecordingSearchResponse : IValidatableObject diff --git a/devolutions-gateway/openapi/gateway-api.yaml b/devolutions-gateway/openapi/gateway-api.yaml index 0219c3365e..9b89ba9087 100644 --- a/devolutions-gateway/openapi/gateway-api.yaml +++ b/devolutions-gateway/openapi/gateway-api.yaml @@ -352,10 +352,13 @@ paths: - Jrec summary: Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance description: |- - The query is a case-insensitive substring, matched against the raw text of each line. - Gateway does not interpret the logs, so any part of a line can match, field names included. - Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, - and a line longer than 256 KiB is matched on its first 256 KiB only. + The query is a case-insensitive substring. + In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read + once unescaped, so field names, numbers, booleans and null never match. + Any other line, such as one still being written, is matched as raw text. + A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually + matched as raw text. + Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. @@ -2240,7 +2243,7 @@ components: - Pkcs1 RecordingSearchHit: type: object - description: A line that holds the search text. + description: A line that matches the search text. required: - recordingId - fileName @@ -2308,7 +2311,7 @@ components: additionalProperties: false RecordingSearchResponse: type: object - description: The lines that hold the search text. + description: The lines that match the search text. required: - hits properties: diff --git a/devolutions-gateway/openapi/ts-angular-client/api/jrec.service.ts b/devolutions-gateway/openapi/ts-angular-client/api/jrec.service.ts index d5c5375815..4ac8c4d073 100644 --- a/devolutions-gateway/openapi/ts-angular-client/api/jrec.service.ts +++ b/devolutions-gateway/openapi/ts-angular-client/api/jrec.service.ts @@ -451,7 +451,7 @@ export class JrecService { /** * Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance - * The query is a case-insensitive substring, matched against the raw text of each line. Gateway does not interpret the logs, so any part of a line can match, field names included. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched, and a line longer than 256 KiB is matched on its first 256 KiB only. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + * The query is a case-insensitive substring. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. * @param recordingSearchRequest JSON-encoded search request * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. diff --git a/devolutions-gateway/openapi/ts-angular-client/model/recordingSearchHit.ts b/devolutions-gateway/openapi/ts-angular-client/model/recordingSearchHit.ts index 4dda6e3877..5c66fe0b67 100644 --- a/devolutions-gateway/openapi/ts-angular-client/model/recordingSearchHit.ts +++ b/devolutions-gateway/openapi/ts-angular-client/model/recordingSearchHit.ts @@ -10,7 +10,7 @@ /** - * A line that holds the search text. + * A line that matches the search text. */ export interface RecordingSearchHit { /** diff --git a/devolutions-gateway/openapi/ts-angular-client/model/recordingSearchResponse.ts b/devolutions-gateway/openapi/ts-angular-client/model/recordingSearchResponse.ts index 142980d484..85f6946f8f 100644 --- a/devolutions-gateway/openapi/ts-angular-client/model/recordingSearchResponse.ts +++ b/devolutions-gateway/openapi/ts-angular-client/model/recordingSearchResponse.ts @@ -11,7 +11,7 @@ import { RecordingSearchHit } from './recordingSearchHit'; /** - * The lines that hold the search text. + * The lines that match the search text. */ export interface RecordingSearchResponse { /** From 85eb7e36a3f585c89b11f54bbbebf05436b93a0c Mon Sep 17 00:00:00 2001 From: Junyi Ou Date: Thu, 22 Oct 2026 12:08:29 -0400 Subject: [PATCH 3/3] chore(openapi): let the recording search query be empty An empty query now matches every line, as in rg and grep, so the spec drops minLength on query and the .NET client drops its length check. The rest is the updated descriptions. Issue: DGW-463 Co-Authored-By: Claude Opus 5.5 --- devolutions-gateway/openapi/doc/index.adoc | 4 ++-- .../openapi/dotnet-client/docs/JrecApi.md | 2 +- .../dotnet-client/docs/RecordingSearchRequest.md | 2 +- .../Devolutions.Gateway.Client/Api/JrecApi.cs | 16 ++++++++-------- .../Model/RecordingSearchRequest.cs | 12 +++--------- devolutions-gateway/openapi/gateway-api.yaml | 7 ++++--- .../ts-angular-client/api/jrec.service.ts | 2 +- .../model/recordingSearchRequest.ts | 2 +- 8 files changed, 21 insertions(+), 26 deletions(-) diff --git a/devolutions-gateway/openapi/doc/index.adoc b/devolutions-gateway/openapi/doc/index.adoc index 146cec7c48..d63c02b8e2 100644 --- a/devolutions-gateway/openapi/doc/index.adoc +++ b/devolutions-gateway/openapi/doc/index.adoc @@ -1435,7 +1435,7 @@ Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored o ===== Description -The query is a case-insensitive substring. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. +The query is a case-insensitive substring, and an empty query matches every line. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. // markup not found, no include::{specDir}jet/jrec/search/POST/spec.adoc[opts=optional] @@ -5388,7 +5388,7 @@ Unknown fields are rejected, so that an option added later is never silently ign | X | | String -| Text to search for, ignoring case; leading and trailing spaces are part of it. +| Text to search for, ignoring case; leading and trailing spaces are part of it, and an empty text matches every line. | | recordings diff --git a/devolutions-gateway/openapi/dotnet-client/docs/JrecApi.md b/devolutions-gateway/openapi/dotnet-client/docs/JrecApi.md index 9aecad37a1..a376db1eb0 100644 --- a/devolutions-gateway/openapi/dotnet-client/docs/JrecApi.md +++ b/devolutions-gateway/openapi/dotnet-client/docs/JrecApi.md @@ -520,7 +520,7 @@ catch (ApiException e) Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance -The query is a case-insensitive substring. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. +The query is a case-insensitive substring, and an empty query matches every line. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. ### Example ```csharp diff --git a/devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchRequest.md b/devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchRequest.md index f97d24c465..81621f924b 100644 --- a/devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchRequest.md +++ b/devolutions-gateway/openapi/dotnet-client/docs/RecordingSearchRequest.md @@ -7,7 +7,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **Cursor** | **string** | Where the page starts: the `nextCursor` of the previous page, sent unchanged with the same `query`, `recordings` and `startTime`. | [optional] **MaxHits** | **int?** | Most hits in the page, from 1 to 1000; 1000 when missing. | [optional] -**Query** | **string** | Text to search for, ignoring case; leading and trailing spaces are part of it. | +**Query** | **string** | Text to search for, ignoring case; leading and trailing spaces are part of it, and an empty text matches every line. | **Recordings** | [**RecordingSelection**](RecordingSelection.md) | | **StartTime** | [**StartTimeRange**](StartTimeRange.md) | | [optional] diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/JrecApi.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/JrecApi.cs index 94638efa23..59869f69ad 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/JrecApi.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/JrecApi.cs @@ -130,7 +130,7 @@ public interface IJrecApiSync : IApiAccessor /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance /// /// - /// The query is a case-insensitive substring. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// The query is a case-insensitive substring, and an empty query matches every line. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. /// /// Thrown when fails to make API call /// JSON-encoded search request @@ -141,7 +141,7 @@ public interface IJrecApiSync : IApiAccessor /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance /// /// - /// The query is a case-insensitive substring. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// The query is a case-insensitive substring, and an empty query matches every line. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. /// /// Thrown when fails to make API call /// JSON-encoded search request @@ -277,7 +277,7 @@ public interface IJrecApiAsync : IApiAccessor /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance /// /// - /// The query is a case-insensitive substring. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// The query is a case-insensitive substring, and an empty query matches every line. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. /// /// Thrown when fails to make API call /// JSON-encoded search request @@ -289,7 +289,7 @@ public interface IJrecApiAsync : IApiAccessor /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance /// /// - /// The query is a case-insensitive substring. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// The query is a case-insensitive substring, and an empty query matches every line. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. /// /// Thrown when fails to make API call /// JSON-encoded search request @@ -1125,7 +1125,7 @@ public Devolutions.Gateway.Client.Client.ApiResponse PullRecordin } /// - /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance The query is a case-insensitive substring. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance The query is a case-insensitive substring, and an empty query matches every line. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. /// /// Thrown when fails to make API call /// JSON-encoded search request @@ -1137,7 +1137,7 @@ public RecordingSearchResponse SearchRecordings(RecordingSearchRequest recording } /// - /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance The query is a case-insensitive substring. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance The query is a case-insensitive substring, and an empty query matches every line. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. /// /// Thrown when fails to make API call /// JSON-encoded search request @@ -1187,7 +1187,7 @@ public Devolutions.Gateway.Client.Client.ApiResponse Se } /// - /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance The query is a case-insensitive substring. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance The query is a case-insensitive substring, and an empty query matches every line. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. /// /// Thrown when fails to make API call /// JSON-encoded search request @@ -1200,7 +1200,7 @@ public Devolutions.Gateway.Client.Client.ApiResponse Se } /// - /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance The query is a case-insensitive substring. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + /// Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance The query is a case-insensitive substring, and an empty query matches every line. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. /// /// Thrown when fails to make API call /// JSON-encoded search request diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchRequest.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchRequest.cs index a74cdf6b41..b9c98407af 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchRequest.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/RecordingSearchRequest.cs @@ -43,7 +43,7 @@ protected RecordingSearchRequest() { } /// /// Where the page starts: the `nextCursor` of the previous page, sent unchanged with the same `query`, `recordings` and `startTime`.. /// Most hits in the page, from 1 to 1000; 1000 when missing.. - /// Text to search for, ignoring case; leading and trailing spaces are part of it. (required). + /// Text to search for, ignoring case; leading and trailing spaces are part of it, and an empty text matches every line. (required). /// recordings (required). /// startTime. public RecordingSearchRequest(string cursor = default(string), int? maxHits = default(int?), string query = default(string), RecordingSelection recordings = default(RecordingSelection), StartTimeRange startTime = default(StartTimeRange)) @@ -80,9 +80,9 @@ protected RecordingSearchRequest() { } public int? MaxHits { get; set; } /// - /// Text to search for, ignoring case; leading and trailing spaces are part of it. + /// Text to search for, ignoring case; leading and trailing spaces are part of it, and an empty text matches every line. /// - /// Text to search for, ignoring case; leading and trailing spaces are part of it. + /// Text to search for, ignoring case; leading and trailing spaces are part of it, and an empty text matches every line. [DataMember(Name = "query", IsRequired = true, EmitDefaultValue = true)] public string Query { get; set; } @@ -143,12 +143,6 @@ IEnumerable IValidatableObject.Validate(ValidationContext vali yield return new ValidationResult("Invalid value for MaxHits, must be a value greater than or equal to 1.", new [] { "MaxHits" }); } - // Query (string) minLength - if (this.Query != null && this.Query.Length < 1) - { - yield return new ValidationResult("Invalid value for Query, length must be greater than 1.", new [] { "Query" }); - } - yield break; } } diff --git a/devolutions-gateway/openapi/gateway-api.yaml b/devolutions-gateway/openapi/gateway-api.yaml index 9b89ba9087..9ec75de7f3 100644 --- a/devolutions-gateway/openapi/gateway-api.yaml +++ b/devolutions-gateway/openapi/gateway-api.yaml @@ -352,7 +352,7 @@ paths: - Jrec summary: Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance description: |- - The query is a case-insensitive substring. + The query is a case-insensitive substring, and an empty query matches every line. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. @@ -2300,8 +2300,9 @@ components: minimum: 1 query: type: string - description: Text to search for, ignoring case; leading and trailing spaces are part of it. - minLength: 1 + description: |- + Text to search for, ignoring case; leading and trailing spaces are part of it, and an empty text matches every + line. recordings: $ref: '#/components/schemas/RecordingSelection' startTime: diff --git a/devolutions-gateway/openapi/ts-angular-client/api/jrec.service.ts b/devolutions-gateway/openapi/ts-angular-client/api/jrec.service.ts index 4ac8c4d073..130ff953ac 100644 --- a/devolutions-gateway/openapi/ts-angular-client/api/jrec.service.ts +++ b/devolutions-gateway/openapi/ts-angular-client/api/jrec.service.ts @@ -451,7 +451,7 @@ export class JrecService { /** * Searches the `.slog` artifacts, such as AI analysis logs, of recordings stored on this instance - * The query is a case-insensitive substring. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. + * The query is a case-insensitive substring, and an empty query matches every line. In a line holding a JSON object, it is matched against the string values of the object at any depth, as they read once unescaped, so field names, numbers, booleans and null never match. Any other line, such as one still being written, is matched as raw text. A line longer than 256 KiB is matched on its first 256 KiB only, by the same rule, so a cut JSON object is usually matched as raw text. Only the files listed under `artifacts` in `recording.json` with the `slog` type are searched. Recordings without a readable `recording.json` and IDs with no recording on this instance give no hits. Results come in pages. To get the next page, send the same `query`, `recordings` and `startTime` again, with the `nextCursor` of the previous page as `cursor`. Pages never go back, so a recording that starts, or a log that grows, behind the cursor while paging is not searched. * @param recordingSearchRequest JSON-encoded search request * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. diff --git a/devolutions-gateway/openapi/ts-angular-client/model/recordingSearchRequest.ts b/devolutions-gateway/openapi/ts-angular-client/model/recordingSearchRequest.ts index a7a7fb8202..9e7873c419 100644 --- a/devolutions-gateway/openapi/ts-angular-client/model/recordingSearchRequest.ts +++ b/devolutions-gateway/openapi/ts-angular-client/model/recordingSearchRequest.ts @@ -24,7 +24,7 @@ export interface RecordingSearchRequest { */ maxHits?: number | null; /** - * Text to search for, ignoring case; leading and trailing spaces are part of it. + * Text to search for, ignoring case; leading and trailing spaces are part of it, and an empty text matches every line. */ query: string; recordings: RecordingSelection;