Skip to content

Commit

Permalink
Merge pull request #1948 from ClickHouse/v2_product_name
Browse files Browse the repository at this point in the history
[client-v2] User-Agent
  • Loading branch information
chernser authored Nov 21, 2024
2 parents b1027ba + b12b821 commit 1a4bdba
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 3 deletions.
57 changes: 55 additions & 2 deletions client-v2/src/main/java/com/clickhouse/client/api/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.clickhouse.client.api.internal.ClickHouseLZ4OutputStream;
import com.clickhouse.client.api.internal.ClientStatisticsHolder;
import com.clickhouse.client.api.internal.ClientV1AdaptorHelper;
import com.clickhouse.client.api.internal.EnvUtils;
import com.clickhouse.client.api.internal.HttpAPIClientHelper;
import com.clickhouse.client.api.internal.MapUtils;
import com.clickhouse.client.api.internal.SettingsConverter;
Expand All @@ -49,6 +50,7 @@
import org.apache.hc.core5.concurrent.DefaultThreadFactory;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ConnectionRequestTimeoutException;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.NoHttpResponseException;
import org.slf4j.Logger;
Expand All @@ -72,6 +74,7 @@
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.StringJoiner;
Expand Down Expand Up @@ -828,7 +831,7 @@ public Builder allowBinaryReaderToReuseBuffers(boolean reuse) {
* @return same instance of the builder
*/
public Builder httpHeader(String key, String value) {
this.configuration.put(ClientConfigProperties.HTTP_HEADER_PREFIX + key, value);
this.configuration.put(ClientConfigProperties.HTTP_HEADER_PREFIX + key.toUpperCase(Locale.US), value);
return this;
}

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

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

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

public Client build() {
setDefaults();
Expand Down Expand Up @@ -1042,7 +1061,41 @@ private void setDefaults() {
if (!configuration.containsKey(ClientConfigProperties.HTTP_USE_BASIC_AUTH.getKey())) {
useHTTPBasicAuth(true);
}

String userAgent = configuration.getOrDefault(ClientConfigProperties.HTTP_HEADER_PREFIX + HttpHeaders.USER_AGENT.toUpperCase(Locale.US), "");
String clientName = configuration.getOrDefault(ClientConfigProperties.CLIENT_NAME.getKey(), "");
httpHeader(HttpHeaders.USER_AGENT, buildUserAgent(userAgent.isEmpty() ? clientName : userAgent));
}

private static String buildUserAgent(String customUserAgent) {

StringBuilder userAgent = new StringBuilder();
if (customUserAgent != null && !customUserAgent.isEmpty()) {
userAgent.append(customUserAgent).append(" ");
}

userAgent.append(CLIENT_USER_AGENT);

String clientVersion = Client.class.getPackage().getImplementationVersion();
if (clientVersion == null) {
clientVersion = LATEST_ARTIFACT_VERSION;
}
userAgent.append(clientVersion);

userAgent.append(" (");
userAgent.append(System.getProperty("os.name"));
userAgent.append("; ");
userAgent.append("jvm:").append(System.getProperty("java.version"));
userAgent.append("; ");

userAgent.setLength(userAgent.length() - 2);
userAgent.append(')');

return userAgent.toString();
}

public static final String LATEST_ARTIFACT_VERSION = "0.7.1-patch1";
public static final String CLIENT_USER_AGENT = "clickhouse-java-v2/";
}

private ClickHouseNode getServerNode() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ public enum ClientConfigProperties {

CONNECTION_REQUEST_TIMEOUT("connection_request_timeout"),

CLIENT_RETRY_ON_FAILURE("client_retry_on_failures");
CLIENT_RETRY_ON_FAILURE("client_retry_on_failures"),

CLIENT_NAME("client_name");

private String key;

Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.clickhouse.client.api.internal;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Collections;

/**
* Environment utility class.
*/
public class EnvUtils {

private static final Logger LOG = LoggerFactory.getLogger(EnvUtils.class);

/**
* Returns the local host name or IP address. Can be used to set {@code Referer} HTTP header.
* If fails to find the local host name or address, returns an empty string.
* @param returnAddress if true, return address; otherwise, return name
* @return string representing the local host name or address
*/
public static String getLocalhostNameOrAddress(final boolean returnAddress) {
try {

for (NetworkInterface networkInterface : Collections.list(NetworkInterface.getNetworkInterfaces())) {
for (InetAddress inetAddress : Collections.list(networkInterface.getInetAddresses())) {
if (inetAddress.isLoopbackAddress()) {
continue;
}
if (returnAddress) {
return inetAddress.getHostAddress();
} else {
return inetAddress.getCanonicalHostName();
}
}
}
} catch (Exception e) {
LOG.error("Failed to get local host name or address", e);
}
return "";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,14 @@ public class HttpAPIClientHelper {

private final Set<ClientFaultCause> defaultRetryCauses;

private String httpClientUserAgentPart;

public HttpAPIClientHelper(Map<String, String> configuration) {
this.chConfiguration = configuration;
this.httpClient = createHttpClient();

this.httpClientUserAgentPart = this.httpClient.getClass().getPackage().getImplementationTitle() + "/" + this.httpClient.getClass().getPackage().getImplementationVersion();

RequestConfig.Builder reqConfBuilder = RequestConfig.custom();
MapUtils.applyLong(chConfiguration, "connection_request_timeout",
(t) -> reqConfBuilder
Expand Down Expand Up @@ -451,6 +455,10 @@ private void addHeaders(HttpPost req, Map<String, String> chConfig, Map<String,
req.removeHeaders(ClickHouseHttpProto.HEADER_DB_USER);
req.removeHeaders(ClickHouseHttpProto.HEADER_DB_PASSWORD);
}

// -- keep last
Header userAgent = req.getFirstHeader(HttpHeaders.USER_AGENT);
req.setHeader(HttpHeaders.USER_AGENT, userAgent == null ? httpClientUserAgentPart : userAgent.getValue() + " " + httpClientUserAgentPart);
}
private void addQueryParams(URIBuilder req, Map<String, String> chConfig, Map<String, Object> requestConfig) {
if (requestConfig == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
import com.clickhouse.client.api.ConnectionReuseStrategy;
import com.clickhouse.client.api.ServerException;
import com.clickhouse.client.api.command.CommandResponse;
import com.clickhouse.client.api.command.CommandSettings;
import com.clickhouse.client.api.enums.Protocol;
import com.clickhouse.client.api.enums.ProxyType;
import com.clickhouse.client.api.insert.InsertResponse;
import com.clickhouse.client.api.insert.InsertSettings;
import com.clickhouse.client.api.query.GenericRecord;
import com.clickhouse.client.api.query.QueryResponse;
import com.clickhouse.client.api.query.QuerySettings;
import com.clickhouse.client.config.ClickHouseClientOption;
import com.clickhouse.client.insert.SamplePOJO;
import com.clickhouse.data.ClickHouseFormat;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
Expand All @@ -38,13 +41,16 @@
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;

import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED;
import static org.junit.Assert.fail;
Expand Down Expand Up @@ -748,4 +754,44 @@ public void testErrorWithSendProgressHeaders() throws Exception {
}
}
}


@Test(groups = { "integration" }, dataProvider = "testUserAgentHasCompleteProductName_dataProvider", dataProviderClass = HttpTransportTests.class)
public void testUserAgentHasCompleteProductName(String clientName, Pattern userAgentPattern) throws Exception {

ClickHouseNode server = getServer(ClickHouseProtocol.HTTP);
try (Client client = new Client.Builder()
.addEndpoint(server.getBaseUri())
.setUsername("default")
.setPassword("")
.setClientName(clientName)
.build()) {

String q1Id = UUID.randomUUID().toString();

client.execute("SELECT 1", (CommandSettings) new CommandSettings().setQueryId(q1Id)).get().close();
client.execute("SYSTEM FLUSH LOGS").get().close();

List<GenericRecord> logRecords = client.queryAll("SELECT http_user_agent, http_referer, " +
" forwarded_for FROM system.query_log WHERE query_id = '" + q1Id + "'");
Assert.assertFalse(logRecords.isEmpty(), "No records found in query log");

for (GenericRecord record : logRecords) {

Assert.assertTrue(userAgentPattern.matcher(record.getString("http_user_agent")).matches(),
record.getString("http_user_agent") + " doesn't match \"" +
userAgentPattern.pattern() + "\"");

}
}
}


@DataProvider(name = "testUserAgentHasCompleteProductName_dataProvider")
public static Object[][] testUserAgentHasCompleteProductName_dataProvider() {
return new Object[][] {
{ "", Pattern.compile("clickhouse-java-v2\\/.+ \\(.+\\) Apache HttpClient\\/[\\d\\.]+$") },
{ "test-client/1.0", Pattern.compile("test-client/1.0 clickhouse-java-v2\\/.+ \\(.+\\) Apache HttpClient\\/[\\d\\.]+$")},
{ "test-client/", Pattern.compile("test-client/ clickhouse-java-v2\\/.+ \\(.+\\) Apache HttpClient\\/[\\d\\.]+$")}};
}
}

0 comments on commit 1a4bdba

Please sign in to comment.