/* * Copyright © 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.metrics.query; import co.cask.cdap.api.dataset.lib.cube.AggregationFunction; import co.cask.cdap.api.dataset.lib.cube.Interpolator; import co.cask.cdap.api.dataset.lib.cube.Interpolators; import co.cask.cdap.api.dataset.lib.cube.TimeValue; import co.cask.cdap.api.metrics.MetricDataQuery; import co.cask.cdap.api.metrics.MetricSearchQuery; import co.cask.cdap.api.metrics.MetricStore; import co.cask.cdap.api.metrics.MetricTimeSeries; import co.cask.cdap.api.metrics.TagValue; import co.cask.cdap.common.conf.Constants; import co.cask.cdap.common.utils.TimeMathParser; import co.cask.cdap.proto.MetricQueryRequest; import co.cask.cdap.proto.MetricQueryResult; import co.cask.cdap.proto.MetricTagValue; import co.cask.http.AbstractHttpHandler; import co.cask.http.HttpResponder; import com.google.common.base.Charsets; import com.google.common.base.Function; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableBiMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import com.google.inject.Inject; import org.jboss.netty.handler.codec.http.HttpHeaders; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import org.jboss.netty.handler.codec.http.QueryStringDecoder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.QueryParam; /** * Search metrics handler. */ @Path(Constants.Gateway.API_VERSION_3 + "/metrics") public class MetricsHandler extends AbstractHttpHandler { private static final Logger LOG = LoggerFactory.getLogger(MetricsHandler.class); private static final Gson GSON = new Gson(); // constants used for request query parsing private static final String PARAM_COUNT = "count"; private static final String PARAM_START_TIME = "start"; private static final String PARAM_RESOLUTION = "resolution"; private static final String PARAM_END_TIME = "end"; private static final String PARAM_INTERPOLATE = "interpolate"; private static final String PARAM_STEP_INTERPOLATOR = "step"; private static final String PARAM_LINEAR_INTERPOLATOR = "linear"; private static final String PARAM_MAX_INTERPOLATE_GAP = "maxInterpolateGap"; private static final String PARAM_AGGREGATE = "aggregate"; private static final String PARAM_AUTO_RESOLUTION = "auto"; public static final String ANY_TAG_VALUE = "*"; private final MetricStore metricStore; private static final Map<String, String> tagNameToHuman; private static final Map<String, String> humanToTagName; static { ImmutableBiMap<String, String> mapping = ImmutableBiMap.<String, String>builder() .put(Constants.Metrics.Tag.NAMESPACE, "namespace") .put(Constants.Metrics.Tag.RUN_ID, "run") .put(Constants.Metrics.Tag.INSTANCE_ID, "instance") .put(Constants.Metrics.Tag.COMPONENT, "component") .put(Constants.Metrics.Tag.HANDLER, "handler") .put(Constants.Metrics.Tag.METHOD, "method") .put(Constants.Metrics.Tag.STREAM, "stream") .put(Constants.Metrics.Tag.DATASET, "dataset") .put(Constants.Metrics.Tag.APP, "app") .put(Constants.Metrics.Tag.SERVICE, "service") // SERVICE_HANDLER is the same HANDLER .put(Constants.Metrics.Tag.WORKER, "worker") .put(Constants.Metrics.Tag.FLOW, "flow") .put(Constants.Metrics.Tag.FLOWLET, "flowlet") .put(Constants.Metrics.Tag.FLOWLET_QUEUE, "queue") .put(Constants.Metrics.Tag.PRODUCER, "producer") .put(Constants.Metrics.Tag.CONSUMER, "consumer") .put(Constants.Metrics.Tag.MAPREDUCE, "mapreduce") .put(Constants.Metrics.Tag.MR_TASK_TYPE, "tasktype") .put(Constants.Metrics.Tag.WORKFLOW, "workflow") .put(Constants.Metrics.Tag.SPARK, "spark").build(); tagNameToHuman = mapping; humanToTagName = mapping.inverse(); } @Inject public MetricsHandler(MetricStore metricStore) { this.metricStore = metricStore; } @POST @Path("/search") public void search(HttpRequest request, HttpResponder responder, @QueryParam("target") String target, @QueryParam("tag") List<String> tags) throws Exception { if (target == null) { responder.sendJson(HttpResponseStatus.BAD_REQUEST, "Required target param is missing"); return; } switch (target) { case "tag": searchTagAndRespond(responder, tags); break; case "metric": searchMetricAndRespond(responder, tags); break; default: responder.sendJson(HttpResponseStatus.BAD_REQUEST, "Unknown target param value: " + target); break; } } private void searchMetricAndRespond(HttpResponder responder, List<String> tagValues) throws Exception { try { responder.sendJson(HttpResponseStatus.OK, getMetrics(humanToTagNames(parseTagValues(tagValues)))); } catch (IllegalArgumentException e) { LOG.warn("Invalid request", e); responder.sendString(HttpResponseStatus.BAD_REQUEST, e.getMessage()); } } private List<MetricTagValue> parseTagValues(List<String> tags) { List<MetricTagValue> result = Lists.newArrayList(); for (String tag : tags) { // split by ':' and add the tagValue to result list String[] tagSplit = tag.split(":", 2); if (tagSplit.length == 2) { String value = tagSplit[1].equals(ANY_TAG_VALUE) ? null : tagSplit[1]; result.add(new MetricTagValue(tagSplit[0], value)); } } return result; } @POST @Path("/query") public void query(HttpRequest request, HttpResponder responder, @QueryParam("metric") List<String> metrics, @QueryParam("groupBy") List<String> groupBy, @QueryParam("tag") List<String> tags) throws Exception { if (new QueryStringDecoder(request.getUri()).getParameters().isEmpty()) { executeBatchQueries(request, responder); return; } tagsQuerying(request, responder, tags, metrics, groupBy); } private void executeBatchQueries(HttpRequest request, HttpResponder responder) { if (HttpHeaders.getContentLength(request) > 0) { try { String json = request.getContent().toString(Charsets.UTF_8); Map<String, QueryRequestFormat> queries = GSON.fromJson(json, new TypeToken<Map<String, QueryRequestFormat>>() { }.getType()); LOG.trace("Received Queries {}", queries); Map<String, MetricQueryResult> queryFinalResponse = Maps.newHashMap(); for (Map.Entry<String, QueryRequestFormat> query : queries.entrySet()) { MetricQueryRequest queryRequest = getQueryRequestFromFormat(query.getValue()); queryFinalResponse.put(query.getKey(), executeQuery(queryRequest)); } responder.sendJson(HttpResponseStatus.OK, queryFinalResponse); } catch (IllegalArgumentException e) { LOG.warn("Invalid request", e); responder.sendString(HttpResponseStatus.BAD_REQUEST, e.getMessage()); } catch (Exception e) { LOG.error("Exception querying metrics ", e); responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, "Internal error while querying for metrics"); } } else { responder.sendJson(HttpResponseStatus.BAD_REQUEST, "Batch request with empty content"); } } private MetricQueryRequest getQueryRequestFromFormat(QueryRequestFormat queryRequestFormat) { Map<String, List<String>> queryParams = Maps.newHashMap(); for (Map.Entry<String, String> entry : queryRequestFormat.getTimeRange().entrySet()) { queryParams.put(entry.getKey(), ImmutableList.of(entry.getValue())); } MetricQueryRequest queryRequest = new MetricQueryRequest(queryRequestFormat.getTags(), queryRequestFormat.getMetrics(), queryRequestFormat.getGroupBy()); setTimeRangeInQueryRequest(queryRequest, queryParams); return queryRequest; } private void tagsQuerying(HttpRequest request, HttpResponder responder, List<String> tags, List<String> metrics, List<String> groupByTags) { try { responder.sendJson(HttpResponseStatus.OK, executeQuery(request, parseTagValuesAsMap(tags), groupByTags, metrics)); } catch (IllegalArgumentException e) { LOG.warn("Invalid request", e); responder.sendString(HttpResponseStatus.BAD_REQUEST, e.getMessage()); } catch (Exception e) { LOG.error("Exception querying metrics ", e); responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, "Internal error while querying for metrics"); } } private MetricQueryResult executeQuery(HttpRequest request, Map<String, String> sliceByTags, List<String> groupByTags, List<String> metrics) throws Exception { MetricQueryRequest queryRequest = new MetricQueryRequest(sliceByTags, metrics, groupByTags); setTimeRangeInQueryRequest(queryRequest, new QueryStringDecoder(request.getUri()).getParameters()); return executeQuery(queryRequest); } private void setTimeRangeInQueryRequest(MetricQueryRequest request, Map<String, List<String>> queryTimeParams) { Long start = queryTimeParams.containsKey(PARAM_START_TIME) ? TimeMathParser.parseTimeInSeconds(queryTimeParams.get(PARAM_START_TIME).get(0)) : null; Long end = queryTimeParams.containsKey(PARAM_END_TIME) ? TimeMathParser.parseTimeInSeconds(queryTimeParams.get(PARAM_END_TIME).get(0)) : null; Integer count = null; boolean aggregate = queryTimeParams.containsKey(PARAM_AGGREGATE) && queryTimeParams.get(PARAM_AGGREGATE).get(0).equals("true") || ((start == null) && (end == null)); Integer resolution = queryTimeParams.containsKey(PARAM_RESOLUTION) ? getResolution(queryTimeParams.get(PARAM_RESOLUTION).get(0), start, end) : 1; Interpolator interpolator = null; if (queryTimeParams.containsKey(PARAM_INTERPOLATE)) { long timeLimit = queryTimeParams.containsKey(PARAM_MAX_INTERPOLATE_GAP) ? Long.parseLong(queryTimeParams.get(PARAM_MAX_INTERPOLATE_GAP).get(0)) : Long.MAX_VALUE; interpolator = getInterpolator(queryTimeParams.get(PARAM_INTERPOLATE).get(0), timeLimit); } if (queryTimeParams.containsKey(PARAM_COUNT)) { count = Integer.valueOf(queryTimeParams.get(PARAM_COUNT).get(0)); if (start == null && end != null) { start = end - count * resolution; } else if (start != null && end == null) { end = start + count * resolution; } } else if (start != null && end != null) { count = (int) (((end / resolution * resolution) - (start / resolution * resolution)) / resolution + 1); } else if (!aggregate) { throw new IllegalArgumentException("At least two of count/start/end parameters " + "are required for time-range queries "); } if (aggregate) { request.setTimeRange(0L, 0L, 1, Integer.MAX_VALUE, null); } else { request.setTimeRange(start, end, count, resolution, interpolator); } } private Interpolator getInterpolator(String interpolator, long timeLimit) { if (PARAM_STEP_INTERPOLATOR.equals(interpolator)) { return new Interpolators.Step(timeLimit); } else if (PARAM_LINEAR_INTERPOLATOR.equals(interpolator)) { return new Interpolators.Linear(timeLimit); } return null; } private Integer getResolution(String resolution, Long start, Long end) { if (resolution.equals(PARAM_AUTO_RESOLUTION)) { if (start != null && end != null) { long difference = end - start; return MetricQueryParser.getResolution(difference).getResolution(); } else { throw new IllegalArgumentException("if resolution=auto, start and end timestamp " + "should be provided to determine resolution"); } } else { // if not auto, check if the given resolution matches available resolutions that we support. int resolutionInterval = TimeMathParser.resolutionInSeconds(resolution); if (!((resolutionInterval == Integer.MAX_VALUE) || (resolutionInterval == 3600) || (resolutionInterval == 60) || (resolutionInterval == 1))) { throw new IllegalArgumentException("Resolution interval not supported, only 1 second, 1 minute and " + "1 hour resolutions are supported currently"); } return resolutionInterval; } } private MetricQueryResult executeQuery(MetricQueryRequest queryRequest) throws Exception { if (queryRequest.getMetrics().size() == 0) { throw new IllegalArgumentException("Missing metrics parameter in the query"); } Map<String, String> tagsSliceBy = humanToTagNames(transformTagMap(queryRequest.getTags())); MetricQueryRequest.TimeRange timeRange = queryRequest.getTimeRange(); MetricDataQuery query = new MetricDataQuery(timeRange.getStart(), timeRange.getEnd(), timeRange.getResolutionInSeconds(), timeRange.getCount(), toMetrics(queryRequest.getMetrics()), tagsSliceBy, transformGroupByTags(queryRequest.getGroupBy()), timeRange.getInterpolate()); Collection<MetricTimeSeries> queryResult = metricStore.query(query); long endTime = timeRange.getEnd(); if (timeRange.getResolutionInSeconds() == Integer.MAX_VALUE && endTime == 0) { // for aggregate query, we set the end time to be query time (current time) endTime = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()); } return decorate(queryResult, timeRange.getStart(), endTime, timeRange.getResolutionInSeconds()); } private Map<String, AggregationFunction> toMetrics(List<String> metrics) { Map<String, AggregationFunction> result = Maps.newHashMap(); for (String metric : metrics) { // todo: figure out metric type result.put(metric, AggregationFunction.SUM); } return result; } private Map<String, String> transformTagMap(Map<String, String> tags) { return Maps.transformValues(tags, new Function<String, String>() { @Override public String apply(String value) { if (ANY_TAG_VALUE.equals(value)) { return null; } else { return value; } } }); } private List<String> transformGroupByTags(List<String> groupBy) { return Lists.transform(groupBy, new Function<String, String>() { @Nullable @Override public String apply(@Nullable String input) { String replacement = humanToTagName.get(input); return replacement != null ? replacement : input; } }); } private Map<String, String> parseTagValuesAsMap(List<String> tags) { List<MetricTagValue> tagValues = parseTagValues(tags); Map<String, String> result = Maps.newHashMap(); for (MetricTagValue tagValue : tagValues) { result.put(tagValue.getName(), tagValue.getValue()); } return result; } private void searchTagAndRespond(HttpResponder responder, List<String> tags) { try { // we want to search the entire range, so startTimestamp is '0' and end Timestamp is Integer.MAX_VALUE and // limit is -1 , to include the entire search result. MetricSearchQuery searchQuery = new MetricSearchQuery(0, Integer.MAX_VALUE, -1, toTagValues(humanToTagNames(parseTagValues(tags)))); Collection<TagValue> nextTags = metricStore.findNextAvailableTags(searchQuery); responder.sendJson(HttpResponseStatus.OK, tagValuesToHuman(nextTags)); } catch (IllegalArgumentException e) { LOG.warn("Invalid request", e); responder.sendString(HttpResponseStatus.BAD_REQUEST, e.getMessage()); } catch (Exception e) { LOG.error("Exception querying metrics ", e); responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, "Internal error while querying for metrics"); } } private List<MetricTagValue> tagValuesToHuman(Collection<TagValue> tagValues) { List<MetricTagValue> result = Lists.newArrayList(); for (TagValue tagValue : tagValues) { String human = tagNameToHuman.get(tagValue.getName()); human = human != null ? human : tagValue.getName(); String value = tagValue.getValue() == null ? ANY_TAG_VALUE : tagValue.getValue(); result.add(new MetricTagValue(human, value)); } return result; } private List<TagValue> toTagValues(List<MetricTagValue> tagValues) { return Lists.transform(tagValues, new Function<MetricTagValue, TagValue>() { @Nullable @Override public TagValue apply(@Nullable MetricTagValue input) { if (input == null) { // SHOULD NEVER happen throw new NullPointerException(); } return new TagValue(input.getName(), input.getValue()); } }); } private List<MetricTagValue> humanToTagNames(List<MetricTagValue> tagValues) { List<MetricTagValue> result = Lists.newArrayList(); for (MetricTagValue tagValue : tagValues) { String tagName = humanToTagName(tagValue.getName()); result.add(new MetricTagValue(tagName, tagValue.getValue())); } return result; } private String humanToTagName(String humanTagName) { String replacement = humanToTagName.get(humanTagName); return replacement != null ? replacement : humanTagName; } private Map<String, String> humanToTagNames(Map<String, String> tagValues) { Map<String, String> result = Maps.newHashMap(); for (Map.Entry<String, String> tagValue : tagValues.entrySet()) { result.put(humanToTagName(tagValue.getKey()), tagValue.getValue()); } return result; } private Collection<String> getMetrics(List<MetricTagValue> tagValues) throws Exception { // we want to search the entire range, so startTimestamp is '0' and end Timestamp is Integer.MAX_VALUE and // limit is -1 , to include the entire search result. MetricSearchQuery searchQuery = new MetricSearchQuery(0, Integer.MAX_VALUE, -1, toTagValues(tagValues)); Collection<String> metricNames = metricStore.findMetricNames(searchQuery); return Lists.newArrayList(Iterables.filter(metricNames, Predicates.notNull())); } private MetricQueryResult decorate(Collection<MetricTimeSeries> series, long startTs, long endTs, int resolution) { MetricQueryResult.TimeSeries[] serieses = new MetricQueryResult.TimeSeries[series.size()]; int i = 0; for (MetricTimeSeries timeSeries : series) { MetricQueryResult.TimeValue[] timeValues = decorate(timeSeries.getTimeValues()); serieses[i++] = new MetricQueryResult.TimeSeries(timeSeries.getMetricName(), tagNamesToHuman(timeSeries.getTagValues()), timeValues); } return new MetricQueryResult(startTs, endTs, serieses, resolution); } private Map<String, String> tagNamesToHuman(Map<String, String> tagValues) { Map<String, String> humanTagValues = Maps.newHashMap(); for (Map.Entry<String, String> tag : tagValues.entrySet()) { humanTagValues.put(tagNameToHuman.get(tag.getKey()), tag.getValue()); } return humanTagValues; } private MetricQueryResult.TimeValue[] decorate(List<TimeValue> points) { MetricQueryResult.TimeValue[] timeValues = new MetricQueryResult.TimeValue[points.size()]; int k = 0; for (TimeValue timeValue : points) { timeValues[k++] = new MetricQueryResult.TimeValue(timeValue.getTimestamp(), timeValue.getValue()); } return timeValues; } /** * Helper class to Deserialize Query requests and based on this * {@link MetricQueryRequest} will be constructed */ private class QueryRequestFormat { Map<String, String> tags; List<String> metrics; List<String> groupBy; Map<String, String> timeRange; public Map<String, String> getTags() { tags = (tags == null) ? Maps.<String, String>newHashMap() : tags; return tags; } public List<String> getMetrics() { return metrics; } public List<String> getGroupBy() { groupBy = (groupBy == null) ? Lists.<String>newArrayList() : groupBy; return groupBy; } /** * time range has aggregate=true or {start, end, count, resolution, interpolate} parameters, * since start, end can be represented as 'now ('+' or '-')' and not just absolute timestamp, * we use this format to get those strings and after parsing and determining other parameters, we can construct * {@link MetricQueryRequest} , similar for resolution. * @return time range prameters */ public Map<String, String> getTimeRange() { timeRange = (timeRange == null || timeRange.size() == 0) ? ImmutableMap.of("aggregate", "true") : timeRange; return timeRange; } } }