/*
* Copyright © 2014-2015 Cask Data, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package co.cask.cdap.client;
import co.cask.cdap.api.annotation.Beta;
import co.cask.cdap.api.metrics.RuntimeMetrics;
import co.cask.cdap.client.config.ClientConfig;
import co.cask.cdap.client.util.RESTClient;
import co.cask.cdap.common.UnauthenticatedException;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.common.metrics.MetricsTags;
import co.cask.cdap.proto.Id;
import co.cask.cdap.proto.MetricQueryResult;
import co.cask.cdap.proto.MetricTagValue;
import co.cask.common.http.HttpMethod;
import co.cask.common.http.HttpResponse;
import co.cask.common.http.ObjectResponse;
import com.google.common.base.Joiner;
import com.google.common.base.Stopwatch;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.reflect.TypeToken;
import java.io.IOException;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.annotation.Nullable;
import javax.inject.Inject;
/**
* Provides ways to interact with CDAP Metrics.
*/
@Beta
public class MetricsClient {
private final RESTClient restClient;
private final ClientConfig config;
private final Set<String> validTimeRangeParams;
@Inject
public MetricsClient(ClientConfig config, RESTClient restClient) {
this.config = config;
this.restClient = restClient;
this.validTimeRangeParams = ImmutableSet.of("start", "end", "aggregate", "resolution",
"interpolate", "maxInterpolateGap", "count");
}
public MetricsClient(ClientConfig config) {
this(config, new RESTClient(config));
}
/**
* Searches for metrics tags matching the given tags.
*
* @param tags the tags to match
* @return the metrics matching the given tags
* @throws IOException if a network error occurred
* @throws UnauthenticatedException if the request is not authorized successfully in the gateway server
*/
public List<MetricTagValue> searchTags(Map<String, String> tags)
throws IOException, UnauthenticatedException {
List<String> queryParts = Lists.newArrayList();
queryParts.add("target=tag");
addTags(tags, queryParts);
URL url = config.resolveURLV3(String.format("metrics/search?%s", Joiner.on("&").join(queryParts)));
HttpResponse response = restClient.execute(HttpMethod.POST, url, config.getAccessToken());
ObjectResponse<List<MetricTagValue>> result = ObjectResponse.fromJsonBody(
response, new TypeToken<List<MetricTagValue>>() { }.getType());
return result.getResponseObject();
}
/**
* Searches for metrics matching the given tags.
*
* @param tags the tags to match
* @return the metrics matching the given tags
* @throws IOException if a network error occurred
* @throws UnauthenticatedException if the request is not authorized successfully in the gateway server
*/
public List<String> searchMetrics(Map<String, String> tags)
throws IOException, UnauthenticatedException {
List<String> queryParts = Lists.newArrayList();
queryParts.add("target=metric");
addTags(tags, queryParts);
URL url = config.resolveURLV3(String.format("metrics/search?%s", Joiner.on("&").join(queryParts)));
HttpResponse response = restClient.execute(HttpMethod.POST, url, config.getAccessToken());
ObjectResponse<List<String>> result = ObjectResponse.fromJsonBody(
response, new TypeToken<List<String>>() { }.getType());
return result.getResponseObject();
}
/**
* Gets the value of the given metrics.
*
* @param tags tags for the request
* @param metric names of the metric
* @return values of the metrics
* @throws IOException if a network error occurred
* @throws UnauthenticatedException if the request is not authorized successfully in the gateway server
*/
public MetricQueryResult query(Map<String, String> tags, String metric)
throws IOException, UnauthenticatedException {
return query(tags, ImmutableList.of(metric), ImmutableList.<String>of(), ImmutableMap.<String, String>of());
}
/**
* Gets the value of the given metrics.
*
* @param tags tags for the request
* @param metrics names of the metrics
* @param groupBys groupBys for the request
* @return values of the metrics
* @throws IOException if a network error occurred
* @throws UnauthenticatedException if the request is not authorized successfully in the gateway server
*/
public MetricQueryResult query(Map<String, String> tags, List<String> metrics, List<String> groupBys,
@Nullable String start, @Nullable String end)
throws IOException, UnauthenticatedException {
Map<String, String> timeRangeParams = Maps.newHashMap();
if (start != null) {
timeRangeParams.put("start", start);
}
if (end != null) {
timeRangeParams.put("end", end);
}
return query(tags, metrics, groupBys, timeRangeParams);
}
/**
* Gets the value of the given metrics.
*
* @param tags tags for the request
* @param metrics names of the metrics
* @param groupBys groupBys for the request
* @return values of the metrics
* @throws IOException if a network error occurred
* @throws UnauthenticatedException if the request is not authorized successfully in the gateway server
*/
// TODO: take in query object shared by MetricsHandler
public MetricQueryResult query(Map<String, String> tags, List<String> metrics, List<String> groupBys,
@Nullable Map<String, String> timeRangeParams)
throws IOException, UnauthenticatedException {
List<String> queryParts = Lists.newArrayList();
queryParts.add("target=tag");
add("metric", metrics, queryParts);
add("groupBy", groupBys, queryParts);
addTags(tags, queryParts);
addTimeRangeParametersToQuery(timeRangeParams, queryParts);
URL url = config.resolveURLV3(String.format("metrics/query?%s", Joiner.on("&").join(queryParts)));
HttpResponse response = restClient.execute(HttpMethod.POST, url, config.getAccessToken());
return ObjectResponse.fromJsonBody(response, MetricQueryResult.class).getResponseObject();
}
private void addTimeRangeParametersToQuery(Map<String, String> timeRangeParams, List<String> queryParts) {
for (Map.Entry<String, String> entry : timeRangeParams.entrySet()) {
if (validTimeRangeParams.contains(entry.getKey())) {
queryParts.add(entry.getKey() + "=" + entry.getValue());
}
}
}
public RuntimeMetrics getFlowletMetrics(Id.Program flowId, String flowletId) {
return getMetrics(MetricsTags.flowlet(flowId, flowletId),
Constants.Metrics.Name.Flow.FLOWLET_INPUT,
Constants.Metrics.Name.Flow.FLOWLET_PROCESSED,
Constants.Metrics.Name.Flow.FLOWLET_EXCEPTIONS);
}
public RuntimeMetrics getServiceMetrics(Id.Program serviceId) {
return getMetrics(MetricsTags.service(serviceId),
Constants.Metrics.Name.Service.SERVICE_INPUT,
Constants.Metrics.Name.Service.SERVICE_PROCESSED,
Constants.Metrics.Name.Service.SERVICE_EXCEPTIONS);
}
private void add(String key, List<String> values, List<String> outQueryParts) {
for (String value : values) {
outQueryParts.add(key + "=" + value);
}
}
private void addTags(Map<String, String> tags, List<String> outQueryParts) {
for (Map.Entry<String, String> tag : tags.entrySet()) {
outQueryParts.add("tag=" + tag.getKey() + ":" + tag.getValue());
}
}
/**
* Gets the {@link RuntimeMetrics} for a particular metrics context.
*
* @param tags the metrics tags
* @param inputName the metrics key for input counter
* @param processedName the metrics key for processed counter
* @param exceptionName the metrics key for exception counter
* @return the {@link RuntimeMetrics}
*/
private RuntimeMetrics getMetrics(final Map<String, String> tags, final String inputName,
final String processedName, final String exceptionName) {
return new RuntimeMetrics() {
@Override
public long getInput() {
return getTotalCounter(tags, inputName);
}
@Override
public long getProcessed() {
return getTotalCounter(tags, processedName);
}
@Override
public long getException() {
return getTotalCounter(tags, exceptionName);
}
@Override
public void waitForinput(long count, long timeout, TimeUnit timeoutUnit)
throws TimeoutException, InterruptedException {
doWaitFor(inputName, count, timeout, timeoutUnit);
}
@Override
public void waitForProcessed(long count, long timeout, TimeUnit timeoutUnit)
throws TimeoutException, InterruptedException {
doWaitFor(processedName, count, timeout, timeoutUnit);
}
@Override
public void waitForException(long count, long timeout, TimeUnit timeoutUnit)
throws TimeoutException, InterruptedException {
doWaitFor(exceptionName, count, timeout, timeoutUnit);
}
@Override
public void waitFor(String name, long count,
long timeout, TimeUnit timeoutUnit) throws TimeoutException, InterruptedException {
doWaitFor(name, count, timeout, timeoutUnit);
}
private void doWaitFor(String name, long count, long timeout, TimeUnit timeoutUnit)
throws TimeoutException, InterruptedException {
long value = getTotalCounter(tags, name);
// Min sleep time is 10ms, max sleep time is 1 seconds
long sleepMillis = Math.max(10, Math.min(timeoutUnit.toMillis(timeout) / 10, TimeUnit.SECONDS.toMillis(1)));
Stopwatch stopwatch = new Stopwatch().start();
while (value < count && stopwatch.elapsedTime(timeoutUnit) < timeout) {
TimeUnit.MILLISECONDS.sleep(sleepMillis);
value = getTotalCounter(tags, name);
}
if (value < count) {
throw new TimeoutException("Time limit reached. Got '" + value + "' instead of '" + count + "'");
}
}
@Override
public String toString() {
return String.format("%s; tags=%d, processed=%d, exception=%d",
Joiner.on(",").withKeyValueSeparator(":").join(tags),
getInput(), getProcessed(), getException());
}
};
}
private long getTotalCounter(Map<String, String> tags, String metricName) {
try {
MetricQueryResult result = query(tags, metricName);
if (result.getSeries().length == 0) {
return 0;
}
MetricQueryResult.TimeValue[] timeValues = result.getSeries()[0].getData();
if (timeValues.length == 0) {
return 0;
}
// since it is totals, we know there's one value only
return timeValues[0].getValue();
} catch (Exception e) {
throw Throwables.propagate(e);
}
}
}