diff --git a/java-client/src/main/java/co/elastic/clients/transport/rest_client/LanguageRuntimeVersions.java b/java-client/src/main/java/co/elastic/clients/transport/rest_client/LanguageRuntimeVersions.java new file mode 100644 index 000000000..5c9008e83 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/transport/rest_client/LanguageRuntimeVersions.java @@ -0,0 +1,137 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package co.elastic.clients.transport.rest_client; + +// Copied verbatim from https://github.com/elastic/jvm-languages-sniffer + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +class LanguageRuntimeVersions { + + /** + * Returns runtime information by looking up classes identifying non-Java JVM + * languages and appending a key with their name and their major.minor version, if available + */ + public static String getRuntimeMetadata() { + StringBuilder s = new StringBuilder(); + String version; + + version = kotlinVersion(); + if (version != null) { + s.append(",kt=").append(version); + } + + version = scalaVersion(); + if (version != null) { + s.append(",sc=").append(version); + } + + version = clojureVersion(); + if (version != null) { + s.append(",clj=").append(version); + } + + version = groovyVersion(); + if (version != null) { + s.append(",gy=").append(version); + } + + version = jRubyVersion(); + if (version != null) { + s.append(",jrb=").append(version); + } + + return s.toString(); + } + + public static String kotlinVersion() { + // KotlinVersion.CURRENT.toString() + return keepMajorMinor(getStaticField("kotlin.KotlinVersion", "CURRENT")); + } + + public static String scalaVersion() { + // scala.util.Properties.versionNumberString() + return keepMajorMinor(callStaticMethod("scala.util.Properties", "versionNumberString")); + } + + public static String clojureVersion() { + // (clojure-version) which translates to + // clojure.core$clojure_version.invokeStatic() + return keepMajorMinor(callStaticMethod("clojure.core$clojure_version", "invokeStatic")); + } + + public static String groovyVersion() { + // groovy.lang.GroovySystem.getVersion() + // There's also getShortVersion(), but only since Groovy 3.0.1 + return keepMajorMinor(callStaticMethod("groovy.lang.GroovySystem", "getVersion")); + } + + public static String jRubyVersion() { + // org.jruby.runtime.Constants.VERSION + return keepMajorMinor(getStaticField("org.jruby.runtime.Constants", "VERSION")); + } + + private static String getStaticField(String className, String fieldName) { + Class clazz; + try { + clazz = Class.forName(className); + } catch (ClassNotFoundException e) { + return null; + } + + try { + Field field = clazz.getField(fieldName); + return field.get(null).toString(); + } catch (Exception e) { + return ""; // can't get version information + } + } + + private static String callStaticMethod(String className, String methodName) { + Class clazz; + try { + clazz = Class.forName(className); + } catch (ClassNotFoundException e) { + return null; + } + + try { + Method m = clazz.getMethod(methodName); + return m.invoke(null).toString(); + } catch (Exception e) { + return ""; // can't get version information + } + } + + static String keepMajorMinor(String version) { + if (version == null) { + return null; + } + + int firstDot = version.indexOf('.'); + int secondDot = version.indexOf('.', firstDot + 1); + if (secondDot < 0) { + return version; + } else { + return version.substring(0, secondDot); + } + } +} diff --git a/java-client/src/main/java/co/elastic/clients/transport/rest_client/RestClientOptions.java b/java-client/src/main/java/co/elastic/clients/transport/rest_client/RestClientOptions.java index 7d8f77117..5d7e9a098 100644 --- a/java-client/src/main/java/co/elastic/clients/transport/rest_client/RestClientOptions.java +++ b/java-client/src/main/java/co/elastic/clients/transport/rest_client/RestClientOptions.java @@ -21,6 +21,8 @@ import co.elastic.clients.transport.TransportOptions; import co.elastic.clients.transport.Version; +import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; +import org.apache.http.util.VersionInfo; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.WarningsHandler; @@ -107,6 +109,9 @@ public RequestOptions.Builder restClientRequestOptionsBuilder() { @Override public TransportOptions.Builder addHeader(String name, String value) { + if (name.equalsIgnoreCase(CLIENT_META)) { + return this; + } if (name.equalsIgnoreCase(USER_AGENT)) { // We must filter out our own user-agent from the options or they'll end up as multiple values for the header RequestOptions options = builder.build(); @@ -159,6 +164,7 @@ public RestClientOptions build() { } private static final String USER_AGENT = "User-Agent"; + private static final String CLIENT_META = "X-Elastic-Client-Meta"; static RestClientOptions initialOptions() { String ua = String.format( @@ -171,8 +177,41 @@ static RestClientOptions initialOptions() { return new RestClientOptions( RequestOptions.DEFAULT.toBuilder() .addHeader(USER_AGENT, ua) + .addHeader(CLIENT_META, getClientMeta()) .addHeader("Accept", RestClientTransport.JsonContentType.toString()) .build() ); } + + private static String getClientMeta() { + + VersionInfo httpClientVersion = null; + try { + httpClientVersion = VersionInfo.loadVersionInfo( + "org.apache.http.nio.client", + HttpAsyncClientBuilder.class.getClassLoader() + ); + } catch (Exception e) { + // Keep unknown + } + + // Use a single 'p' suffix for all prerelease versions (snapshot, beta, etc). + String metaVersion = Version.VERSION == null ? "" : Version.VERSION.toString(); + int dashPos = metaVersion.indexOf('-'); + if (dashPos > 0) { + metaVersion = metaVersion.substring(0, dashPos) + "p"; + } + + // service, language, transport, followed by additional information + return "es=" + + metaVersion + + ",jv=" + + System.getProperty("java.specification.version") + + ",hl=2" + + ",t=" + + metaVersion + + ",hc=" + + (httpClientVersion == null ? "" : httpClientVersion.getRelease()) + + LanguageRuntimeVersions.getRuntimeMetadata(); + } } diff --git a/java-client/src/test/java/co/elastic/clients/transport/RequestOptionsTest.java b/java-client/src/test/java/co/elastic/clients/transport/RequestOptionsTest.java index 2c29bee25..fdf598815 100644 --- a/java-client/src/test/java/co/elastic/clients/transport/RequestOptionsTest.java +++ b/java-client/src/test/java/co/elastic/clients/transport/RequestOptionsTest.java @@ -48,7 +48,6 @@ public class RequestOptionsTest extends Assert { private static HttpServer httpServer; private static RestClient restClient; - @Before public void classSetup() throws IOException { @@ -110,6 +109,7 @@ public void testDefaultHeaders() throws IOException { assertTrue(props.getProperty("header-user-agent").startsWith("elastic-java/" + Version.VERSION.toString())); assertTrue(props.getProperty("header-x-elastic-client-meta").contains("es=")); + assertTrue(props.getProperty("header-x-elastic-client-meta").contains("hl=2")); assertEquals( "application/vnd.elasticsearch+json; compatible-with=" + String.valueOf(Version.VERSION.major()), props.getProperty("header-accept")