Skip to content

Commit cb89735

Browse files
authored
Add User-Agent string to API calls to Elasticsearch (#3784)
This commit adds a User-Agent to all API calls to Elasticsearch, that includes the client name and version, along with OS, Framework and assembly. Where System.Runtime.InteropServices.RuntimeInformation is available, use the properties of this type. For .NET 4.6.1 which does not support this type, use native Windows functions to determine the version of Windows, and Environment.OSVersion.VersionString for other platforms. The latter is not used for Windows because it can report incorrect version information depending on runtime version and service packs installed, and the context in which calls are executed i.e. it is not reliable. Add static readonly strings for default user agents for both the low level and high level clients
1 parent b79e879 commit cb89735

File tree

10 files changed

+259
-14
lines changed

10 files changed

+259
-14
lines changed

src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,63 +6,97 @@
66
using System.Diagnostics;
77
using System.Diagnostics.CodeAnalysis;
88
using System.Net.Security;
9+
using System.Reflection;
910
using System.Security.Cryptography.X509Certificates;
1011
using System.Threading;
1112

1213
#if DOTNETCORE
1314
using System.Net.Http;
15+
using System.Runtime.InteropServices;
1416
#endif
1517

16-
1718
namespace Elasticsearch.Net
1819
{
1920
/// <summary>
20-
/// ConnectionConfiguration allows you to control how ElasticLowLevelClient behaves and where/how it connects
21-
/// to elasticsearch
21+
/// Allows you to control how <see cref="ElasticLowLevelClient"/> behaves and where/how it connects to Elasticsearch
2222
/// </summary>
2323
public class ConnectionConfiguration : ConnectionConfiguration<ConnectionConfiguration>
2424
{
25+
/// <summary>
26+
/// The default ping timeout. Defaults to 2 seconds
27+
/// </summary>
2528
public static readonly TimeSpan DefaultPingTimeout = TimeSpan.FromSeconds(2);
29+
30+
/// <summary>
31+
/// The default ping timeout when the connection is over HTTPS. Defaults to
32+
/// 5 seconds
33+
/// </summary>
2634
public static readonly TimeSpan DefaultPingTimeoutOnSSL = TimeSpan.FromSeconds(5);
35+
36+
/// <summary>
37+
/// The default timeout before the client aborts a request to Elasticsearch.
38+
/// Defaults to 1 minute
39+
/// </summary>
2740
public static readonly TimeSpan DefaultTimeout = TimeSpan.FromMinutes(1);
41+
42+
/// <summary>
43+
/// The default connection limit for both Elasticsearch.Net and Nest. Defaults to <c>80</c> except for
44+
/// HttpClientHandler implementations based on curl, which defaults to
45+
/// <see cref="Environment.ProcessorCount"/>
46+
/// </summary>
2847
public static readonly int DefaultConnectionLimit = IsCurlHandler ? Environment.ProcessorCount : 80;
2948

49+
/// <summary>
50+
/// The default user agent for Elasticsearch.Net
51+
/// </summary>
52+
public static readonly string DefaultUserAgent = $"elasticsearch-net/{typeof(IConnectionConfigurationValues).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion} ({RuntimeInformation.OSDescription}; {RuntimeInformation.FrameworkDescription}; Elasticsearch.Net)";
3053

3154
/// <summary>
32-
/// ConnectionConfiguration allows you to control how ElasticLowLevelClient behaves and where/how it connects
33-
/// to elasticsearch
55+
/// Creates a new instance of <see cref="ConnectionConfiguration"/>
3456
/// </summary>
35-
/// <param name="uri">The root of the elasticsearch node we want to connect to. Defaults to http://localhost:9200</param>
57+
/// <param name="uri">The root of the Elasticsearch node we want to connect to. Defaults to http://localhost:9200</param>
3658
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
3759
public ConnectionConfiguration(Uri uri = null)
3860
: this(new SingleNodeConnectionPool(uri ?? new Uri("http://localhost:9200"))) { }
3961

4062
/// <summary>
41-
/// ConnectionConfiguration allows you to control how ElasticLowLevelClient behaves and where/how it connects
42-
/// to elasticsearch
63+
/// Creates a new instance of <see cref="ConnectionConfiguration"/>
4364
/// </summary>
44-
/// <param name="connectionPool">A connection pool implementation that'll tell the client what nodes are available</param>
65+
/// <param name="connectionPool">A connection pool implementation that tells the client what nodes are available</param>
4566
public ConnectionConfiguration(IConnectionPool connectionPool)
4667
// ReSharper disable once IntroduceOptionalParameters.Global
4768
: this(connectionPool, null, null) { }
4869

70+
/// <summary>
71+
/// Creates a new instance of <see cref="ConnectionConfiguration"/>
72+
/// </summary>
73+
/// <param name="connectionPool">A connection pool implementation that tells the client what nodes are available</param>
74+
/// <param name="connection">An connection implementation that can make API requests</param>
4975
public ConnectionConfiguration(IConnectionPool connectionPool, IConnection connection)
5076
// ReSharper disable once IntroduceOptionalParameters.Global
5177
: this(connectionPool, connection, null) { }
5278

79+
/// <summary>
80+
/// Creates a new instance of <see cref="ConnectionConfiguration"/>
81+
/// </summary>
82+
/// <param name="connectionPool">A connection pool implementation that tells the client what nodes are available</param>
83+
/// <param name="serializer">A serializer implementation used to serialize requests and deserialize responses</param>
5384
public ConnectionConfiguration(IConnectionPool connectionPool, IElasticsearchSerializer serializer)
5485
: this(connectionPool, null, serializer) { }
5586

56-
// ReSharper disable once MemberCanBePrivate.Global
57-
// eventhough we use don't use this we very much would like to expose this constructor
58-
87+
/// <summary>
88+
/// Creates a new instance of <see cref="ConnectionConfiguration"/>
89+
/// </summary>
90+
/// <param name="connectionPool">A connection pool implementation that tells the client what nodes are available</param>
91+
/// <param name="connection">An connection implementation that can make API requests</param>
92+
/// <param name="serializer">A serializer implementation used to serialize requests and deserialize responses</param>
5993
public ConnectionConfiguration(IConnectionPool connectionPool, IConnection connection, IElasticsearchSerializer serializer)
6094
: base(connectionPool, connection, serializer) { }
6195

6296
internal static bool IsCurlHandler { get; } =
6397
#if DOTNETCORE
64-
typeof(HttpClientHandler).Assembly.GetType("System.Net.Http.CurlHandler") != null;
65-
#else
98+
typeof(HttpClientHandler).Assembly.GetType("System.Net.Http.CurlHandler") != null;
99+
#else
66100
false;
67101
#endif
68102
}
@@ -139,6 +173,8 @@ public abstract class ConnectionConfiguration<T> : IConnectionConfigurationValue
139173

140174
private readonly ElasticsearchUrlFormatter _urlFormatter;
141175

176+
private string _userAgent = ConnectionConfiguration.DefaultUserAgent;
177+
142178
protected ConnectionConfiguration(IConnectionPool connectionPool, IConnection connection, IElasticsearchSerializer requestResponseSerializer)
143179
{
144180
_connectionPool = connectionPool;
@@ -198,6 +234,7 @@ protected ConnectionConfiguration(IConnectionPool connectionPool, IConnection co
198234
bool IConnectionConfigurationValues.SniffsOnStartup => _sniffOnStartup;
199235
bool IConnectionConfigurationValues.ThrowExceptions => _throwExceptions;
200236
ElasticsearchUrlFormatter IConnectionConfigurationValues.UrlFormatter => _urlFormatter;
237+
string IConnectionConfigurationValues.UserAgent => _userAgent;
201238

202239
void IDisposable.Dispose() => DisposeManagedResources();
203240

@@ -500,6 +537,12 @@ public T ClientCertificate(string certificatePath) =>
500537
public T SkipDeserializationForStatusCodes(params int[] statusCodes) =>
501538
Assign(new ReadOnlyCollection<int>(statusCodes), (a, v) => a._skipDeserializationForStatusCodes = v);
502539

540+
/// <summary>
541+
/// The user agent string to send with requests. Useful for debugging purposes to understand client and framework
542+
/// versions that initiate requests to Elasticsearch
543+
/// </summary>
544+
public T UserAgent(string userAgent) => Assign(userAgent, (a, v) => a._userAgent = v);
545+
503546
protected virtual void DisposeManagedResources()
504547
{
505548
_connectionPool?.Dispose();

src/Elasticsearch.Net/Configuration/IConnectionConfigurationValues.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,5 +213,11 @@ public interface IConnectionConfigurationValues : IDisposable
213213
bool ThrowExceptions { get; }
214214

215215
ElasticsearchUrlFormatter UrlFormatter { get; }
216+
217+
/// <summary>
218+
/// The user agent string to send with requests. Useful for debugging purposes to understand client and framework
219+
/// versions that initiate requests to Elasticsearch
220+
/// </summary>
221+
string UserAgent { get; }
216222
}
217223
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,12 @@ protected virtual HttpRequestMessage CreateRequestMessage(RequestData requestDat
222222
requestMessage.Headers.ConnectionClose = false;
223223
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(requestData.Accept));
224224

225+
if (!string.IsNullOrWhiteSpace(requestData.UserAgent))
226+
{
227+
requestMessage.Headers.UserAgent.Clear();
228+
requestMessage.Headers.UserAgent.TryParseAdd(requestData.UserAgent);
229+
}
230+
225231
if (!requestData.RunAs.IsNullOrEmpty())
226232
requestMessage.Headers.Add(RequestData.RunAsSecurityHeader, requestData.RunAs);
227233

src/Elasticsearch.Net/Connection/HttpWebRequestConnection.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,10 @@ protected virtual HttpWebRequest CreateWebRequest(RequestData requestData)
185185
request.Headers.Add("Accept-Encoding", "gzip,deflate");
186186
request.Headers.Add("Content-Encoding", "gzip");
187187
}
188+
189+
if (!string.IsNullOrWhiteSpace(requestData.UserAgent))
190+
request.UserAgent = requestData.UserAgent;
191+
188192
if (!string.IsNullOrWhiteSpace(requestData.RunAs))
189193
request.Headers.Add(RequestData.RunAsSecurityHeader, requestData.RunAs);
190194

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
// https://raw.githubusercontent.com/dotnet/core-setup/master/src/managed/Microsoft.DotNet.PlatformAbstractions/Native/NativeMethods.Windows.cs
5+
6+
using System.Runtime.InteropServices;
7+
8+
namespace Elasticsearch.Net
9+
{
10+
internal static class NativeMethods
11+
{
12+
public static class Windows
13+
{
14+
// This call avoids the shimming Windows does to report old versions
15+
[DllImport("ntdll")]
16+
private static extern int RtlGetVersion(out RTL_OSVERSIONINFOEX lpVersionInformation);
17+
18+
internal static string RtlGetVersion()
19+
{
20+
var osvi = new RTL_OSVERSIONINFOEX();
21+
osvi.dwOSVersionInfoSize = (uint)Marshal.SizeOf(osvi);
22+
if (RtlGetVersion(out osvi) == 0)
23+
return $"Microsoft Windows {osvi.dwMajorVersion}.{osvi.dwMinorVersion}.{osvi.dwBuildNumber}";
24+
25+
return null;
26+
}
27+
28+
[StructLayout(LayoutKind.Sequential)]
29+
internal struct RTL_OSVERSIONINFOEX
30+
{
31+
internal uint dwOSVersionInfoSize;
32+
internal uint dwMajorVersion;
33+
internal uint dwMinorVersion;
34+
internal uint dwBuildNumber;
35+
internal uint dwPlatformId;
36+
37+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
38+
internal string szCSDVersion;
39+
}
40+
}
41+
}
42+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System;
2+
#if NET461
3+
using System.Reflection;
4+
5+
namespace Elasticsearch.Net
6+
{
7+
internal static class RuntimeInformation
8+
{
9+
private static string _frameworkDescription;
10+
private static string _osDescription;
11+
12+
public static string FrameworkDescription
13+
{
14+
get
15+
{
16+
if (_frameworkDescription == null)
17+
{
18+
var assemblyFileVersionAttribute =
19+
(AssemblyFileVersionAttribute)typeof(object).GetTypeInfo().Assembly.GetCustomAttribute(typeof(AssemblyFileVersionAttribute));
20+
_frameworkDescription = $".NET Framework {assemblyFileVersionAttribute.Version}";
21+
}
22+
return _frameworkDescription;
23+
}
24+
}
25+
26+
public static string OSDescription
27+
{
28+
get
29+
{
30+
if (_osDescription == null)
31+
{
32+
var platform = (int)Environment.OSVersion.Platform;
33+
var isWindows = platform != 4 && platform != 6 && platform != 128;
34+
if (isWindows)
35+
_osDescription = NativeMethods.Windows.RtlGetVersion() ?? "Microsoft Windows";
36+
else
37+
_osDescription = Environment.OSVersion.VersionString;
38+
}
39+
return _osDescription;
40+
}
41+
}
42+
}
43+
}
44+
#endif

src/Elasticsearch.Net/Transport/Pipeline/RequestData.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ IMemoryStreamFactory memoryStreamFactory
6767
BasicAuthorizationCredentials = local?.BasicAuthenticationCredentials ?? global.BasicAuthenticationCredentials;
6868
AllowedStatusCodes = local?.AllowedStatusCodes ?? Enumerable.Empty<int>();
6969
ClientCertificates = local?.ClientCertificates ?? global.ClientCertificates;
70+
UserAgent = global.UserAgent;
7071
}
7172

7273
public string Accept { get; }
@@ -104,6 +105,7 @@ IMemoryStreamFactory memoryStreamFactory
104105
public string RunAs { get; }
105106
public IReadOnlyCollection<int> SkipDeserializationForStatusCodes { get; }
106107
public bool ThrowExceptions { get; }
108+
public string UserAgent { get; }
107109

108110
public Uri Uri => Node != null ? new Uri(Node.Uri, PathAndQuery) : null;
109111

src/Nest/CommonAbstractions/ConnectionSettings/ConnectionSettingsBase.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,21 @@
66
using System.Reflection;
77
using Elasticsearch.Net;
88

9+
#if DOTNETCORE
10+
using System.Runtime.InteropServices;
11+
#endif
12+
913
namespace Nest
1014
{
1115
/// <inheritdoc cref="IConnectionSettingsValues" />
1216
public class ConnectionSettings : ConnectionSettingsBase<ConnectionSettings>
1317
{
18+
/// <summary>
19+
/// The default user agent for Nest
20+
/// </summary>
21+
public static readonly string DefaultUserAgent =
22+
$"elasticsearch-net/{typeof(IConnectionSettingsValues).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion} ({RuntimeInformation.OSDescription}; {RuntimeInformation.FrameworkDescription}; Nest)";
23+
1424
/// <summary>
1525
/// A delegate used to construct a serializer to serialize CLR types representing documents and other types related to documents.
1626
/// By default, the internal serializer will be used to serializer all types.
@@ -101,6 +111,8 @@ IPropertyMappingProvider propertyMappingProvider
101111
_defaultRelationNames = new FluentDictionary<Type, string>();
102112

103113
_inferrer = new Inferrer(this);
114+
115+
UserAgent(ConnectionSettings.DefaultUserAgent);
104116
}
105117

106118
Func<string, string> IConnectionSettingsValues.DefaultFieldNameInferrer => _defaultFieldNameInferrer;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
// https://raw.githubusercontent.com/dotnet/core-setup/master/src/managed/Microsoft.DotNet.PlatformAbstractions/Native/NativeMethods.Windows.cs
5+
6+
using System.Runtime.InteropServices;
7+
8+
namespace Nest
9+
{
10+
internal static class NativeMethods
11+
{
12+
public static class Windows
13+
{
14+
// This call avoids the shimming Windows does to report old versions
15+
[DllImport("ntdll")]
16+
private static extern int RtlGetVersion(out RTL_OSVERSIONINFOEX lpVersionInformation);
17+
18+
internal static string RtlGetVersion()
19+
{
20+
var osvi = new RTL_OSVERSIONINFOEX();
21+
osvi.dwOSVersionInfoSize = (uint)Marshal.SizeOf(osvi);
22+
if (RtlGetVersion(out osvi) == 0)
23+
return $"Microsoft Windows {osvi.dwMajorVersion}.{osvi.dwMinorVersion}.{osvi.dwBuildNumber}";
24+
25+
return null;
26+
}
27+
28+
[StructLayout(LayoutKind.Sequential)]
29+
internal struct RTL_OSVERSIONINFOEX
30+
{
31+
internal uint dwOSVersionInfoSize;
32+
internal uint dwMajorVersion;
33+
internal uint dwMinorVersion;
34+
internal uint dwBuildNumber;
35+
internal uint dwPlatformId;
36+
37+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
38+
internal string szCSDVersion;
39+
}
40+
}
41+
}
42+
}

0 commit comments

Comments
 (0)