Skip to content

Commit 53cafe2

Browse files
authored
[client] Add client metadata header on RestClient requests (#68353)
Adds a X-Elastic-Client-Meta header to http requests sent by RestClient. This header contains information about the runtime environment that is meant to allow analyzing usage context by collecting this information on the receiving side of requests, like a proxy server in front of ES. Using a custom header allows client applications to change the User-Agent header for their own purpose without losing this information. Backport of #66303
1 parent 6ee1baa commit 53cafe2

File tree

5 files changed

+284
-6
lines changed

5 files changed

+284
-6
lines changed

client/rest/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ dependencies {
4343
testImplementation "org.elasticsearch:mocksocket:${versions.mocksocket}"
4444
}
4545

46+
tasks.named("processResources").configure {
47+
expand versions: versions
48+
}
49+
4650
tasks.withType(CheckForbiddenApis).configureEach {
4751
//client does not depend on server, so only jdk and http signatures should be checked
4852
replaceSignatureFiles('jdk-signatures', 'http-signatures')
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.client;
21+
22+
import java.lang.reflect.Field;
23+
import java.lang.reflect.Method;
24+
25+
// Copied verbatim from https://github.com/elastic/jvm-languages-sniffer
26+
27+
class LanguageRuntimeVersions {
28+
29+
/**
30+
* Returns runtime information by looking up classes identifying non-Java JVM
31+
* languages and appending a key with their name and their major.minor version, if available
32+
*/
33+
public static String getRuntimeMetadata() {
34+
StringBuilder s = new StringBuilder();
35+
String version;
36+
37+
version= kotlinVersion();
38+
if (version != null) {
39+
s.append(",kt=").append(version);
40+
}
41+
42+
version = scalaVersion();
43+
if (version != null) {
44+
s.append(",sc=").append(version);
45+
}
46+
47+
version = clojureVersion();
48+
if (version != null) {
49+
s.append(",clj=").append(version);
50+
}
51+
52+
version = groovyVersion();
53+
if (version != null) {
54+
s.append(",gy=").append(version);
55+
}
56+
57+
version = jRubyVersion();
58+
if (version != null) {
59+
s.append(",jrb=").append(version);
60+
}
61+
62+
return s.toString();
63+
}
64+
65+
public static String kotlinVersion() {
66+
//KotlinVersion.CURRENT.toString()
67+
return keepMajorMinor(getStaticField("kotlin.KotlinVersion", "CURRENT"));
68+
}
69+
70+
public static String scalaVersion() {
71+
// scala.util.Properties.versionNumberString()
72+
return keepMajorMinor(callStaticMethod("scala.util.Properties", "versionNumberString"));
73+
}
74+
75+
public static String clojureVersion() {
76+
// (clojure-version) which translates to
77+
// clojure.core$clojure_version.invokeStatic()
78+
return keepMajorMinor(callStaticMethod("clojure.core$clojure_version", "invokeStatic"));
79+
}
80+
81+
public static String groovyVersion() {
82+
// groovy.lang.GroovySystem.getVersion()
83+
// There's also getShortVersion(), but only since Groovy 3.0.1
84+
return keepMajorMinor(callStaticMethod("groovy.lang.GroovySystem", "getVersion"));
85+
}
86+
87+
public static String jRubyVersion() {
88+
// org.jruby.runtime.Constants.VERSION
89+
return keepMajorMinor(getStaticField("org.jruby.runtime.Constants", "VERSION"));
90+
}
91+
92+
private static String getStaticField(String className, String fieldName) {
93+
Class<?> clazz;
94+
try {
95+
clazz = Class.forName(className);
96+
} catch (ClassNotFoundException e) {
97+
return null;
98+
}
99+
100+
try {
101+
Field field = clazz.getField(fieldName);
102+
return field.get(null).toString();
103+
} catch (Exception e) {
104+
return ""; // can't get version information
105+
}
106+
}
107+
108+
private static String callStaticMethod(String className, String methodName) {
109+
Class<?> clazz;
110+
try {
111+
clazz = Class.forName(className);
112+
} catch (ClassNotFoundException e) {
113+
return null;
114+
}
115+
116+
try {
117+
Method m = clazz.getMethod(methodName);
118+
return m.invoke(null).toString();
119+
} catch (Exception e) {
120+
return ""; // can't get version information
121+
}
122+
}
123+
124+
static String keepMajorMinor(String version) {
125+
if (version == null) {
126+
return null;
127+
}
128+
129+
int firstDot = version.indexOf('.');
130+
int secondDot = version.indexOf('.', firstDot + 1);
131+
if (secondDot < 0) {
132+
return version;
133+
} else {
134+
return version.substring(0, secondDot);
135+
}
136+
}
137+
}

client/rest/src/main/java/org/elasticsearch/client/RestClientBuilder.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,26 @@
2020
package org.elasticsearch.client;
2121

2222
import org.apache.http.Header;
23+
import org.apache.http.HttpRequest;
2324
import org.apache.http.client.config.RequestConfig;
2425
import org.apache.http.impl.client.CloseableHttpClient;
2526
import org.apache.http.impl.client.HttpClientBuilder;
2627
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
2728
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
2829
import org.apache.http.nio.conn.SchemeIOSessionStrategy;
30+
import org.apache.http.protocol.HttpContext;
31+
import org.apache.http.util.VersionInfo;
2932

3033
import javax.net.ssl.SSLContext;
34+
import java.io.IOException;
35+
import java.io.InputStream;
3136
import java.security.AccessController;
3237
import java.security.NoSuchAlgorithmException;
3338
import java.security.PrivilegedAction;
3439
import java.util.List;
40+
import java.util.Locale;
3541
import java.util.Objects;
42+
import java.util.Properties;
3643

3744
/**
3845
* Helps creating a new {@link RestClient}. Allows to set the most common http client configuration options when internally
@@ -45,6 +52,11 @@ public final class RestClientBuilder {
4552
public static final int DEFAULT_MAX_CONN_PER_ROUTE = 10;
4653
public static final int DEFAULT_MAX_CONN_TOTAL = 30;
4754

55+
static final String VERSION;
56+
static final String META_HEADER_NAME = "X-Elastic-Client-Meta";
57+
private static final String META_HEADER_VALUE;
58+
private static final String USER_AGENT_HEADER_VALUE;
59+
4860
private static final Header[] EMPTY_HEADERS = new Header[0];
4961

5062
private final List<Node> nodes;
@@ -56,6 +68,48 @@ public final class RestClientBuilder {
5668
private NodeSelector nodeSelector = NodeSelector.ANY;
5769
private boolean strictDeprecationMode = false;
5870
private boolean compressionEnabled = false;
71+
private boolean metaHeaderEnabled = true;
72+
73+
static {
74+
75+
// Never fail on unknown version, even if an environment messed up their classpath enough that we can't find it.
76+
// Better have incomplete telemetry than crashing user applications.
77+
String version = null;
78+
try (InputStream is = RestClient.class.getResourceAsStream("version.properties")) {
79+
if (is != null) {
80+
Properties versions = new Properties();
81+
versions.load(is);
82+
version = versions.getProperty("elasticsearch-client");
83+
}
84+
} catch (IOException e) {
85+
// Keep version unknown
86+
}
87+
88+
if (version == null) {
89+
version = ""; // unknown values are reported as empty strings in X-Elastic-Client-Meta
90+
}
91+
92+
VERSION = version;
93+
94+
USER_AGENT_HEADER_VALUE = String.format(Locale.ROOT, "elasticsearch-java/%s (Java/%s)",
95+
VERSION.isEmpty() ? "Unknown" : VERSION, System.getProperty("java.version"));
96+
97+
VersionInfo httpClientVersion = null;
98+
try {
99+
httpClientVersion = AccessController.doPrivileged((PrivilegedAction<VersionInfo>)() ->
100+
VersionInfo.loadVersionInfo("org.apache.http.nio.client", HttpAsyncClientBuilder.class.getClassLoader())
101+
);
102+
} catch (Exception e) {
103+
// Keep unknown
104+
}
105+
106+
// service, language, transport, followed by additional information
107+
META_HEADER_VALUE = "es=" + VERSION +
108+
",jv=" + System.getProperty("java.specification.version") +
109+
",t=" + VERSION +
110+
",hc=" + (httpClientVersion == null ? "" : httpClientVersion.getRelease()) +
111+
LanguageRuntimeVersions.getRuntimeMetadata();
112+
}
59113

60114
/**
61115
* Creates a new builder instance and sets the hosts that the client will send requests to.
@@ -191,6 +245,17 @@ public RestClientBuilder setCompressionEnabled(boolean compressionEnabled) {
191245
return this;
192246
}
193247

248+
/**
249+
* Whether to send a {@code X-Elastic-Client-Meta} header that describes the runtime environment. It contains
250+
* information that is similar to what could be found in {@code User-Agent}. Using a separate header allows
251+
* applications to use {@code User-Agent} for their own needs, e.g. to identify application version or other
252+
* environment information. Defaults to {@code true}.
253+
*/
254+
public RestClientBuilder setMetaHeaderEnabled(boolean metadataEnabled) {
255+
this.metaHeaderEnabled = metadataEnabled;
256+
return this;
257+
}
258+
194259
/**
195260
* Creates a new {@link RestClient} based on the provided configuration.
196261
*/
@@ -220,11 +285,20 @@ private CloseableHttpAsyncClient createHttpClient() {
220285
//default settings for connection pooling may be too constraining
221286
.setMaxConnPerRoute(DEFAULT_MAX_CONN_PER_ROUTE).setMaxConnTotal(DEFAULT_MAX_CONN_TOTAL)
222287
.setSSLContext(SSLContext.getDefault())
288+
.setUserAgent(USER_AGENT_HEADER_VALUE)
223289
.setTargetAuthenticationStrategy(new PersistentCredentialsAuthenticationStrategy());
224290
if (httpClientConfigCallback != null) {
225291
httpClientBuilder = httpClientConfigCallback.customizeHttpClient(httpClientBuilder);
226292
}
227293

294+
// Always add metadata header last so that it's not overwritten
295+
httpClientBuilder.addInterceptorLast((HttpRequest request, HttpContext context) -> {
296+
if (metaHeaderEnabled) {
297+
request.setHeader(META_HEADER_NAME, META_HEADER_VALUE);
298+
} else {
299+
request.removeHeaders(META_HEADER_NAME);
300+
}
301+
});
228302
final HttpAsyncClientBuilder finalBuilder = httpClientBuilder;
229303
return AccessController.doPrivileged((PrivilegedAction<CloseableHttpAsyncClient>) finalBuilder::build);
230304
} catch (NoSuchAlgorithmException e) {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#
2+
# Licensed to Elasticsearch under one or more contributor
3+
# license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright
5+
# ownership. Elasticsearch licenses this file to you under
6+
# the Apache License, Version 2.0 (the "License"); you may
7+
# not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing,
13+
# software distributed under the License is distributed on an
14+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
# KIND, either express or implied. See the License for the
16+
# specific language governing permissions and limitations
17+
# under the License.
18+
#
19+
20+
elasticsearch-client=${versions.elasticsearch}

0 commit comments

Comments
 (0)