diff --git a/docs/migration-guide.asciidoc b/docs/migration-guide.asciidoc index 21f78589c1..1d3ae76032 100644 --- a/docs/migration-guide.asciidoc +++ b/docs/migration-guide.asciidoc @@ -292,13 +292,43 @@ As a last resort, the low-level client `Elastic.Transport` can be used to create [source,csharp] ---- +public class MyRequestParameters : RequestParameters +{ + public bool Pretty + { + get => Q("pretty"); + init => Q("pretty", value); + } +} + +// ... + var body = """ - { - "name": "my-api-key", - "expiration": "1d", - "...": "..." - } - """; - -var response = await client.Transport.RequestAsync(HttpMethod.POST, "/_security/api_key", PostData.String(body)); + { + "name": "my-api-key", + "expiration": "1d", + "...": "..." + } + """; + +MyRequestParameters requestParameters = new() +{ + Pretty = true +}; + +var pathAndQuery = requestParameters.CreatePathWithQueryStrings("/_security/api_key", + client.ElasticsearchClientSettings); +var endpointPath = new EndpointPath(Elastic.Transport.HttpMethod.POST, pathAndQuery); + +// Or, if the path does not contain query parameters: +// new EndpointPath(Elastic.Transport.HttpMethod.POST, "my_path") + +var response = await client.Transport + .RequestAsync( + endpointPath, + PostData.String(body), + null, + null, + cancellationToken: default) + .ConfigureAwait(false); ---- \ No newline at end of file diff --git a/docs/usage/index.asciidoc b/docs/usage/index.asciidoc index c6ae095064..87edb3f8d3 100644 --- a/docs/usage/index.asciidoc +++ b/docs/usage/index.asciidoc @@ -14,9 +14,10 @@ If you're new to {es}, make sure also to read {ref}/getting-started.html[Elastic NOTE: This is still a work in progress, more sections will be added in the near future. -include::recommendations.asciidoc[] +include::aggregations.asciidoc[] +include::esql.asciidoc[] include::examples.asciidoc[] -include::query.asciidoc[] include::mappings.asciidoc[] -include::aggregations.asciidoc[] -include::esql.asciidoc[] \ No newline at end of file +include::query.asciidoc[] +include::recommendations.asciidoc[] +include::transport.asciidoc[] diff --git a/docs/usage/transport.asciidoc b/docs/usage/transport.asciidoc new file mode 100644 index 0000000000..3e15fbd0b9 --- /dev/null +++ b/docs/usage/transport.asciidoc @@ -0,0 +1,47 @@ +[[transport]] +== Transport example + +This page demonstrates how to use the low level transport to send requests. + +[source,csharp] +---- +public class MyRequestParameters : RequestParameters +{ + public bool Pretty + { + get => Q("pretty"); + init => Q("pretty", value); + } +} + +// ... + +var body = """ + { + "name": "my-api-key", + "expiration": "1d", + "...": "..." + } + """; + +MyRequestParameters requestParameters = new() +{ + Pretty = true +}; + +var pathAndQuery = requestParameters.CreatePathWithQueryStrings("/_security/api_key", + client.ElasticsearchClientSettings); +var endpointPath = new EndpointPath(Elastic.Transport.HttpMethod.POST, pathAndQuery); + +// Or, if the path does not contain query parameters: +// new EndpointPath(Elastic.Transport.HttpMethod.POST, "my_path") + +var response = await client.Transport + .RequestAsync( + endpointPath, + PostData.String(body), + null, + null, + cancellationToken: default) + .ConfigureAwait(false); +---- diff --git a/src/Elastic.Clients.Elasticsearch.Serverless/Core/ElasticsearchClientProductRegistration.cs b/src/Elastic.Clients.Elasticsearch.Serverless/Core/ElasticsearchClientProductRegistration.cs index 1427878606..cfe03c5161 100644 --- a/src/Elastic.Clients.Elasticsearch.Serverless/Core/ElasticsearchClientProductRegistration.cs +++ b/src/Elastic.Clients.Elasticsearch.Serverless/Core/ElasticsearchClientProductRegistration.cs @@ -77,7 +77,7 @@ public class ApiVersionMetaHeaderProducer : MetaHeaderProducer public override string HeaderName => "Elastic-Api-Version"; - public override string ProduceHeaderValue(RequestData requestData, bool isAsync) => _apiVersion; + public override string ProduceHeaderValue(BoundConfiguration boundConfiguration, bool isAsync) => _apiVersion; public ApiVersionMetaHeaderProducer(VersionInfo version) { diff --git a/src/Elastic.Clients.Elasticsearch.Serverless/Elastic.Clients.Elasticsearch.Serverless.csproj b/src/Elastic.Clients.Elasticsearch.Serverless/Elastic.Clients.Elasticsearch.Serverless.csproj index b35a3290ea..6ecdefcccf 100644 --- a/src/Elastic.Clients.Elasticsearch.Serverless/Elastic.Clients.Elasticsearch.Serverless.csproj +++ b/src/Elastic.Clients.Elasticsearch.Serverless/Elastic.Clients.Elasticsearch.Serverless.csproj @@ -21,7 +21,7 @@ annotations - + diff --git a/src/Elastic.Clients.Elasticsearch/Elastic.Clients.Elasticsearch.csproj b/src/Elastic.Clients.Elasticsearch/Elastic.Clients.Elasticsearch.csproj index 71cdf552e8..b56f8cbba3 100644 --- a/src/Elastic.Clients.Elasticsearch/Elastic.Clients.Elasticsearch.csproj +++ b/src/Elastic.Clients.Elasticsearch/Elastic.Clients.Elasticsearch.csproj @@ -21,7 +21,7 @@ annotations - + diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Api/Esql/EsqlQueryRequest.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Api/Esql/EsqlQueryRequest.cs index f4b002621f..981a599fcc 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Api/Esql/EsqlQueryRequest.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Api/Esql/EsqlQueryRequest.cs @@ -17,7 +17,7 @@ namespace Elastic.Clients.Elasticsearch.Esql; internal sealed class EsqlResponseBuilder : TypedResponseBuilder { - protected override EsqlQueryResponse? Build(ApiCallDetails apiCallDetails, RequestData requestData, + protected override EsqlQueryResponse? Build(ApiCallDetails apiCallDetails, BoundConfiguration boundConfiguration, Stream responseStream, string contentType, long contentLength) { @@ -38,7 +38,7 @@ static byte[] BytesFromStream(Stream stream) } } - protected override async Task BuildAsync(ApiCallDetails apiCallDetails, RequestData requestData, + protected override async Task BuildAsync(ApiCallDetails apiCallDetails, BoundConfiguration boundConfiguration, Stream responseStream, string contentType, long contentLength, CancellationToken cancellationToken = default) { diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Client/ElasticsearchClient.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Client/ElasticsearchClient.cs index 1e61176db5..faee0c33fb 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Client/ElasticsearchClient.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Client/ElasticsearchClient.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; using System.Text.Json; @@ -165,11 +166,11 @@ private ValueTask DoRequestCoreAsync SendRequest() { var (endpointPath, resolvedRouteValues, postData) = PrepareRequest(request); - var openTelemetryData = PrepareOpenTelemetryData(request, resolvedRouteValues); + var openTelemetryDataMutator = GetOpenTelemetryDataMutator(request, resolvedRouteValues); return isAsync - ? new ValueTask(_transport.RequestAsync(endpointPath, postData, in openTelemetryData, request.RequestConfig, cancellationToken)) - : new ValueTask(_transport.Request(endpointPath, postData, in openTelemetryData, request.RequestConfig)); + ? new ValueTask(_transport.RequestAsync(endpointPath, postData, openTelemetryDataMutator, request.RequestConfig, cancellationToken)) + : new ValueTask(_transport.Request(endpointPath, postData, openTelemetryDataMutator, request.RequestConfig)); } async ValueTask SendRequestWithProductCheck() @@ -211,19 +212,19 @@ async ValueTask SendRequestWithProductCheckCore() // Send request var (endpointPath, resolvedRouteValues, postData) = PrepareRequest(request); - var openTelemetryData = PrepareOpenTelemetryData(request, resolvedRouteValues); + var openTelemetryDataMutator = GetOpenTelemetryDataMutator(request, resolvedRouteValues); TResponse response; if (isAsync) { response = await _transport - .RequestAsync(endpointPath, postData, in openTelemetryData, requestConfig, cancellationToken) + .RequestAsync(endpointPath, postData, openTelemetryDataMutator, requestConfig, cancellationToken) .ConfigureAwait(false); } else { - response = _transport.Request(endpointPath, postData, in openTelemetryData, requestConfig); + response = _transport.Request(endpointPath, postData, openTelemetryDataMutator, requestConfig); } // Evaluate product check result @@ -252,39 +253,41 @@ async ValueTask SendRequestWithProductCheckCore() } } - private static OpenTelemetryData PrepareOpenTelemetryData(TRequest request, Dictionary resolvedRouteValues) + private static Action? GetOpenTelemetryDataMutator(TRequest request, Dictionary? resolvedRouteValues) where TRequest : Request where TRequestParameters : RequestParameters, new() { // If there are no subscribed listeners, we avoid some work and allocations if (!Elastic.Transport.Diagnostics.OpenTelemetry.ElasticTransportActivitySourceHasListeners) - return default; + return null; - // We fall back to a general operation name in cases where the derived request fails to override the property - var operationName = !string.IsNullOrEmpty(request.OperationName) ? request.OperationName : request.HttpMethod.GetStringValue(); + return OpenTelemetryDataMutator; - // TODO: Optimisation: We should consider caching these, either for cases where resolvedRouteValues is null, or - // caching per combination of route values. - // We should benchmark this first to assess the impact for common workloads. - // The former is likely going to save some short-lived allocations, but only for requests to endpoints without required path parts. - // The latter may bloat the cache as some combinations of path parts may rarely re-occur. - var attributes = new Dictionary + void OpenTelemetryDataMutator(Activity activity) { - [OpenTelemetry.SemanticConventions.DbOperation] = !string.IsNullOrEmpty(request.OperationName) ? request.OperationName : "unknown", - [$"{OpenTelemetrySpanAttributePrefix}schema_url"] = OpenTelemetrySchemaVersion - }; + // We fall back to a general operation name in cases where the derived request fails to override the property + var operationName = !string.IsNullOrEmpty(request.OperationName) ? request.OperationName : request.HttpMethod.GetStringValue(); + + // TODO: Optimisation: We should consider caching these, either for cases where resolvedRouteValues is null, or + // caching per combination of route values. + // We should benchmark this first to assess the impact for common workloads. + // The former is likely going to save some short-lived allocations, but only for requests to endpoints without required path parts. + // The latter may bloat the cache as some combinations of path parts may rarely re-occur. + + activity.DisplayName = operationName; + + activity.SetTag(OpenTelemetry.SemanticConventions.DbOperation, !string.IsNullOrEmpty(request.OperationName) ? request.OperationName : "unknown"); + activity.SetTag($"{OpenTelemetrySpanAttributePrefix}schema_url", OpenTelemetrySchemaVersion); + + if (resolvedRouteValues is null) + return; - if (resolvedRouteValues is not null) - { foreach (var value in resolvedRouteValues) { if (!string.IsNullOrEmpty(value.Key) && !string.IsNullOrEmpty(value.Value)) - attributes.Add($"{OpenTelemetrySpanAttributePrefix}path_parts.{value.Key}", value.Value); + activity.SetTag($"{OpenTelemetrySpanAttributePrefix}path_parts.{value.Key}", value.Value); } } - - var openTelemetryData = new OpenTelemetryData { SpanName = operationName, SpanAttributes = attributes }; - return openTelemetryData; } private (EndpointPath endpointPath, Dictionary? resolvedRouteValues, PostData data) PrepareRequest(TRequest request) diff --git a/src/Playground/Playground.csproj b/src/Playground/Playground.csproj index 455e7a6ebd..a70b9ce735 100644 --- a/src/Playground/Playground.csproj +++ b/src/Playground/Playground.csproj @@ -10,7 +10,7 @@ - + diff --git a/tests/Tests.Core/Client/FixedResponseClient.cs b/tests/Tests.Core/Client/FixedResponseClient.cs index 310a216f0f..5a23d866ba 100644 --- a/tests/Tests.Core/Client/FixedResponseClient.cs +++ b/tests/Tests.Core/Client/FixedResponseClient.cs @@ -17,7 +17,7 @@ public static ElasticsearchClient Create( object response, int statusCode = 200, Func modifySettings = null, - string contentType = RequestData.DefaultContentType, + string contentType = BoundConfiguration.DefaultContentType, Exception exception = null ) { @@ -29,7 +29,7 @@ public static ElasticsearchClientSettings CreateConnectionSettings( object response, int statusCode = 200, Func modifySettings = null, - string contentType = RequestData.DefaultContentType, + string contentType = BoundConfiguration.DefaultContentType, Exception exception = null, Serializer serializer = null ) @@ -46,7 +46,7 @@ public static ElasticsearchClientSettings CreateConnectionSettings( break; default: { - responseBytes = contentType == RequestData.DefaultContentType + responseBytes = contentType == BoundConfiguration.DefaultContentType ? serializer.SerializeToBytes(response, TestClient.Default.ElasticsearchClientSettings.MemoryStreamFactory) : Encoding.UTF8.GetBytes(response.ToString()); diff --git a/tests/Tests/ClientConcepts/OpenTelemetry/OpenTelemetryTests.cs b/tests/Tests/ClientConcepts/OpenTelemetry/OpenTelemetryTests.cs index 3fb2b95588..05ae1f13b5 100644 --- a/tests/Tests/ClientConcepts/OpenTelemetry/OpenTelemetryTests.cs +++ b/tests/Tests/ClientConcepts/OpenTelemetry/OpenTelemetryTests.cs @@ -32,34 +32,34 @@ public async Task BasicOpenTelemetryTest() client.Ping(); - VerifyActivity(oTelActivity, "ping"); + VerifyActivity(oTelActivity, "ping", "HEAD"); await client.PingAsync(); - VerifyActivity(oTelActivity, "ping"); + VerifyActivity(oTelActivity, "ping", "HEAD"); await client.SearchAsync(s => s.Index("test").Query(q => q.MatchAll(m => { }))); - VerifyActivity(oTelActivity, "search", "http://localhost:9200/test/_search?pretty=true&error_trace=true"); + VerifyActivity(oTelActivity, "search", "POST", "http://localhost:9200/test/_search?pretty=true&error_trace=true"); - static void VerifyActivity(Activity oTelActivity, string operation, string url = null) + static void VerifyActivity(Activity oTelActivity, string displayName, string operation, string url = null) { oTelActivity.Should().NotBeNull(); oTelActivity.Kind.Should().Be(ActivityKind.Client); - oTelActivity.DisplayName.Should().Be(operation); oTelActivity.OperationName.Should().Be(operation); + oTelActivity.DisplayName.Should().Be(displayName); oTelActivity.Tags.Should().Contain(n => n.Key == "elastic.transport.product.name" && n.Value == "elasticsearch-net"); oTelActivity.Tags.Should().Contain(n => n.Key == "db.system" && n.Value == "elasticsearch"); - oTelActivity.Tags.Should().Contain(n => n.Key == "db.operation" && n.Value == operation); + oTelActivity.Tags.Should().Contain(n => n.Key == "db.operation" && n.Value == displayName); oTelActivity.Tags.Should().Contain(n => n.Key == "db.user" && n.Value == "elastic"); oTelActivity.Tags.Should().Contain(n => n.Key == "url.full" && n.Value == (url ?? "http://localhost:9200/?pretty=true&error_trace=true")); oTelActivity.Tags.Should().Contain(n => n.Key == "server.address" && n.Value == "localhost"); - oTelActivity.Tags.Should().Contain(n => n.Key == "http.request.method" && n.Value == (operation == "ping" ? "HEAD" : "POST")); + oTelActivity.Tags.Should().Contain(n => n.Key == "http.request.method" && n.Value == (displayName == "ping" ? "HEAD" : "POST")); - switch (operation) + switch (displayName) { case "search": oTelActivity.Tags.Should().Contain(n => n.Key == "db.elasticsearch.path_parts.index" && n.Value == "test");