Skip to content

Commit 0da5e0d

Browse files
committed
added specific User-Agent header
1 parent d9cbaba commit 0da5e0d

File tree

4 files changed

+113
-2
lines changed

4 files changed

+113
-2
lines changed

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

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.apache.hc.core5.concurrent.DefaultThreadFactory;
5252
import org.apache.hc.core5.http.ClassicHttpResponse;
5353
import org.apache.hc.core5.http.ConnectionRequestTimeoutException;
54+
import org.apache.hc.core5.http.HttpHeaders;
5455
import org.apache.hc.core5.http.HttpStatus;
5556
import org.apache.hc.core5.http.NoHttpResponseException;
5657
import org.slf4j.Logger;
@@ -74,6 +75,7 @@
7475
import java.util.HashSet;
7576
import java.util.LinkedHashMap;
7677
import java.util.List;
78+
import java.util.Locale;
7779
import java.util.Map;
7880
import java.util.Set;
7981
import java.util.StringJoiner;
@@ -811,7 +813,7 @@ public Builder allowBinaryReaderToReuseBuffers(boolean reuse) {
811813
* @return same instance of the builder
812814
*/
813815
public Builder httpHeader(String key, String value) {
814-
this.configuration.put(ClientSettings.HTTP_HEADER_PREFIX + key, value);
816+
this.configuration.put(ClientSettings.HTTP_HEADER_PREFIX + key.toUpperCase(Locale.US), value);
815817
return this;
816818
}
817819

@@ -822,7 +824,7 @@ public Builder httpHeader(String key, String value) {
822824
* @return same instance of the builder
823825
*/
824826
public Builder httpHeader(String key, Collection<String> values) {
825-
this.configuration.put(ClientSettings.HTTP_HEADER_PREFIX + key, ClientSettings.commaSeparated(values));
827+
this.configuration.put(ClientSettings.HTTP_HEADER_PREFIX + key.toUpperCase(Locale.US), ClientSettings.commaSeparated(values));
826828
return this;
827829
}
828830

@@ -880,12 +882,30 @@ public Builder columnToMethodMatchingStrategy(ColumnToMethodMatchingStrategy str
880882
* Whether to use HTTP basic authentication. Default value is true.
881883
* Password that contain UTF8 characters may not be passed through http headers and BASIC authentication
882884
* is the only option here.
885+
* @param useBasicAuth - indicates if basic authentication should be used
886+
* @return same instance of the builder
883887
*/
884888
public Builder useHTTPBasicAuth(boolean useBasicAuth) {
885889
this.configuration.put(ClientSettings.HTTP_USE_BASIC_AUTH, String.valueOf(useBasicAuth));
886890
return this;
887891
}
888892

893+
/**
894+
* Sets additional information about calling application. This string will be passed to server as a client name.
895+
* In case of HTTP protocol it will be passed as a {@code User-Agent} header.
896+
* Warn: If custom value of User-Agent header is set it will override this value for HTTP transport
897+
* </br>
898+
* Client name is used by server to identify client application when investigating {@code system.query_log}. In case of HTTP
899+
* transport this value will be in the {@code system.query_log.http_user_agent} column. Currently only HTTP transport is used.
900+
*
901+
* @param clientName - client application display name.
902+
* @return same instance of the builder
903+
*/
904+
public Builder setClientName(String clientName) {
905+
this.configuration.put(ClientSettings.CLIENT_NAME, clientName);
906+
return this;
907+
}
908+
889909
public Client build() {
890910
setDefaults();
891911

@@ -1023,7 +1043,41 @@ private void setDefaults() {
10231043
if (!configuration.containsKey(ClientSettings.HTTP_USE_BASIC_AUTH)) {
10241044
useHTTPBasicAuth(true);
10251045
}
1046+
1047+
String userAgent = configuration.getOrDefault(ClientSettings.HTTP_HEADER_PREFIX + HttpHeaders.USER_AGENT.toUpperCase(Locale.US), "");
1048+
String clientName = configuration.getOrDefault(ClientSettings.CLIENT_NAME, "");
1049+
httpHeader(HttpHeaders.USER_AGENT, buildUserAgent(userAgent.isEmpty() ? clientName : userAgent));
1050+
}
1051+
1052+
private static String buildUserAgent(String customUserAgent) {
1053+
1054+
StringBuilder userAgent = new StringBuilder();
1055+
if (customUserAgent != null && !customUserAgent.isEmpty()) {
1056+
userAgent.append(customUserAgent).append(" ");
1057+
}
1058+
1059+
userAgent.append(CLIENT_USER_AGENT);
1060+
1061+
String clientVersion = Client.class.getPackage().getImplementationVersion();
1062+
if (clientVersion == null) {
1063+
clientVersion = LATEST_ARTIFACT_VERSION;
1064+
}
1065+
userAgent.append(clientVersion);
1066+
1067+
userAgent.append(" (");
1068+
userAgent.append(System.getProperty("os.name"));
1069+
userAgent.append("; ");
1070+
userAgent.append("jvm:").append(System.getProperty("java.version"));
1071+
userAgent.append("; ");
1072+
1073+
userAgent.setLength(userAgent.length() - 2);
1074+
userAgent.append(')');
1075+
1076+
return userAgent.toString();
10261077
}
1078+
1079+
public static final String LATEST_ARTIFACT_VERSION = "0.7.1-patch1";
1080+
public static final String CLIENT_USER_AGENT = "ch-j-v2/";
10271081
}
10281082

10291083
private ClickHouseNode getServerNode() {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ public static List<String> valuesFromCommaSeparated(String value) {
4040

4141
public static final String HTTP_USE_BASIC_AUTH = "http_use_basic_auth";
4242

43+
public static final String CLIENT_NAME = "client_name";
44+
45+
4346
// -- Experimental features --
4447

4548
/**

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
@@ -89,10 +89,14 @@ public class HttpAPIClientHelper {
8989

9090
private final Set<ClientFaultCause> defaultRetryCauses;
9191

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

98+
this.httpClientUserAgentPart = this.httpClient.getClass().getPackage().getImplementationTitle() + "/" + this.httpClient.getClass().getPackage().getImplementationVersion();
99+
96100
RequestConfig.Builder reqConfBuilder = RequestConfig.custom();
97101
MapUtils.applyLong(chConfiguration, "connection_request_timeout",
98102
(t) -> reqConfBuilder
@@ -456,6 +460,10 @@ private void addHeaders(HttpPost req, Map<String, String> chConfig, Map<String,
456460
req.removeHeaders(ClickHouseHttpProto.HEADER_DB_USER);
457461
req.removeHeaders(ClickHouseHttpProto.HEADER_DB_PASSWORD);
458462
}
463+
464+
// -- keep last
465+
Header userAgent = req.getFirstHeader(HttpHeaders.USER_AGENT);
466+
req.setHeader(HttpHeaders.USER_AGENT, userAgent == null ? httpClientUserAgentPart : userAgent.getValue() + " " + httpClientUserAgentPart);
459467
}
460468
private void addQueryParams(URIBuilder req, Map<String, String> chConfig, Map<String, Object> requestConfig) {
461469
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("ch-j-v2\\/.+ \\(.+\\) Apache HttpClient\\/[\\d\\.]+$") },
794+
{ "test-client/1.0", Pattern.compile("test-client/1.0 ch-j-v2\\/.+ \\(.+\\) Apache HttpClient\\/[\\d\\.]+$")},
795+
{ "test-client/", Pattern.compile("test-client/ ch-j-v2\\/.+ \\(.+\\) Apache HttpClient\\/[\\d\\.]+$")}};
796+
}
751797
}

0 commit comments

Comments
 (0)