Implements azure core http client (#388)

* Introduces KustoQuery wrapper object

* working on async client

* updating client with KustoQuery object

* updating client with KustoQuery object

* updating client with KustoQuery object

* will simplify later.

* will simplify later.

* will simplify later.

* ran formatter, fixed test, fixed bad merge

* AtomicBooleans in already modified test. Save some lines, might as well.

* hide KustoQuery object

* rename and hide KustoRequest

* adding E2ETests for async queries

* ran formatter

* no complete in sink

* move blocking operations off the io thread

* run formatter

* replace execute utilization

* fixes teardown. async still doesn't work yet.

* fixes msal4j issue

* build.yaml

* Introduces KustoQuery wrapper object

* working on async client

* updating client with KustoQuery object

* updating client with KustoQuery object

* updating client with KustoQuery object

* will simplify later.

* will simplify later.

* will simplify later.

* ran formatter, fixed test, fixed bad merge

* AtomicBooleans in already modified test. Save some lines, might as well.

* hide KustoQuery object

* rename and hide KustoRequest

* adding E2ETests for async queries

* ran formatter

* no complete in sink

* move blocking operations off the io thread

* run formatter

* replace execute utilization

* fixes teardown. async still doesn't work yet.

* fixes msal4j issue

* rebased

* rework azure branch

* remove unused dependencies

* fix some more issues with imports

* add default timeout 10 min

* re-add readme

* modified changelog

* modified changelog

* removes krL

* remove async test file

* remove closeable from query clients

* fixes pom version change and removes unused reactor property

* CI_EXECUTION

* storageaccountset guard clause

* close clients in E2ETest

* remove extra assertions from teardown

* remove reactor step verifiers for now

* readd deflate to cloudinfo

* renoved reactor test library for now

* run formatter

* added fixme

* move URIBuilder to further facilitate removal of Apache

* add more detail to assertion failure

* Made InvalidConnectionString extend DataClientException so users can optionally catch the more specific exception

* commenting everything async until implementation works

* commenting everything async until implementation works

* ran formatter

* remove unused Apache classes

* removed commented code

* move message to appropriate assertion

* removed last of the commented code and ran formatter

* readd the auto-delete policy

* make request object usage private

* put back more Apache things.

* renove trailing space

* Update BaseClient.java

---------

Co-authored-by: cole <cole.snyder@gmork.tech>
Co-authored-by: asafmahlev <asafmahlev@microsoft.com>
Co-authored-by: ohad bitton <32278684+ohadbitt@users.noreply.github.com>
This commit is contained in:
Cole 2024-10-27 09:11:44 -04:00 коммит произвёл GitHub
Родитель fb832817eb
Коммит d73eeb3663
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
70 изменённых файлов: 1654 добавлений и 1327 удалений

Просмотреть файл

@ -4,11 +4,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [6.0.0] - 2024-09-21
### Changed
- Replaced Apache CloseableHttpClient with configurable azure-core client.
- [BREAKING] HttpClientFactory now accepts clients implementing azure-core HttpClient.
- [BREAKING] HttpClientProperties and HttpClientPropertiesBuilder now use azure-core ProxyOptions.
- Data client now wraps internal HTTP client.
- Moved HTTP request tracing logic into a builder class.
- Moved HTTP request building logic into a builder class.
- [BREAKING] Redirects are disabled by default. Use ClientRequestProperties "client_max_redirect_count" option
to enable. Default changed to 0.
## [5.2.0] - 2024-08-27
### Fixed
- Used Msal user prompt old code which is deprecated in the new version coming from last bom update resulted in method not found exception.

Просмотреть файл

@ -59,7 +59,7 @@ And the SDK will know to use these values automatically.
Alternatively, you can define a proxy programmatically when creating a client, using `HttpClientProperties`:
```java
HttpClientProperties httpClientProperties = HttpClientProperties.builder()
.proxy(new HttpHost("1.2.3.4", 8989))
.proxy(new ProxyOptions(ProxyOptions.Type.HTTP, new InetSocketAddress("myproxy.contoso.com", 8080)))
.build();
Client = ClientFactory.createClient(<engine_connection_string>, httpClientProperties);

Просмотреть файл

@ -167,7 +167,7 @@
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${httpclient.version}</version>
<version>${apache.httpclient.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
@ -211,11 +211,6 @@
<version>${slf4j.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>${reactor-core.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
@ -274,10 +269,5 @@
<artifactId>resilience4j-core</artifactId>
<version>${resilience4j.version}</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>io.vavr</groupId>-->
<!-- <artifactId>vavr</artifactId>-->
<!-- <version>${io.vavr.version}</version>-->
<!-- </dependency>-->
</dependencies>
</project>

Просмотреть файл

@ -0,0 +1,169 @@
package com.microsoft.azure.kusto.data;
import com.azure.core.http.*;
import com.azure.core.util.Context;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.microsoft.azure.kusto.data.exceptions.*;
import com.microsoft.azure.kusto.data.http.CloseParentResourcesStream;
import com.microsoft.azure.kusto.data.http.HttpRequestBuilder;
import com.microsoft.azure.kusto.data.http.HttpStatus;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.conn.EofSensorInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandles;
import java.util.Optional;
public abstract class BaseClient implements Client, StreamingClient {
private static final int MAX_REDIRECT_COUNT = 1;
// Make logger available to implementations
protected static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private final HttpClient httpClient;
public BaseClient(HttpClient httpClient) {
this.httpClient = httpClient;
}
protected String post(HttpRequest request) throws DataServiceException {
// Execute and get the response
try (HttpResponse response = httpClient.sendSync(request, Context.NONE)) {
return processResponseBody(response);
}
}
private String processResponseBody(HttpResponse response) throws DataServiceException {
String responseBody = Utils.isGzipResponse(response) ? Utils.gzipedInputToString(response.getBodyAsBinaryData().toStream())
: response.getBodyAsBinaryData().toString();
if (responseBody == null) {
return null;
}
switch (response.getStatusCode()) {
case HttpStatus.OK:
return responseBody;
case HttpStatus.TOO_MANY_REQS:
throw new ThrottleException(response.getRequest().getUrl().toString());
default:
throw createExceptionFromResponse(response.getRequest().getUrl().toString(), response, null, responseBody);
}
}
protected InputStream postToStreamingOutput(HttpRequest request) throws DataServiceException {
return postToStreamingOutput(request, 0);
}
// Todo: Implement async version of this method
protected InputStream postToStreamingOutput(HttpRequest request, int redirectCount) throws DataServiceException {
boolean returnInputStream = false;
String errorFromResponse = null;
HttpResponse httpResponse = null;
try {
httpResponse = httpClient.sendSync(request, Context.NONE);
int responseStatusCode = httpResponse.getStatusCode();
if (responseStatusCode == HttpStatus.OK) {
returnInputStream = true;
return new EofSensorInputStream(new CloseParentResourcesStream(httpResponse), null);
}
errorFromResponse = httpResponse.getBodyAsBinaryData().toString();
// Ideal to close here (as opposed to finally) so that if any data can't be flushed, the exception will be properly thrown and handled
httpResponse.close();
if (shouldPostToOriginalUrlDueToRedirect(redirectCount, responseStatusCode)) {
Optional<HttpHeader> redirectLocation = Optional.ofNullable(httpResponse.getHeaders().get(HttpHeaderName.LOCATION));
if (redirectLocation.isPresent() && !redirectLocation.get().getValue().equals(request.getUrl().toString())) {
HttpRequest redirectRequest = HttpRequestBuilder
.fromExistingRequest(request)
.withURL(redirectLocation.get().getValue())
.build();
return postToStreamingOutput(redirectRequest, redirectCount + 1);
}
}
} catch (IOException ex) {
throw new DataServiceException(request.getUrl().toString(),
"postToStreamingOutput failed to get or decompress response stream", ex, false);
} catch (Exception ex) {
throw createExceptionFromResponse(request.getUrl().toString(), httpResponse, ex, errorFromResponse);
} finally {
closeResourcesIfNeeded(returnInputStream, httpResponse);
}
throw createExceptionFromResponse(request.getUrl().toString(), httpResponse, null, errorFromResponse);
}
public static DataServiceException createExceptionFromResponse(String url, HttpResponse httpResponse, Exception thrownException, String errorFromResponse) {
if (httpResponse == null) {
return new DataServiceException(url, "POST failed to send request", thrownException, false);
}
/*
* TODO: When we add another streaming API that returns a KustoOperationResult, we'll need to handle the 2 types of content errors this API can return:
* (1) Inline error (engine identifies error after it starts building the json result), or (2) in the KustoOperationResult's QueryCompletionInformation,
* both of which present with "200 OK". See .Net's DataReaderParser.
*/
String activityId = determineActivityId(httpResponse);
String message = errorFromResponse;
WebException formattedException = new WebException(errorFromResponse, httpResponse, thrownException);
boolean isPermanent = false;
if (!StringUtils.isBlank(errorFromResponse)) {
try {
JsonNode jsonObject = Utils.getObjectMapper().readTree(errorFromResponse);
if (jsonObject.has("error")) {
formattedException = new DataWebException(errorFromResponse, httpResponse, thrownException);
OneApiError apiError = ((DataWebException) formattedException).getApiError();
message = apiError.getDescription();
isPermanent = apiError.isPermanent();
} else if (jsonObject.has("message")) {
message = jsonObject.get("message").asText();
}
} catch (JsonProcessingException e) {
// It's not ideal to use an exception here for control flow, but we can't know if it's a valid JSON until we try to parse it
LOGGER.debug("json processing error happened while parsing errorFromResponse {}", e.getMessage(), e);
}
} else {
message = String.format("Http StatusCode='%s'", httpResponse.getStatusCode());
}
return new DataServiceException(
url,
String.format("%s, ActivityId='%s'", message, activityId),
formattedException,
isPermanent);
}
private static void closeResourcesIfNeeded(boolean returnInputStream, HttpResponse httpResponse) {
// If we close the resources after returning the InputStream to the user, he won't be able to read from it - used in streaming query
if (!returnInputStream) {
if (httpResponse != null) {
httpResponse.close();
}
}
}
private static boolean shouldPostToOriginalUrlDueToRedirect(int redirectCount, int status) {
return (status == HttpStatus.FOUND || status == HttpStatus.TEMP_REDIRECT) && redirectCount + 1 <= MAX_REDIRECT_COUNT;
}
private static String determineActivityId(HttpResponse httpResponse) {
String activityId = "";
Optional<HttpHeader> activityIdHeader = Optional.ofNullable(
httpResponse.getHeaders().get(HttpHeaderName.fromString("x-ms-activity-id")));
if (activityIdHeader.isPresent()) {
activityId = activityIdHeader.get().getValue();
}
return activityId;
}
}

Просмотреть файл

@ -6,14 +6,7 @@ package com.microsoft.azure.kusto.data;
import com.microsoft.azure.kusto.data.exceptions.DataClientException;
import com.microsoft.azure.kusto.data.exceptions.DataServiceException;
import java.io.Closeable;
public interface Client extends Closeable {
KustoOperationResult execute(String command) throws DataServiceException, DataClientException;
KustoOperationResult execute(String database, String command) throws DataServiceException, DataClientException;
KustoOperationResult execute(String database, String command, ClientRequestProperties properties) throws DataServiceException, DataClientException;
public interface Client {
KustoOperationResult executeQuery(String command) throws DataServiceException, DataClientException;
@ -27,7 +20,7 @@ public interface Client extends Closeable {
KustoOperationResult executeMgmt(String database, String command, ClientRequestProperties properties) throws DataServiceException, DataClientException;
String executeToJsonResult(String database) throws DataServiceException, DataClientException;
String executeToJsonResult(String command) throws DataServiceException, DataClientException;
String executeToJsonResult(String database, String command) throws DataServiceException, DataClientException;

Просмотреть файл

@ -3,12 +3,14 @@
package com.microsoft.azure.kusto.data;
import com.azure.core.http.HttpClient;
import com.microsoft.azure.kusto.data.auth.ConnectionStringBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import com.microsoft.azure.kusto.data.http.HttpClientProperties;
import java.net.URISyntaxException;
public class ClientFactory {
private ClientFactory() {
// Hide the default constructor, as this is a factory with static methods
}
@ -43,12 +45,12 @@ public class ClientFactory {
* customized with the given properties.
*
* @param csb the connection string builder
* @param client CloseableHttpClient client. It will not be closed when {@link Client#close} is called.
* @param client HttpClient client.
* @return a fully constructed {@linkplain Client} instance
* @throws URISyntaxException if the cluster URL is invalid
*/
public static Client createClient(ConnectionStringBuilder csb, CloseableHttpClient client) throws URISyntaxException {
return client == null ? createClient(csb, (HttpClientProperties) null) : new ClientImpl(csb, client, true);
public static Client createClient(ConnectionStringBuilder csb, HttpClient client) throws URISyntaxException {
return client == null ? createClient(csb, (HttpClientProperties) null) : new ClientImpl(csb, client);
}
/**
@ -85,7 +87,7 @@ public class ClientFactory {
* @return a fully constructed {@linkplain StreamingClient} instance
* @throws URISyntaxException if the cluster URL is invalid
*/
public static StreamingClient createStreamingClient(ConnectionStringBuilder csb, CloseableHttpClient httpClient) throws URISyntaxException {
return new ClientImpl(csb, httpClient, true);
public static StreamingClient createStreamingClient(ConnectionStringBuilder csb, HttpClient httpClient) throws URISyntaxException {
return new ClientImpl(csb, httpClient);
}
}

Просмотреть файл

@ -3,126 +3,57 @@
package com.microsoft.azure.kusto.data;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.azure.core.http.HttpClient;
import com.azure.core.http.HttpRequest;
import com.azure.core.util.BinaryData;
import com.microsoft.azure.kusto.data.auth.CloudInfo;
import com.microsoft.azure.kusto.data.auth.ConnectionStringBuilder;
import com.microsoft.azure.kusto.data.auth.TokenProviderBase;
import com.microsoft.azure.kusto.data.auth.TokenProviderFactory;
import com.microsoft.azure.kusto.data.auth.endpoints.KustoTrustedEndpoints;
import com.microsoft.azure.kusto.data.exceptions.DataClientException;
import com.microsoft.azure.kusto.data.exceptions.DataServiceException;
import com.microsoft.azure.kusto.data.exceptions.KustoClientInvalidConnectionStringException;
import com.microsoft.azure.kusto.data.exceptions.KustoServiceQueryError;
import com.microsoft.azure.kusto.data.http.HttpClientFactory;
import com.microsoft.azure.kusto.data.http.HttpPostUtils;
import com.microsoft.azure.kusto.data.http.UncloseableStream;
import com.microsoft.azure.kusto.data.instrumentation.MonitoredActivity;
import com.microsoft.azure.kusto.data.instrumentation.SupplierTwoExceptions;
import com.microsoft.azure.kusto.data.instrumentation.TraceableAttributes;
import com.microsoft.azure.kusto.data.exceptions.*;
import com.microsoft.azure.kusto.data.http.*;
import com.microsoft.azure.kusto.data.instrumentation.*;
import com.microsoft.azure.kusto.data.req.KustoRequest;
import com.microsoft.azure.kusto.data.req.KustoRequestContext;
import com.microsoft.azure.kusto.data.res.JsonResult;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpHeaders;
import org.apache.http.ParseException;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.concurrent.TimeUnit;
class ClientImpl implements Client, StreamingClient {
private static final String ADMIN_COMMANDS_PREFIX = ".";
class ClientImpl extends BaseClient {
public static final String MGMT_ENDPOINT_VERSION = "v1";
public static final String QUERY_ENDPOINT_VERSION = "v2";
public static final String STREAMING_VERSION = "v1";
private static final String DEFAULT_DATABASE_NAME = "NetDefaultDb";
private static final Long COMMAND_TIMEOUT_IN_MILLISECS = TimeUnit.MINUTES.toMillis(10);
private static final Long QUERY_TIMEOUT_IN_MILLISECS = TimeUnit.MINUTES.toMillis(4);
private static final Long STREAMING_INGEST_TIMEOUT_IN_MILLISECS = TimeUnit.MINUTES.toMillis(10);
private static final int CLIENT_SERVER_DELTA_IN_MILLISECS = (int) TimeUnit.SECONDS.toMillis(30);
public static final String CLIENT_VERSION_HEADER = "x-ms-client-version";
public static final String APP_HEADER = "x-ms-app";
public static final String USER_HEADER = "x-ms-user";
public static final String FEDERATED_SECURITY_SUFFIX = ";fed=true";
public static final String JAVA_INGEST_ACTIVITY_TYPE_PREFIX = "DN.JavaClient.Execute";
private final TokenProviderBase aadAuthenticationHelper;
private final String clusterUrl;
private final ClientDetails clientDetails;
private final CloseableHttpClient httpClient;
private final boolean leaveHttpClientOpen;
private boolean endpointValidated = false;
private final ObjectMapper objectMapper = Utils.getObjectMapper();
public ClientImpl(ConnectionStringBuilder csb) throws URISyntaxException {
this(csb, HttpClientProperties.builder().build());
}
public ClientImpl(ConnectionStringBuilder csb, HttpClientProperties properties) throws URISyntaxException {
this(csb, HttpClientFactory.create(properties), false);
this(csb, HttpClientFactory.create(properties));
}
// Accepting a CloseableHttpClient so that we can create InputStream from response
public ClientImpl(ConnectionStringBuilder csb, CloseableHttpClient httpClient, boolean leaveHttpClientOpen) throws URISyntaxException {
URI clusterUrlForParsing = new URI(csb.getClusterUrl());
String host = clusterUrlForParsing.getHost();
Objects.requireNonNull(clusterUrlForParsing.getAuthority(), "clusterUri must have uri authority component");
String auth = clusterUrlForParsing.getAuthority().toLowerCase();
if (host == null) {
host = StringUtils.removeEndIgnoreCase(auth, FEDERATED_SECURITY_SUFFIX);
}
URIBuilder uriBuilder = new URIBuilder().setScheme(clusterUrlForParsing.getScheme())
.setHost(host);
String path = clusterUrlForParsing.getPath();
if (path != null && !path.isEmpty()) {
path = StringUtils.removeEndIgnoreCase(path, FEDERATED_SECURITY_SUFFIX);
path = StringUtils.removeEndIgnoreCase(path, "/");
uriBuilder.setPath(path);
}
if (clusterUrlForParsing.getPort() != -1) {
uriBuilder.setPort(clusterUrlForParsing.getPort());
}
csb.setClusterUrl(uriBuilder.build().toString());
public ClientImpl(ConnectionStringBuilder csb, HttpClient httpClient) throws URISyntaxException {
super(httpClient);
String clusterURL = UriUtils.createClusterURLFrom(csb.getClusterUrl());
csb.setClusterUrl(clusterURL);
clusterUrl = csb.getClusterUrl();
aadAuthenticationHelper = clusterUrl.toLowerCase().startsWith(CloudInfo.LOCALHOST) ? null : TokenProviderFactory.createTokenProvider(csb, httpClient);
clientDetails = new ClientDetails(csb.getApplicationNameForTracing(), csb.getUserNameForTracing(), csb.getClientVersionForTracing());
this.httpClient = httpClient;
this.leaveHttpClientOpen = leaveHttpClientOpen;
}
@Override
public KustoOperationResult execute(String command) throws DataServiceException, DataClientException {
return execute(DEFAULT_DATABASE_NAME, command);
}
@Override
public KustoOperationResult execute(String database, String command) throws DataServiceException, DataClientException {
return execute(database, command, null);
}
@Override
public KustoOperationResult execute(String database, String command, ClientRequestProperties properties) throws DataServiceException, DataClientException {
return execute(database, command, properties, determineCommandType(command));
}
private KustoOperationResult execute(String database, String command, ClientRequestProperties properties, CommandType commandType)
throws DataServiceException, DataClientException {
return MonitoredActivity.invoke(
(SupplierTwoExceptions<KustoOperationResult, DataServiceException, DataClientException>) () -> executeImpl(database, command, properties,
commandType),
commandType.getActivityTypeSuffix().concat(".execute"),
updateAndGetExecuteTracingAttributes(database, properties));
}
@Override
@ -157,6 +88,16 @@ class ClientImpl implements Client, StreamingClient {
return execute(database, command, properties, CommandType.ADMIN_COMMAND);
}
private KustoOperationResult execute(String database, String command, ClientRequestProperties properties, CommandType commandType)
throws DataServiceException, DataClientException {
KustoRequest kr = new KustoRequest(command, database, properties, commandType);
return MonitoredActivity.invoke(
(SupplierTwoExceptions<KustoOperationResult, DataServiceException, DataClientException>) () -> executeImpl(kr),
commandType.getActivityTypeSuffix().concat(".execute"),
updateAndGetExecuteTracingAttributes(database, properties));
}
private Map<String, String> updateAndGetExecuteTracingAttributes(String database, TraceableAttributes traceableAttributes) {
Map<String, String> attributes = new HashMap<>();
attributes.put("cluster", clusterUrl);
@ -167,21 +108,55 @@ class ClientImpl implements Client, StreamingClient {
return attributes;
}
@NotNull
private KustoOperationResult executeImpl(String database, String command, ClientRequestProperties properties, CommandType commandType)
throws DataServiceException, DataClientException {
String response = executeToJsonResult(database, command, properties);
String clusterEndpoint = String.format(commandType.getEndpoint(), clusterUrl);
private KustoOperationResult executeImpl(KustoRequest kr) throws DataServiceException, DataClientException {
String response = executeToJsonResult(kr);
String clusterEndpoint = String.format(kr.getCommandType().getEndpoint(), clusterUrl);
return processJsonResult(new JsonResult(response, clusterEndpoint));
}
private KustoOperationResult processJsonResult(JsonResult res) throws DataServiceException, DataClientException {
try {
return new KustoOperationResult(response, clusterEndpoint.endsWith("v2/rest/query") ? "v2" : "v1");
return new KustoOperationResult(res.getResult(), res.getEndpoint().endsWith("v2/rest/query") ? "v2" : "v1");
} catch (KustoServiceQueryError e) {
throw new DataServiceException(clusterEndpoint,
throw new DataServiceException(res.getEndpoint(),
"Error found while parsing json response as KustoOperationResult:" + e.getMessage(), e, e.isPermanent());
} catch (Exception e) {
throw new DataClientException(clusterEndpoint, e.getMessage(), e);
throw new DataClientException(res.getEndpoint(), e.getMessage(), e);
}
}
KustoRequestContext prepareRequest(@NotNull KustoRequest kr) throws DataServiceException, DataClientException {
// Validate and optimize the query object
kr.validateAndOptimize();
String clusterEndpoint = String.format(kr.getCommandType().getEndpoint(), clusterUrl);
String authorization = getAuthorizationHeaderValue();
// Validate the endpoint (?)
validateEndpoint();
// Build the tracing object
HttpTracing tracing = HttpTracing
.newBuilder()
.withProperties(kr.getProperties())
.withRequestPrefix("KJC.execute")
.withActivitySuffix(kr.getCommandType().getActivityTypeSuffix())
.withClientDetails(clientDetails)
.build();
// Build the HTTP request
HttpRequest request = HttpRequestBuilder
.newPost(clusterEndpoint)
.createCommandPayload(kr)
.withTracing(tracing)
.withAuthorization(authorization)
.build();
// Wrap the Http request and SDK request in a singular object, so we can use BiConsumer later.
return new KustoRequestContext(kr, request);
}
@Override
public String executeToJsonResult(String command) throws DataServiceException, DataClientException {
return executeToJsonResult(DEFAULT_DATABASE_NAME, command);
@ -194,39 +169,21 @@ class ClientImpl implements Client, StreamingClient {
@Override
public String executeToJsonResult(String database, String command, ClientRequestProperties properties) throws DataServiceException, DataClientException {
// Argument validation
if (StringUtils.isEmpty(database)) {
throw new IllegalArgumentException("Database is empty");
}
if (StringUtils.isEmpty(command)) {
throw new IllegalArgumentException("Command is empty");
}
command = command.trim();
CommandType commandType = determineCommandType(command);
long timeoutMs = determineTimeout(properties, commandType, clusterUrl);
// TODO save the uri once - no need to format everytime
String clusterEndpoint = String.format(commandType.getEndpoint(), clusterUrl);
Map<String, String> headers;
try {
headers = generateIngestAndCommandHeaders(properties, "KJC.execute",
commandType.getActivityTypeSuffix());
validateEndpoint();
} catch (KustoClientInvalidConnectionStringException e) {
throw new DataClientException(clusterUrl, e.getMessage(), e);
}
addCommandHeaders(headers);
String jsonPayload = generateCommandPayload(database, command, properties);
StringEntity requestEntity = new StringEntity(jsonPayload, ContentType.APPLICATION_JSON);
// trace execution
return MonitoredActivity.invoke(
(SupplierTwoExceptions<String, DataServiceException, DataClientException>) () -> HttpPostUtils.post(httpClient, clusterEndpoint, requestEntity,
timeoutMs + CLIENT_SERVER_DELTA_IN_MILLISECS, headers),
commandType.getActivityTypeSuffix().concat(".executeToJsonResult"));
KustoRequest kr = new KustoRequest(command, database, properties);
return executeToJsonResult(kr);
}
private void validateEndpoint() throws DataServiceException, KustoClientInvalidConnectionStringException {
private String executeToJsonResult(KustoRequest kr) throws DataServiceException, DataClientException {
KustoRequestContext request = prepareRequest(kr);
// Get the response and trace the call
return MonitoredActivity.invoke(
(SupplierOneException<String, DataServiceException>) () -> post(request.getHttpRequest()),
request.getSdkRequest().getCommandType().getActivityTypeSuffix().concat(".executeToJsonResult"));
}
private void validateEndpoint() throws DataServiceException, DataClientException {
if (!endpointValidated) {
KustoTrustedEndpoints.validateTrustedEndpoint(clusterUrl,
CloudInfo.retrieveCloudInfoForCluster(clusterUrl).getLoginEndpoint());
@ -260,52 +217,71 @@ class ClientImpl implements Client, StreamingClient {
}
private KustoOperationResult executeStreamingIngestImpl(String clusterEndpoint, InputStream stream, String blobUrl, ClientRequestProperties properties,
boolean leaveOpen)
throws DataServiceException, DataClientException {
boolean leaveOpen) throws DataServiceException, DataClientException {
boolean isStreamSource = stream != null;
Map<String, String> headers = generateIngestAndCommandHeaders(properties,
"KJC.executeStreamingIngest" + (isStreamSource ? "" : "FromBlob"),
CommandType.STREAMING_INGEST.getActivityTypeSuffix());
Map<String, String> headers = new HashMap<>();
String authorization = getAuthorizationHeaderValue();
String contentEncoding = null;
String contentType;
if (isStreamSource) {
headers.put(HttpHeaders.CONTENT_ENCODING, "gzip");
contentEncoding = "gzip";
}
Long timeoutMs = populateHeadersAndGetTimeout(properties, headers);
try (InputStream ignored = (isStreamSource && !leaveOpen) ? stream : null) {
validateEndpoint();
// We use UncloseableStream to prevent HttpClient From closing it
AbstractHttpEntity entity = isStreamSource ? new InputStreamEntity(new UncloseableStream(stream))
: new StringEntity(new IngestionSourceStorage(blobUrl).toString(), ContentType.APPLICATION_JSON);
String response;
// trace executeStreamingIngest
response = MonitoredActivity.invoke(
(SupplierTwoExceptions<String, DataServiceException, DataClientException>) () -> HttpPostUtils.post(httpClient, clusterEndpoint, entity,
timeoutMs + CLIENT_SERVER_DELTA_IN_MILLISECS, headers),
"ClientImpl.executeStreamingIngest");
return new KustoOperationResult(response, "v1");
} catch (KustoServiceQueryError e) {
throw new DataClientException(clusterEndpoint, "Error converting json response to KustoOperationResult:" + e.getMessage(), e);
} catch (KustoClientInvalidConnectionStringException | IOException e) {
throw new DataClientException(clusterUrl, e.getMessage(), e);
}
}
private Long populateHeadersAndGetTimeout(ClientRequestProperties properties, Map<String, String> headers) throws DataClientException {
Long timeoutMs = null;
// This was a separate method but was moved into the body of this method because it performs a side effect
if (properties != null) {
timeoutMs = determineTimeout(properties, CommandType.STREAMING_INGEST, clusterUrl);
Iterator<Map.Entry<String, Object>> iterator = properties.getOptions();
while (iterator.hasNext()) {
Map.Entry<String, Object> pair = iterator.next();
headers.put(pair.getKey(), pair.getValue().toString());
}
}
if (timeoutMs == null) {
timeoutMs = STREAMING_INGEST_TIMEOUT_IN_MILLISECS;
}
return timeoutMs;
try (InputStream ignored = (isStreamSource && !leaveOpen) ? stream : null) {
// Validate the endpoint
validateEndpoint();
BinaryData data;
if (isStreamSource) {
// We use UncloseableStream to prevent HttpClient From closing it
data = BinaryData.fromStream(new UncloseableStream(stream));
contentType = "application/octet-stream";
} else {
data = BinaryData.fromString(new IngestionSourceStorage(blobUrl).toString());
contentType = "application/json";
}
// Build the tracing object
HttpTracing tracing = HttpTracing
.newBuilder()
.withProperties(properties)
.withRequestPrefix("KJC.executeStreamingIngest" + (isStreamSource ? "" : "FromBlob"))
.withActivitySuffix(CommandType.STREAMING_INGEST.getActivityTypeSuffix())
.withClientDetails(clientDetails)
.build();
// Build the HTTP request. Since this is an ingestion and not a command, content headers aren't auto-applied.
HttpRequest request = HttpRequestBuilder
.newPost(clusterEndpoint)
.withTracing(tracing)
.withHeaders(headers)
.withAuthorization(authorization)
.withContentType(contentType)
.withContentEncoding(contentEncoding)
.withBody(data)
.build();
// Get the response, and trace the call.
String response = MonitoredActivity.invoke(
(SupplierOneException<String, DataServiceException>) () -> post(request), "ClientImpl.executeStreamingIngest");
return new KustoOperationResult(response, "v1");
} catch (KustoServiceQueryError e) {
throw new DataClientException(clusterEndpoint, "Error converting json response to KustoOperationResult:" + e.getMessage(), e);
} catch (IOException e) {
throw new DataClientException(clusterUrl, e.getMessage(), e);
}
}
private String buildClusterEndpoint(String database, String table, String format, String mappingName) {
@ -339,140 +315,57 @@ class ClientImpl implements Client, StreamingClient {
@Override
public InputStream executeStreamingQuery(String database, String command, ClientRequestProperties properties)
throws DataServiceException, DataClientException {
if (StringUtils.isEmpty(database)) {
throw new IllegalArgumentException("Database is empty");
}
if (StringUtils.isEmpty(command)) {
throw new IllegalArgumentException("Command is empty");
}
command = command.trim();
CommandType commandType = determineCommandType(command);
long timeoutMs = determineTimeout(properties, commandType, clusterUrl);
String clusterEndpoint = String.format(commandType.getEndpoint(), clusterUrl);
KustoRequest kr = new KustoRequest(command, database, properties);
return executeStreamingQuery(kr);
}
Map<String, String> headers;
headers = generateIngestAndCommandHeaders(properties, "KJC.executeStreaming",
commandType.getActivityTypeSuffix());
private InputStream executeStreamingQuery(@NotNull KustoRequest kr) throws DataServiceException, DataClientException {
addCommandHeaders(headers);
String jsonPayload = generateCommandPayload(database, command, properties);
// Validate and optimize the query object
kr.validateAndOptimize();
try {
validateEndpoint();
} catch (KustoClientInvalidConnectionStringException e) {
throw new DataClientException(clusterUrl, e.getMessage(), e);
}
String clusterEndpoint = String.format(kr.getCommandType().getEndpoint(), clusterUrl);
String authorization = getAuthorizationHeaderValue();
// trace httpCall
// Validate the endpoint
validateEndpoint();
// Build the tracing object
HttpTracing tracing = HttpTracing
.newBuilder()
.withProperties(kr.getProperties())
.withRequestPrefix("KJC.executeStreaming")
.withActivitySuffix(kr.getCommandType().getActivityTypeSuffix())
.withClientDetails(clientDetails)
.build();
// Build the HTTP request
HttpRequest request = HttpRequestBuilder
.newPost(clusterEndpoint)
.createCommandPayload(kr)
.withTracing(tracing)
.withAuthorization(authorization)
.build();
// Get the response and trace the call
return MonitoredActivity.invoke(
(SupplierTwoExceptions<InputStream, DataServiceException, DataClientException>) () -> HttpPostUtils.postToStreamingOutput(httpClient,
clusterEndpoint,
jsonPayload, timeoutMs + CLIENT_SERVER_DELTA_IN_MILLISECS, headers, properties.getRedirectCount()),
"ClientImpl.executeStreamingQuery", updateAndGetExecuteTracingAttributes(database, properties));
(SupplierOneException<InputStream, DataServiceException>) () -> postToStreamingOutput(request, kr.getProperties().getRedirectCount()),
"ClientImpl.executeStreamingQuery", updateAndGetExecuteTracingAttributes(kr.getDatabase(), kr.getProperties()));
}
private long determineTimeout(ClientRequestProperties properties, CommandType commandType, String clusterUrl) throws DataClientException {
Long timeoutMs;
try {
timeoutMs = properties == null ? null : properties.getTimeoutInMilliSec();
} catch (ParseException e) {
throw new DataClientException(clusterUrl, "Failed to parse timeout from ClientRequestProperties");
}
if (timeoutMs == null) {
if (commandType == CommandType.ADMIN_COMMAND) {
timeoutMs = COMMAND_TIMEOUT_IN_MILLISECS;
} else {
timeoutMs = QUERY_TIMEOUT_IN_MILLISECS;
}
}
return timeoutMs;
}
private CommandType determineCommandType(String command) {
if (command.startsWith(ADMIN_COMMANDS_PREFIX)) {
return CommandType.ADMIN_COMMAND;
}
return CommandType.QUERY;
}
private Map<String, String> generateIngestAndCommandHeaders(ClientRequestProperties properties,
String clientRequestIdPrefix,
String activityTypeSuffix)
throws DataServiceException, DataClientException {
Map<String, String> headers = extractTracingHeaders(properties);
private String getAuthorizationHeaderValue() throws DataServiceException, DataClientException {
if (aadAuthenticationHelper != null) {
headers.put(HttpHeaders.AUTHORIZATION, String.format("Bearer %s", aadAuthenticationHelper.acquireAccessToken()));
return String.format("Bearer %s", aadAuthenticationHelper.acquireAccessToken());
}
String clientRequestId;
if (properties != null && StringUtils.isNotBlank(properties.getClientRequestId())) {
clientRequestId = properties.getClientRequestId();
} else {
clientRequestId = String.format("%s;%s", clientRequestIdPrefix, UUID.randomUUID());
}
headers.put("x-ms-client-request-id", clientRequestId);
headers.put("Connection", "Keep-Alive");
UUID activityId = UUID.randomUUID();
String activityContext = String.format("%s%s/%s, ActivityId=%s, ParentId=%s, ClientRequestId=%s", JAVA_INGEST_ACTIVITY_TYPE_PREFIX, activityTypeSuffix,
activityId, activityId, activityId, clientRequestId);
headers.put("x-ms-activitycontext", activityContext);
// replace non-ascii characters in header values with '?'
headers.replaceAll((_i, v) -> v == null ? null : v.replaceAll("[^\\x00-\\x7F]", "?"));
return headers;
}
Map<String, String> extractTracingHeaders(ClientRequestProperties properties) {
Map<String, String> headers = new HashMap<>();
String version = clientDetails.getClientVersionForTracing();
if (StringUtils.isNotBlank(version)) {
headers.put(CLIENT_VERSION_HEADER, version);
}
String app = (properties == null || properties.getApplication() == null) ? clientDetails.getApplicationForTracing() : properties.getApplication();
if (StringUtils.isNotBlank(app)) {
headers.put(APP_HEADER, app);
}
String user = (properties == null || properties.getUser() == null) ? clientDetails.getUserNameForTracing() : properties.getUser();
if (StringUtils.isNotBlank(user)) {
headers.put(USER_HEADER, user);
}
return headers;
}
private String generateCommandPayload(String database, String command, ClientRequestProperties properties) {
ObjectNode json = objectMapper.createObjectNode()
.put("db", database)
.put("csl", command);
if (properties != null) {
json.put("properties", properties.toString());
}
return json.toString();
}
private void addCommandHeaders(Map<String, String> headers) {
headers.put(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8");
return null;
}
public String getClusterUrl() {
return clusterUrl;
}
@Override
public void close() throws IOException {
if (!leaveHttpClientOpen) {
httpClient.close();
}
ClientDetails getClientDetails() {
return clientDetails;
}
}

Просмотреть файл

@ -341,17 +341,6 @@ public class ClientRequestProperties implements Serializable, TraceableAttribute
return attributes;
}
/**
* Gets the amount of time a query may execute on the service before it times out, formatted as a KQL timespan.
* @param timeoutObj amount of time before timeout, which may be a Long, String or Integer.
* Value must be between 1 minute and 1 hour, and so value below the minimum or above the maximum will be adjusted accordingly.
* @Deprecated use {@link #getTimeoutAsCslTimespan(Object)} instead.
*/
@Deprecated
String getTimeoutAsString(Object timeoutObj) {
return getTimeoutAsCslTimespan(timeoutObj);
}
/**
* Gets the amount of time a query may execute on the service before it times out, formatted as a KQL timespan.
* @param timeoutObj amount of time before timeout, which may be a Long, String or Integer.

Просмотреть файл

@ -1,8 +1,6 @@
package com.microsoft.azure.kusto.data;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.microsoft.azure.kusto.data.http.HttpPostUtils;
class IngestionSourceStorage {
public String sourceUri;
@ -12,9 +10,8 @@ class IngestionSourceStorage {
}
public String toString() {
ObjectMapper objectMapper = Utils.getObjectMapper();
try {
return objectMapper.writeValueAsString(this);
return Utils.getObjectMapper().writeValueAsString(this);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}

Просмотреть файл

@ -6,10 +6,9 @@ package com.microsoft.azure.kusto.data;
import com.microsoft.azure.kusto.data.exceptions.DataClientException;
import com.microsoft.azure.kusto.data.exceptions.DataServiceException;
import java.io.Closeable;
import java.io.InputStream;
public interface StreamingClient extends Closeable {
public interface StreamingClient {
/**
* <p>Ingest data from a given stream directly into Kusto database.</p>
* This method ingests the data from a given stream directly into Kusto database, using streaming ingestion endpoint,
@ -49,16 +48,6 @@ public interface StreamingClient extends Closeable {
InputStream executeStreamingQuery(String command) throws DataServiceException, DataClientException;
/**
* <p>Execute the provided command against the default database.</p>
*
* @param command The command to execute
* @return {@link KustoOperationResult} object including the ingestion result
* @throws DataClientException An exception originating from a client activity
* @throws DataServiceException An exception returned from the service
*/
KustoOperationResult execute(String command) throws DataServiceException, DataClientException;
KustoOperationResult executeStreamingIngestFromBlob(String databaseName, String tableName, String blobUrl, ClientRequestProperties clientRequestProperties,
String dataFormat, String ingestionMappingReference) throws DataServiceException, DataClientException;
}

Просмотреть файл

@ -4,14 +4,44 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.utils.URIBuilder;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Paths;
import java.util.Objects;
public class UriUtils {
public static final String FEDERATED_SECURITY_SUFFIX = ";fed=true";
private UriUtils() {
// Providing hidden constructor to hide default public constructor in utils class
}
public static String createClusterURLFrom(final String clusterURI) throws URISyntaxException {
URI clusterUrlForParsing = new URI(clusterURI);
String host = clusterUrlForParsing.getHost();
Objects.requireNonNull(clusterUrlForParsing.getAuthority(), "clusterUri must have uri authority component");
String auth = clusterUrlForParsing.getAuthority().toLowerCase();
if (host == null) {
host = StringUtils.removeEndIgnoreCase(auth, FEDERATED_SECURITY_SUFFIX);
}
URIBuilder uriBuilder = new URIBuilder()
.setScheme(clusterUrlForParsing.getScheme())
.setHost(host);
String path = clusterUrlForParsing.getPath();
if (path != null && !path.isEmpty()) {
path = StringUtils.removeEndIgnoreCase(path, FEDERATED_SECURITY_SUFFIX);
path = StringUtils.removeEndIgnoreCase(path, "/");
uriBuilder.setPath(path);
}
if (clusterUrlForParsing.getPort() != -1) {
uriBuilder.setPort(clusterUrlForParsing.getPort());
}
return uriBuilder.build().toString();
}
public static String setPathForUri(String uri, String path, boolean ensureTrailingSlash) throws URISyntaxException {
path = StringUtils.prependIfMissing(path, "/");

Просмотреть файл

@ -1,5 +1,9 @@
package com.microsoft.azure.kusto.data;
import com.azure.core.http.HttpHeader;
import com.azure.core.http.HttpHeaderName;
import com.azure.core.http.HttpResponse;
import com.azure.core.implementation.StringBuilderWriter;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
@ -15,13 +19,18 @@ import javax.net.ssl.SSLException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.Writer;
import java.io.InputStreamReader;
import java.net.NoRouteToHostException;
import java.net.UnknownHostException;
import java.time.Duration;
import java.util.HashSet;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.zip.DeflaterInputStream;
import java.util.zip.GZIPInputStream;
public class Utils {
private static final int MAX_RETRY_ATTEMPTS = 4;
@ -111,4 +120,59 @@ public class Utils {
.retryOnException(predicate)
.build();
}
// TODO Copied from apache IoUtils - should we take it back ? don't recall why removed
public static String gzipedInputToString(InputStream in) {
try (GZIPInputStream gz = new GZIPInputStream(in)) {
StringBuilder stringBuilder = new StringBuilder();
try (StringBuilderWriter sw = new StringBuilderWriter(stringBuilder)) {
copy(gz, sw);
return stringBuilder.toString();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Checks if an HTTP response is GZIP compressed.
* @param response The HTTP response to check
* @return a boolean indicating if the CONTENT_ENCODING header contains "gzip"
*/
public static boolean isGzipResponse(HttpResponse response) {
Optional<HttpHeader> contentEncoding = Optional.ofNullable(response.getHeaders().get(HttpHeaderName.CONTENT_ENCODING));
return contentEncoding
.filter(header -> header.getValue().contains("gzip"))
.isPresent();
}
/**
* Gets an HTTP response body as an InputStream.
* @param response The response object to convert to an InputStream
* @return The response body as an InputStream
* @throws IOException An exception indicating an IO failure
*/
public static InputStream getResponseAsStream(HttpResponse response) throws IOException {
InputStream contentStream = response.getBodyAsBinaryData().toStream();
String contentEncoding = response.getHeaders().get(HttpHeaderName.CONTENT_ENCODING).getValue();
if (contentEncoding.contains("gzip")) {
return new GZIPInputStream(contentStream);
} else if (contentEncoding.contains("deflate")) {
return new DeflaterInputStream(contentStream);
}
return contentStream;
}
public static int copy(final InputStream input, final Writer writer)
throws IOException {
final InputStreamReader reader = new InputStreamReader(input);
final char[] buffer = new char[1024];
int count = 0;
int n;
while (-1 != (n = reader.read(buffer))) {
writer.write(buffer, 0, n);
count += n;
}
return count;
}
}

Просмотреть файл

@ -9,7 +9,7 @@ import com.microsoft.aad.msal4j.IConfidentialClientApplication;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import org.apache.http.client.HttpClient;
import com.azure.core.http.HttpClient;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Просмотреть файл

@ -9,7 +9,7 @@ import com.microsoft.aad.msal4j.IConfidentialClientApplication;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import org.apache.http.client.HttpClient;
import com.azure.core.http.HttpClient;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Просмотреть файл

@ -2,7 +2,7 @@ package com.microsoft.azure.kusto.data.auth;
import com.azure.core.credential.AccessToken;
import com.azure.core.credential.TokenRequestContext;
import org.apache.http.client.HttpClient;
import com.azure.core.http.HttpClient;
import com.azure.identity.AzureCliCredential;
import com.azure.identity.AzureCliCredentialBuilder;
import com.microsoft.azure.kusto.data.exceptions.DataClientException;

Просмотреть файл

@ -3,9 +3,9 @@
package com.microsoft.azure.kusto.data.auth;
import com.azure.core.http.HttpClient;
import com.microsoft.azure.kusto.data.exceptions.DataClientException;
import org.apache.http.client.HttpClient;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Просмотреть файл

@ -1,6 +1,6 @@
package com.microsoft.azure.kusto.data.auth;
import org.apache.http.client.HttpClient;
import com.azure.core.http.HttpClient;
@FunctionalInterface
public interface CallbackTokenProviderFunction {

Просмотреть файл

@ -3,17 +3,16 @@
package com.microsoft.azure.kusto.data.auth;
import com.azure.core.http.HttpClient;
import com.microsoft.azure.kusto.data.instrumentation.SupplierTwoExceptions;
import com.microsoft.azure.kusto.data.exceptions.DataClientException;
import com.microsoft.azure.kusto.data.exceptions.DataServiceException;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import com.microsoft.azure.kusto.data.instrumentation.MonitoredActivity;
import org.apache.http.client.HttpClient;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Просмотреть файл

@ -1,5 +1,7 @@
package com.microsoft.azure.kusto.data.auth;
import com.azure.core.http.*;
import com.azure.core.util.Context;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -7,27 +9,21 @@ import com.microsoft.azure.kusto.data.ExponentialRetry;
import com.microsoft.azure.kusto.data.Utils;
import com.microsoft.azure.kusto.data.exceptions.DataClientException;
import com.microsoft.azure.kusto.data.http.HttpClientFactory;
import com.microsoft.azure.kusto.data.instrumentation.SupplierOneException;
import com.microsoft.azure.kusto.data.UriUtils;
import com.microsoft.azure.kusto.data.exceptions.DataServiceException;
import com.microsoft.azure.kusto.data.http.HttpStatus;
import com.microsoft.azure.kusto.data.instrumentation.SupplierOneException;
import com.microsoft.azure.kusto.data.instrumentation.TraceableAttributes;
import com.microsoft.azure.kusto.data.instrumentation.MonitoredActivity;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.util.EntityUtils;
import org.jetbrains.annotations.Nullable;
import java.io.Closeable;
import java.io.IOException;
import java.io.Serializable;
import java.io.*;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
public class CloudInfo implements TraceableAttributes, Serializable {
private static final Map<String, CloudInfo> cache = new HashMap<>();
@ -114,7 +110,6 @@ public class CloudInfo implements TraceableAttributes, Serializable {
}
return null;
});
}
}
@ -122,46 +117,46 @@ public class CloudInfo implements TraceableAttributes, Serializable {
CloudInfo result;
HttpClient localHttpClient = givenHttpClient == null ? HttpClientFactory.create(null) : givenHttpClient;
try {
HttpGet request = new HttpGet(UriUtils.appendPathToUri(clusterUrl, METADATA_ENDPOINT));
request.addHeader(HttpHeaders.ACCEPT_ENCODING, "gzip,deflate");
request.addHeader(HttpHeaders.ACCEPT, "application/json");
HttpRequest request = new HttpRequest(HttpMethod.GET, UriUtils.appendPathToUri(clusterUrl, METADATA_ENDPOINT));
request.setHeader(HttpHeaderName.ACCEPT_ENCODING, "gzip,deflate");
request.setHeader(HttpHeaderName.ACCEPT, "application/json");
// trace CloudInfo.httpCall
HttpResponse response = MonitoredActivity.invoke(
(SupplierOneException<HttpResponse, IOException>) () -> localHttpClient.execute(request),
"CloudInfo.httpCall");
try {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == HttpStatus.SC_OK) {
String content = EntityUtils.toString(response.getEntity());
// Fixme: Make this async in the future
try (HttpResponse response = MonitoredActivity.invoke(
(SupplierOneException<HttpResponse, IOException>) () -> localHttpClient.sendSync(request, Context.NONE),
"CloudInfo.httpCall")) {
int statusCode = response.getStatusCode();
if (statusCode == HttpStatus.OK) {
String content = null;
if (Utils.isGzipResponse(response)) {
content = Utils.gzipedInputToString(response.getBodyAsBinaryData().toStream());
} else {
content = response.getBodyAsBinaryData().toString();
}
if (content == null || content.equals("") || content.equals("{}")) {
throw new DataServiceException(clusterUrl, "Error in metadata endpoint, received no data", true);
}
result = parseCloudInfo(content);
} else if (statusCode == 404) {
} else if (statusCode == HttpStatus.NOT_FOUND) {
result = DEFAULT_CLOUD;
} else {
String errorFromResponse = EntityUtils.toString(response.getEntity());
String errorFromResponse = response.getBodyAsBinaryData().toString();
if (errorFromResponse.isEmpty()) {
errorFromResponse = response.getStatusLine().getReasonPhrase();
// Fixme: Missing reason phrase to add to exception. Potentially want to use an enum.
errorFromResponse = "";
}
throw new DataServiceException(clusterUrl,
"Error in metadata endpoint, got code: " + statusCode + "\nWith error: " + errorFromResponse,
statusCode != HttpStatus.SC_TOO_MANY_REQUESTS);
}
} finally {
if (response instanceof Closeable) {
((Closeable) response).close();
throw new DataServiceException(clusterUrl, "Error in metadata endpoint, got code: " + statusCode +
"\nWith error: " + errorFromResponse, statusCode != HttpStatus.TOO_MANY_REQS);
}
}
} finally {
if (givenHttpClient == null && localHttpClient != null) {
if (givenHttpClient == null && localHttpClient instanceof Closeable) {
((Closeable) localHttpClient).close();
}
}
cache.put(clusterUrl, result);
return result;
// });
}
private static CloudInfo parseCloudInfo(String content) throws JsonProcessingException {

Просмотреть файл

@ -17,7 +17,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.http.client.HttpClient;
import com.azure.core.http.HttpClient;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Просмотреть файл

@ -2,7 +2,7 @@ package com.microsoft.azure.kusto.data.auth;
import com.microsoft.aad.msal4j.*;
import org.apache.http.client.HttpClient;
import com.azure.core.http.HttpClient;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Просмотреть файл

@ -1,26 +1,14 @@
package com.microsoft.azure.kusto.data.auth;
import com.azure.core.http.HttpRequest;
import com.azure.core.http.HttpResponse;
import com.azure.core.http.*;
import com.azure.core.util.Context;
import com.azure.core.util.CoreUtils;
import com.microsoft.aad.msal4j.IHttpClient;
import com.microsoft.aad.msal4j.IHttpResponse;
import org.apache.http.Header;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.*;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.message.BasicHeader;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import reactor.core.publisher.Mono;
import java.util.stream.Collectors;
/**
* This class wraps both of the azure http client interfaces - IHttpClient and HttpClient to use our apache http client.
@ -28,9 +16,8 @@ import java.util.concurrent.Executors;
* HttpClient is synchronous, so the implementation is straight-forward.
* IHttpClient is asynchronous, so we need to be more clever about integrating it with the synchronous apache client.
*/
public class HttpClientWrapper implements com.azure.core.http.HttpClient, IHttpClient {
public class HttpClientWrapper implements HttpClient, IHttpClient {
private static final Executor EXECUTOR = Executors.newCachedThreadPool();
private final HttpClient httpClient;
public HttpClientWrapper(HttpClient httpClient) {
@ -40,93 +27,7 @@ public class HttpClientWrapper implements com.azure.core.http.HttpClient, IHttpC
// Implementation of the asynchronous IHttpClient
@Override
public Mono<HttpResponse> send(HttpRequest httpRequest) {
// Creating the apache request
HttpUriRequest request;
String uri = httpRequest.getUrl().toString();
switch (httpRequest.getHttpMethod()) {
case GET:
request = new HttpGet(uri);
break;
case POST:
request = new HttpPost(uri);
break;
case PUT:
request = new HttpPut(uri);
break;
case PATCH:
request = new HttpPatch(uri);
break;
case DELETE:
request = new HttpDelete(uri);
break;
case HEAD:
request = new HttpHead(uri);
break;
case OPTIONS:
request = new HttpOptions(uri);
break;
case TRACE:
request = new HttpTrace(uri);
break;
default:
throw new IllegalArgumentException("Unsupported HTTP method: " + httpRequest.getHttpMethod());
}
// Translating the headers
request.setHeaders(httpRequest.getHeaders().stream().filter(h -> isNotContentLength(h.getName())).map(h -> new BasicHeader(h.getName(), h.getValue()))
.toArray(Header[]::new));
// Setting the request's body/entity
// This empty operation
if (request instanceof HttpEntityEnclosingRequest) {
HttpEntityEnclosingRequest entityEnclosingRequest = (HttpEntityEnclosingRequest) request;
PipedOutputStream osPipe = new PipedOutputStream();
PipedInputStream isPipe = null;
try {
isPipe = new PipedInputStream(osPipe);
} catch (IOException ignored) {
// This should never happen
}
EXECUTOR.execute(() -> {
httpRequest.getBody().publishOn(Schedulers.boundedElastic()).map(buf -> {
try {
if (buf.hasArray()) {
osPipe.write(buf.array(), buf.position(), buf.remaining());
} else {
byte[] bytes = new byte[buf.remaining()];
buf.get(bytes);
osPipe.write(bytes);
}
} catch (IOException e) {
return false;
}
return true;
}).blockLast();
try {
osPipe.close();
} catch (IOException e) {
// This should never happen
}
});
String contentLength = httpRequest.getHeaders().getValue("Content-Length");
String contentType = httpRequest.getHeaders().getValue("Content-Type");
entityEnclosingRequest.setEntity(new InputStreamEntity(isPipe, contentLength == null ? -1 : Long.parseLong(contentLength),
contentType == null ? null : ContentType.parse(contentType)));
}
// The types of the Monos are different, but we ignore the results anyway (since we only care about the input stream) so this is fine.
return Mono.create(monoSink -> {
try {
org.apache.http.HttpResponse response = httpClient.execute(request);
monoSink.success(new HttpResponseWrapper(httpRequest, response));
} catch (IOException e) {
monoSink.error(e);
}
});
return httpClient.send(httpRequest);
}
private static boolean isNotContentLength(String name) {
@ -135,34 +36,32 @@ public class HttpClientWrapper implements com.azure.core.http.HttpClient, IHttpC
// Implementation of the synchronous HttpClient
@Override
public IHttpResponse send(com.microsoft.aad.msal4j.HttpRequest httpRequest) throws Exception {
HttpUriRequest request;
// Translating the request
String uri = httpRequest.url().toString();
public IHttpResponse send(com.microsoft.aad.msal4j.HttpRequest httpRequest) {
HttpMethod method;
switch (httpRequest.httpMethod()) {
case GET:
request = new HttpGet(uri);
method = HttpMethod.GET;
break;
case POST:
request = new HttpPost(uri);
method = HttpMethod.POST;
break;
default:
throw new IllegalArgumentException("Unsupported HTTP method: " + httpRequest.httpMethod());
}
// Translating the headers
request.setHeaders(httpRequest.headers().entrySet().stream().filter(h -> isNotContentLength(h.getKey()))
.map(h -> new BasicHeader(h.getKey(), h.getValue())).toArray(Header[]::new));
// Setting the request's body/entity
if (request instanceof HttpEntityEnclosingRequest) {
HttpEntityEnclosingRequest entityEnclosingRequest = (HttpEntityEnclosingRequest) request;
String body = httpRequest.body();
entityEnclosingRequest.setEntity(new ByteArrayEntity(body.getBytes(StandardCharsets.UTF_8)));
// Generate an azure core HttpRequest from the existing msal4j HttpRequest
HttpRequest request = new HttpRequest(method, httpRequest.url(), new HttpHeaders(httpRequest.headers()));
if (!CoreUtils.isNullOrEmpty(httpRequest.body())) {
request.setBody(httpRequest.body());
}
return new HttpResponseWrapper(httpClient.execute(request));
try (HttpResponse response = httpClient.sendSync(request, Context.NONE)) {
com.microsoft.aad.msal4j.HttpResponse msalResponse = new com.microsoft.aad.msal4j.HttpResponse();
msalResponse.statusCode(response.getStatusCode());
msalResponse.body(response.getBodyAsBinaryData().toString());
msalResponse.addHeaders(response.getHeaders().stream().collect(Collectors.toMap(HttpHeader::getName,
HttpHeader::getValuesList)));
return msalResponse;
}
}
}

Просмотреть файл

@ -1,127 +0,0 @@
package com.microsoft.azure.kusto.data.auth;
import com.azure.core.http.HttpHeader;
import com.azure.core.http.HttpHeaders;
import com.azure.core.http.HttpRequest;
import com.azure.core.http.HttpResponse;
import com.microsoft.aad.msal4j.IHttpResponse;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.http.Header;
import org.apache.http.HttpStatus;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* This class wraps our azure HttpResponse, into both the asynchronous HttpResponse class and the synchronous IHttpResponse interface from the azure core
* library.
* This class completes the {@link HttpClientWrapper} class, which is responsible for wrapping the http client, this client wraps the response from the
* client.
*/
public class HttpResponseWrapper extends HttpResponse implements IHttpResponse {
org.apache.http.HttpResponse response;
private byte[] body = null;
protected HttpResponseWrapper(HttpRequest request, org.apache.http.HttpResponse response) {
super(request);
this.response = response;
}
protected HttpResponseWrapper(org.apache.http.HttpResponse response) {
this(null, response);
}
// HttpResponse methods
@Override
public int getStatusCode() {
return response.getStatusLine().getStatusCode();
}
@Override
public String getHeaderValue(String s) {
Header firstHeader = response.getFirstHeader(s);
return firstHeader != null ? firstHeader.getValue() : null;
}
@Override
public HttpHeaders getHeaders() {
Map<String, List<String>> newHeaders = new HashMap<>();
Header[] allHeaders = response.getAllHeaders();
// This is required since one header can have multiple values.
for (Header header : allHeaders) {
if (newHeaders.containsKey(header.getName())) {
newHeaders.get(header.getName()).add(header.getValue());
} else {
List<String> values = new ArrayList<>();
values.add(header.getValue());
newHeaders.put(header.getName(), values);
}
}
return new HttpHeaders(newHeaders.entrySet().stream().map(e -> new HttpHeader(e.getKey(), e.getValue())).collect(Collectors.toList()));
}
@Override
public Flux<ByteBuffer> getBody() {
return getBodyAsByteArray().map(ByteBuffer::wrap).flux();
}
@Override
public Mono<byte[]> getBodyAsByteArray() {
// We read the body only once, and then wrap it in a Mono.
if (body == null) {
try {
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_NO_CONTENT) {
body = ArrayUtils.EMPTY_BYTE_ARRAY;
} else {
body = EntityUtils.toByteArray(response.getEntity());
}
} catch (IOException ignored) {
body = new byte[0];
}
}
return Mono.just(body);
}
@Override
public Mono<String> getBodyAsString() {
return getBodyAsByteArray().map(String::new);
}
@Override
public Mono<String> getBodyAsString(Charset charset) {
return getBodyAsByteArray().map(bytes -> new String(bytes, charset));
}
// IHttpResponse methods
@Override
public int statusCode() {
return getStatusCode();
}
@Override
public Map<String, List<String>> headers() {
return getHeaders().stream().collect(Collectors.toMap(com.azure.core.util.Header::getName, com.azure.core.util.Header::getValuesList));
}
@Override
public String body() {
String block = getBodyAsString().block();
return block;
}
}

Просмотреть файл

@ -2,13 +2,13 @@ package com.microsoft.azure.kusto.data.auth;
import com.azure.core.credential.AccessToken;
import com.azure.core.credential.TokenRequestContext;
import com.azure.core.http.HttpClient;
import com.azure.identity.ManagedIdentityCredential;
import com.azure.identity.ManagedIdentityCredentialBuilder;
import com.microsoft.azure.kusto.data.exceptions.DataClientException;
import com.microsoft.azure.kusto.data.exceptions.DataServiceException;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.HttpClient;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Просмотреть файл

@ -10,7 +10,7 @@ import com.microsoft.azure.kusto.data.exceptions.DataServiceException;
import com.microsoft.azure.kusto.data.instrumentation.MonitoredActivity;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.HttpClient;
import com.azure.core.http.HttpClient;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Просмотреть файл

@ -8,7 +8,7 @@ import com.microsoft.azure.kusto.data.exceptions.DataClientException;
import com.microsoft.azure.kusto.data.exceptions.DataServiceException;
import org.apache.http.client.HttpClient;
import com.azure.core.http.HttpClient;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Просмотреть файл

@ -6,7 +6,7 @@ import com.microsoft.aad.msal4j.IConfidentialClientApplication;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import org.apache.http.client.HttpClient;
import com.azure.core.http.HttpClient;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Просмотреть файл

@ -1,5 +1,6 @@
package com.microsoft.azure.kusto.data.auth;
import com.azure.core.http.HttpClient;
import com.microsoft.azure.kusto.data.instrumentation.SupplierTwoExceptions;
import com.microsoft.azure.kusto.data.UriUtils;
import com.microsoft.azure.kusto.data.exceptions.DataClientException;
@ -10,7 +11,6 @@ import java.util.Map;
import com.microsoft.azure.kusto.data.instrumentation.MonitoredActivity;
import com.microsoft.azure.kusto.data.instrumentation.TraceableAttributes;
import org.apache.http.client.HttpClient;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;

Просмотреть файл

@ -7,7 +7,7 @@ import com.microsoft.aad.msal4j.ClientCredentialFactory;
import com.microsoft.aad.msal4j.IClientCertificate;
import com.microsoft.aad.msal4j.IClientSecret;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.HttpClient;
import com.azure.core.http.HttpClient;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Просмотреть файл

@ -10,7 +10,7 @@ import com.microsoft.aad.msal4j.PublicClientApplication;
import com.microsoft.azure.kusto.data.exceptions.DataClientException;
import com.microsoft.azure.kusto.data.exceptions.DataServiceException;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.HttpClient;
import com.azure.core.http.HttpClient;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Просмотреть файл

@ -60,7 +60,7 @@ public class KustoTrustedEndpoints {
try {
validateTrustedEndpoint(new URI(uri), loginEndpoint);
} catch (URISyntaxException ex) {
throw new KustoClientInvalidConnectionStringException(ex);
throw new KustoClientInvalidConnectionStringException(uri, ex.getMessage(), ex);
}
}

Просмотреть файл

@ -7,6 +7,14 @@ package com.microsoft.azure.kusto.data.exceptions;
This class represents an error that happened on the client side and is therefore considered permanent
*/
public class DataClientException extends KustoDataExceptionBase {
public DataClientException(Exception ex) {
this(ex.getMessage());
}
public DataClientException(String message) {
this(null, message);
}
public DataClientException(String ingestionSource, String message) {
this(ingestionSource, message, null);
}

Просмотреть файл

@ -3,10 +3,10 @@
package com.microsoft.azure.kusto.data.exceptions;
import com.azure.core.http.HttpResponse;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.microsoft.azure.kusto.data.Utils;
import org.apache.http.HttpResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Просмотреть файл

@ -3,7 +3,7 @@ package com.microsoft.azure.kusto.data.exceptions;
/**
* Raised when Kusto client is initialized with an invalid endpoint
*/
public class KustoClientInvalidConnectionStringException extends Exception {
public class KustoClientInvalidConnectionStringException extends DataClientException {
public KustoClientInvalidConnectionStringException(Exception e) {
super(e);
}
@ -11,4 +11,8 @@ public class KustoClientInvalidConnectionStringException extends Exception {
public KustoClientInvalidConnectionStringException(String msg) {
super(msg);
}
public KustoClientInvalidConnectionStringException(String clusterURL, String msg, Exception e) {
super(clusterURL, msg, e);
}
}

Просмотреть файл

@ -1,6 +1,6 @@
package com.microsoft.azure.kusto.data.exceptions;
import org.apache.http.HttpResponse;
import com.azure.core.http.HttpResponse;
import org.jetbrains.annotations.Nullable;
public class WebException extends Exception {
@ -23,6 +23,6 @@ public class WebException extends Exception {
@Nullable
public Integer getStatusCode() {
return httpResponse != null ? httpResponse.getStatusLine().getStatusCode() : null;
return httpResponse != null ? httpResponse.getStatusCode() : null;
}
}

Просмотреть файл

@ -3,7 +3,8 @@
package com.microsoft.azure.kusto.data.http;
import org.apache.http.client.methods.CloseableHttpResponse;
import com.azure.core.http.HttpResponse;
import com.microsoft.azure.kusto.data.Utils;
import java.io.IOException;
import java.io.InputStream;
@ -16,10 +17,10 @@ import java.io.InputStream;
*/
public class CloseParentResourcesStream extends InputStream {
private final InputStream innerStream;
private final CloseableHttpResponse httpResponse;
private final HttpResponse httpResponse;
public CloseParentResourcesStream(CloseableHttpResponse httpResponse) throws IOException {
this.innerStream = httpResponse.getEntity().getContent();
public CloseParentResourcesStream(HttpResponse httpResponse) throws IOException {
this.innerStream = Utils.getResponseAsStream(httpResponse);
this.httpResponse = httpResponse;
}

Просмотреть файл

@ -1,108 +1,71 @@
package com.microsoft.azure.kusto.data.http;
import com.microsoft.azure.kusto.data.HttpClientProperties;
import org.apache.http.HeaderElement;
import org.apache.http.HeaderElementIterator;
import org.apache.http.HttpResponse;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import com.azure.core.http.HttpClient;
import com.azure.core.http.HttpHeaderName;
import com.azure.core.util.Header;
import com.azure.core.util.HttpClientOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLContext;
import java.security.NoSuchAlgorithmException;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
/**
* A singleton factory of HTTP clients.
* A static factory for HTTP clients.
*/
public class HttpClientFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(HttpClientFactory.class);
/**
* Creates a new Apache HTTP client.
* Creates a new HTTP client.
*
* @param providedProperties custom HTTP client properties
* @return a new Apache HTTP client
* @param properties custom HTTP client properties
* @return a new HTTP client
*/
public static CloseableHttpClient create(HttpClientProperties providedProperties) {
LOGGER.info("Creating new CloseableHttpClient client");
final HttpClientProperties properties = Optional.ofNullable(providedProperties)
.orElse(HttpClientProperties.builder().build());
final HttpClientBuilder httpClientBuilder = HttpClientBuilder.create()
.useSystemProperties()
.setMaxConnTotal(properties.maxConnectionTotal())
.setMaxConnPerRoute(properties.maxConnectionRoute())
.evictExpiredConnections()
.evictIdleConnections(properties.maxIdleTime(), TimeUnit.SECONDS)
.disableRedirectHandling();
public static HttpClient create(HttpClientProperties properties) {
LOGGER.info("Creating new HTTP Client");
HttpClientOptions options = new HttpClientOptions();
if (properties.isKeepAlive()) {
final ConnectionKeepAliveStrategy keepAliveStrategy = new CustomConnectionKeepAliveStrategy(properties.maxKeepAliveTime());
httpClientBuilder.setKeepAliveStrategy(keepAliveStrategy);
// If all properties are null, create with default client options
if (properties == null) {
options.setResponseTimeout(Duration.ofMinutes(10));
return HttpClient.createDefault(options);
}
if (properties.getPlanner() != null) {
httpClientBuilder.setRoutePlanner(properties.getPlanner());
// MS Docs indicate that all setters handle nulls so even if these values are null everything should "just work"
// Note that the first discovered HttpClientProvider class is loaded. HttpClientProviders can be swapped in or out
// by simply
// Docs: https://learn.microsoft.com/en-us/java/api/com.azure.core.util.httpclientoptions?view=azure-java-stable
options.setMaximumConnectionPoolSize(properties.maxConnectionTotal());
options.setConnectionIdleTimeout(Duration.ofSeconds(properties.maxIdleTime()));
options.setResponseTimeout(properties.timeout());
options.setHttpClientProvider(properties.provider());
// Set Keep-Alive headers if they were requested.
// NOTE: Servers are not obligated to honor client requested Keep-Alive values
if (properties.isKeepAlive()) {
Header keepAlive = new Header(HttpHeaderName.CONNECTION.getCaseSensitiveName(), "Keep-Alive");
// Keep-Alive is Non-standard from the client so core does not have an enum for it
Header keepAliveTimeout = new Header("Keep-Alive", "timeout=" + properties.maxKeepAliveTime());
List<Header> headers = new ArrayList<>();
headers.add(keepAlive);
headers.add(keepAliveTimeout);
options.setHeaders(headers);
}
if (properties.getProxy() != null) {
httpClientBuilder.setProxy(properties.getProxy());
options.setProxyOptions(properties.getProxy());
}
if (properties.supportedProtocols() != null) {
// Todo: Is the per route connection maximum needed anymore?
try {
httpClientBuilder.setSSLSocketFactory(new SSLConnectionSocketFactory(SSLContext.getDefault(), properties.supportedProtocols(), null,
SSLConnectionSocketFactory.getDefaultHostnameVerifier()));
} catch (NoSuchAlgorithmException e) {
LOGGER.error("Failed to set supported protocols", e);
}
}
return httpClientBuilder.build();
}
/**
* A custom connection keep-alive strategy that uses the server instructions set in the {@code Keep-Alive}
* response header; if the response doesn't contain a {@code Keep-Alive} header, the client will use a configurable
* keep-alive period.
*/
static class CustomConnectionKeepAliveStrategy implements ConnectionKeepAliveStrategy {
private final int defaultKeepAlive;
/**
* The default keep-alive time.
*
* @param defaultKeepAlive the keep-alive time expressed in seconds
*/
CustomConnectionKeepAliveStrategy(int defaultKeepAlive) {
this.defaultKeepAlive = defaultKeepAlive;
}
@Override
public long getKeepAliveDuration(HttpResponse httpResponse, HttpContext httpContext) {
// honor 'keep-alive' header
HeaderElementIterator it = new BasicHeaderElementIterator(httpResponse.headerIterator(HTTP.CONN_KEEP_ALIVE));
while (it.hasNext()) {
HeaderElement he = it.nextElement();
String param = he.getName();
String value = he.getValue();
if (value != null && param.equalsIgnoreCase("timeout")) {
return Long.parseLong(value) * 1000;
}
}
// otherwise keep alive for default seconds
return defaultKeepAlive * 1000L;
}
return HttpClient.createDefault(options);
}
}

Просмотреть файл

@ -1,11 +1,12 @@
package com.microsoft.azure.kusto.data;
package com.microsoft.azure.kusto.data.http;
import org.apache.http.HttpHost;
import org.apache.http.conn.routing.HttpRoutePlanner;
import com.azure.core.http.HttpClientProvider;
import com.azure.core.http.ProxyOptions;
import java.time.Duration;
/**
* HTTP client properties.
* TODO: move to http package on next major
*/
public class HttpClientProperties {
private final Integer maxIdleTime;
@ -13,9 +14,9 @@ public class HttpClientProperties {
private final Integer maxKeepAliveTime;
private final Integer maxConnectionTotal;
private final Integer maxConnectionRoute;
private final HttpHost proxy;
private final HttpRoutePlanner routePlanner;
private final String[] supportedProtocols;
private final Duration timeout;
private final Class<? extends HttpClientProvider> provider;
private final ProxyOptions proxy;
private HttpClientProperties(HttpClientPropertiesBuilder builder) {
this.maxIdleTime = builder.maxIdleTime;
@ -23,9 +24,9 @@ public class HttpClientProperties {
this.maxKeepAliveTime = builder.maxKeepAliveTime;
this.maxConnectionTotal = builder.maxConnectionsTotal;
this.maxConnectionRoute = builder.maxConnectionsPerRoute;
this.timeout = builder.timeout;
this.provider = builder.provider;
this.proxy = builder.proxy;
this.routePlanner = builder.routePlanner;
this.supportedProtocols = builder.supportedProtocols;
}
/**
@ -48,7 +49,7 @@ public class HttpClientProperties {
}
/**
* Indicates whether a custom connection keep-alive time should be used. If set to {@code false}, the HTTP
* Indicates whether or not a custom connection keep-alive time should be used. If set to {@code false}, the HTTP
* client will use the default connection keep-alive strategy, which is to use only the server instructions
* (if any) set in the {@code Keep-Alive} response header.
* If set to {@code true}, the HTTP client will use a custom connection keep-alive strategy which uses the
@ -94,23 +95,33 @@ public class HttpClientProperties {
return maxConnectionRoute;
}
/**
* The default response timeout to apply to HTTP requests
*
* @return the timeout
*/
public Duration timeout() {
return timeout;
}
/**
* Gets the HTTP Client Provider used by Azure Core when constructing HTTP Client instances.
*
* @return the provider
*/
public Class<? extends HttpClientProvider> provider() {
return provider;
}
/**
* The proxy to use when connecting to the remote server.
*
* @return the proxy
*/
public HttpHost getProxy() {
public ProxyOptions getProxy() {
return proxy;
}
public HttpRoutePlanner getPlanner() {
return routePlanner;
}
public String[] supportedProtocols() {
return supportedProtocols;
}
public static class HttpClientPropertiesBuilder {
private Integer maxIdleTime = 120;
@ -118,9 +129,9 @@ public class HttpClientProperties {
private Integer maxKeepAliveTime = 120;
private Integer maxConnectionsTotal = 40;
private Integer maxConnectionsPerRoute = 40;
private HttpHost proxy = null;
private HttpRoutePlanner routePlanner = null;
private String[] supportedProtocols = null;
private Duration timeout = Duration.ofMinutes(10);
private Class<? extends HttpClientProvider> provider = null;
private ProxyOptions proxy = null;
private HttpClientPropertiesBuilder() {
}
@ -139,7 +150,7 @@ public class HttpClientProperties {
}
/**
* Set whether a custom connection keep-alive time should be used. If set to {@code false}, the HTTP
* Set whether or not a custom connection keep-alive time should be used. If set to {@code false}, the HTTP
* client will use the default connection keep-alive strategy, which is to use only the server instructions
* (if any) set in the {@code Keep-Alive} response header.
* If set to {@code true}, the HTTP client will use a custom connection keep-alive strategy which uses the
@ -194,41 +205,42 @@ public class HttpClientProperties {
return this;
}
/**
* Sets the HTTP Client Provider used by Azure Core when constructing HTTP Client instances.
*
* @param provider the requested HTTP Client provider class
* @return the builder instance
*/
public HttpClientPropertiesBuilder provider(Class<? extends HttpClientProvider> provider) {
this.provider = provider;
return this;
}
/**
* Sets a response timeout to use by default on the client's requests.
*
* @param timeout the requested response timeout
* @return the builder instance
*/
public HttpClientPropertiesBuilder timeout(Duration timeout) {
this.timeout = timeout;
return this;
}
/**
* Sets a proxy server to use for the client.
*
* @param proxy the proxy server
* @return the builder instance
*/
public HttpClientPropertiesBuilder proxy(HttpHost proxy) {
public HttpClientPropertiesBuilder proxy(ProxyOptions proxy) {
this.proxy = proxy;
return this;
}
/**
* Overrides the {@link #proxy} parameter, and can be used to create more complex proxies.
*
* @param routePlanner the custom route planner
* @return the builder instance
*/
public HttpClientPropertiesBuilder routePlanner(HttpRoutePlanner routePlanner) {
this.routePlanner = routePlanner;
return this;
}
/**
* Sets the list of supported SSL/TLS protocols.
* @param tlsProtocols the list of supported protocols
* @return the builder instance
*/
public HttpClientPropertiesBuilder supportedProtocols(String[] tlsProtocols) {
this.supportedProtocols = tlsProtocols;
return this;
}
public HttpClientProperties build() {
return new HttpClientProperties(this);
}
}
}

Просмотреть файл

@ -1,259 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package com.microsoft.azure.kusto.data.http;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.microsoft.azure.kusto.data.Utils;
import com.microsoft.azure.kusto.data.auth.CloudInfo;
import com.microsoft.azure.kusto.data.exceptions.*;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.EofSensorInputStream;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandles;
import java.net.*;
import java.util.*;
import java.util.zip.DeflaterInputStream;
import java.util.zip.GZIPInputStream;
public class HttpPostUtils {
private static final int MAX_REDIRECT_COUNT = 1;
private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private HttpPostUtils() {
// Hide constructor, as this is a static utility class
}
public static String post(CloseableHttpClient httpClient, String urlStr, AbstractHttpEntity requestEntity, long timeoutMs,
Map<String, String> headers)
throws DataServiceException, DataClientException {
URI url = parseUriFromUrlString(urlStr);
try {
HttpPost request = setupHttpPostRequest(url, requestEntity, headers);
int requestTimeout = timeoutMs > Integer.MAX_VALUE ? Integer.MAX_VALUE : Math.toIntExact(timeoutMs);
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(requestTimeout).build();
request.setConfig(requestConfig);
// Execute and get the response
try (CloseableHttpResponse response = httpClient.execute(request)) {
HttpEntity entity = response.getEntity();
if (entity != null) {
StatusLine statusLine = response.getStatusLine();
String responseContent = EntityUtils.toString(entity);
switch (statusLine.getStatusCode()) {
case HttpStatus.SC_OK:
return responseContent;
case HttpStatus.SC_TOO_MANY_REQUESTS:
throw new ThrottleException(urlStr);
default:
throw createExceptionFromResponse(urlStr, response, null, responseContent);
}
}
}
} catch (IOException e) {
throw new DataServiceException(urlStr, "IOException in post request:" + e.getMessage(), !Utils.isRetriableIOException(e));
}
return null;
}
public static InputStream postToStreamingOutput(CloseableHttpClient httpClient, String url, String payload, long timeoutMs, Map<String, String> headers)
throws DataServiceException, DataClientException {
return postToStreamingOutput(httpClient, url, payload, timeoutMs, headers, 0);
}
public static InputStream postToStreamingOutput(CloseableHttpClient httpClient, String url, String payload, long timeoutMs, Map<String, String> headers,
int redirectCount)
throws DataServiceException, DataClientException {
long timeoutTimeMs = System.currentTimeMillis() + timeoutMs;
URI uri = parseUriFromUrlString(url);
boolean returnInputStream = false;
String errorFromResponse = null;
/*
* The caller must close the inputStream to close the following underlying resources (httpClient and httpResponse). We use CloseParentResourcesStream so
* that when the stream is closed, these resources are closed as well. We shouldn't need to do that, per
* https://hc.apache.org/httpcomponents-client-4.5.x/current/tutorial/html/fundamentals.html: "In order to ensure proper release of system resources one
* must close either the content stream associated with the entity or the response itself." However, in my testing this wasn't reliably the case. We
* further use EofSensorInputStream to close the stream even if not explicitly closed, once all content is consumed.
*/
CloseableHttpResponse httpResponse = null;
try {
StringEntity requestEntity = new StringEntity(payload, ContentType.APPLICATION_JSON);
HttpPost httpPost = setupHttpPostRequest(uri, requestEntity, headers);
int requestTimeout = Math.toIntExact(timeoutTimeMs - System.currentTimeMillis());
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(requestTimeout).build();
httpPost.setConfig(requestConfig);
httpResponse = httpClient.execute(httpPost);
int responseStatusCode = httpResponse.getStatusLine().getStatusCode();
if (responseStatusCode == HttpStatus.SC_OK) {
InputStream contentStream = new EofSensorInputStream(new CloseParentResourcesStream(httpResponse), null);
Optional<Header> contentEncoding = Arrays.stream(httpResponse.getHeaders(HttpHeaders.CONTENT_ENCODING)).findFirst();
if (contentEncoding.isPresent()) {
if (contentEncoding.get().getValue().contains("gzip")) {
GZIPInputStream gzipInputStream = new GZIPInputStream(contentStream);
returnInputStream = true;
return gzipInputStream;
} else if (contentEncoding.get().getValue().contains("deflate")) {
DeflaterInputStream deflaterInputStream = new DeflaterInputStream(contentStream);
returnInputStream = true;
return deflaterInputStream;
}
}
// Though the server responds with a gzip/deflate Content-Encoding header, we reach here because httpclient uses LazyDecompressingStream which
// handles the above logic
returnInputStream = true;
return contentStream;
}
errorFromResponse = EntityUtils.toString(httpResponse.getEntity());
// Ideal to close here (as opposed to finally) so that if any data can't be flushed, the exception will be properly thrown and handled
httpResponse.close();
if (shouldPostToOriginalUrlDueToRedirect(redirectCount, responseStatusCode)) {
Optional<Header> redirectLocation = Arrays.stream(httpResponse.getHeaders(HttpHeaders.LOCATION)).findFirst();
if (redirectLocation.isPresent() && !redirectLocation.get().getValue().equals(url)) {
return postToStreamingOutput(httpClient, redirectLocation.get().getValue(), payload, timeoutMs, headers, redirectCount + 1);
}
}
} catch (IOException ex) {
throw new DataServiceException(url, "postToStreamingOutput failed to get or decompress response stream",
ex, false);
} catch (Exception ex) {
throw createExceptionFromResponse(url, httpResponse, ex, errorFromResponse);
} finally {
closeResourcesIfNeeded(returnInputStream, httpResponse);
}
throw createExceptionFromResponse(url, httpResponse, null, errorFromResponse);
}
public static DataServiceException createExceptionFromResponse(String url, HttpResponse httpResponse, Exception thrownException, String errorFromResponse) {
if (httpResponse == null) {
return new DataServiceException(url, "POST failed to send request", thrownException, false);
} else {
/*
* TODO: When we add another streaming API that returns a KustoOperationResult, we'll need to handle the 2 types of content errors this API can
* return: (1) Inline error (engine identifies error after it starts building the json result), or (2) in the KustoOperationResult's
* QueryCompletionInformation, both of which present with "200 OK". See .Net's DataReaderParser.
*/
String activityId = determineActivityId(httpResponse);
String message = errorFromResponse;
WebException formattedException = new WebException(errorFromResponse, httpResponse, thrownException);
boolean isPermanent = false;
if (!StringUtils.isBlank(errorFromResponse)) {
try {
JsonNode jsonObject = Utils.getObjectMapper().readTree(errorFromResponse);
if (jsonObject.has("error")) {
formattedException = new DataWebException(errorFromResponse, httpResponse, thrownException);
OneApiError apiError = ((DataWebException) formattedException).getApiError();
message = apiError.getDescription();
isPermanent = apiError.isPermanent();
} else if (jsonObject.has("message")) {
message = jsonObject.get("message").asText();
}
} catch (JsonProcessingException e) {
// It's not ideal to use an exception here for control flow, but we can't know if it's a valid JSON until we try to parse it
LOGGER.debug("json processing error happened while parsing errorFromResponse {}" + e.getMessage(), e);
}
} else {
message = String.format("Http StatusCode='%s'", httpResponse.getStatusLine().toString());
}
return new DataServiceException(
url,
String.format("%s, ActivityId='%s'", message, activityId),
formattedException,
isPermanent);
}
}
private static void closeResourcesIfNeeded(boolean returnInputStream, CloseableHttpResponse httpResponse) {
// If we close the resources after returning the InputStream to the caller, they won't be able to read from it
if (!returnInputStream) {
try {
if (httpResponse != null) {
httpResponse.close();
}
} catch (IOException ex) {
// There's nothing we can do if the resources can't be closed, and we don't want to add an exception to the signature
LOGGER.error("Couldn't close HttpResponse. This won't affect the POST call, but should be investigated.");
}
}
}
private static boolean shouldPostToOriginalUrlDueToRedirect(int redirectCount, int status) {
return (status == HttpStatus.SC_MOVED_TEMPORARILY || status == HttpStatus.SC_TEMPORARY_REDIRECT)
&& redirectCount + 1 <= MAX_REDIRECT_COUNT;
}
private static String determineActivityId(HttpResponse httpResponse) {
String activityId = "";
Optional<Header> activityIdHeader = Arrays.stream(httpResponse.getHeaders("x-ms-activity-id")).findFirst();
if (activityIdHeader.isPresent()) {
activityId = activityIdHeader.get().getValue();
}
return activityId;
}
private static HttpPost setupHttpPostRequest(URI uri, AbstractHttpEntity requestEntity, Map<String, String> headers) {
HttpPost request = new HttpPost(uri);
// Request parameters and other properties
request.setEntity(requestEntity);
request.addHeader(HttpHeaders.ACCEPT_ENCODING, "gzip,deflate");
request.addHeader(HttpHeaders.ACCEPT, "application/json");
// TODO - maybe save this in a resouce
String KUSTO_API_VERSION = "2019-02-13";
request.addHeader("x-ms-version", KUSTO_API_VERSION);
for (Map.Entry<String, String> entry : headers.entrySet()) {
request.addHeader(entry.getKey(), entry.getValue());
}
return request;
}
@NotNull
private static URI parseUriFromUrlString(String url) throws DataClientException {
try {
URL cleanUrl = new URL(url);
if ("https".equalsIgnoreCase(cleanUrl.getProtocol()) || url.toLowerCase().startsWith(CloudInfo.LOCALHOST)) {
return new URI(cleanUrl.getProtocol(), cleanUrl.getUserInfo(), cleanUrl.getHost(), cleanUrl.getPort(), cleanUrl.getPath(), cleanUrl.getQuery(),
cleanUrl.getRef());
} else {
throw new DataClientException(url, "Cannot forward security token to a remote service over insecure " +
"channel (http://)");
}
} catch (URISyntaxException | MalformedURLException e) {
throw new DataClientException(url, "Error parsing target URL in post request:" + e.getMessage(), e);
}
}
}

Просмотреть файл

@ -0,0 +1,183 @@
package com.microsoft.azure.kusto.data.http;
import com.azure.core.http.HttpHeaderName;
import com.azure.core.http.HttpMethod;
import com.azure.core.http.HttpRequest;
import com.azure.core.util.BinaryData;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.microsoft.azure.kusto.data.Utils;
import com.microsoft.azure.kusto.data.auth.CloudInfo;
import com.microsoft.azure.kusto.data.exceptions.DataClientException;
import com.microsoft.azure.kusto.data.req.KustoRequest;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class HttpRequestBuilder {
// TODO - maybe save this in a resource
private static final String KUSTO_API_VERSION = "2019-02-13";
private static final String JAVA_INGEST_ACTIVITY_TYPE_PREFIX = "DN.JavaClient.Execute";
private static final String CLIENT_VERSION_HEADER = "x-ms-client-version";
private static final String APP_HEADER = "x-ms-app";
private static final String USER_HEADER = "x-ms-user";
private final HttpRequest request;
public static HttpRequestBuilder fromExistingRequest(HttpRequest request) {
return new HttpRequestBuilder(request);
}
public static HttpRequestBuilder newPost(String url) throws DataClientException {
return new HttpRequestBuilder(HttpMethod.POST, url);
}
private HttpRequestBuilder(HttpRequest request) {
this.request = request;
}
public HttpRequestBuilder(HttpMethod method, String url) throws DataClientException {
URL cleanURL = parseURLString(url);
request = new HttpRequest(method, cleanURL);
}
public HttpRequestBuilder createCommandPayload(KustoRequest kr) {
ObjectNode json = Utils.getObjectMapper().createObjectNode()
.put("db", kr.getDatabase())
.put("csl", kr.getCommand());
if (kr.getProperties() != null) {
json.put("properties", kr.getProperties().toString());
}
request.setBody(json.toString());
// When executing a query/command, we always add content type
// Updated to remove Fed True from command headers per PR #342
request.setHeader(HttpHeaderName.CONTENT_TYPE, "application/json; charset=utf-8");
return this;
}
public HttpRequestBuilder withBody(BinaryData body) {
request.setBody(body);
return this;
}
public HttpRequestBuilder withAuthorization(String value) {
if (value != null) {
request.setHeader(HttpHeaderName.AUTHORIZATION, value);
}
return this;
}
public HttpRequestBuilder withContentEncoding(String contentEncoding) {
if (contentEncoding != null) {
request.setHeader(HttpHeaderName.CONTENT_ENCODING, contentEncoding);
}
return this;
}
public HttpRequestBuilder withContentType(String contentType) {
if (contentType != null) {
request.setHeader(HttpHeaderName.CONTENT_TYPE, contentType);
}
return this;
}
public HttpRequestBuilder withHeaders(Map<String, String> headers) {
for (Map.Entry<String, String> entry : headers.entrySet()) {
request.setHeader(HttpHeaderName.fromString(entry.getKey()), entry.getValue());
}
return this;
}
public HttpRequestBuilder withTracing(HttpTracing tracing) {
return this.withHeaders(getTracingHeaders(tracing));
}
public HttpRequestBuilder withURL(String url) throws DataClientException {
URL cleanURL = parseURLString(url);
request.setUrl(cleanURL);
return this;
}
public HttpRequest build() {
// Set global headers that get added to all requests
request.setHeader(HttpHeaderName.ACCEPT_ENCODING, "gzip,deflate");
request.setHeader(HttpHeaderName.ACCEPT, "application/json");
// Removed content type from this method because the request should already have a type that is not always json
request.setHeader(HttpHeaderName.fromString("x-ms-version"), KUSTO_API_VERSION);
return request;
}
@NotNull
private static URL parseURLString(String url) throws DataClientException {
try {
// By nature of the try/catch only valid URLs pass through this function
URL cleanUrl = new URL(url);
// Further checking here to ensure the URL uses HTTPS if not pointed at localhost
if ("https".equalsIgnoreCase(cleanUrl.getProtocol()) || url.toLowerCase().startsWith(CloudInfo.LOCALHOST)) {
return cleanUrl;
} else {
throw new DataClientException(url, "Cannot forward security token to a remote service over insecure " +
"channel (http://)");
}
} catch (MalformedURLException e) {
throw new DataClientException(url, "Error parsing target URL in post request:" + e.getMessage(), e);
}
}
private Map<String, String> getTracingHeaders(HttpTracing tracing) {
Map<String, String> headers = new HashMap<>();
String version = tracing.getClientDetails().getClientVersionForTracing();
if (StringUtils.isNotBlank(version)) {
headers.put(CLIENT_VERSION_HEADER, version);
}
String app = (tracing.getProperties() == null || tracing.getProperties().getApplication() == null)
? tracing.getClientDetails().getApplicationForTracing()
: tracing.getProperties().getApplication();
if (StringUtils.isNotBlank(app)) {
headers.put(APP_HEADER, app);
}
String user = (tracing.getProperties() == null || tracing.getProperties().getUser() == null) ? tracing.getClientDetails().getUserNameForTracing()
: tracing.getProperties().getUser();
if (StringUtils.isNotBlank(user)) {
headers.put(USER_HEADER, user);
}
String clientRequestId;
if (tracing.getProperties() != null && StringUtils.isNotBlank(tracing.getProperties().getClientRequestId())) {
clientRequestId = tracing.getProperties().getClientRequestId();
} else {
clientRequestId = String.format("%s;%s", tracing.getClientRequestIdPrefix(), UUID.randomUUID());
}
headers.put("x-ms-client-request-id", clientRequestId);
// Configures Keep-Alive on all requests traced
headers.put("Connection", "Keep-Alive");
UUID activityId = UUID.randomUUID();
String activityContext = String.format("%s%s/%s, ActivityId=%s, ParentId=%s, ClientRequestId=%s",
JAVA_INGEST_ACTIVITY_TYPE_PREFIX, tracing.getActivityTypeSuffix(), activityId, activityId, activityId, clientRequestId);
headers.put("x-ms-activitycontext", activityContext);
// replace non-ascii characters in header values with '?'
headers.replaceAll((_i, v) -> v == null ? null : v.replaceAll("[^\\x00-\\x7F]", "?"));
return headers;
}
}

Просмотреть файл

@ -0,0 +1,11 @@
package com.microsoft.azure.kusto.data.http;
public class HttpStatus {
public static final int OK = 200;
public static final int FOUND = 302;
public static final int TEMP_REDIRECT = 207;
public static final int UNAUTHORIZED = 401;
public static final int FORBIDDEN = 403;
public static final int NOT_FOUND = 404;
public static final int TOO_MANY_REQS = 429;
}

Просмотреть файл

@ -0,0 +1,83 @@
package com.microsoft.azure.kusto.data.http;
import com.microsoft.azure.kusto.data.ClientDetails;
import com.microsoft.azure.kusto.data.ClientRequestProperties;
public class HttpTracing {
private ClientRequestProperties properties;
private String clientRequestIdPrefix;
private String activityTypeSuffix;
private ClientDetails clientDetails;
private HttpTracing() {
}
public ClientRequestProperties getProperties() {
return properties;
}
public void setProperties(ClientRequestProperties properties) {
this.properties = properties;
}
public String getClientRequestIdPrefix() {
return clientRequestIdPrefix;
}
public void setClientRequestIdPrefix(String clientRequestIdPrefix) {
this.clientRequestIdPrefix = clientRequestIdPrefix;
}
public String getActivityTypeSuffix() {
return activityTypeSuffix;
}
public void setActivityTypeSuffix(String activityTypeSuffix) {
this.activityTypeSuffix = activityTypeSuffix;
}
public ClientDetails getClientDetails() {
return clientDetails;
}
public void setClientDetails(ClientDetails clientDetails) {
this.clientDetails = clientDetails;
}
public static HttpTracingBuilder newBuilder() {
return new HttpTracingBuilder();
}
public static class HttpTracingBuilder {
private final HttpTracing tracing = new HttpTracing();
public HttpTracingBuilder() {
}
public HttpTracingBuilder withClientDetails(ClientDetails clientDetails) {
tracing.setClientDetails(clientDetails);
return this;
}
public HttpTracingBuilder withProperties(ClientRequestProperties properties) {
tracing.setProperties(properties);
return this;
}
public HttpTracingBuilder withRequestPrefix(String requestPrefix) {
tracing.setClientRequestIdPrefix(requestPrefix);
return this;
}
public HttpTracingBuilder withActivitySuffix(String activitySuffix) {
tracing.setActivityTypeSuffix(activitySuffix);
return this;
}
public HttpTracing build() {
return tracing;
}
}
}

Просмотреть файл

@ -2,6 +2,7 @@ package com.microsoft.azure.kusto.data.instrumentation;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
public class MonitoredActivity {
public static void invoke(Runnable runnable, String nameOfSpan) {
@ -14,6 +15,16 @@ public class MonitoredActivity {
}
}
public static <T> T invoke(SupplierNoException<T> supplier, String nameOfSpan) {
return invoke(supplier, nameOfSpan, new HashMap<>());
}
public static <T> T invoke(SupplierNoException<T> supplier, String nameOfSpan, Map<String, String> attributes) {
try (Tracer.Span span = Tracer.startSpan(nameOfSpan, attributes)) {
return supplier.get();
}
}
public static <T, U extends Exception> T invoke(SupplierOneException<T, U> supplier, String nameOfSpan) throws U {
return invoke((SupplierTwoExceptions<T, U, U>) supplier::get, nameOfSpan, new HashMap<>());
}

Просмотреть файл

@ -0,0 +1,16 @@
package com.microsoft.azure.kusto.data.instrumentation;
/**
* Supplier is a functional interface that can be used to execute data operations.
* @param <T>
*/
@FunctionalInterface
public interface SupplierNoException<T> {
/**
* Execute a data operation.
* @return T
*/
T get();
}

Просмотреть файл

@ -0,0 +1,197 @@
package com.microsoft.azure.kusto.data.req;
import com.microsoft.azure.kusto.data.ClientRequestProperties;
import com.microsoft.azure.kusto.data.CommandType;
import org.apache.commons.lang3.StringUtils;
public class KustoRequest {
private static final String ADMIN_COMMANDS_PREFIX = ".";
private static final String DEFAULT_DATABASE_NAME = "NetDefaultDb";
private String command;
private CommandType commandType;
private String database;
private ClientRequestProperties properties;
/**
* A constructor providing only the command to be executed.
* @param command the command to be executed
*/
public KustoRequest(String command) {
this.command = command;
}
/**
* A constructor providing the command to be executed, and the command type.
* @param command the command to be executed
* @param commandType the command type
*/
public KustoRequest(String command, CommandType commandType) {
this.command = command;
this.commandType = commandType;
}
/**
* A constructor providing the command to be executed and the database to target.
* @param command the command to be executed
* @param database the name of the database to target
*/
public KustoRequest(String command, String database) {
this.command = command;
this.database = database;
}
/**
* A constructor providing the command to be executed, the database to target, and the command type.
* @param command the command to be executed
* @param database the name of the database to target
* @param commandType the command type
*/
public KustoRequest(String command, String database, CommandType commandType) {
this.command = command;
this.database = database;
this.commandType = commandType;
}
/**
* A constructor providing the command to be executed and sanitized query parameters.
* @param command the command to be executed
* @param properties a map of query parameters and other request properties
*/
public KustoRequest(String command, ClientRequestProperties properties) {
this.command = command;
this.properties = properties;
}
/**
* A constructor providing the command to be executed, the database to target, and the command type.
* @param command the command to be executed
* @param properties a map of query parameters and other request properties
* @param commandType the command type
*/
public KustoRequest(String command, ClientRequestProperties properties, CommandType commandType) {
this.command = command;
this.properties = properties;
this.commandType = commandType;
}
/**
* A constructor providing the command to be executed, sanitized query parameters, and the database to target.
* @param command the command to be executed
* @param database the name of the database to target
* @param properties a map of query parameters and other request properties
*/
public KustoRequest(String command, String database, ClientRequestProperties properties) {
this.command = command;
this.database = database;
this.properties = properties;
}
/**
* An all args constructor providing the command to be executed, sanitized query parameters, the database to target, and the command type.
* @param command the command to be executed
* @param database the name of the database to target
* @param properties a map of query parameters and other request properties
* @param commandType the command type
*/
public KustoRequest(String command, String database, ClientRequestProperties properties, CommandType commandType) {
this.command = command;
this.database = database;
this.properties = properties;
this.commandType = commandType;
}
/**
* A getter for this KustoRequest object's inner command String.
* @return the command
*/
public String getCommand() {
return command;
}
/**
* A setter for this KustoRequest object's inner command String.
* @param command the command
*/
public void setCommand(String command) {
this.command = command;
}
/**
* A getter for this KustoRequest object's inner command type.
* @return the command type
*/
public CommandType getCommandType() {
return commandType;
}
/**
* A setter for this KustoRequest object's inner command type.
* @param commandType the command type
*/
public void setCommandType(CommandType commandType) {
this.commandType = commandType;
}
/**
* A getter for this KustoRequest object's inner database String.
* @return the database name
*/
public String getDatabase() {
return database;
}
/**
* A setter for this KustoRequest object's inner database String.
* @param database the database name
*/
public void setDatabase(String database) {
this.database = database;
}
/**
* A getter for this KustoRequest object's inner ClientRequestProperties.
* @return the properties
*/
public ClientRequestProperties getProperties() {
return properties;
}
/**
* A setter for this KustoRequest object's inner ClientRequestProperties.
* @param properties the properties
*/
public void setProperties(ClientRequestProperties properties) {
this.properties = properties;
}
/** Validates and optimizes the KustoQuery object. */
public void validateAndOptimize() {
if (database == null) {
database = DEFAULT_DATABASE_NAME;
}
// Argument validation
if (StringUtils.isEmpty(database)) {
throw new IllegalArgumentException("Database is empty");
}
if (StringUtils.isEmpty(command)) {
throw new IllegalArgumentException("Command is empty");
}
// Optimize the command by removing superfluous whitespace
command = command.trim();
// Set command type if it wasn't provided. This is solely used by the deprecated methods in Client interface
// and the executeToJSON methods since they bypass the query/mgmt methods.
if (commandType == null) {
commandType = determineCommandType(command);
}
}
private CommandType determineCommandType(String command) {
if (command.startsWith(ADMIN_COMMANDS_PREFIX)) {
return CommandType.ADMIN_COMMAND;
}
return CommandType.QUERY;
}
}

Просмотреть файл

@ -0,0 +1,29 @@
package com.microsoft.azure.kusto.data.req;
import com.azure.core.http.HttpRequest;
public class KustoRequestContext {
private KustoRequest sdkRequest;
private HttpRequest httpRequest;
public KustoRequestContext(KustoRequest sdkRequest, HttpRequest httpRequest) {
this.sdkRequest = sdkRequest;
this.httpRequest = httpRequest;
}
public KustoRequest getSdkRequest() {
return sdkRequest;
}
public void setSdkRequest(KustoRequest sdkRequest) {
this.sdkRequest = sdkRequest;
}
public HttpRequest getHttpRequest() {
return httpRequest;
}
public void setHttpRequest(HttpRequest httpRequest) {
this.httpRequest = httpRequest;
}
}

Просмотреть файл

@ -0,0 +1,27 @@
package com.microsoft.azure.kusto.data.res;
public class JsonResult {
private String result;
private String endpoint;
public JsonResult(String result, String endpoint) {
this.result = result;
this.endpoint = endpoint;
}
public String getResult() {
return result;
}
public void setResult(String result) {
this.result = result;
}
public String getEndpoint() {
return endpoint;
}
public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}
}

Просмотреть файл

@ -53,7 +53,7 @@ class ClientRequestPropertiesTest {
// before setting value should be null
Assertions.assertNull(clientRequestProperties.getTimeoutInMilliSec());
Object timeoutObj = clientRequestProperties.getOption(OPTION_SERVER_TIMEOUT);
Assertions.assertNull(clientRequestProperties.getTimeoutAsString(timeoutObj));
Assertions.assertNull(clientRequestProperties.getTimeoutAsCslTimespan(timeoutObj));
clientRequestProperties.setTimeoutInMilliSec(serverTimeoutOptionMillis);
Assertions.assertEquals(clientRequestProperties.getTimeoutInMilliSec(), expectedMillis);
@ -71,7 +71,7 @@ class ClientRequestPropertiesTest {
// before setting value should be null
Assertions.assertNull(clientRequestProperties.getTimeoutInMilliSec());
Object timeoutObj = clientRequestProperties.getOption(OPTION_SERVER_TIMEOUT);
Assertions.assertNull(clientRequestProperties.getTimeoutAsString(timeoutObj));
Assertions.assertNull(clientRequestProperties.getTimeoutAsCslTimespan(timeoutObj));
clientRequestProperties.setOption(ClientRequestProperties.OPTION_SERVER_TIMEOUT, serverTimeoutOptionMillis);
Assertions.assertEquals(clientRequestProperties.getTimeoutInMilliSec(), expectedMillis);
@ -88,7 +88,7 @@ class ClientRequestPropertiesTest {
// before setting value should be null
Assertions.assertNull(clientRequestProperties.getTimeoutInMilliSec());
Object timeoutObj = clientRequestProperties.getOption(OPTION_SERVER_TIMEOUT);
Assertions.assertNull(clientRequestProperties.getTimeoutAsString(timeoutObj));
Assertions.assertNull(clientRequestProperties.getTimeoutAsCslTimespan(timeoutObj));
clientRequestProperties.setOption(ClientRequestProperties.OPTION_SERVER_TIMEOUT, serverTimeoutOptionTimespan);
Assertions.assertEquals(clientRequestProperties.getTimeoutInMilliSec(), expectedMillis);

Просмотреть файл

@ -1,33 +1,75 @@
package com.microsoft.azure.kusto.data;
import com.azure.core.http.HttpHeaders;
import com.azure.core.http.HttpRequest;
import com.microsoft.azure.kusto.data.auth.ConnectionStringBuilder;
import com.microsoft.azure.kusto.data.exceptions.DataClientException;
import com.microsoft.azure.kusto.data.http.HttpRequestBuilder;
import com.microsoft.azure.kusto.data.http.HttpTracing;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
public class HeaderTest {
@Test
public void testHeadersDefault() throws URISyntaxException {
public void testHeadersDefault() throws URISyntaxException, DataClientException {
ConnectionStringBuilder csb = ConnectionStringBuilder.createWithAadManagedIdentity("https://testcluster.kusto.windows.net");
ClientImpl client = (ClientImpl) ClientFactory.createClient(csb);
Map<String, String> headers = client.extractTracingHeaders(new ClientRequestProperties());
ClientRequestProperties crp = new ClientRequestProperties();
HttpTracing tracing = HttpTracing
.newBuilder()
.withProperties(crp)
.withRequestPrefix("KJC.execute")
.withActivitySuffix("QueryCommand")
.withClientDetails(client.getClientDetails())
.build();
HttpRequest request = HttpRequestBuilder
.newPost("https://www.example.com")
.withTracing(tracing)
.build();
Map<String, String> headers = extractHeadersFromAzureRequest(request);
Assertions.assertNotNull(headers.get("x-ms-app"));
Assertions.assertNotNull(headers.get("x-ms-user"));
Assertions.assertTrue(headers.get("x-ms-client-version").startsWith("Kusto.Java.Client"));
}
@Test
public void testHeadersWithCustomCsb() throws URISyntaxException {
public void testHeadersWithCustomCsb() throws URISyntaxException, DataClientException {
ConnectionStringBuilder csb = ConnectionStringBuilder.createWithAadManagedIdentity("https://testcluster.kusto.windows.net");
csb.setApplicationNameForTracing("testApp");
csb.setUserNameForTracing("testUser");
csb.setClientVersionForTracing("testVersion");
ClientImpl client = (ClientImpl) ClientFactory.createClient(csb);
Map<String, String> headers = client.extractTracingHeaders(new ClientRequestProperties());
ClientRequestProperties crp = new ClientRequestProperties();
HttpTracing tracing = HttpTracing
.newBuilder()
.withProperties(crp)
.withRequestPrefix("KJC.execute")
.withActivitySuffix("QueryCommand")
.withClientDetails(client.getClientDetails())
.build();
HttpRequest request = HttpRequestBuilder
.newPost("https://www.example.com")
.withTracing(tracing)
.build();
Map<String, String> headers = extractHeadersFromAzureRequest(request);
Assertions.assertEquals("testApp", headers.get("x-ms-app"));
Assertions.assertEquals("testUser", headers.get("x-ms-user"));
Assertions.assertTrue(headers.get("x-ms-client-version").startsWith("Kusto.Java.Client"));
@ -35,17 +77,33 @@ public class HeaderTest {
}
@Test
public void testHeadersWithCustomCsbAndClientRequestProperties() throws URISyntaxException {
public void testHeadersWithCustomCsbAndClientRequestProperties() throws URISyntaxException, DataClientException {
ConnectionStringBuilder csb = ConnectionStringBuilder.createWithAadManagedIdentity("https://testcluster.kusto.windows.net");
csb.setApplicationNameForTracing("testApp");
csb.setUserNameForTracing("testUser");
csb.setClientVersionForTracing("testVersion");
ClientImpl client = (ClientImpl) ClientFactory.createClient(csb);
ClientRequestProperties crp = new ClientRequestProperties();
crp.setApplication("crpApp");
crp.setUser("crpUser");
Map<String, String> headers = client.extractTracingHeaders(crp);
HttpTracing tracing = HttpTracing
.newBuilder()
.withProperties(crp)
.withRequestPrefix("KJC.execute")
.withActivitySuffix("QueryCommand")
.withClientDetails(client.getClientDetails())
.build();
HttpRequest request = HttpRequestBuilder
.newPost("https://www.example.com")
.withTracing(tracing)
.build();
Map<String, String> headers = extractHeadersFromAzureRequest(request);
Assertions.assertEquals("crpApp", headers.get("x-ms-app"));
Assertions.assertEquals("crpUser", headers.get("x-ms-user"));
Assertions.assertTrue(headers.get("x-ms-client-version").startsWith("Kusto.Java.Client"));
@ -53,13 +111,29 @@ public class HeaderTest {
}
@Test
public void testSetConnectorNameAndVersion() throws URISyntaxException {
public void testSetConnectorNameAndVersion() throws URISyntaxException, DataClientException {
ConnectionStringBuilder csb = ConnectionStringBuilder.createWithAadManagedIdentity("https://testcluster.kusto.windows.net");
csb.setConnectorDetails("myConnector", "myVersion", null, null, false, null);
ClientImpl client = (ClientImpl) ClientFactory.createClient(csb);
ClientRequestProperties crp = new ClientRequestProperties();
Map<String, String> headers = client.extractTracingHeaders(crp);
HttpTracing tracing = HttpTracing
.newBuilder()
.withProperties(crp)
.withRequestPrefix("KJC.execute")
.withActivitySuffix("QueryCommand")
.withClientDetails(client.getClientDetails())
.build();
HttpRequest request = HttpRequestBuilder
.newPost("https://www.example.com")
.withTracing(tracing)
.build();
Map<String, String> headers = extractHeadersFromAzureRequest(request);
Assertions.assertEquals("[none]", headers.get("x-ms-user"));
Assertions.assertTrue(headers.get("x-ms-client-version").startsWith("Kusto.Java.Client:"));
@ -67,13 +141,29 @@ public class HeaderTest {
}
@Test
public void testSetConnectorNoAppVersion() throws URISyntaxException {
public void testSetConnectorNoAppVersion() throws URISyntaxException, DataClientException {
ConnectionStringBuilder csb = ConnectionStringBuilder.createWithAadManagedIdentity("https://testcluster.kusto.windows.net");
csb.setConnectorDetails("myConnector", "myVersion", null, null, true, "myApp");
ClientImpl client = (ClientImpl) ClientFactory.createClient(csb);
ClientRequestProperties crp = new ClientRequestProperties();
Map<String, String> headers = client.extractTracingHeaders(crp);
HttpTracing tracing = HttpTracing
.newBuilder()
.withProperties(crp)
.withRequestPrefix("KJC.execute")
.withActivitySuffix("QueryCommand")
.withClientDetails(client.getClientDetails())
.build();
HttpRequest request = HttpRequestBuilder
.newPost("https://www.example.com")
.withTracing(tracing)
.build();
Map<String, String> headers = extractHeadersFromAzureRequest(request);
Assertions.assertTrue(headers.get("x-ms-user").length() > 0);
Assertions.assertTrue(headers.get("x-ms-client-version").startsWith("Kusto.Java.Client:"));
@ -81,16 +171,40 @@ public class HeaderTest {
}
@Test
public void testSetConnectorFull() throws URISyntaxException {
public void testSetConnectorFull() throws URISyntaxException, DataClientException {
ConnectionStringBuilder csb = ConnectionStringBuilder.createWithAadManagedIdentity("https://testcluster.kusto.windows.net");
csb.setConnectorDetails("myConnector", "myVersion", "myApp", "myAppVersion", true, "myUser", Pair.of("myField", "myValue"));
ClientImpl client = (ClientImpl) ClientFactory.createClient(csb);
ClientRequestProperties crp = new ClientRequestProperties();
Map<String, String> headers = client.extractTracingHeaders(crp);
HttpTracing tracing = HttpTracing
.newBuilder()
.withProperties(crp)
.withRequestPrefix("KJC.execute")
.withActivitySuffix("QueryCommand")
.withClientDetails(client.getClientDetails())
.build();
HttpRequest request = HttpRequestBuilder
.newPost("https://www.example.com")
.withTracing(tracing)
.build();
Map<String, String> headers = extractHeadersFromAzureRequest(request);
Assertions.assertEquals("myUser", headers.get("x-ms-user"));
Assertions.assertTrue(headers.get("x-ms-client-version").startsWith("Kusto.Java.Client:"));
Assertions.assertEquals("Kusto.myConnector:{myVersion}|App.{myApp}:{myAppVersion}|myField:{myValue}", headers.get("x-ms-app"));
}
private Map<String, String> extractHeadersFromAzureRequest(HttpRequest request) {
Map<String, String> uncomplicatedHeaders = new HashMap<>();
HttpHeaders headers = request.getHeaders();
headers.forEach(header -> uncomplicatedHeaders.put(header.getName(), header.getValue()));
return uncomplicatedHeaders;
}
}

Просмотреть файл

@ -1,12 +1,12 @@
package com.microsoft.azure.kusto.data;
import com.azure.core.http.HttpResponse;
import com.microsoft.azure.kusto.data.exceptions.DataServiceException;
import com.microsoft.azure.kusto.data.exceptions.DataWebException;
import com.microsoft.azure.kusto.data.http.HttpPostUtils;
import org.apache.http.HttpStatus;
import org.apache.http.ProtocolVersion;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.message.BasicStatusLine;
import com.microsoft.azure.kusto.data.http.HttpStatus;
import com.microsoft.azure.kusto.data.http.TestHttpResponse;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
@ -22,7 +22,7 @@ class UtilitiesTest {
@Test
@DisplayName("Test exception creation when the web response is null")
void createExceptionFromResponseNoResponse() {
DataServiceException error = HttpPostUtils.createExceptionFromResponse("https://sample.kusto.windows.net", null, new Exception(), "error");
DataServiceException error = BaseClient.createExceptionFromResponse("https://sample.kusto.windows.net", null, new Exception(), "error");
Assertions.assertEquals("POST failed to send request", error.getMessage());
Assertions.assertFalse(error.isPermanent());
}
@ -30,9 +30,9 @@ class UtilitiesTest {
@Test
@DisplayName("Test exception creation on a 404 error")
void createExceptionFromResponse404Error() {
BasicHttpResponse basicHttpResponse = getBasicHttpResponse(404);
DataServiceException error = HttpPostUtils.createExceptionFromResponse("https://sample.kusto.windows.net", basicHttpResponse, new Exception(), "error");
Assertions.assertTrue(error.getStatusCode() != null && error.getStatusCode() == HttpStatus.SC_NOT_FOUND);
HttpResponse basicHttpResponse = getHttpResponse(HttpStatus.NOT_FOUND);
DataServiceException error = BaseClient.createExceptionFromResponse("https://sample.kusto.windows.net", basicHttpResponse, new Exception(), "error");
Assertions.assertTrue(error.getStatusCode() != null && error.getStatusCode() == HttpStatus.NOT_FOUND);
}
@Test
@ -60,61 +60,61 @@ class UtilitiesTest {
" },\n" +
" \"@permanent\": true\n" +
" }}";
BasicHttpResponse basicHttpResponse = getBasicHttpResponse(401);
DataServiceException error = HttpPostUtils.createExceptionFromResponse("https://sample.kusto.windows.net", basicHttpResponse, new Exception(),
HttpResponse basicHttpResponse = getHttpResponse(HttpStatus.UNAUTHORIZED);
DataServiceException error = BaseClient.createExceptionFromResponse("https://sample.kusto.windows.net", basicHttpResponse, new Exception(),
OneApiError);
Assertions.assertEquals("Query execution has exceeded the allowed limits (80DA0003): ., ActivityId='1234'", error.getMessage());
Assertions.assertTrue(error.getCause() instanceof DataWebException);
Assertions.assertInstanceOf(DataWebException.class, error.getCause());
Assertions.assertTrue(error.isPermanent());
Assertions.assertEquals(401, Objects.requireNonNull(error.getStatusCode()).intValue());
Assertions.assertEquals(HttpStatus.UNAUTHORIZED, Objects.requireNonNull(error.getStatusCode()).intValue());
}
@Test
@DisplayName("Test exception creation from a message object")
void createExceptionFromMessageError() {
String errorMessage = "{\"message\": \"Test Error Message\"}";
BasicHttpResponse basicHttpResponse = getBasicHttpResponse(401);
DataServiceException error = HttpPostUtils.createExceptionFromResponse("https://sample.kusto.windows.net", basicHttpResponse, new Exception(),
HttpResponse basicHttpResponse = getHttpResponse(HttpStatus.UNAUTHORIZED);
DataServiceException error = BaseClient.createExceptionFromResponse("https://sample.kusto.windows.net", basicHttpResponse, new Exception(),
errorMessage);
Assertions.assertEquals("Test Error Message, ActivityId='1234'", error.getMessage());
Assertions.assertFalse(error.isPermanent());
Assertions.assertEquals(401, Objects.requireNonNull(error.getStatusCode()).intValue());
Assertions.assertEquals(HttpStatus.UNAUTHORIZED, Objects.requireNonNull(error.getStatusCode()).intValue());
}
@Test
@DisplayName("Test exception creation from a bad json")
void createExceptionFromBadJson() {
String errorMessage = "\"message\": \"Test Error Message\"";
BasicHttpResponse basicHttpResponse = getBasicHttpResponse(401);
DataServiceException error = HttpPostUtils.createExceptionFromResponse("https://sample.kusto.windows.net", basicHttpResponse, new Exception(),
HttpResponse basicHttpResponse = getHttpResponse(HttpStatus.UNAUTHORIZED);
DataServiceException error = BaseClient.createExceptionFromResponse("https://sample.kusto.windows.net", basicHttpResponse, new Exception(),
errorMessage);
Assertions.assertEquals("\"message\": \"Test Error Message\", ActivityId='1234'", error.getMessage());
Assertions.assertFalse(error.isPermanent());
Assertions.assertEquals(401, Objects.requireNonNull(error.getStatusCode()).intValue());
Assertions.assertEquals(HttpStatus.UNAUTHORIZED, Objects.requireNonNull(error.getStatusCode()).intValue());
}
@Test
@DisplayName("Test exception creation from an unexpected json")
void createExceptionFromOtherJson() {
String errorMessage = "{\"response\": \"Test Error Message\"}";
BasicHttpResponse basicHttpResponse = getBasicHttpResponse(401);
DataServiceException error = HttpPostUtils.createExceptionFromResponse("https://sample.kusto.windows.net", basicHttpResponse, new Exception(),
HttpResponse basicHttpResponse = getHttpResponse(HttpStatus.UNAUTHORIZED);
DataServiceException error = BaseClient.createExceptionFromResponse("https://sample.kusto.windows.net", basicHttpResponse, new Exception(),
errorMessage);
Assertions.assertEquals("{\"response\": \"Test Error Message\"}, ActivityId='1234'", error.getMessage());
Assertions.assertFalse(error.isPermanent());
Assertions.assertEquals(401, Objects.requireNonNull(error.getStatusCode()).intValue());
Assertions.assertEquals(HttpStatus.UNAUTHORIZED, Objects.requireNonNull(error.getStatusCode()).intValue());
}
@Test
@DisplayName("Test exception creation from a blank error message")
void createExceptionFromBlankErrorMessage() {
String errorMessage = " ";
BasicHttpResponse basicHttpResponse = getBasicHttpResponse(401);
DataServiceException error = HttpPostUtils.createExceptionFromResponse("https://sample.kusto.windows.net", basicHttpResponse, new Exception(),
HttpResponse basicHttpResponse = getHttpResponse(HttpStatus.UNAUTHORIZED);
DataServiceException error = BaseClient.createExceptionFromResponse("https://sample.kusto.windows.net", basicHttpResponse, new Exception(),
errorMessage);
Assertions.assertEquals("Http StatusCode='http/1.1 401 Some Error', ActivityId='1234'", error.getMessage());
Assertions.assertEquals("Http StatusCode='401', ActivityId='1234'", error.getMessage());
Assertions.assertFalse(error.isPermanent());
Assertions.assertEquals(401, Objects.requireNonNull(error.getStatusCode()).intValue());
Assertions.assertEquals(HttpStatus.UNAUTHORIZED, Objects.requireNonNull(error.getStatusCode()).intValue());
}
@Test
@ -126,7 +126,7 @@ class UtilitiesTest {
@Test
@DisplayName("Assert file name extracted from some cmd line")
void extractFileNameFromCommandLine() {
String cmdLine = Paths.get(" home", "user", "someFile.jar").toString() + " -arg1 val";
String cmdLine = Paths.get(" home", "user", "someFile.jar") + " -arg1 val";
Assertions.assertEquals(UriUtils.stripFileNameFromCommandLine(cmdLine), "someFile.jar");
}
@ -136,17 +136,19 @@ class UtilitiesTest {
IOException e = new UnknownHostException("Doesnt exist");
Assertions.assertFalse(Utils.isRetriableIOException(e));
e = new org.apache.http.conn.HttpHostConnectException(new ConnectException("Connection refused"), null);
e = new ConnectException("Connection refused");
Assertions.assertFalse(Utils.isRetriableIOException(e));
e = new org.apache.http.conn.HttpHostConnectException(new ConnectException("Connection timed out"), null);
e = new ConnectException("Connection timed out");
Assertions.assertTrue(Utils.isRetriableIOException(e));
}
@NotNull
private BasicHttpResponse getBasicHttpResponse(int statusCode) {
BasicHttpResponse basicHttpResponse = new BasicHttpResponse(new BasicStatusLine(new ProtocolVersion("http", 1, 1), statusCode, "Some Error"));
basicHttpResponse.addHeader("x-ms-activity-id", "1234");
return basicHttpResponse;
private HttpResponse getHttpResponse(int statusCode) {
return TestHttpResponse
.newBuilder()
.withStatusCode(statusCode)
.addHeader("x-ms-activity-id", "1234")
.build();
}
}

Просмотреть файл

@ -1,7 +1,7 @@
package com.microsoft.azure.kusto.data.http;
import com.microsoft.azure.kusto.data.HttpClientProperties;
import org.apache.http.impl.client.CloseableHttpClient;
import com.azure.core.http.HttpClient;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@ -17,7 +17,7 @@ class HttpClientFactoryTest {
@DisplayName("test create http client from properties")
void testProperties() {
HttpClientProperties properties = HttpClientProperties.builder().build();
final CloseableHttpClient httpClient = HttpClientFactory.create(properties);
final HttpClient httpClient = HttpClientFactory.create(properties);
Assertions.assertNotNull(httpClient);
}
}

Просмотреть файл

@ -0,0 +1,93 @@
package com.microsoft.azure.kusto.data.http;
import com.azure.core.http.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
public class TestHttpResponse extends HttpResponse {
private int fakeStatusCode;
private final HttpHeaders fakeHeaders = new HttpHeaders();
private TestHttpResponse() {
super(null);
}
protected TestHttpResponse(HttpRequest request) {
super(request);
}
@Override
public int getStatusCode() {
return fakeStatusCode;
}
private void setStatusCode(int statusCode) {
this.fakeStatusCode = statusCode;
}
@Override
public String getHeaderValue(String s) {
return fakeHeaders.getValue(HttpHeaderName.fromString(s));
}
@Override
public HttpHeaders getHeaders() {
return fakeHeaders;
}
private void addHeader(String name, String value) {
fakeHeaders.add(HttpHeaderName.fromString(name), value);
}
@Override
public Flux<ByteBuffer> getBody() {
return null;
}
@Override
public Mono<byte[]> getBodyAsByteArray() {
return null;
}
@Override
public Mono<String> getBodyAsString() {
return null;
}
@Override
public Mono<String> getBodyAsString(Charset charset) {
return null;
}
public static TestHttpResponseBuilder newBuilder() {
return new TestHttpResponseBuilder();
}
public static class TestHttpResponseBuilder {
private final TestHttpResponse res = new TestHttpResponse();
private TestHttpResponseBuilder() {
}
public TestHttpResponseBuilder withStatusCode(int statusCode) {
res.setStatusCode(statusCode);
return this;
}
public TestHttpResponseBuilder addHeader(String name, String value) {
res.addHeader(name, value);
return this;
}
public TestHttpResponse build() {
return res;
}
}
}

Просмотреть файл

@ -105,9 +105,14 @@
<dependency>org.xerial:sqlite-jdbc</dependency>
</ignoredUnusedDeclaredDependencies>
<ignoredUsedUndeclaredDependencies>
<dependency>org.reactivestreams:reactive-streams:jar</dependency>
<dependency>com.microsoft.azure:msal4j:jar</dependency>
<dependency>io.projectreactor:reactor-core:jar</dependency>
</ignoredUsedUndeclaredDependencies>
<ignoreNonCompile>true</ignoreNonCompile>
<ignoredNonTestScopedDependencies>
<dependency>org.reactivestreams:reactive-streams:jar</dependency>
</ignoredNonTestScopedDependencies>
</configuration>
<executions>
<execution>
@ -172,10 +177,6 @@
<groupId>com.azure</groupId>
<artifactId>azure-core</artifactId>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-core-http-netty</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
<groupId>org.slf4j</groupId>
@ -206,7 +207,7 @@
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${httpclient.version}</version>
<version>${apache.httpclient.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
@ -218,12 +219,6 @@
<artifactId>univocity-parsers</artifactId>
<version>${univocity-parsers.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>${httpcore.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
@ -264,11 +259,6 @@
<artifactId>commons-codec</artifactId>
<version>${commons-codec.version}</version>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>${reactor-core.version}</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-retry</artifactId>
@ -279,5 +269,11 @@
<artifactId>vavr</artifactId>
<version>${io.vavr.version}</version>
</dependency>
<dependency>
<groupId>com.microsoft.azure.kusto</groupId>
<artifactId>kusto-data</artifactId>
<version>6.0.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

Просмотреть файл

@ -3,9 +3,9 @@
package com.microsoft.azure.kusto.ingest;
import com.microsoft.azure.kusto.data.HttpClientProperties;
import com.azure.core.http.HttpClient;
import com.microsoft.azure.kusto.data.http.HttpClientProperties;
import com.microsoft.azure.kusto.data.auth.ConnectionStringBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.jetbrains.annotations.Nullable;
import java.net.URISyntaxException;
@ -226,7 +226,7 @@ public class IngestClientFactory {
* @throws URISyntaxException if the connection string is invalid
*/
public static ManagedStreamingIngestClient createManagedStreamingIngestClientFromDmCsb(ConnectionStringBuilder connectionStringBuilder,
@Nullable CloseableHttpClient httpClient, boolean autoCorrectEndpoint)
@Nullable HttpClient httpClient, boolean autoCorrectEndpoint)
throws URISyntaxException {
return new ManagedStreamingIngestClient(connectionStringBuilder, httpClient, autoCorrectEndpoint);
}
@ -241,7 +241,7 @@ public class IngestClientFactory {
* @throws URISyntaxException if the connection string is invalid
*/
public static ManagedStreamingIngestClient createManagedStreamingIngestClientFromDmCsb(ConnectionStringBuilder connectionStringBuilder,
@Nullable CloseableHttpClient httpClient)
@Nullable HttpClient httpClient)
throws URISyntaxException {
return new ManagedStreamingIngestClient(connectionStringBuilder, httpClient, true);
}

Просмотреть файл

@ -6,7 +6,7 @@ import com.azure.storage.blob.BlobClientBuilder;
import com.azure.storage.blob.models.BlobStorageException;
import com.azure.storage.common.policy.RequestRetryOptions;
import com.microsoft.azure.kusto.data.Ensure;
import com.microsoft.azure.kusto.data.HttpClientProperties;
import com.microsoft.azure.kusto.data.http.HttpClientProperties;
import com.microsoft.azure.kusto.data.StreamingClient;
import com.microsoft.azure.kusto.data.auth.ConnectionStringBuilder;
import com.microsoft.azure.kusto.data.exceptions.DataServiceException;
@ -19,7 +19,6 @@ import com.microsoft.azure.kusto.ingest.source.*;
import com.microsoft.azure.kusto.data.ExponentialRetry;
import com.microsoft.azure.kusto.ingest.utils.IngestionUtils;
import org.apache.http.impl.client.CloseableHttpClient;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -51,7 +50,7 @@ public class ManagedStreamingIngestClient extends IngestClientBase implements Qu
final QueuedIngestClientImpl queuedIngestClient;
final StreamingIngestClient streamingIngestClient;
private final ExponentialRetry exponentialRetryTemplate;
private CloseableHttpClient httpClient = null;
private HttpClient httpClient = null;
/**
* @param dmConnectionString dm connection string
@ -179,7 +178,7 @@ public class ManagedStreamingIngestClient extends IngestClientBase implements Qu
* @throws URISyntaxException if the connection string is invalid
*/
public ManagedStreamingIngestClient(ConnectionStringBuilder connectionStringBuilder,
@Nullable CloseableHttpClient httpClient, boolean autoCorrectEndpoint) throws URISyntaxException {
@Nullable HttpClient httpClient, boolean autoCorrectEndpoint) throws URISyntaxException {
log.info("Creating a new ManagedStreamingIngestClient from connection strings");
queuedIngestClient = new QueuedIngestClientImpl(connectionStringBuilder, httpClient, autoCorrectEndpoint);
streamingIngestClient = new StreamingIngestClient(connectionStringBuilder, httpClient, autoCorrectEndpoint);
@ -221,7 +220,7 @@ public class ManagedStreamingIngestClient extends IngestClientBase implements Qu
* @throws URISyntaxException if the connection string is invalid
*/
public ManagedStreamingIngestClient(ConnectionStringBuilder connectionStringBuilder,
@Nullable CloseableHttpClient httpClient) throws URISyntaxException {
@Nullable HttpClient httpClient) throws URISyntaxException {
log.info("Creating a new ManagedStreamingIngestClient from connection strings");
queuedIngestClient = new QueuedIngestClientImpl(connectionStringBuilder, httpClient, true);
streamingIngestClient = new StreamingIngestClient(connectionStringBuilder, httpClient, true);

Просмотреть файл

@ -3,6 +3,7 @@
package com.microsoft.azure.kusto.ingest;
import com.azure.core.http.HttpClient;
import com.azure.data.tables.models.TableEntity;
import com.azure.data.tables.models.TableServiceException;
import com.azure.storage.blob.models.BlobStorageException;
@ -11,7 +12,7 @@ import com.azure.storage.queue.models.QueueStorageException;
import com.microsoft.azure.kusto.data.*;
import com.microsoft.azure.kusto.data.auth.ConnectionStringBuilder;
import com.microsoft.azure.kusto.data.http.HttpClientFactory;
import com.microsoft.azure.kusto.data.HttpClientProperties;
import com.microsoft.azure.kusto.data.http.HttpClientProperties;
import com.microsoft.azure.kusto.ingest.exceptions.IngestionClientException;
import com.microsoft.azure.kusto.ingest.exceptions.IngestionServiceException;
import com.microsoft.azure.kusto.ingest.result.*;
@ -20,7 +21,6 @@ import com.microsoft.azure.kusto.ingest.utils.IngestionUtils;
import com.microsoft.azure.kusto.ingest.utils.SecurityUtils;
import com.microsoft.azure.kusto.ingest.utils.TableWithSas;
import com.univocity.parsers.csv.CsvRoutines;
import org.apache.http.impl.client.CloseableHttpClient;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -51,7 +51,7 @@ public class QueuedIngestClientImpl extends IngestClientBase implements QueuedIn
this(csb, properties == null ? null : HttpClientFactory.create(properties), autoCorrectEndpoint);
}
QueuedIngestClientImpl(ConnectionStringBuilder csb, CloseableHttpClient httpClient, boolean autoCorrectEndpoint) throws URISyntaxException {
QueuedIngestClientImpl(ConnectionStringBuilder csb, HttpClient httpClient, boolean autoCorrectEndpoint) throws URISyntaxException {
log.info("Creating a new IngestClient");
ConnectionStringBuilder csbWithEndpoint = new ConnectionStringBuilder(csb);
csbWithEndpoint.setClusterUrl(autoCorrectEndpoint ? getIngestionEndpoint(csbWithEndpoint.getClusterUrl()) : csbWithEndpoint.getClusterUrl());
@ -146,8 +146,6 @@ public class QueuedIngestClientImpl extends IngestClientBase implements QueuedIn
throw new IngestionServiceException("Failed to ingest from blob", e);
} catch (IOException | URISyntaxException e) {
throw new IngestionClientException("Failed to ingest from blob", e);
} catch (IngestionServiceException e) {
throw e;
}
}
@ -188,8 +186,6 @@ public class QueuedIngestClientImpl extends IngestClientBase implements QueuedIn
throw new IngestionServiceException("Failed to ingest from file", e);
} catch (IOException e) {
throw new IngestionClientException("Failed to ingest from file", e);
} catch (IngestionServiceException e) {
throw e;
}
}
@ -238,8 +234,6 @@ public class QueuedIngestClientImpl extends IngestClientBase implements QueuedIn
throw new IngestionServiceException("Failed to ingest from stream", e);
} catch (IOException e) {
throw new IngestionClientException("Failed to ingest from stream", e);
} catch (IngestionServiceException e) {
throw e;
}
}

Просмотреть файл

@ -4,16 +4,16 @@
package com.microsoft.azure.kusto.ingest;
import com.azure.core.http.HttpClient;
import com.azure.core.http.netty.NettyAsyncHttpClientBuilder;
import com.azure.storage.common.policy.RequestRetryOptions;
import com.microsoft.azure.kusto.data.Client;
import com.microsoft.azure.kusto.data.KustoOperationResult;
import com.microsoft.azure.kusto.data.KustoResultSetTable;
import com.microsoft.azure.kusto.data.Utils;
import com.microsoft.azure.kusto.data.auth.HttpClientWrapper;
import com.microsoft.azure.kusto.data.exceptions.DataClientException;
import com.microsoft.azure.kusto.data.exceptions.DataServiceException;
import com.microsoft.azure.kusto.data.exceptions.ThrottleException;
import com.microsoft.azure.kusto.data.http.HttpClientFactory;
import com.microsoft.azure.kusto.data.http.HttpClientProperties;
import com.microsoft.azure.kusto.data.instrumentation.MonitoredActivity;
import com.microsoft.azure.kusto.data.instrumentation.SupplierTwoExceptions;
import com.microsoft.azure.kusto.ingest.exceptions.IngestionClientException;
@ -27,13 +27,11 @@ import com.microsoft.azure.kusto.ingest.utils.TableWithSas;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import io.vavr.CheckedFunction0;
import org.apache.http.impl.client.CloseableHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.util.annotation.Nullable;
import java.io.Closeable;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.net.URISyntaxException;
import java.time.Duration;
@ -72,12 +70,12 @@ class ResourceManager implements Closeable, IngestionResourceManager {
protected RefreshIngestionAuthTokenTask refreshIngestionAuthTokenTask;
protected RefreshIngestionResourcesTask refreshIngestionResourcesTask;
public ResourceManager(Client client, long defaultRefreshTime, long refreshTimeOnFailure, @Nullable CloseableHttpClient httpClient) {
public ResourceManager(Client client, long defaultRefreshTime, long refreshTimeOnFailure, @Nullable HttpClient httpClient) {
this.client = client;
// Using ctor with client so that the dependency is used
this.httpClient = httpClient == null
? new NettyAsyncHttpClientBuilder().responseTimeout(Duration.ofMinutes(UPLOAD_TIMEOUT_MINUTES)).build()
: new HttpClientWrapper(httpClient);
? HttpClientFactory.create(HttpClientProperties.builder().timeout(Duration.ofMinutes(UPLOAD_TIMEOUT_MINUTES)).build())
: httpClient;
// Refresh tasks
this.refreshTasksTimer = new Timer(true);
@ -89,7 +87,7 @@ class ResourceManager implements Closeable, IngestionResourceManager {
this.storageAccountSet = new RankedStorageAccountSet();
}
public ResourceManager(Client client, @Nullable CloseableHttpClient httpClient) {
public ResourceManager(Client client, @Nullable HttpClient httpClient) {
this(client, REFRESH_INGESTION_RESOURCES_PERIOD, REFRESH_INGESTION_RESOURCES_PERIOD_ON_FAILURE, httpClient);
}
@ -98,11 +96,6 @@ class ResourceManager implements Closeable, IngestionResourceManager {
refreshTasksTimer.cancel();
refreshTasksTimer.purge();
refreshTasksTimer = null;
try {
client.close();
} catch (IOException e) {
log.error("Couldn't close client: " + e.getMessage(), e);
}
}
abstract static class RefreshResourceTask extends TimerTask {
@ -150,7 +143,7 @@ class ResourceManager implements Closeable, IngestionResourceManager {
IngestionResourceSet newIngestionResourceSet = new IngestionResourceSet();
Retry retry = Retry.of("get ingestion resources", taskRetryConfig);
CheckedFunction0<KustoOperationResult> retryExecute = Retry.decorateCheckedSupplier(retry,
() -> client.execute(Commands.INGESTION_RESOURCES_SHOW_COMMAND));
() -> client.executeMgmt(Commands.INGESTION_RESOURCES_SHOW_COMMAND));
KustoOperationResult ingestionResourcesResults = retryExecute.apply();
if (ingestionResourcesResults != null) {
KustoResultSetTable table = ingestionResourcesResults.getPrimaryResults();
@ -246,7 +239,7 @@ class ResourceManager implements Closeable, IngestionResourceManager {
log.info("Refreshing Ingestion Auth Token");
Retry retry = Retry.of("get Ingestion Auth Token resources", taskRetryConfig);
CheckedFunction0<KustoOperationResult> retryExecute = Retry.decorateCheckedSupplier(retry,
() -> client.execute(Commands.IDENTITY_GET_COMMAND));
() -> client.executeMgmt(Commands.IDENTITY_GET_COMMAND));
KustoOperationResult identityTokenResult = retryExecute.apply();
if (identityTokenResult != null
&& identityTokenResult.hasNext()
@ -397,10 +390,10 @@ class ResourceManager implements Closeable, IngestionResourceManager {
@Override
public void reportIngestionResult(ResourceWithSas<?> resource, boolean success) {
if (storageAccountSet == null) {
log.error("StorageAccountSet is null, so can't report ingestion result");
} else {
storageAccountSet.addResultToAccount(resource.getAccountName(), success);
log.warn("StorageAccountSet is null");
return;
}
storageAccountSet.addResultToAccount(resource.getAccountName(), success);
}
enum ResourceType {

Просмотреть файл

@ -3,6 +3,7 @@
package com.microsoft.azure.kusto.ingest;
import com.azure.core.http.HttpClient;
import com.azure.storage.blob.BlobClient;
import com.azure.storage.blob.BlobClientBuilder;
import com.azure.storage.blob.models.BlobStorageException;
@ -10,7 +11,7 @@ import com.microsoft.azure.kusto.data.*;
import com.microsoft.azure.kusto.data.auth.ConnectionStringBuilder;
import com.microsoft.azure.kusto.data.exceptions.DataClientException;
import com.microsoft.azure.kusto.data.exceptions.DataServiceException;
import com.microsoft.azure.kusto.data.HttpClientProperties;
import com.microsoft.azure.kusto.data.http.HttpClientProperties;
import com.microsoft.azure.kusto.data.instrumentation.MonitoredActivity;
import com.microsoft.azure.kusto.data.instrumentation.SupplierTwoExceptions;
import com.microsoft.azure.kusto.ingest.exceptions.IngestionClientException;
@ -26,7 +27,6 @@ import com.microsoft.azure.kusto.ingest.source.StreamSourceInfo;
import com.microsoft.azure.kusto.ingest.utils.IngestionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -52,7 +52,7 @@ public class StreamingIngestClient extends IngestClientBase implements IngestCli
this.connectionDataSource = csbWithEndpoint.getClusterUrl();
}
StreamingIngestClient(ConnectionStringBuilder csb, @Nullable CloseableHttpClient httpClient, boolean autoCorrectEndpoint) throws URISyntaxException {
StreamingIngestClient(ConnectionStringBuilder csb, @Nullable HttpClient httpClient, boolean autoCorrectEndpoint) throws URISyntaxException {
log.info("Creating a new StreamingIngestClient");
ConnectionStringBuilder csbWithEndpoint = new ConnectionStringBuilder(csb);
csbWithEndpoint.setClusterUrl(autoCorrectEndpoint ? getQueryEndpoint(csbWithEndpoint.getClusterUrl()) : csbWithEndpoint.getClusterUrl());

Просмотреть файл

@ -3,6 +3,9 @@
package com.microsoft.azure.kusto.ingest;
import com.azure.core.http.HttpClient;
import com.azure.core.http.HttpResponse;
import com.azure.core.util.Context;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
@ -15,7 +18,8 @@ import com.microsoft.azure.kusto.data.exceptions.DataClientException;
import com.microsoft.azure.kusto.data.exceptions.DataServiceException;
import com.microsoft.azure.kusto.data.format.CslDateTimeFormat;
import com.microsoft.azure.kusto.data.format.CslTimespanFormat;
import com.microsoft.azure.kusto.data.HttpClientProperties;
import com.microsoft.azure.kusto.data.http.HttpClientProperties;
import com.microsoft.azure.kusto.data.http.HttpClientFactory;
import com.microsoft.azure.kusto.ingest.IngestionMapping.IngestionMappingKind;
import com.microsoft.azure.kusto.ingest.IngestionProperties.DataFormat;
import com.microsoft.azure.kusto.ingest.exceptions.IngestionClientException;
@ -28,9 +32,8 @@ import com.microsoft.azure.kusto.ingest.source.StreamSourceInfo;
import com.microsoft.azure.kusto.ingest.utils.SecurityUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.apache.http.conn.util.InetAddressUtils;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
@ -38,7 +41,6 @@ import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Paths;
@ -57,20 +59,35 @@ import java.util.concurrent.ThreadLocalRandom;
import static com.microsoft.azure.kusto.ingest.IngestClientBase.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeast;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class E2ETest {
private static IngestClient ingestClient;
private static StreamingIngestClient streamingIngestClient;
private static ManagedStreamingIngestClient managedStreamingIngestClient;
private static Client queryClient;
private static Client dmCslClient;
private static StreamingClient streamingClient;
private static final String databaseName = System.getenv("TEST_DATABASE");
private static final String appId = System.getenv("APP_ID");
private static final String appKey = System.getenv("APP_KEY");
private static final String tenantId = System.getenv().getOrDefault("TENANT_ID", "microsoft.com");
private static ConnectionStringBuilder engineCsb;
// IN ORDER TO RUN THESE E2E TESTS YOU NEED THESE ENVIRONMENT VARIABLES
// Your test database created in the Azure Portal or via IaC
private static final String DB_NAME = System.getenv("TEST_DATABASE");
private static final String APP_ID = System.getenv("APP_ID");
private static final String APP_KEY = System.getenv("APP_KEY");
private static final String TENANT_ID = System.getenv().getOrDefault("TENANT_ID", "microsoft.com");
private static final String DM_CONN_STR = System.getenv("DM_CONNECTION_STRING");
private static final String ENG_CONN_STR = System.getenv("ENGINE_CONNECTION_STRING");
private static final String UNAME_HINT = System.getenv("USERNAME_HINT");
private static final String TOKEN = System.getenv("TOKEN");
private static final String PUBLIC_X509CER_FILE_LOC = System.getenv("PUBLIC_X509CER_FILE_LOC");
private static final String PRIVATE_PKCS8_FILE_LOC = System.getenv("PRIVATE_PKCS8_FILE_LOC");
private static final String CI_EXECUTION = System.getenv("CI_EXECUTION");
private static String principalFqn;
private static String resourcesPath;
private static int currentCount = 0;
@ -84,9 +101,9 @@ class E2ETest {
public static void setUp() {
tableName = "JavaTest_" + new SimpleDateFormat("yyyy_MM_dd_hh_mm_ss_SSS").format(Calendar.getInstance().getTime()) + "_"
+ ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE);
principalFqn = String.format("aadapp=%s;%s", appId, tenantId);
principalFqn = String.format("aadapp=%s;%s", APP_ID, TENANT_ID);
ConnectionStringBuilder dmCsb = createConnection(System.getenv("DM_CONNECTION_STRING"));
ConnectionStringBuilder dmCsb = createConnection(DM_CONN_STR);
dmCsb.setUserNameForTracing("testUser");
try {
dmCslClient = ClientFactory.createClient(dmCsb);
@ -98,10 +115,11 @@ class E2ETest {
.maxConnectionsTotal(50)
.build());
} catch (URISyntaxException ex) {
Assertions.fail("Failed to create ingest client", ex);
}
engineCsb = createConnection(System.getenv("ENGINE_CONNECTION_STRING"));
ConnectionStringBuilder engineCsb = createConnection(ENG_CONN_STR);
engineCsb.setUserNameForTracing("Java_E2ETest_ø");
try {
streamingIngestClient = IngestClientFactory.createStreamingIngestClient(engineCsb);
@ -117,18 +135,16 @@ class E2ETest {
}
private static @NotNull ConnectionStringBuilder createConnection(String connectionString) {
if (appKey == null || appKey.isEmpty()) {
if (APP_KEY == null || APP_KEY.isEmpty()) {
return ConnectionStringBuilder.createWithAzureCli(connectionString);
}
return ConnectionStringBuilder.createWithAadApplicationCredentials(connectionString, appId, appKey,
tenantId);
return ConnectionStringBuilder.createWithAadApplicationCredentials(connectionString, APP_ID, APP_KEY, TENANT_ID);
}
@AfterAll
public static void tearDown() {
try {
queryClient.executeToJsonResult(databaseName, String.format(".drop table %s ifexists skip-seal", tableName));
queryClient.executeToJsonResult(DB_NAME, String.format(".drop table %s ifexists skip-seal", tableName));
ingestClient.close();
managedStreamingIngestClient.close();
} catch (Exception ex) {
@ -137,18 +153,17 @@ class E2ETest {
}
private static boolean isManualExecution() {
return false;
// return System.getenv("CI_EXECUTION") == null || !System.getenv("CI_EXECUTION").equals("1");
return CI_EXECUTION == null || !CI_EXECUTION.equals("1");
}
private static void createTableAndMapping() {
try {
queryClient.executeToJsonResult(databaseName, String.format(".drop table %s ifexists", tableName));
queryClient.executeToJsonResult(databaseName, String.format(".create table %s %s", tableName, tableColumns));
queryClient.executeToJsonResult(DB_NAME, String.format(".drop table %s ifexists", tableName));
queryClient.executeToJsonResult(DB_NAME, String.format(".create table %s %s", tableName, tableColumns));
LocalDateTime time = LocalDateTime.ofInstant(Instant.now(), ZoneId.of("UTC")).plusDays(1);
String expiryDate = DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(time);
String autoDeletePolicy = "@'{ \"ExpiryDate\" : \"" + expiryDate + "\", \"DeleteIfNotEmpty\": true }'";
queryClient.executeToJsonResult(databaseName, String.format(".alter table %s policy auto_delete %s", tableName, autoDeletePolicy));
queryClient.executeToJsonResult(DB_NAME, String.format(".alter table %s policy auto_delete %s", tableName, autoDeletePolicy));
} catch (Exception ex) {
Assertions.fail("Failed to drop and create new table", ex);
}
@ -156,35 +171,35 @@ class E2ETest {
resourcesPath = Paths.get(System.getProperty("user.dir"), "src", "test", "resources").toString();
try {
String mappingAsString = new String(Files.readAllBytes(Paths.get(resourcesPath, "dataset_mapping.json")));
queryClient.executeToJsonResult(databaseName, String.format(".create table %s ingestion json mapping '%s' '%s'",
queryClient.executeToJsonResult(DB_NAME, String.format(".create table %s ingestion json mapping '%s' '%s'",
tableName, mappingReference, mappingAsString));
} catch (Exception ex) {
Assertions.fail("Failed to create ingestion mapping", ex);
}
try {
queryClient.executeToJsonResult(databaseName, ".clear database cache streamingingestion schema");
queryClient.executeToJsonResult(DB_NAME, ".clear database cache streamingingestion schema");
} catch (Exception ex) {
Assertions.fail("Failed to refresh cache", ex);
}
}
private static void createTestData() {
IngestionProperties ingestionPropertiesWithoutMapping = new IngestionProperties(databaseName, tableName);
IngestionProperties ingestionPropertiesWithoutMapping = new IngestionProperties(DB_NAME, tableName);
ingestionPropertiesWithoutMapping.setFlushImmediately(true);
ingestionPropertiesWithoutMapping.setDataFormat(DataFormat.CSV);
IngestionProperties ingestionPropertiesWithIgnoreFirstRecord = new IngestionProperties(databaseName, tableName);
IngestionProperties ingestionPropertiesWithIgnoreFirstRecord = new IngestionProperties(DB_NAME, tableName);
ingestionPropertiesWithIgnoreFirstRecord.setFlushImmediately(true);
ingestionPropertiesWithIgnoreFirstRecord.setDataFormat(DataFormat.CSV);
ingestionPropertiesWithIgnoreFirstRecord.setIgnoreFirstRecord(true);
IngestionProperties ingestionPropertiesWithMappingReference = new IngestionProperties(databaseName, tableName);
IngestionProperties ingestionPropertiesWithMappingReference = new IngestionProperties(DB_NAME, tableName);
ingestionPropertiesWithMappingReference.setFlushImmediately(true);
ingestionPropertiesWithMappingReference.setIngestionMapping(mappingReference, IngestionMappingKind.JSON);
ingestionPropertiesWithMappingReference.setDataFormat(DataFormat.JSON);
IngestionProperties ingestionPropertiesWithColumnMapping = new IngestionProperties(databaseName, tableName);
IngestionProperties ingestionPropertiesWithColumnMapping = new IngestionProperties(DB_NAME, tableName);
ingestionPropertiesWithColumnMapping.setFlushImmediately(true);
ingestionPropertiesWithColumnMapping.setDataFormat(DataFormat.JSON);
ColumnMapping first = new ColumnMapping("rownumber", "int");
@ -195,7 +210,7 @@ class E2ETest {
ingestionPropertiesWithColumnMapping.setIngestionMapping(columnMapping, IngestionMappingKind.JSON);
ingestionPropertiesWithColumnMapping.setDataFormat(DataFormat.JSON);
IngestionProperties ingestionPropertiesWithTableReportMethod = new IngestionProperties(databaseName, tableName);
IngestionProperties ingestionPropertiesWithTableReportMethod = new IngestionProperties(DB_NAME, tableName);
ingestionPropertiesWithTableReportMethod.setFlushImmediately(true);
ingestionPropertiesWithTableReportMethod.setDataFormat(DataFormat.CSV);
ingestionPropertiesWithTableReportMethod.setReportMethod(IngestionProperties.IngestionReportMethod.TABLE);
@ -267,7 +282,7 @@ class E2ETest {
Thread.sleep(i * 100);
if (checkViaJson) {
String result = queryClient.executeToJsonResult(databaseName, String.format("%s | count", tableName));
String result = queryClient.executeToJsonResult(DB_NAME, String.format("%s | count", tableName));
JsonNode jsonNode = objectMapper.readTree(result);
ArrayNode jsonArray = jsonNode.isArray() ? (ArrayNode) jsonNode : null;
JsonNode primaryResult = null;
@ -281,7 +296,7 @@ class E2ETest {
Assertions.assertNotNull(primaryResult, "Primary result cant be null since we need the row count");
actualRowsCount = (primaryResult.get("Rows")).get(0).get(0).asInt() - currentCount;
} else {
KustoOperationResult result = queryClient.execute(databaseName, String.format("%s | count", tableName));
KustoOperationResult result = queryClient.executeQuery(DB_NAME, String.format("%s | count", tableName));
KustoResultSetTable mainTableResult = result.getPrimaryResults();
mainTableResult.next();
actualRowsCount = mainTableResult.getInt(0) - currentCount;
@ -307,13 +322,16 @@ class E2ETest {
private boolean isDatabasePrincipal(Client localQueryClient) {
KustoOperationResult result = null;
boolean found = false;
try {
result = localQueryClient.execute(databaseName, String.format(".show database %s principals", databaseName));
// result = localQueryClient.execute(databaseName, String.format(".show version"));
result = localQueryClient.executeMgmt(DB_NAME, String.format(".show database %s principals", DB_NAME));
} catch (Exception ex) {
Assertions.fail("Failed to execute show database principals command", ex);
Assertions.fail("Failed to execute show database principals command.", ex);
}
return resultContainsPrincipal(result);
}
private boolean resultContainsPrincipal(KustoOperationResult result) {
boolean found = false;
KustoResultSetTable mainTableResultSet = result.getPrimaryResults();
while (mainTableResultSet.next()) {
if (mainTableResultSet.getString("PrincipalFQN").equals(principalFqn)) {
@ -439,72 +457,68 @@ class E2ETest {
@Test
void testCreateWithUserPrompt() {
Assumptions.assumeTrue(isManualExecution());
ConnectionStringBuilder engineCsb = ConnectionStringBuilder.createWithUserPrompt(System.getenv("ENGINE_CONNECTION_STRING"), null,
System.getenv("USERNAME_HINT"));
assertTrue(canAuthenticate(engineCsb));
ConnectionStringBuilder engineCsb = ConnectionStringBuilder.createWithUserPrompt(ENG_CONN_STR, null, UNAME_HINT);
assertTrue(canAuthenticate(engineCsb), "Is USERNAME_HINT set to your email in your environment variables?");
}
@Test
void testCreateWithConnectionStringAndUserPrompt() {
Assumptions.assumeTrue(isManualExecution());
ConnectionStringBuilder engineCsb = new ConnectionStringBuilder(
"Data Source=" + System.getenv("ENGINE_CONNECTION_STRING") + ";User ID=" + System.getenv("USERNAME_HINT"));
assertTrue(canAuthenticate(engineCsb));
ConnectionStringBuilder engineCsb = new ConnectionStringBuilder("Data Source=" + ENG_CONN_STR + ";User ID=" + UNAME_HINT);
assertTrue(canAuthenticate(engineCsb), "Is USERNAME_HINT set to your email in your environment variables?");
}
@Test
void testCreateWithDeviceAuthentication() {
Assumptions.assumeTrue(isManualExecution());
ConnectionStringBuilder engineCsb = ConnectionStringBuilder.createWithDeviceCode(System.getenv("ENGINE_CONNECTION_STRING"), null);
ConnectionStringBuilder engineCsb = ConnectionStringBuilder.createWithDeviceCode(ENG_CONN_STR, null);
assertTrue(canAuthenticate(engineCsb));
}
@Test
void testCreateWithAadApplicationCertificate() throws GeneralSecurityException, IOException {
Assumptions.assumeTrue(StringUtils.isNotBlank(System.getenv("PUBLIC_X509CER_FILE_LOC")));
Assumptions.assumeTrue(StringUtils.isNotBlank(System.getenv("PRIVATE_PKCS8_FILE_LOC")));
X509Certificate cer = SecurityUtils.getPublicCertificate(System.getenv("PUBLIC_X509CER_FILE_LOC"));
PrivateKey privateKey = SecurityUtils.getPrivateKey(System.getenv("PRIVATE_PKCS8_FILE_LOC"));
ConnectionStringBuilder engineCsb = ConnectionStringBuilder.createWithAadApplicationCertificate(System.getenv("ENGINE_CONNECTION_STRING"), appId, cer,
privateKey, "microsoft.onmicrosoft.com");
Assumptions.assumeTrue(StringUtils.isNotBlank(PUBLIC_X509CER_FILE_LOC));
Assumptions.assumeTrue(StringUtils.isNotBlank(PRIVATE_PKCS8_FILE_LOC));
X509Certificate cer = SecurityUtils.getPublicCertificate(PUBLIC_X509CER_FILE_LOC);
PrivateKey privateKey = SecurityUtils.getPrivateKey(PRIVATE_PKCS8_FILE_LOC);
ConnectionStringBuilder engineCsb = ConnectionStringBuilder.createWithAadApplicationCertificate(
ENG_CONN_STR, APP_ID, cer, privateKey, "microsoft.onmicrosoft.com");
assertTrue(canAuthenticate(engineCsb));
}
@Test
void testCreateWithAadApplicationCredentials() {
Assumptions.assumeTrue(appKey != null);
ConnectionStringBuilder engineCsb = createConnection(System.getenv("ENGINE_CONNECTION_STRING"));
Assumptions.assumeTrue(APP_KEY != null);
ConnectionStringBuilder engineCsb = createConnection(ENG_CONN_STR);
assertTrue(canAuthenticate(engineCsb));
}
@Test
void testCreateWithConnectionStringAndAadApplicationCredentials() {
Assumptions.assumeTrue(appKey != null);
Assumptions.assumeTrue(APP_KEY != null);
ConnectionStringBuilder engineCsb = new ConnectionStringBuilder(
"Data Source=" + System.getenv("ENGINE_CONNECTION_STRING") + ";AppClientId=" + appId + ";AppKey=" + appKey + ";Authority ID=" + tenantId);
"Data Source=" + ENG_CONN_STR + ";AppClientId=" + APP_ID + ";AppKey=" + APP_KEY + ";Authority ID=" + TENANT_ID);
assertTrue(canAuthenticate(engineCsb));
}
@Test
void testCreateWithAadAccessTokenAuthentication() {
Assumptions.assumeTrue(StringUtils.isNotBlank(System.getenv("TOKEN")));
String token = System.getenv("TOKEN");
ConnectionStringBuilder engineCsb = ConnectionStringBuilder.createWithAadAccessTokenAuthentication(System.getenv("ENGINE_CONNECTION_STRING"), token);
Assumptions.assumeTrue(StringUtils.isNotBlank(TOKEN));
ConnectionStringBuilder engineCsb = ConnectionStringBuilder.createWithAadAccessTokenAuthentication(ENG_CONN_STR, TOKEN);
assertTrue(canAuthenticate(engineCsb));
}
@Test
void testCreateWithAadTokenProviderAuthentication() {
Assumptions.assumeTrue(StringUtils.isNotBlank(System.getenv("TOKEN")));
Callable<String> tokenProviderCallable = () -> System.getenv("TOKEN");
ConnectionStringBuilder engineCsb = ConnectionStringBuilder.createWithAadTokenProviderAuthentication(System.getenv("ENGINE_CONNECTION_STRING"),
tokenProviderCallable);
Assumptions.assumeTrue(StringUtils.isNotBlank(TOKEN));
Callable<String> tokenProviderCallable = () -> TOKEN;
ConnectionStringBuilder engineCsb = ConnectionStringBuilder.createWithAadTokenProviderAuthentication(ENG_CONN_STR, tokenProviderCallable);
assertTrue(canAuthenticate(engineCsb));
}
@Test
void testCloudInfoWithCluster() throws DataServiceException {
String clusterUrl = System.getenv("ENGINE_CONNECTION_STRING");
String clusterUrl = ENG_CONN_STR;
CloudInfo cloudInfo = CloudInfo.retrieveCloudInfoForCluster(clusterUrl);
assertNotSame(CloudInfo.DEFAULT_CLOUD, cloudInfo);
assertNotNull(cloudInfo);
@ -519,7 +533,7 @@ class E2ETest {
@Test
void testParameterizedQuery() throws DataServiceException, DataClientException {
IngestionProperties ingestionPropertiesWithoutMapping = new IngestionProperties(databaseName, tableName);
IngestionProperties ingestionPropertiesWithoutMapping = new IngestionProperties(DB_NAME, tableName);
ingestionPropertiesWithoutMapping.setFlushImmediately(true);
ingestionPropertiesWithoutMapping.setDataFormat(DataFormat.CSV);
@ -551,7 +565,7 @@ class E2ETest {
String query = String.format(
"declare query_parameters(xdoubleParam:real, xboolParam:bool, xint16Param:int, xint64Param:long, xdateParam:datetime, xtextParam:string, xtimeParam:time); %s | where xdouble == xdoubleParam and xbool == xboolParam and xint16 == xint16Param and xint64 == xint64Param and xdate == xdateParam and xtext == xtextParam and xtime == xtimeParam",
tableName);
KustoOperationResult resultObj = queryClient.execute(databaseName, query, crp);
KustoOperationResult resultObj = queryClient.executeQuery(DB_NAME, query, crp);
KustoResultSetTable mainTableResult = resultObj.getPrimaryResults();
mainTableResult.next();
String results = mainTableResult.getString(13);
@ -566,7 +580,7 @@ class E2ETest {
// Standard approach - API converts json to KustoOperationResult
stopWatch.start();
KustoOperationResult resultObj = queryClient.execute(databaseName, query, clientRequestProperties);
KustoOperationResult resultObj = queryClient.executeQuery(DB_NAME, query, clientRequestProperties);
stopWatch.stop();
long timeConvertedToJavaObj = stopWatch.getTime();
System.out.printf("Convert json to KustoOperationResult result count='%s' returned in '%s'ms%n", resultObj.getPrimaryResults().count(),
@ -575,7 +589,7 @@ class E2ETest {
// Specialized use case - API returns raw json for performance
stopWatch.reset();
stopWatch.start();
String jsonResult = queryClient.executeToJsonResult(databaseName, query, clientRequestProperties);
String jsonResult = queryClient.executeToJsonResult(DB_NAME, query, clientRequestProperties);
stopWatch.stop();
long timeRawJson = stopWatch.getTime();
System.out.printf("Raw json result size='%s' returned in '%s'ms%n", jsonResult.length(), timeRawJson);
@ -587,7 +601,7 @@ class E2ETest {
stopWatch.reset();
stopWatch.start();
// The InputStream *must* be closed by the caller to prevent memory leaks
try (InputStream is = streamingClient.executeStreamingQuery(databaseName, query, clientRequestProperties);
try (InputStream is = streamingClient.executeStreamingQuery(DB_NAME, query, clientRequestProperties);
BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
StringBuilder streamedResult = new StringBuilder();
char[] buffer = new char[65536];
@ -612,19 +626,21 @@ class E2ETest {
}
@Test
void testSameHttpClientInstance() throws DataClientException, DataServiceException, URISyntaxException, IOException {
ConnectionStringBuilder engineCsb = createConnection(System.getenv("ENGINE_CONNECTION_STRING"));
CloseableHttpClient httpClient = HttpClientBuilder.create().build();
CloseableHttpClient httpClientSpy = Mockito.spy(httpClient);
void testSameHttpClientInstance() throws DataClientException, DataServiceException, URISyntaxException {
ConnectionStringBuilder engineCsb = createConnection(ENG_CONN_STR);
HttpClient httpClient = HttpClientFactory.create(null);
HttpClient httpClientSpy = Mockito.spy(httpClient);
Client clientImpl = ClientFactory.createClient(engineCsb, httpClientSpy);
ClientRequestProperties clientRequestProperties = new ClientRequestProperties();
String query = tableName + " | take 1000";
clientImpl.execute(databaseName, query, clientRequestProperties);
clientImpl.execute(databaseName, query, clientRequestProperties);
clientImpl.executeQuery(DB_NAME, query, clientRequestProperties);
clientImpl.executeQuery(DB_NAME, query, clientRequestProperties);
try (HttpResponse httpResponse = Mockito.verify(httpClientSpy, atLeast(2)).sendSync(any(), eq(Context.NONE))) {
}
Mockito.verify(httpClientSpy, atLeast(2)).execute(any());
}
@Test
@ -632,10 +648,11 @@ class E2ETest {
KustoTrustedEndpoints.addTrustedHosts(Collections.singletonList(new MatchRule("statusreturner.azurewebsites.net", false)), false);
List<Integer> redirectCodes = Arrays.asList(301, 302, 307, 308);
redirectCodes.parallelStream().map(code -> {
try (Client client = ClientFactory.createClient(
ConnectionStringBuilder.createWithAadAccessTokenAuthentication("https://statusreturner.azurewebsites.net/nocloud/" + code, "token"))) {
try {
Client client = ClientFactory.createClient(
ConnectionStringBuilder.createWithAadAccessTokenAuthentication("https://statusreturner.azurewebsites.net/nocloud/" + code, "token"));
try {
client.execute("db", "table");
client.executeQuery("db", "table");
Assertions.fail("Expected exception");
} catch (DataServiceException e) {
Assertions.assertTrue(e.getMessage().contains("" + code));
@ -657,17 +674,20 @@ class E2ETest {
KustoTrustedEndpoints.addTrustedHosts(Collections.singletonList(new MatchRule("statusreturner.azurewebsites.net", false)), false);
List<Integer> redirectCodes = Arrays.asList(301, 302, 307, 308);
redirectCodes.parallelStream().map(code -> {
try (Client client = ClientFactory.createClient(
ConnectionStringBuilder.createWithAadAccessTokenAuthentication("https://statusreturner.azurewebsites.net/" + code, "token"))) {
try {
Client client = ClientFactory.createClient(
ConnectionStringBuilder.createWithAadAccessTokenAuthentication("https://statusreturner.azurewebsites.net/" + code, "token"));
try {
client.execute("db", "table");
client.executeQuery("db", "table");
Assertions.fail("Expected exception");
} catch (DataServiceException e) {
Assertions.assertTrue(e.getMessage().contains("" + code));
Assertions.assertFalse(e.getMessage().contains("metadata"));
} catch (Exception e) {
return e;
}
} catch (Exception e) {
return e;
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
return null;
}).forEach(e -> {
@ -704,22 +724,4 @@ class E2ETest {
}
}
}
@Test
void testProxyPlanner() throws URISyntaxException {
String[] excludedPrefixes = new String[] {
new URI(engineCsb.getClusterUrl()).getHost(),
"login.microsoftonline.com"
};
HttpClientProperties providedProperties = HttpClientProperties.builder()
.routePlanner(new SimpleProxyPlanner("localhost", 8080, "http", excludedPrefixes))
.build();
try (Client client = ClientFactory.createClient(engineCsb, providedProperties)) {
KustoOperationResult execute = client.execute(".show version");
} catch (Exception e) {
Assertions.fail(e);
}
}
}

Просмотреть файл

@ -4,7 +4,6 @@
package com.microsoft.azure.kusto.ingest;
import com.azure.data.tables.models.TableEntity;
import com.microsoft.azure.kusto.data.HttpClientProperties;
import com.microsoft.azure.kusto.data.auth.ConnectionStringBuilder;
import com.microsoft.azure.kusto.ingest.IngestionProperties.DataFormat;
import com.microsoft.azure.kusto.ingest.exceptions.IngestionClientException;
@ -64,7 +63,7 @@ class QueuedIngestClientTest {
}
@BeforeEach
void setUpEach() throws IngestionServiceException, IngestionClientException {
void setUpEach() throws IngestionServiceException {
doReturn(Collections.singletonList(TestUtils.containerWithSasFromContainerName("storage")),
Collections.singletonList(TestUtils.containerWithSasFromContainerName("storage2"))).when(resourceManagerMock)
.getShuffledContainers();
@ -378,6 +377,6 @@ class QueuedIngestClientTest {
}
private String getSampleResultSetDump() {
return System.getProperty("line.separator").equals("\n") ? "1,leo\n2,yui\n" : "1,leo\r\n2,yui\r\n";
return System.lineSeparator().equals("\n") ? "1,leo\n2,yui\n" : "1,leo\r\n2,yui\r\n";
}
}

Просмотреть файл

@ -47,10 +47,10 @@ class ResourceManagerTest {
@BeforeAll
static void setUp() throws DataClientException, DataServiceException {
// Using answer so that we get a new result set with reset iterator
when(clientMock.execute(Commands.INGESTION_RESOURCES_SHOW_COMMAND))
when(clientMock.executeMgmt(Commands.INGESTION_RESOURCES_SHOW_COMMAND))
.thenAnswer(invocationOnMock -> generateIngestionResourcesResult());
when(clientMock.execute(Commands.IDENTITY_GET_COMMAND))
when(clientMock.executeMgmt(Commands.IDENTITY_GET_COMMAND))
.thenAnswer(invocationOnMock -> generateIngestionAuthTokenResult());
setUpStorageResources(0);
@ -195,10 +195,10 @@ class ResourceManagerTest {
throws IngestionServiceException, DataServiceException, DataClientException {
long waitTime = 1000;
Client clientMockLocal = mock(Client.class);
when(clientMockLocal.execute(Commands.INGESTION_RESOURCES_SHOW_COMMAND))
when(clientMockLocal.executeMgmt(Commands.INGESTION_RESOURCES_SHOW_COMMAND))
.thenAnswer(invocationOnMock -> generateIngestionResourcesResult());
when(clientMockLocal.execute(Commands.IDENTITY_GET_COMMAND))
when(clientMockLocal.executeMgmt(Commands.IDENTITY_GET_COMMAND))
.thenAnswer(invocationOnMock -> generateIngestionAuthTokenResult());
ResourceManager resourceManagerWithLowRefresh = new ResourceManager(clientMockLocal, waitTime, waitTime, null);
@ -225,7 +225,7 @@ class ResourceManagerTest {
public boolean shouldFail;
}
final Fail fail = new Fail();
when(clientMockFail.execute(Commands.INGESTION_RESOURCES_SHOW_COMMAND))
when(clientMockFail.executeMgmt(Commands.INGESTION_RESOURCES_SHOW_COMMAND))
.thenAnswer(invocationOnMock -> {
if (!fail.shouldFail) {
return generateIngestionResourcesResult();

Просмотреть файл

@ -1,11 +1,11 @@
package com.microsoft.azure.kusto.ingest;
import com.microsoft.azure.kusto.data.BaseClient;
import com.microsoft.azure.kusto.data.Client;
import com.microsoft.azure.kusto.data.KustoOperationResult;
import com.microsoft.azure.kusto.data.exceptions.DataClientException;
import com.microsoft.azure.kusto.data.exceptions.DataServiceException;
import com.microsoft.azure.kusto.data.exceptions.KustoServiceQueryError;
import com.microsoft.azure.kusto.data.http.HttpPostUtils;
import org.junit.jupiter.api.Test;
import org.mockito.stubbing.Answer;
@ -13,6 +13,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.microsoft.azure.kusto.ingest.ResourceManagerTest.generateIngestionAuthTokenResult;
import static com.microsoft.azure.kusto.ingest.ResourceManagerTest.generateIngestionResourcesResult;
@ -21,19 +22,17 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class ResourceManagerTimerTest {
@Test
void timerTest() throws DataClientException, DataServiceException, InterruptedException, KustoServiceQueryError, IOException {
Client mockedClient = mock(Client.class);
final List<Date> refreshTimestamps = new ArrayList<>();
class BooleanHolder {
boolean gotHere = false;
}
BooleanHolder booleanHolder = new BooleanHolder();
when(mockedClient.execute(Commands.IDENTITY_GET_COMMAND))
AtomicBoolean gotHere = new AtomicBoolean(false);
when(mockedClient.executeMgmt(Commands.IDENTITY_GET_COMMAND))
.thenReturn(generateIngestionAuthTokenResult());
when(mockedClient.execute(Commands.INGESTION_RESOURCES_SHOW_COMMAND)).then((Answer<KustoOperationResult>) invocationOnMock -> {
when(mockedClient.executeMgmt(Commands.INGESTION_RESOURCES_SHOW_COMMAND)).then((Answer<KustoOperationResult>) invocationOnMock -> {
refreshTimestamps.add((new Date()));
booleanHolder.gotHere = true;
gotHere.set(true);
if (refreshTimestamps.size() == 2) {
throw new Exception();
}
@ -46,7 +45,7 @@ class ResourceManagerTimerTest {
assertTrue(resourceManager.refreshIngestionAuthTokenTask.refreshedAtLeastOnce.isEmpty());
int runtime = 0;
while (!booleanHolder.gotHere && runtime < 5000) {
while (!gotHere.get() && runtime < 5000) {
Thread.sleep(100);
runtime += 100;
}
@ -65,21 +64,19 @@ class ResourceManagerTimerTest {
void timerTestFailureGettingResources() throws DataClientException, DataServiceException, InterruptedException {
Client mockedClient = mock(Client.class);
final List<Date> refreshTimestamps = new ArrayList<>();
class BooleanHolder {
boolean gotHere = false;
}
BooleanHolder booleanHolder = new BooleanHolder();
when(mockedClient.execute(Commands.IDENTITY_GET_COMMAND))
.thenThrow(HttpPostUtils.createExceptionFromResponse("https://sample.kusto.windows.net", null, new Exception(), "error"));
when(mockedClient.execute(Commands.INGESTION_RESOURCES_SHOW_COMMAND)).then((Answer<KustoOperationResult>) invocationOnMock -> {
refreshTimestamps.add((new Date()));
booleanHolder.gotHere = true;
throw HttpPostUtils.createExceptionFromResponse("https://sample.kusto.windows.net", null, new Exception(), "error");
});
AtomicBoolean gotHere = new AtomicBoolean(false);
when(mockedClient.executeMgmt(Commands.IDENTITY_GET_COMMAND))
.thenThrow(BaseClient.createExceptionFromResponse("https://sample.kusto.windows.net", null, new Exception(), "error"));
when(mockedClient.executeMgmt(Commands.INGESTION_RESOURCES_SHOW_COMMAND))
.then((Answer<KustoOperationResult>) invocationOnMock -> {
refreshTimestamps.add((new Date()));
gotHere.set(true);
throw BaseClient.createExceptionFromResponse("https://sample.kusto.windows.net", null, new Exception(), "error");
});
ResourceManager resourceManager = new ResourceManager(mockedClient, 1000L, 500L, null);
int runtime = 0;
while (!booleanHolder.gotHere && runtime < 5000) {
while (!gotHere.get() && runtime < 5000) {
Thread.sleep(100);
runtime += 100;
}

Просмотреть файл

@ -1,36 +0,0 @@
package com.microsoft.azure.kusto.ingest;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.impl.conn.DefaultRoutePlanner;
import org.apache.http.impl.conn.DefaultSchemePortResolver;
import org.apache.http.protocol.HttpContext;
// Simple implementation of DefaultRoutePlanner that checks if the target url hostname is prefixed by given value
public class SimpleProxyPlanner extends DefaultRoutePlanner {
private HttpHost proxy;
private String[] nonProxyHostsPrefixes;
public SimpleProxyPlanner(String proxyHost, int proxyPort, String scheme, String[] nonProxyHostsPrefixes) {
super(DefaultSchemePortResolver.INSTANCE);
proxy = new HttpHost(proxyHost, proxyPort, scheme);
this.nonProxyHostsPrefixes = nonProxyHostsPrefixes;
}
@Override
protected HttpHost determineProxy(
final HttpHost target,
final HttpRequest request,
final HttpContext context) {
if (nonProxyHostsPrefixes != null) {
for (String nonProxyHostsPrefix : nonProxyHostsPrefixes) {
if (target.getHostName().startsWith(nonProxyHostsPrefix)) {
return null;
}
}
}
return proxy;
}
}

10
pom.xml
Просмотреть файл

@ -32,7 +32,7 @@
</developers>
<properties>
<revision>5.2.0</revision> <!-- CHANGE THIS to adjust project version-->
<revision>6.0.0</revision> <!-- CHANGE THIS to adjust project version-->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<azure-bom-version>1.2.25</azure-bom-version>
@ -43,20 +43,18 @@
<slf4j.version>1.7.36</slf4j.version>
<commons-lang3.version>3.14.0</commons-lang3.version>
<commons-text.version>1.11.0</commons-text.version>
<commons-codec.version>1.16.1</commons-codec.version>
<httpclient.version>4.5.14</httpclient.version>
<commons-codec.version>1.17.0</commons-codec.version>
<apache.httpclient.version>4.5.14</apache.httpclient.version>
<httpcore.version>4.4.16</httpcore.version>
<fasterxml.jackson.core.version>2.16.0</fasterxml.jackson.core.version>
<reactor-core.version>3.4.34</reactor-core.version>
<univocity-parsers.version>2.9.1</univocity-parsers.version>
<resilience4j.version>1.7.1</resilience4j.version>
<io.vavr.version>0.10.4</io.vavr.version>
<!-- Test dependencies -->
<bouncycastle.version>1.77</bouncycastle.version>
<jsonassert.version>1.5.0</jsonassert.version>
<sqlite-jdbc.version>3.45.2.0</sqlite-jdbc.version>
<sqlite-jdbc.version>3.45.3.0</sqlite-jdbc.version>
<annotations.version>24.1.0</annotations.version>
<!-- Other dependencies -->
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
<maven-assembly-plugin.version>3.7.1</maven-assembly-plugin.version>

Просмотреть файл

@ -33,7 +33,7 @@
</developers>
<properties>
<revision>5.0.3</revision> <!-- CHANGE THIS to match project version in the root (not technically parent) pom -->
<revision>6.0.0</revision> <!-- CHANGE THIS to match project version in the root (not technically parent) pom -->
<java.version>1.8</java.version>
<maven-jar-plugin.version>3.2.0</maven-jar-plugin.version>
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>

Просмотреть файл

@ -156,7 +156,7 @@ public class Utils {
KustoOperationResult result;
try {
result = kustoClient.execute(databaseName, command, clientRequestProperties);
result = kustoClient.executeQuery(databaseName, command, clientRequestProperties);
System.out.printf("Response from executed command '%s':%n", command);
KustoResultSetTable primaryResults = result.getPrimaryResults();

Просмотреть файл

@ -12,7 +12,6 @@
<version>${revision}</version>
</parent>
<build>
<plugins>
<plugin>

Просмотреть файл

@ -4,7 +4,7 @@
import com.microsoft.azure.kusto.data.ClientFactory;
import com.microsoft.azure.kusto.data.Client;
import com.microsoft.azure.kusto.data.ClientRequestProperties;
import com.microsoft.azure.kusto.data.HttpClientProperties;
import com.microsoft.azure.kusto.data.http.HttpClientProperties;
import com.microsoft.azure.kusto.data.KustoOperationResult;
import com.microsoft.azure.kusto.data.KustoResultSetTable;
import com.microsoft.azure.kusto.data.auth.ConnectionStringBuilder;
@ -40,7 +40,7 @@ public class AdvancedQuery {
"range x from 1 to 100 step 1",
"| extend ts = totimespan(strcat(x,'.00:00:00'))",
"| project timestamp = now(ts), eventName = strcat('event ', x)");
client.execute(database, tableCommand);
client.executeMgmt(database, tableCommand);
// Query for an event where the name is "event 1".
// Utilize ClientRequestProperties to pass query parameters to protect against injection attacks.
@ -51,7 +51,7 @@ public class AdvancedQuery {
"declare query_parameters(eventNameFilter:string);",
"Events",
"| where eventName == eventNameFilter");
KustoOperationResult results = client.execute(database, query, clientRequestProperties);
KustoOperationResult results = client.executeQuery(database, query, clientRequestProperties);
KustoResultSetTable mainTableResult = results.getPrimaryResults();
System.out.printf("Kusto sent back %s rows.%n", mainTableResult.count());

Просмотреть файл

@ -1,21 +1,24 @@
import com.azure.core.http.ProxyOptions;
import com.microsoft.azure.kusto.data.Client;
import com.microsoft.azure.kusto.data.ClientFactory;
import com.microsoft.azure.kusto.data.HttpClientProperties;
import com.microsoft.azure.kusto.data.http.HttpClientProperties;
import com.microsoft.azure.kusto.data.auth.ConnectionStringBuilder;
import org.apache.http.HttpHost;
import org.apache.http.conn.routing.HttpRoute;
import java.net.InetSocketAddress;
public class Proxy {
public static void main(String[] args) {
// See file test\java\com\microsoft\azure\kusto\data\http\SimpleProxyPlanner for an example of a custom route planner class
ProxyOptions proxyOptions = new ProxyOptions(ProxyOptions.Type.HTTP,
InetSocketAddress.createUnresolved("host.example.com", 8080));
HttpClientProperties providedProperties = HttpClientProperties.builder()
.routePlanner((host, request, context) -> {
// Your custom route planning logic here
return new HttpRoute(new HttpHost("example.com"));
}).build();
try (Client client = ClientFactory.createClient(ConnectionStringBuilder.createWithUserPrompt("https://ohadev.swedencentral.dev.kusto.windows.net"),
providedProperties)) {
client.execute(".show version");
.proxy(proxyOptions)
.build();
try {
Client client = ClientFactory.createClient(ConnectionStringBuilder.createWithUserPrompt(
"https://ohadev.swedencentral.dev.kusto.windows.net"), providedProperties);
client.executeMgmt(".show version");
} catch (Exception e) {
e.printStackTrace();

Просмотреть файл

@ -4,7 +4,7 @@
import com.microsoft.azure.kusto.data.Client;
import com.microsoft.azure.kusto.data.ClientFactory;
import com.microsoft.azure.kusto.data.ClientRequestProperties;
import com.microsoft.azure.kusto.data.HttpClientProperties;
import com.microsoft.azure.kusto.data.http.HttpClientProperties;
import com.microsoft.azure.kusto.data.KustoOperationResult;
import com.microsoft.azure.kusto.data.KustoResultSetTable;
import com.microsoft.azure.kusto.data.auth.ConnectionStringBuilder;
@ -31,7 +31,7 @@ public class Query {
Client client = ClientFactory.createClient(csb, properties);
KustoOperationResult results = client.execute(".show version");
KustoOperationResult results = client.executeQuery(".show version");
KustoResultSetTable mainTableResult = results.getPrimaryResults();
System.out.printf("Kusto sent back %s rows.%n", mainTableResult.count());
@ -44,7 +44,7 @@ public class Query {
ClientRequestProperties clientRequestProperties = new ClientRequestProperties();
clientRequestProperties.setTimeoutInMilliSec(TimeUnit.MINUTES.toMillis(1));
results = client.execute(System.getProperty("dbName"), System.getProperty("query"), clientRequestProperties);
results = client.executeQuery(System.getProperty("dbName"), System.getProperty("query"), clientRequestProperties);
} catch (Exception e) {
e.printStackTrace();
}