Skip to content

Commit 3472e7b

Browse files
authored
[Backport 6.x] Implement meta header for client requests (#5302)
* Implement meta header for client requests (cherry picked (and adapted) from commit fec16ef) * Skip nested sort test prior to 6.1
1 parent 9897ebf commit 3472e7b

32 files changed

+1319
-48
lines changed

docs/client-concepts/connection/configuration-options.asciidoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ Ensures the response bytes are always available on the `ElasticsearchResponse<T>
6464
+
6565
IMPORTANT: Depending on the registered serializer, this may cause the response to be buffered in memory first, potentially affecting performance.
6666

67+
`DisableMetaHeader`::
68+
69+
Disables the meta header which is included on all requests by default. This header contains lightweight information about the client and runtime.
70+
6771
`DisablePing`::
6872

6973
When a node is used for the very first time or when it's used for the first time after it has been marked dead a ping with a very low timeout is send to the node to make sure that when it's still dead it reports it as fast as possible. You can disable these pings globally here if you rather have it fail on the possible slower original request

src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -28,36 +28,7 @@ public class ConnectionConfiguration : ConnectionConfiguration<ConnectionConfigu
2828
/// As the old curl based handler is known to bleed TCP connections:
2929
/// <para>https://github.com/dotnet/runtime/issues/22366</para>
3030
/// </summary>
31-
private static bool UsingCurlHandler
32-
{
33-
get
34-
{
35-
#if !DOTNETCORE
36-
return false;
37-
#else
38-
var curlHandlerExists = typeof(HttpClientHandler).Assembly.GetType("System.Net.Http.CurlHandler") != null;
39-
if (!curlHandlerExists) return false;
40-
41-
var socketsHandlerExists = typeof(HttpClientHandler).Assembly.GetType("System.Net.Http.SocketsHttpHandler") != null;
42-
// running on a .NET core version with CurlHandler, before the existence of SocketsHttpHandler.
43-
// Must be using CurlHandler.
44-
if (!socketsHandlerExists) return true;
45-
46-
if (AppContext.TryGetSwitch("System.Net.Http.UseSocketsHttpHandler", out var isEnabled))
47-
return !isEnabled;
48-
49-
var environmentVariable =
50-
Environment.GetEnvironmentVariable("DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER");
51-
52-
// SocketsHandler exists and no environment variable exists to disable it.
53-
// Must be using SocketsHandler and not CurlHandler
54-
if (environmentVariable == null) return false;
55-
56-
return environmentVariable.Equals("false", StringComparison.OrdinalIgnoreCase) ||
57-
environmentVariable.Equals("0");
58-
#endif
59-
}
60-
}
31+
private static bool UsingCurlHandler => ConnectionInfo.UsingCurlHandler;
6132

6233
/// <summary>
6334
/// The default ping timeout. Defaults to 2 seconds
@@ -174,6 +145,7 @@ public abstract class ConnectionConfiguration<T> : IConnectionConfigurationValue
174145
private bool _disableAutomaticProxyDetection = false;
175146

176147
private bool _disableDirectStreaming = false;
148+
private bool _disableMetaHeader;
177149
private bool _disablePings;
178150
private bool _enableHttpCompression;
179151
private bool _enableHttpPipelining = true;
@@ -235,6 +207,7 @@ protected ConnectionConfiguration(IConnectionPool connectionPool, IConnection co
235207
TimeSpan? IConnectionConfigurationValues.DeadTimeout => _deadTimeout;
236208
bool IConnectionConfigurationValues.DisableAutomaticProxyDetection => _disableAutomaticProxyDetection;
237209
bool IConnectionConfigurationValues.DisableDirectStreaming => _disableDirectStreaming;
210+
bool IConnectionConfigurationValues.DisableMetaHeader => _disableMetaHeader;
238211
bool IConnectionConfigurationValues.DisablePings => _disablePings;
239212
bool IConnectionConfigurationValues.EnableHttpCompression => _enableHttpCompression;
240213
NameValueCollection IConnectionConfigurationValues.Headers => _headers;
@@ -270,6 +243,8 @@ protected ConnectionConfiguration(IConnectionPool connectionPool, IConnection co
270243
ElasticsearchUrlFormatter IConnectionConfigurationValues.UrlFormatter => _urlFormatter;
271244
string IConnectionConfigurationValues.UserAgent => _userAgent;
272245

246+
MetaHeaderProvider IConnectionConfigurationValues.MetaHeaderProvider { get; } = new MetaHeaderProvider();
247+
273248
void IDisposable.Dispose() => DisposeManagedResources();
274249

275250
private static void DefaultCompletedRequestHandler(IApiCallDetails response) { }
@@ -362,6 +337,12 @@ public T SniffOnConnectionFault(bool sniffsOnConnectionFault = true) =>
362337
/// </summary>
363338
public T DisableAutomaticProxyDetection(bool disable = true) => Assign(disable, (a, v) => a._disableAutomaticProxyDetection = v);
364339

340+
/// <summary>
341+
/// Disables the meta header which is included on all requests by default. This header contains lightweight information
342+
/// about the client and runtime.
343+
/// </summary>
344+
public T DisableMetaHeader(bool disable = true) => Assign(disable, (a, v) => a._disableMetaHeader = v);
345+
365346
/// <summary>
366347
/// Instead of following a c/go like error checking on response.IsValid always throw an exception
367348
/// on the client when a call resulted in an exception on either the client or the Elasticsearch server.

src/Elasticsearch.Net/Configuration/IConnectionConfigurationValues.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ public interface IConnectionConfigurationValues : IDisposable
6161
/// </summary>
6262
bool DisableDirectStreaming { get; }
6363

64+
/// <summary>
65+
/// When set to true will disable sending the meta header on requests. Defaults to false
66+
/// </summary>
67+
bool DisableMetaHeader { get; }
68+
6469
/// <summary>
6570
/// This signals that we do not want to send initial pings to unknown/previously dead nodes
6671
/// and just send the call straightaway
@@ -229,5 +234,10 @@ public interface IConnectionConfigurationValues : IDisposable
229234
#endif
230235
/// </summary>
231236
TimeSpan DnsRefreshTimeout { get; }
237+
238+
/// <summary>
239+
/// Produces the client meta header for a request.
240+
/// </summary>
241+
MetaHeaderProvider MetaHeaderProvider { get; }
232242
}
233243
}

src/Elasticsearch.Net/Configuration/RequestConfiguration.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ public interface IRequestConfiguration
9393
/// <para>Reasons for such exceptions could be search parser errors, index missing exceptions, etc...</para>
9494
/// </summary>
9595
bool ThrowExceptions { get; set; }
96+
97+
/// <summary>
98+
/// Holds additional meta data about the request.
99+
/// </summary>
100+
RequestMetaData RequestMetaData { get; set; }
96101
}
97102

98103
public class RequestConfiguration : IRequestConfiguration
@@ -113,6 +118,7 @@ public class RequestConfiguration : IRequestConfiguration
113118
public string OpaqueId { get; set; }
114119
public TimeSpan? PingTimeout { get; set; }
115120
public TimeSpan? RequestTimeout { get; set; }
121+
public RequestMetaData RequestMetaData { get; set; }
116122

117123
/// <summary>
118124
/// Submit the request on behalf in the context of a different user
@@ -162,6 +168,7 @@ public RequestConfigurationDescriptor(IRequestConfiguration config)
162168
string IRequestConfiguration.RunAs { get; set; }
163169
private IRequestConfiguration Self => this;
164170
bool IRequestConfiguration.ThrowExceptions { get; set; }
171+
RequestMetaData IRequestConfiguration.RequestMetaData { get; set; }
165172

166173
/// <summary>
167174
/// Submit the request on behalf in the context of a different shield user
@@ -284,5 +291,12 @@ public RequestConfigurationDescriptor ClientCertificate(X509Certificate certific
284291
/// <summary> Use the following client certificate to authenticate this request to Elasticsearch </summary>
285292
public RequestConfigurationDescriptor ClientCertificate(string certificatePath) =>
286293
ClientCertificates(new X509Certificate2Collection { new X509Certificate(certificatePath) });
294+
295+
/// <inheritdoc cref="IRequestConfiguration.RequestMetaData" />
296+
public RequestConfigurationDescriptor RequestMetaData(RequestMetaData metaData)
297+
{
298+
Self.RequestMetaData = metaData;
299+
return this;
300+
}
287301
}
288302
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System;
6+
7+
namespace Elasticsearch.Net
8+
{
9+
public static class RequestConfigurationExtensions
10+
{
11+
public static void SetRequestMetaData(this IRequestConfiguration requestConfiguration, RequestMetaData requestMetaData)
12+
{
13+
if (requestConfiguration is null)
14+
throw new ArgumentNullException(nameof(requestConfiguration));
15+
16+
if (requestMetaData is null)
17+
throw new ArgumentNullException(nameof(requestMetaData));
18+
19+
requestConfiguration.RequestMetaData = requestMetaData;
20+
}
21+
}
22+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System.Collections.Generic;
2+
3+
namespace Elasticsearch.Net
4+
{
5+
/// <summary>
6+
/// Holds meta data about a client request.
7+
/// </summary>
8+
public sealed class RequestMetaData
9+
{
10+
/// <summary>
11+
/// Reserved key for a meta data entry which identifies the helper which produced the request.
12+
/// </summary>
13+
public const string HelperKey = "helper";
14+
15+
private Dictionary<string, string> _metaDataItems;
16+
17+
public bool TryAddMetaData (string key, string value)
18+
{
19+
_metaDataItems ??= new Dictionary<string, string>();
20+
21+
if (_metaDataItems.ContainsKey(key))
22+
return false;
23+
24+
_metaDataItems.Add(key, value);
25+
return true;
26+
}
27+
28+
public IReadOnlyDictionary<string, string> Items => _metaDataItems ?? EmptyReadOnly<string, string>.Dictionary;
29+
}
30+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System;
6+
#if DOTNETCORE
7+
using System.Net.Http;
8+
#endif
9+
10+
namespace Elasticsearch.Net
11+
{
12+
public static class ConnectionInfo
13+
{
14+
public static bool UsingCurlHandler
15+
{
16+
get
17+
{
18+
#if !DOTNETCORE
19+
return false;
20+
#else
21+
var curlHandlerExists = typeof(HttpClientHandler).Assembly.GetType("System.Net.Http.CurlHandler") != null;
22+
if (!curlHandlerExists)
23+
return false;
24+
25+
var socketsHandlerExists = typeof(HttpClientHandler).Assembly.GetType("System.Net.Http.SocketsHttpHandler") != null;
26+
// running on a .NET core version with CurlHandler, before the existence of SocketsHttpHandler.
27+
// Must be using CurlHandler.
28+
if (!socketsHandlerExists)
29+
return true;
30+
31+
if (AppContext.TryGetSwitch("System.Net.Http.UseSocketsHttpHandler", out var isEnabled))
32+
return !isEnabled;
33+
34+
var environmentVariable =
35+
Environment.GetEnvironmentVariable("DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER");
36+
37+
// SocketsHandler exists and no environment variable exists to disable it.
38+
// Must be using SocketsHandler and not CurlHandler
39+
if (environmentVariable == null)
40+
return false;
41+
42+
return environmentVariable.Equals("false", StringComparison.OrdinalIgnoreCase) ||
43+
environmentVariable.Equals("0");
44+
#endif
45+
}
46+
}
47+
}
48+
}

src/Elasticsearch.Net/Connection/HttpConnection-CoreFx.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ public virtual async Task<TResponse> RequestAsync<TResponse>(RequestData request
9999
Stream responseStream = null;
100100
Exception ex = null;
101101
string mimeType = null;
102+
requestData.IsAsync = true;
102103
try
103104
{
104105
var requestMessage = CreateHttpRequestMessage(requestData);
@@ -233,6 +234,13 @@ protected virtual HttpRequestMessage CreateRequestMessage(RequestData requestDat
233234
stream.Position = 0;
234235
}
235236

237+
if (requestData.MetaHeaderProvider is object) {
238+
var value = requestData.MetaHeaderProvider.ProduceHeaderValue(requestData);
239+
240+
if (!string.IsNullOrEmpty(value))
241+
requestMessage.Headers.TryAddWithoutValidation(requestData.MetaHeaderProvider.HeaderName, value);
242+
}
243+
236244
return requestMessage;
237245
}
238246

src/Elasticsearch.Net/Connection/HttpWebRequestConnection.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ CancellationToken cancellationToken
8282
Stream responseStream = null;
8383
Exception ex = null;
8484
string mimeType = null;
85+
requestData.IsAsync = true;
8586
try
8687
{
8788
var data = requestData.PostData;
@@ -195,6 +196,13 @@ protected virtual HttpWebRequest CreateWebRequest(RequestData requestData)
195196
if (requestData.Headers != null && requestData.Headers.HasKeys())
196197
request.Headers.Add(requestData.Headers);
197198

199+
if (requestData.MetaHeaderProvider is object) {
200+
var value = requestData.MetaHeaderProvider.ProduceHeaderValue(requestData);
201+
202+
if (!string.IsNullOrEmpty(value))
203+
request.Headers.Add(requestData.MetaHeaderProvider.HeaderName, requestData.MetaHeaderProvider.ProduceHeaderValue(requestData));
204+
}
205+
198206
var timeout = (int)requestData.RequestTimeout.TotalMilliseconds;
199207
request.Timeout = timeout;
200208
request.ReadWriteTimeout = timeout;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System;
6+
using System.Diagnostics;
7+
using System.Reflection;
8+
using System.Text.RegularExpressions;
9+
10+
namespace Elasticsearch.Net
11+
{
12+
internal sealed class ClientVersionInfo : VersionInfo
13+
{
14+
private static readonly Regex VersionRegex = new Regex(@"(\d+\.)(\d+\.)(\d)");
15+
16+
public static readonly ClientVersionInfo Empty = new ClientVersionInfo { Version = new Version(0, 0, 0), IsPrerelease = false };
17+
18+
private ClientVersionInfo() { }
19+
20+
public static ClientVersionInfo Create<T>()
21+
{
22+
var fullVersion = DetermineClientVersion(typeof(T));
23+
24+
var clientVersion = new ClientVersionInfo();
25+
clientVersion.StoreVersion(fullVersion);
26+
return clientVersion;
27+
}
28+
29+
private static string DetermineClientVersion(Type type)
30+
{
31+
try
32+
{
33+
var productVersion = FileVersionInfo.GetVersionInfo(type.GetTypeInfo().Assembly.Location)?.ProductVersion ?? EmptyVersion;
34+
35+
if (productVersion == EmptyVersion)
36+
productVersion = Assembly.GetAssembly(type).GetName().Version.ToString();
37+
38+
var match = VersionRegex.Match(productVersion);
39+
40+
return match.Success ? match.Value : EmptyVersion;
41+
}
42+
catch
43+
{
44+
// ignore failures and fall through
45+
}
46+
47+
return EmptyVersion;
48+
}
49+
}
50+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System.Text;
6+
7+
namespace Elasticsearch.Net
8+
{
9+
internal sealed class MetaDataHeader
10+
{
11+
private const char _separator = ',';
12+
13+
private readonly string _headerValue;
14+
15+
public MetaDataHeader(VersionInfo version, string serviceIdentifier, bool isAsync)
16+
{
17+
ClientVersion = version.ToString();
18+
RuntimeVersion = new RuntimeVersionInfo().ToString();
19+
ServiceIdentifier = serviceIdentifier;
20+
21+
// This code is expected to be called infrequently so we're not concerns with over optimising this
22+
23+
_headerValue = new StringBuilder(64)
24+
.Append(serviceIdentifier).Append("=").Append(ClientVersion).Append(_separator)
25+
.Append("a=").Append(isAsync ? "1" : "0").Append(_separator)
26+
.Append("net=").Append(RuntimeVersion).Append(_separator)
27+
.Append(_httpClientIdentifier).Append("=").Append(RuntimeVersion)
28+
.ToString();
29+
}
30+
31+
private static readonly string _httpClientIdentifier =
32+
#if DOTNETCORE
33+
ConnectionInfo.UsingCurlHandler ? "cu" : "so";
34+
#else
35+
"wr";
36+
#endif
37+
38+
public string ServiceIdentifier { get; private set; }
39+
public string ClientVersion { get; private set; }
40+
public string RuntimeVersion { get; private set; }
41+
42+
public override string ToString() => _headerValue;
43+
}
44+
}

0 commit comments

Comments
 (0)