co.cask.cdap.metrics.query.MetricsRequestParser.java Source code

Java tutorial

Introduction

Here is the source code for co.cask.cdap.metrics.query.MetricsRequestParser.java

Source

/*
 * Copyright  2014 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.common.conf.Constants;
import co.cask.cdap.common.metrics.MetricsScope;
import co.cask.cdap.common.utils.ImmutablePair;
import co.cask.cdap.common.utils.TimeMathParser;
import co.cask.cdap.metrics.MetricsConstants;
import co.cask.cdap.metrics.data.Interpolator;
import co.cask.cdap.metrics.data.Interpolators;
import com.google.common.base.Splitter;
import org.apache.commons.lang.CharEncoding;
import org.jboss.netty.handler.codec.http.QueryStringDecoder;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * For parsing metrics REST request.
 */
final class MetricsRequestParser {

    private static final String COUNT = "count";
    private static final String START_TIME = "start";
    private static final String END_TIME = "end";
    private static final String INTERPOLATE = "interpolate";
    private static final String STEP_INTERPOLATOR = "step";
    private static final String LINEAR_INTERPOLATOR = "linear";
    private static final String MAX_INTERPOLATE_GAP = "maxInterpolateGap";
    private static final String CLUSTER_METRICS_CONTEXT = "-.cluster";
    private static final String TRANSACTION_METRICS_CONTEXT = "transactions";

    public enum PathType {
        APPS, DATASETS, STREAMS, CLUSTER, SERVICES;
    }

    public enum RequestType {
        FLOWS("f"), MAPREDUCE("b"), PROCEDURES("p"), HANDLERS("h"), SERVICES("s");

        private final String code;

        private RequestType(String code) {
            this.code = code;
        }

        public String getCode() {
            return code;
        }
    }

    private enum MapReduceType {
        MAPPERS("m"), REDUCERS("r");

        private final String id;

        private MapReduceType(String id) {
            this.id = id;
        }

        private String getId() {
            return id;
        }
    }

    private static String urlDecode(String str) {
        try {
            return URLDecoder.decode(str, CharEncoding.UTF_8);
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException("unsupported encoding in path element", e);
        }
    }

    /**
     * Given a full metrics path like '/v2/metrics/system/apps/collect.events', strip the preceding version and
     * metrics to return 'system/apps/collect.events', representing the context and metric, which can then be
     * parsed by this parser.
     *
     * @param path request path.
     * @return request path stripped of version and metrics.
     */
    static String stripVersionAndMetricsFromPath(String path) {
        // +8 for "/metrics"
        int startPos = Constants.Gateway.GATEWAY_VERSION.length() + 8;
        return path.substring(startPos, path.length());
    }

    static MetricsRequest parse(URI requestURI) throws MetricsPathException {
        return parseRequestAndContext(requestURI).getFirst();
    }

    static ImmutablePair<MetricsRequest, MetricsRequestContext> parseRequestAndContext(URI requestURI)
            throws MetricsPathException {
        MetricsRequestBuilder builder = new MetricsRequestBuilder(requestURI);

        // metric will be at the end.
        String uriPath = requestURI.getRawPath();
        int index = uriPath.lastIndexOf("/");
        builder.setMetricPrefix(urlDecode(uriPath.substring(index + 1)));

        // strip the metric from the end of the path
        String strippedPath = uriPath.substring(0, index);

        MetricsRequestContext metricsRequestContext;
        if (strippedPath.startsWith("/system/cluster")) {
            builder.setContextPrefix(CLUSTER_METRICS_CONTEXT);
            builder.setScope(MetricsScope.SYSTEM);
            metricsRequestContext = new MetricsRequestContext.Builder().build();
        } else if (strippedPath.startsWith("/system/transactions")) {
            builder.setContextPrefix(TRANSACTION_METRICS_CONTEXT);
            builder.setScope(MetricsScope.SYSTEM);
            metricsRequestContext = new MetricsRequestContext.Builder().build();
        } else {
            metricsRequestContext = parseContext(strippedPath, builder);
        }
        parseQueryString(requestURI, builder);
        return new ImmutablePair<MetricsRequest, MetricsRequestContext>(builder.build(), metricsRequestContext);
    }

    /**
     * Parse the context path, setting the relevant context fields in the builder.
     * Context starts after the scope and looks something like:
     * system/apps/{app-id}/{program-type}/{program-id}/{component-type}/{component-id}
     */
    static MetricsRequestContext parseContext(String path, MetricsRequestBuilder builder)
            throws MetricsPathException {
        Iterator<String> pathParts = Splitter.on('/').omitEmptyStrings().split(path).iterator();
        MetricsRequestContext.Builder contextBuilder = new MetricsRequestContext.Builder();

        // scope is the first part of the path
        String scopeStr = pathParts.next();
        try {
            builder.setScope(MetricsScope.valueOf(scopeStr.toUpperCase()));
        } catch (IllegalArgumentException e) {
            throw new MetricsPathException("invalid scope: " + scopeStr);
        }

        // streams, datasets, apps, or nothing.
        if (!pathParts.hasNext()) {
            return contextBuilder.build();
        }

        // apps, streams, or datasets
        String pathTypeStr = pathParts.next();
        PathType pathType;
        try {
            pathType = PathType.valueOf(pathTypeStr.toUpperCase());
            contextBuilder.setPathType(pathType);
        } catch (IllegalArgumentException e) {
            throw new MetricsPathException("invalid type: " + pathTypeStr);
        }

        switch (pathType) {
        case APPS:
            parseSubContext(pathParts, contextBuilder);
            break;
        case STREAMS:
            if (!pathParts.hasNext()) {
                throw new MetricsPathException("'streams' must be followed by a stream name");
            }
            contextBuilder.setTag(MetricsRequestContext.TagType.STREAM, urlDecode(pathParts.next()));
            break;
        case DATASETS:
            if (!pathParts.hasNext()) {
                throw new MetricsPathException("'datasets' must be followed by a dataset name");
            }
            contextBuilder.setTag(MetricsRequestContext.TagType.DATASET, urlDecode(pathParts.next()));
            // path can be /metric/scope/datasets/{dataset}/apps/...
            if (pathParts.hasNext()) {
                if (!pathParts.next().equals("apps")) {
                    throw new MetricsPathException("expecting 'apps' after stream or dataset name");
                }
                parseSubContext(pathParts, contextBuilder);
            }
            break;
        case SERVICES:
            if (!pathParts.hasNext()) {
                throw new MetricsPathException("'services must be followed by a service name");
            }
            parseSubContext(pathParts, contextBuilder);
            break;
        }

        if (pathParts.hasNext()) {
            throw new MetricsPathException("path contains too many elements");
        }
        MetricsRequestContext context = contextBuilder.build();
        builder.setContextPrefix(context.getContextPrefix());
        if (context.getTag() != null) {
            builder.setTagPrefix(context.getTag());
        }
        return context;
    }

    /**
     * pathParts should look like {app-id}/{program-type}/{program-id}/{component-type}/{component-id}.
     */
    static void parseSubContext(Iterator<String> pathParts, MetricsRequestContext.Builder builder)
            throws MetricsPathException {

        if (!pathParts.hasNext()) {
            return;
        }
        builder.setTypeId(urlDecode(pathParts.next()));

        if (!pathParts.hasNext()) {
            return;
        }

        // request-type: flows, procedures, or mapreduce or handlers or services(user)
        String pathProgramTypeStr = pathParts.next();
        RequestType requestType;
        try {
            requestType = RequestType.valueOf(pathProgramTypeStr.toUpperCase());
            builder.setRequestType(requestType);
        } catch (IllegalArgumentException e) {
            throw new MetricsPathException("invalid program type: " + pathProgramTypeStr);
        }

        // contextPrefix should look like appId.f right now, if we're looking at a flow
        if (!pathParts.hasNext()) {
            return;
        }
        builder.setRequestId(urlDecode(pathParts.next()));

        if (!pathParts.hasNext()) {
            return;
        }

        switch (requestType) {
        case MAPREDUCE:
            String mrTypeStr = pathParts.next();
            MapReduceType mrType;
            try {
                mrType = MapReduceType.valueOf(mrTypeStr.toUpperCase());
            } catch (IllegalArgumentException e) {
                throw new MetricsPathException(
                        "invalid mapreduce component: " + mrTypeStr + ".  must be 'mappers' or 'reducers'.");
            }
            builder.setComponentId(mrType.getId());
            break;
        case FLOWS:
            buildFlowletContext(pathParts, builder);
            break;
        case HANDLERS:
            buildHandlerContext(pathParts, builder);
            break;
        case SERVICES:
            buildUserServiceContext(pathParts, builder);
        }

        if (pathParts.hasNext()) {
            throw new MetricsPathException("path contains too many elements");
        }
    }

    private static void buildUserServiceContext(Iterator<String> pathParts, MetricsRequestContext.Builder builder)
            throws MetricsPathException {
        if (!pathParts.next().equals("runnables")) {
            throw new MetricsPathException("expecting 'runnables' after the service name");
        }
        if (!pathParts.hasNext()) {
            throw new MetricsPathException("runnables must be followed by a runnable name");
        }
        builder.setComponentId(urlDecode(pathParts.next()));
    }

    /**
     * At this point, pathParts should look like methods/{method-name}
     */
    private static void buildHandlerContext(Iterator<String> pathParts, MetricsRequestContext.Builder builder)
            throws MetricsPathException {
        if (!pathParts.next().equals("methods")) {
            throw new MetricsPathException("expecting 'methods' after the handler name");
        }
        if (!pathParts.hasNext()) {
            throw new MetricsPathException("methods must be followed by a method name");
        }
        builder.setComponentId(urlDecode(pathParts.next()));
    }

    /**
     * At this point, pathParts should look like flowlets/{flowlet-id}/queues/{queue-id}, with queues being optional.
     */
    private static void buildFlowletContext(Iterator<String> pathParts, MetricsRequestContext.Builder builder)
            throws MetricsPathException {
        if (!pathParts.next().equals("flowlets")) {
            throw new MetricsPathException("expecting 'flowlets' after the flow name");
        }
        if (!pathParts.hasNext()) {
            throw new MetricsPathException("flowlets must be followed by a flowlet name");
        }
        builder.setComponentId(urlDecode(pathParts.next()));

        if (pathParts.hasNext()) {
            if (!pathParts.next().equals("queues")) {
                throw new MetricsPathException("expecting 'queues' after the flowlet name");
            }
            if (!pathParts.hasNext()) {
                throw new MetricsPathException("'queues' must be followed by a queue name");
            }
            builder.setTag(MetricsRequestContext.TagType.QUEUE, urlDecode(pathParts.next()));
        }
    }

    /**
     * From the query string determine the query type and related parameters.
     */
    private static void parseQueryString(URI requestURI, MetricsRequestBuilder builder) {

        Map<String, List<String>> queryParams = new QueryStringDecoder(requestURI).getParameters();

        // Extracts the query type.
        if (isTimeseriesRequest(queryParams)) {
            parseTimeseries(queryParams, builder);
        } else {
            boolean foundType = false;
            for (MetricsRequest.Type type : MetricsRequest.Type.values()) {
                if (Boolean.parseBoolean(getQueryParam(queryParams, type.name().toLowerCase(), "false"))) {
                    builder.setType(type);
                    foundType = true;
                    break;
                }
            }

            if (!foundType) {
                throw new IllegalArgumentException("Unknown query type for " + requestURI);
            }
        }
    }

    private static boolean isTimeseriesRequest(Map<String, List<String>> queryParams) {
        return queryParams.containsKey(COUNT) || queryParams.containsKey(START_TIME)
                || queryParams.containsKey(END_TIME);
    }

    private static void parseTimeseries(Map<String, List<String>> queryParams, MetricsRequestBuilder builder) {
        int count;
        long startTime;
        long endTime;
        long now = TimeUnit.SECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS);

        if (queryParams.containsKey(START_TIME) && queryParams.containsKey(END_TIME)) {
            startTime = TimeMathParser.parseTime(now, queryParams.get(START_TIME).get(0));
            endTime = TimeMathParser.parseTime(now, queryParams.get(END_TIME).get(0));
            count = (int) (endTime - startTime) + 1;
        } else if (queryParams.containsKey(COUNT)) {
            count = Integer.parseInt(queryParams.get(COUNT).get(0));
            // both start and end times are inclusive, which is the reason for the +-1.
            if (queryParams.containsKey(START_TIME)) {
                startTime = TimeMathParser.parseTime(now, queryParams.get(START_TIME).get(0));
                endTime = startTime + count - 1;
            } else if (queryParams.containsKey(END_TIME)) {
                endTime = TimeMathParser.parseTime(now, queryParams.get(END_TIME).get(0));
                startTime = endTime - count + 1;
            } else {
                // if only count is specified, assume the current time is desired as the end.
                endTime = now - MetricsConstants.QUERY_SECOND_DELAY;
                startTime = endTime - count + 1;
            }
        } else {
            throw new IllegalArgumentException("must specify 'count', or both 'start' and 'end'");
        }

        builder.setStartTime(startTime);
        builder.setEndTime(endTime);
        builder.setCount(count);
        builder.setType(MetricsRequest.Type.TIME_SERIES);
        setInterpolator(queryParams, builder);
    }

    private static void setInterpolator(Map<String, List<String>> queryParams, MetricsRequestBuilder builder) {
        Interpolator interpolator = null;

        if (queryParams.containsKey(INTERPOLATE)) {
            String interpolatorType = queryParams.get(INTERPOLATE).get(0);
            // timeLimit used in case there is a big gap in the data and we don't want to interpolate points.
            // the limit defines how big the gap has to be in seconds before we just say they're all zeroes.
            long timeLimit = queryParams.containsKey(MAX_INTERPOLATE_GAP)
                    ? Long.parseLong(queryParams.get(MAX_INTERPOLATE_GAP).get(0))
                    : Long.MAX_VALUE;

            if (STEP_INTERPOLATOR.equals(interpolatorType)) {
                interpolator = new Interpolators.Step(timeLimit);
            } else if (LINEAR_INTERPOLATOR.equals(interpolatorType)) {
                interpolator = new Interpolators.Linear(timeLimit);
            }
        }
        builder.setInterpolator(interpolator);
    }

    /**
     * Gets a query string parameter by the given key. It will returns the first value if available or the default value
     * if it is absent.
     */
    private static String getQueryParam(Map<String, List<String>> queries, String key, String defaultValue) {
        if (!queries.containsKey(key)) {
            return defaultValue;
        }
        List<String> values = queries.get(key);
        if (values.isEmpty()) {
            return defaultValue;
        }
        return values.get(0);
    }
}