Skip to content

Commit 1a4bdba

Browse files
authored
Merge pull request #1948 from ClickHouse/v2_product_name
[client-v2] User-Agent
2 parents b1027ba + b12b821 commit 1a4bdba

File tree

6 files changed

+155
-3
lines changed

6 files changed

+155
-3
lines changed

client-v2/src/main/java/com/clickhouse/client/api/Client.java

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.clickhouse.client.api.internal.ClickHouseLZ4OutputStream;
2727
import com.clickhouse.client.api.internal.ClientStatisticsHolder;
2828
import com.clickhouse.client.api.internal.ClientV1AdaptorHelper;
29+
import com.clickhouse.client.api.internal.EnvUtils;
2930
import com.clickhouse.client.api.internal.HttpAPIClientHelper;
3031
import com.clickhouse.client.api.internal.MapUtils;
3132
import com.clickhouse.client.api.internal.SettingsConverter;
@@ -49,6 +50,7 @@
4950
import org.apache.hc.core5.concurrent.DefaultThreadFactory;
5051
import org.apache.hc.core5.http.ClassicHttpResponse;
5152
import org.apache.hc.core5.http.ConnectionRequestTimeoutException;
53+
import org.apache.hc.core5.http.HttpHeaders;
5254
import org.apache.hc.core5.http.HttpStatus;
5355
import org.apache.hc.core5.http.NoHttpResponseException;
5456
import org.slf4j.Logger;
@@ -72,6 +74,7 @@
7274
import java.util.HashSet;
7375
import java.util.LinkedHashMap;
7476
import java.util.List;
77+
import java.util.Locale;
7578
import java.util.Map;
7679
import java.util.Set;
7780
import java.util.StringJoiner;
@@ -828,7 +831,7 @@ public Builder allowBinaryReaderToReuseBuffers(boolean reuse) {
828831
* @return same instance of the builder
829832
*/
830833
public Builder httpHeader(String key, String value) {
831-
this.configuration.put(ClientConfigProperties.HTTP_HEADER_PREFIX + key, value);
834+
this.configuration.put(ClientConfigProperties.HTTP_HEADER_PREFIX + key.toUpperCase(Locale.US), value);
832835
return this;
833836
}
834837

@@ -839,7 +842,7 @@ public Builder httpHeader(String key, String value) {
839842
* @return same instance of the builder
840843
*/
841844
public Builder httpHeader(String key, Collection<String> values) {
842-
this.configuration.put(ClientConfigProperties.HTTP_HEADER_PREFIX + key, ClientConfigProperties.commaSeparated(values));
845+
this.configuration.put(ClientConfigProperties.HTTP_HEADER_PREFIX + key.toUpperCase(Locale.US), ClientConfigProperties.commaSeparated(values));
843846
return this;
844847
}
845848

@@ -897,12 +900,28 @@ public Builder columnToMethodMatchingStrategy(ColumnToMethodMatchingStrategy str
897900
* Whether to use HTTP basic authentication. Default value is true.
898901
* Password that contain UTF8 characters may not be passed through http headers and BASIC authentication
899902
* is the only option here.
903+
* @param useBasicAuth - indicates if basic authentication should be used
904+
* @return same instance of the builder
900905
*/
901906
public Builder useHTTPBasicAuth(boolean useBasicAuth) {
902907
this.configuration.put(ClientConfigProperties.HTTP_USE_BASIC_AUTH.getKey(), String.valueOf(useBasicAuth));
903908
return this;
904909
}
905910

911+
/**
912+
* Sets additional information about calling application. This string will be passed to server as a client name.
913+
* In case of HTTP protocol it will be passed as a {@code User-Agent} header.
914+
* Warn: If custom value of User-Agent header is set it will override this value for HTTP transport
915+
* Client name is used by server to identify client application when investigating {@code system.query_log}. In case of HTTP
916+
* transport this value will be in the {@code system.query_log.http_user_agent} column. Currently only HTTP transport is used.
917+
*
918+
* @param clientName - client application display name.
919+
* @return same instance of the builder
920+
*/
921+
public Builder setClientName(String clientName) {
922+
this.configuration.put(ClientConfigProperties.CLIENT_NAME.getKey(), clientName);
923+
return this;
924+
}
906925

907926
public Client build() {
908927
setDefaults();
@@ -1042,7 +1061,41 @@ private void setDefaults() {
10421061
if (!configuration.containsKey(ClientConfigProperties.HTTP_USE_BASIC_AUTH.getKey())) {
10431062
useHTTPBasicAuth(true);
10441063
}
1064+
1065+
String userAgent = configuration.getOrDefault(ClientConfigProperties.HTTP_HEADER_PREFIX + HttpHeaders.USER_AGENT.toUpperCase(Locale.US), "");
1066+
String clientName = configuration.getOrDefault(ClientConfigProperties.CLIENT_NAME.getKey(), "");
1067+
httpHeader(HttpHeaders.USER_AGENT, buildUserAgent(userAgent.isEmpty() ? clientName : userAgent));
10451068
}
1069+
1070+
private static String buildUserAgent(String customUserAgent) {
1071+
1072+
StringBuilder userAgent = new StringBuilder();
1073+
if (customUserAgent != null && !customUserAgent.isEmpty()) {
1074+
userAgent.append(customUserAgent).append(" ");
1075+
}
1076+
1077+
userAgent.append(CLIENT_USER_AGENT);
1078+
1079+
String clientVersion = Client.class.getPackage().getImplementationVersion();
1080+
if (clientVersion == null) {
1081+
clientVersion = LATEST_ARTIFACT_VERSION;
1082+
}
1083+
userAgent.append(clientVersion);
1084+
1085+
userAgent.append(" (");
1086+
userAgent.append(System.getProperty("os.name"));
1087+
userAgent.append("; ");
1088+
userAgent.append("jvm:").append(System.getProperty("java.version"));
1089+
userAgent.append("; ");
1090+
1091+
userAgent.setLength(userAgent.length() - 2);
1092+
userAgent.append(')');
1093+
1094+
return userAgent.toString();
1095+
}
1096+
1097+
public static final String LATEST_ARTIFACT_VERSION = "0.7.1-patch1";
1098+
public static final String CLIENT_USER_AGENT = "clickhouse-java-v2/";
10461099
}
10471100

10481101
private ClickHouseNode getServerNode() {

client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,9 @@ public enum ClientConfigProperties {
112112

113113
CONNECTION_REQUEST_TIMEOUT("connection_request_timeout"),
114114

115-
CLIENT_RETRY_ON_FAILURE("client_retry_on_failures");
115+
CLIENT_RETRY_ON_FAILURE("client_retry_on_failures"),
116+
117+
CLIENT_NAME("client_name");
116118

117119
private String key;
118120

client-v2/src/main/java/com/clickhouse/client/api/ClientSettings.java

Whitespace-only changes.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.clickhouse.client.api.internal;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
6+
import java.net.InetAddress;
7+
import java.net.NetworkInterface;
8+
import java.util.Collections;
9+
10+
/**
11+
* Environment utility class.
12+
*/
13+
public class EnvUtils {
14+
15+
private static final Logger LOG = LoggerFactory.getLogger(EnvUtils.class);
16+
17+
/**
18+
* Returns the local host name or IP address. Can be used to set {@code Referer} HTTP header.
19+
* If fails to find the local host name or address, returns an empty string.
20+
* @param returnAddress if true, return address; otherwise, return name
21+
* @return string representing the local host name or address
22+
*/
23+
public static String getLocalhostNameOrAddress(final boolean returnAddress) {
24+
try {
25+
26+
for (NetworkInterface networkInterface : Collections.list(NetworkInterface.getNetworkInterfaces())) {
27+
for (InetAddress inetAddress : Collections.list(networkInterface.getInetAddresses())) {
28+
if (inetAddress.isLoopbackAddress()) {
29+
continue;
30+
}
31+
if (returnAddress) {
32+
return inetAddress.getHostAddress();
33+
} else {
34+
return inetAddress.getCanonicalHostName();
35+
}
36+
}
37+
}
38+
} catch (Exception e) {
39+
LOG.error("Failed to get local host name or address", e);
40+
}
41+
return "";
42+
}
43+
}

client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,14 @@ public class HttpAPIClientHelper {
8888

8989
private final Set<ClientFaultCause> defaultRetryCauses;
9090

91+
private String httpClientUserAgentPart;
92+
9193
public HttpAPIClientHelper(Map<String, String> configuration) {
9294
this.chConfiguration = configuration;
9395
this.httpClient = createHttpClient();
9496

97+
this.httpClientUserAgentPart = this.httpClient.getClass().getPackage().getImplementationTitle() + "/" + this.httpClient.getClass().getPackage().getImplementationVersion();
98+
9599
RequestConfig.Builder reqConfBuilder = RequestConfig.custom();
96100
MapUtils.applyLong(chConfiguration, "connection_request_timeout",
97101
(t) -> reqConfBuilder
@@ -451,6 +455,10 @@ private void addHeaders(HttpPost req, Map<String, String> chConfig, Map<String,
451455
req.removeHeaders(ClickHouseHttpProto.HEADER_DB_USER);
452456
req.removeHeaders(ClickHouseHttpProto.HEADER_DB_PASSWORD);
453457
}
458+
459+
// -- keep last
460+
Header userAgent = req.getFirstHeader(HttpHeaders.USER_AGENT);
461+
req.setHeader(HttpHeaders.USER_AGENT, userAgent == null ? httpClientUserAgentPart : userAgent.getValue() + " " + httpClientUserAgentPart);
454462
}
455463
private void addQueryParams(URIBuilder req, Map<String, String> chConfig, Map<String, Object> requestConfig) {
456464
if (requestConfig == null) {

client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77
import com.clickhouse.client.api.ConnectionReuseStrategy;
88
import com.clickhouse.client.api.ServerException;
99
import com.clickhouse.client.api.command.CommandResponse;
10+
import com.clickhouse.client.api.command.CommandSettings;
1011
import com.clickhouse.client.api.enums.Protocol;
1112
import com.clickhouse.client.api.enums.ProxyType;
1213
import com.clickhouse.client.api.insert.InsertResponse;
14+
import com.clickhouse.client.api.insert.InsertSettings;
1315
import com.clickhouse.client.api.query.GenericRecord;
1416
import com.clickhouse.client.api.query.QueryResponse;
1517
import com.clickhouse.client.api.query.QuerySettings;
1618
import com.clickhouse.client.config.ClickHouseClientOption;
19+
import com.clickhouse.client.insert.SamplePOJO;
1720
import com.clickhouse.data.ClickHouseFormat;
1821
import com.github.tomakehurst.wiremock.WireMockServer;
1922
import com.github.tomakehurst.wiremock.client.WireMock;
@@ -38,13 +41,16 @@
3841
import java.time.temporal.ChronoUnit;
3942
import java.util.Arrays;
4043
import java.util.Base64;
44+
import java.util.Collections;
4145
import java.util.EnumSet;
4246
import java.util.List;
4347
import java.util.Random;
48+
import java.util.UUID;
4449
import java.util.concurrent.CompletableFuture;
4550
import java.util.concurrent.ExecutionException;
4651
import java.util.concurrent.TimeUnit;
4752
import java.util.concurrent.atomic.AtomicInteger;
53+
import java.util.regex.Pattern;
4854

4955
import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED;
5056
import static org.junit.Assert.fail;
@@ -748,4 +754,44 @@ public void testErrorWithSendProgressHeaders() throws Exception {
748754
}
749755
}
750756
}
757+
758+
759+
@Test(groups = { "integration" }, dataProvider = "testUserAgentHasCompleteProductName_dataProvider", dataProviderClass = HttpTransportTests.class)
760+
public void testUserAgentHasCompleteProductName(String clientName, Pattern userAgentPattern) throws Exception {
761+
762+
ClickHouseNode server = getServer(ClickHouseProtocol.HTTP);
763+
try (Client client = new Client.Builder()
764+
.addEndpoint(server.getBaseUri())
765+
.setUsername("default")
766+
.setPassword("")
767+
.setClientName(clientName)
768+
.build()) {
769+
770+
String q1Id = UUID.randomUUID().toString();
771+
772+
client.execute("SELECT 1", (CommandSettings) new CommandSettings().setQueryId(q1Id)).get().close();
773+
client.execute("SYSTEM FLUSH LOGS").get().close();
774+
775+
List<GenericRecord> logRecords = client.queryAll("SELECT http_user_agent, http_referer, " +
776+
" forwarded_for FROM system.query_log WHERE query_id = '" + q1Id + "'");
777+
Assert.assertFalse(logRecords.isEmpty(), "No records found in query log");
778+
779+
for (GenericRecord record : logRecords) {
780+
781+
Assert.assertTrue(userAgentPattern.matcher(record.getString("http_user_agent")).matches(),
782+
record.getString("http_user_agent") + " doesn't match \"" +
783+
userAgentPattern.pattern() + "\"");
784+
785+
}
786+
}
787+
}
788+
789+
790+
@DataProvider(name = "testUserAgentHasCompleteProductName_dataProvider")
791+
public static Object[][] testUserAgentHasCompleteProductName_dataProvider() {
792+
return new Object[][] {
793+
{ "", Pattern.compile("clickhouse-java-v2\\/.+ \\(.+\\) Apache HttpClient\\/[\\d\\.]+$") },
794+
{ "test-client/1.0", Pattern.compile("test-client/1.0 clickhouse-java-v2\\/.+ \\(.+\\) Apache HttpClient\\/[\\d\\.]+$")},
795+
{ "test-client/", Pattern.compile("test-client/ clickhouse-java-v2\\/.+ \\(.+\\) Apache HttpClient\\/[\\d\\.]+$")}};
796+
}
751797
}

0 commit comments

Comments
 (0)