package gobblin.compaction.audit;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Maps;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import gobblin.configuration.State;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.annotation.ThreadSafe;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* A {@link AuditCountClient} which uses {@link org.apache.http.client.HttpClient}
* to perform audit count query.
*/
@Slf4j
@ThreadSafe
public class KafkaAuditCountHttpClient implements AuditCountClient {
// Keys
public static final String KAFKA_AUDIT_HTTP = "kafka.audit.http";
public static final String CONNECTION_MAX_TOTAL = KAFKA_AUDIT_HTTP + "max.total";
public static final int DEFAULT_CONNECTION_MAX_TOTAL = 10;
public static final String MAX_PER_ROUTE = KAFKA_AUDIT_HTTP + "max.per.route";
public static final int DEFAULT_MAX_PER_ROUTE = 10;
public static final String KAFKA_AUDIT_REST_BASE_URL = "kafka.audit.rest.base.url";
public static final String KAFKA_AUDIT_REST_MAX_TRIES = "kafka.audit.rest.max.tries";
public static final String KAFKA_AUDIT_REST_START_QUERYSTRING_KEY = "kafka.audit.rest.querystring.start";
public static final String KAFKA_AUDIT_REST_END_QUERYSTRING_KEY = "kafka.audit.rest.querystring.end";
public static final String KAFKA_AUDIT_REST_START_QUERYSTRING_DEFAULT = "begin";
public static final String KAFKA_AUDIT_REST_END_QUERYSTRING_DEFAULT = "end";
// Http Client
private PoolingHttpClientConnectionManager cm;
private CloseableHttpClient httpClient;
private static final JsonParser PARSER = new JsonParser();
private final String baseUrl;
private final String startQueryString;
private final String endQueryString;
private final int maxNumTries;
/**
* Constructor
*/
public KafkaAuditCountHttpClient (State state) {
int maxTotal = state.getPropAsInt(CONNECTION_MAX_TOTAL, DEFAULT_CONNECTION_MAX_TOTAL);
int maxPerRoute = state.getPropAsInt(MAX_PER_ROUTE, DEFAULT_MAX_PER_ROUTE);
cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(maxTotal);
cm.setDefaultMaxPerRoute(maxPerRoute);
httpClient = HttpClients.custom()
.setConnectionManager(cm)
.build();
this.baseUrl = state.getProp(KAFKA_AUDIT_REST_BASE_URL);
this.maxNumTries = state.getPropAsInt(KAFKA_AUDIT_REST_MAX_TRIES, 5);
this.startQueryString = state.getProp(KAFKA_AUDIT_REST_START_QUERYSTRING_KEY, KAFKA_AUDIT_REST_START_QUERYSTRING_DEFAULT);
this.endQueryString = state.getProp(KAFKA_AUDIT_REST_END_QUERYSTRING_KEY, KAFKA_AUDIT_REST_END_QUERYSTRING_DEFAULT);
}
public Map<String, Long> fetch (String datasetName, long start, long end) throws IOException {
String fullUrl =
(this.baseUrl.endsWith("/") ? this.baseUrl : this.baseUrl + "/") + StringUtils.replaceChars(datasetName, '/', '.')
+ "?" + this.startQueryString + "=" + start + "&" + this.endQueryString + "=" + end;
log.info("Full URL is " + fullUrl);
String response = getHttpResponse(fullUrl);
return parseResponse (fullUrl, response, datasetName);
}
/**
* Expects <code>response</code> being parsed to be as below.
*
* <pre>
* {
* "result": {
* "hadoop-tracking-lva1tarock-08": 79341895,
* "hadoop-tracking-uno-08": 79341892,
* "kafka-08-tracking-local": 79341968,
* "kafka-corp-lca1-tracking-agg": 79341968,
* "kafka-corp-ltx1-tracking-agg": 79341968,
* "producer": 69483513
* }
* }
* </pre>
*/
@VisibleForTesting
public static Map<String, Long> parseResponse(String fullUrl, String response, String topic) throws IOException {
Map<String, Long> result = Maps.newHashMap();
JsonObject countsPerTier = null;
try {
JsonObject jsonObj = PARSER.parse(response).getAsJsonObject();
countsPerTier = jsonObj.getAsJsonObject("result");
} catch (Exception e) {
throw new IOException(String.format("Unable to parse JSON response: %s for request url: %s ", response,
fullUrl), e);
}
Set<Map.Entry<String, JsonElement>> entrySet = countsPerTier.entrySet();
for(Map.Entry<String, JsonElement> entry : entrySet) {
String tier = entry.getKey();
long count = Long.parseLong(entry.getValue().getAsString());
result.put(tier, count);
}
return result;
}
private String getHttpResponse(String fullUrl) throws IOException {
HttpUriRequest req = new HttpGet(fullUrl);
for (int numTries = 0;; numTries++) {
try (CloseableHttpResponse response = this.httpClient.execute(req)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode < 200 || statusCode >= 300) {
throw new IOException(
String.format("status code: %d, reason: %s", statusCode, response.getStatusLine().getReasonPhrase()));
}
return EntityUtils.toString(response.getEntity());
} catch (IOException e) {
String errMsg = "Unable to get or parse HTTP response for " + fullUrl;
if (numTries >= this.maxNumTries) {
throw new IOException (errMsg, e);
}
long backOffSec = (numTries + 1) * 2;
log.error(errMsg + ", will retry in " + backOffSec + " sec", e);
try {
Thread.sleep(TimeUnit.SECONDS.toMillis(backOffSec));
} catch (InterruptedException e1) {
Thread.currentThread().interrupt();
}
}
}
}
}