// This file is part of OpenTSDB.
// Copyright (C) 2013 The OpenTSDB Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 2.1 of the License, or (at your
// option) any later version. This program is distributed in the hope that it
// will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
// General Public License for more details. You should have received a copy
// of the GNU Lesser General Public License along with this program. If not,
// see <http://www.gnu.org/licenses/>.
package net.opentsdb.tsd;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import org.hbase.async.HBaseException;
import org.hbase.async.RpcTimedOutException;
import org.hbase.async.Bytes.ByteMap;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.stumbleupon.async.Callback;
import com.stumbleupon.async.Deferred;
import com.stumbleupon.async.DeferredGroupException;
import net.opentsdb.core.DataPoints;
import net.opentsdb.core.IncomingDataPoint;
import net.opentsdb.core.Query;
import net.opentsdb.core.QueryException;
import net.opentsdb.core.RateOptions;
import net.opentsdb.core.TSDB;
import net.opentsdb.core.TSQuery;
import net.opentsdb.core.TSSubQuery;
import net.opentsdb.core.Tags;
import net.opentsdb.meta.Annotation;
import net.opentsdb.meta.TSUIDQuery;
import net.opentsdb.query.expression.ExpressionTree;
import net.opentsdb.query.expression.Expressions;
import net.opentsdb.query.filter.TagVFilter;
import net.opentsdb.stats.QueryStats;
import net.opentsdb.stats.StatsCollector;
import net.opentsdb.uid.NoSuchUniqueName;
import net.opentsdb.uid.UniqueId;
import net.opentsdb.utils.DateTime;
import net.opentsdb.utils.JSON;
/**
* Handles queries for timeseries datapoints. Each request is parsed into a
* TSQuery object, the values given validated, and if all tests pass, the
* query is converted into TsdbQueries and each one is executed to fetch the
* data. The resulting DataPoints[] are then passed to serializers for
* formatting.
* <p>
* Some private methods are included for parsing query string data into a
* TSQuery object.
* @since 2.0
*/
final class QueryRpc implements HttpRpc {
private static final Logger LOG = LoggerFactory.getLogger(QueryRpc.class);
/** Various counters and metrics for reporting query stats */
static final AtomicLong query_invalid = new AtomicLong();
static final AtomicLong query_exceptions = new AtomicLong();
static final AtomicLong query_success = new AtomicLong();
/**
* Implements the /api/query endpoint to fetch data from OpenTSDB.
* @param tsdb The TSDB to use for fetching data
* @param query The HTTP query for parsing and responding
*/
@Override
public void execute(final TSDB tsdb, final HttpQuery query)
throws IOException {
// only accept GET/POST/DELETE
if (query.method() != HttpMethod.GET && query.method() != HttpMethod.POST &&
query.method() != HttpMethod.DELETE) {
throw new BadRequestException(HttpResponseStatus.METHOD_NOT_ALLOWED,
"Method not allowed", "The HTTP method [" + query.method().getName() +
"] is not permitted for this endpoint");
}
if (query.method() == HttpMethod.DELETE &&
!tsdb.getConfig().getBoolean("tsd.http.query.allow_delete")) {
throw new BadRequestException(HttpResponseStatus.BAD_REQUEST,
"Bad request",
"Deleting data is not enabled (tsd.http.query.allow_delete=false)");
}
final String[] uri = query.explodeAPIPath();
final String endpoint = uri.length > 1 ? uri[1] : "";
if (endpoint.toLowerCase().equals("last")) {
handleLastDataPointQuery(tsdb, query);
} else if (endpoint.toLowerCase().equals("gexp")){
handleQuery(tsdb, query, true);
} else if (endpoint.toLowerCase().equals("exp")) {
handleExpressionQuery(tsdb, query);
return;
} else {
handleQuery(tsdb, query, false);
}
}
/**
* Processing for a data point query
* @param tsdb The TSDB to which we belong
* @param query The HTTP query to parse/respond
* @param allow_expressions Whether or not expressions should be parsed
* (based on the endpoint)
*/
private void handleQuery(final TSDB tsdb, final HttpQuery query,
final boolean allow_expressions) {
final long start = DateTime.currentTimeMillis();
final TSQuery data_query;
final List<ExpressionTree> expressions;
if (query.method() == HttpMethod.POST) {
switch (query.apiVersion()) {
case 0:
case 1:
data_query = query.serializer().parseQueryV1();
break;
default:
query_invalid.incrementAndGet();
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"Requested API version not implemented", "Version " +
query.apiVersion() + " is not implemented");
}
expressions = null;
} else {
expressions = new ArrayList<ExpressionTree>();
data_query = parseQuery(tsdb, query, expressions);
}
if (query.getAPIMethod() == HttpMethod.DELETE &&
tsdb.getConfig().getBoolean("tsd.http.query.allow_delete")) {
data_query.setDelete(true);
}
// validate and then compile the queries
try {
LOG.debug(data_query.toString());
data_query.validateAndSetQuery();
} catch (Exception e) {
throw new BadRequestException(HttpResponseStatus.BAD_REQUEST,
e.getMessage(), data_query.toString(), e);
}
// if the user tried this query multiple times from the same IP and src port
// they'll be rejected on subsequent calls
final QueryStats query_stats =
new QueryStats(query.getRemoteAddress(), data_query,
query.getPrintableHeaders());
data_query.setQueryStats(query_stats);
query.setStats(query_stats);
final int nqueries = data_query.getQueries().size();
final ArrayList<DataPoints[]> results = new ArrayList<DataPoints[]>(nqueries);
final List<Annotation> globals = new ArrayList<Annotation>();
/** This has to be attached to callbacks or we may never respond to clients */
class ErrorCB implements Callback<Object, Exception> {
public Object call(final Exception e) throws Exception {
Throwable ex = e;
try {
LOG.error("Query exception: ", e);
if (ex instanceof DeferredGroupException) {
ex = e.getCause();
while (ex != null && ex instanceof DeferredGroupException) {
ex = ex.getCause();
}
if (ex == null) {
LOG.error("The deferred group exception didn't have a cause???");
}
}
if (ex instanceof RpcTimedOutException) {
query_stats.markSerialized(HttpResponseStatus.REQUEST_TIMEOUT, ex);
query.badRequest(new BadRequestException(
HttpResponseStatus.REQUEST_TIMEOUT, ex.getMessage()));
query_exceptions.incrementAndGet();
} else if (ex instanceof HBaseException) {
query_stats.markSerialized(HttpResponseStatus.FAILED_DEPENDENCY, ex);
query.badRequest(new BadRequestException(
HttpResponseStatus.FAILED_DEPENDENCY, ex.getMessage()));
query_exceptions.incrementAndGet();
} else if (ex instanceof QueryException) {
query_stats.markSerialized(((QueryException)ex).getStatus(), ex);
query.badRequest(new BadRequestException(
((QueryException)ex).getStatus(), ex.getMessage()));
query_exceptions.incrementAndGet();
} else if (ex instanceof BadRequestException) {
query_stats.markSerialized(((BadRequestException)ex).getStatus(), ex);
query.badRequest((BadRequestException)ex);
query_invalid.incrementAndGet();
} else if (ex instanceof NoSuchUniqueName) {
query_stats.markSerialized(HttpResponseStatus.BAD_REQUEST, ex);
query.badRequest(new BadRequestException(ex));
query_invalid.incrementAndGet();
} else {
query_stats.markSerialized(HttpResponseStatus.INTERNAL_SERVER_ERROR, ex);
query.badRequest(new BadRequestException(ex));
query_exceptions.incrementAndGet();
}
} catch (RuntimeException ex2) {
LOG.error("Exception thrown during exception handling", ex2);
query_stats.markSerialized(HttpResponseStatus.INTERNAL_SERVER_ERROR, ex2);
query.sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR,
ex2.getMessage().getBytes());
query_exceptions.incrementAndGet();
}
return null;
}
}
/**
* After all of the queries have run, we get the results in the order given
* and add dump the results in an array
*/
class QueriesCB implements Callback<Object, ArrayList<DataPoints[]>> {
public Object call(final ArrayList<DataPoints[]> query_results)
throws Exception {
if (allow_expressions) {
// process each of the expressions into a new list, then merge it
// with the original. This avoids possible recursion loops.
final List<DataPoints[]> expression_results =
new ArrayList<DataPoints[]>(expressions.size());
// let exceptions bubble up
for (final ExpressionTree expression : expressions) {
expression_results.add(expression.evaluate(query_results));
}
results.addAll(expression_results);
} else {
results.addAll(query_results);
}
/** Simply returns the buffer once serialization is complete and logs it */
class SendIt implements Callback<Object, ChannelBuffer> {
public Object call(final ChannelBuffer buffer) throws Exception {
query.sendReply(buffer);
query_success.incrementAndGet();
return null;
}
}
switch (query.apiVersion()) {
case 0:
case 1:
query.serializer().formatQueryAsyncV1(data_query, results,
globals).addCallback(new SendIt()).addErrback(new ErrorCB());
break;
default:
query_invalid.incrementAndGet();
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"Requested API version not implemented", "Version " +
query.apiVersion() + " is not implemented");
}
return null;
}
}
/**
* Callback executed after we have resolved the metric, tag names and tag
* values to their respective UIDs. This callback then runs the actual
* queries and fetches their results.
*/
class BuildCB implements Callback<Deferred<Object>, Query[]> {
@Override
public Deferred<Object> call(final Query[] queries) {
final ArrayList<Deferred<DataPoints[]>> deferreds =
new ArrayList<Deferred<DataPoints[]>>(queries.length);
for (final Query query : queries) {
deferreds.add(query.runAsync());
}
return Deferred.groupInOrder(deferreds).addCallback(new QueriesCB());
}
}
/** Handles storing the global annotations after fetching them */
class GlobalCB implements Callback<Object, List<Annotation>> {
public Object call(final List<Annotation> annotations) throws Exception {
globals.addAll(annotations);
return data_query.buildQueriesAsync(tsdb).addCallback(new BuildCB());
}
}
// if we the caller wants to search for global annotations, fire that off
// first then scan for the notes, then pass everything off to the formatter
// when complete
if (!data_query.getNoAnnotations() && data_query.getGlobalAnnotations()) {
Annotation.getGlobalAnnotations(tsdb,
data_query.startTime() / 1000, data_query.endTime() / 1000)
.addCallback(new GlobalCB()).addErrback(new ErrorCB());
} else {
data_query.buildQueriesAsync(tsdb).addCallback(new BuildCB())
.addErrback(new ErrorCB());
}
}
/**
* Handles an expression query
* @param tsdb The TSDB to which we belong
* @param query The HTTP query to parse/respond
* @since 2.3
*/
private void handleExpressionQuery(final TSDB tsdb, final HttpQuery query) {
final net.opentsdb.query.pojo.Query v2_query =
JSON.parseToObject(query.getContent(), net.opentsdb.query.pojo.Query.class);
v2_query.validate();
final QueryExecutor executor = new QueryExecutor(tsdb, v2_query);
executor.execute(query);
}
/**
* Processes a last data point query
* @param tsdb The TSDB to which we belong
* @param query The HTTP query to parse/respond
*/
private void handleLastDataPointQuery(final TSDB tsdb, final HttpQuery query) {
final LastPointQuery data_query;
if (query.method() == HttpMethod.POST) {
switch (query.apiVersion()) {
case 0:
case 1:
data_query = query.serializer().parseLastPointQueryV1();
break;
default:
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"Requested API version not implemented", "Version " +
query.apiVersion() + " is not implemented");
}
} else {
data_query = this.parseLastPointQuery(tsdb, query);
}
if (data_query.sub_queries == null || data_query.sub_queries.isEmpty()) {
throw new BadRequestException(HttpResponseStatus.BAD_REQUEST,
"Missing sub queries");
}
// a list of deferreds to wait on
final ArrayList<Deferred<Object>> calls = new ArrayList<Deferred<Object>>();
// final results for serialization
final List<IncomingDataPoint> results = new ArrayList<IncomingDataPoint>();
/**
* Used to catch exceptions
*/
final class ErrBack implements Callback<Object, Exception> {
public Object call(final Exception e) throws Exception {
Throwable ex = e;
while (ex.getClass().equals(DeferredGroupException.class)) {
if (ex.getCause() == null) {
LOG.warn("Unable to get to the root cause of the DGE");
break;
}
ex = ex.getCause();
}
if (ex instanceof RuntimeException) {
throw new BadRequestException(ex);
} else {
throw e;
}
}
@Override
public String toString() {
return "Error back";
}
}
final class FetchCB implements Callback<Deferred<Object>, ArrayList<IncomingDataPoint>> {
@Override
public Deferred<Object> call(final ArrayList<IncomingDataPoint> dps) throws Exception {
synchronized(results) {
for (final IncomingDataPoint dp : dps) {
if (dp != null) {
results.add(dp);
}
}
}
return Deferred.fromResult(null);
}
@Override
public String toString() {
return "Fetched data points CB";
}
}
/**
* Called after scanning the tsdb-meta table for TSUIDs that match the given
* metric and/or tags. If matches were found, it fires off a number of
* getLastPoint requests, adding the deferreds to the calls list
*/
final class TSUIDQueryCB implements Callback<Deferred<Object>, ByteMap<Long>> {
public Deferred<Object> call(final ByteMap<Long> tsuids) throws Exception {
if (tsuids == null || tsuids.isEmpty()) {
return null;
}
final ArrayList<Deferred<IncomingDataPoint>> deferreds =
new ArrayList<Deferred<IncomingDataPoint>>(tsuids.size());
for (Map.Entry<byte[], Long> entry : tsuids.entrySet()) {
deferreds.add(TSUIDQuery.getLastPoint(tsdb, entry.getKey(),
data_query.getResolveNames(), data_query.getBackScan(),
entry.getValue()));
}
return Deferred.group(deferreds).addCallbackDeferring(new FetchCB());
}
@Override
public String toString() {
return "TSMeta scan CB";
}
}
/**
* Used to wait on the list of data point deferreds. Once they're all done
* this will return the results to the call via the serializer
*/
final class FinalCB implements Callback<Object, ArrayList<Object>> {
public Object call(final ArrayList<Object> done) throws Exception {
query.sendReply(query.serializer().formatLastPointQueryV1(results));
return null;
}
@Override
public String toString() {
return "Final CB";
}
}
try {
// start executing the queries
for (final LastPointSubQuery sub_query : data_query.getQueries()) {
final ArrayList<Deferred<IncomingDataPoint>> deferreds =
new ArrayList<Deferred<IncomingDataPoint>>();
// TSUID queries take precedence so if there are any TSUIDs listed,
// process the TSUIDs and ignore the metric/tags
if (sub_query.getTSUIDs() != null && !sub_query.getTSUIDs().isEmpty()) {
for (final String tsuid : sub_query.getTSUIDs()) {
final TSUIDQuery tsuid_query = new TSUIDQuery(tsdb,
UniqueId.stringToUid(tsuid));
deferreds.add(tsuid_query.getLastPoint(data_query.getResolveNames(),
data_query.getBackScan()));
}
} else {
@SuppressWarnings("unchecked")
final TSUIDQuery tsuid_query =
new TSUIDQuery(tsdb, sub_query.getMetric(),
sub_query.getTags() != null ?
sub_query.getTags() : Collections.EMPTY_MAP);
if (data_query.getBackScan() > 0) {
deferreds.add(tsuid_query.getLastPoint(data_query.getResolveNames(),
data_query.getBackScan()));
} else {
calls.add(tsuid_query.getLastWriteTimes()
.addCallbackDeferring(new TSUIDQueryCB()));
}
}
if (deferreds.size() > 0) {
calls.add(Deferred.group(deferreds).addCallbackDeferring(new FetchCB()));
}
}
Deferred.group(calls)
.addCallback(new FinalCB())
.addErrback(new ErrBack())
.joinUninterruptibly();
} catch (Exception e) {
Throwable ex = e;
while (ex.getClass().equals(DeferredGroupException.class)) {
if (ex.getCause() == null) {
LOG.warn("Unable to get to the root cause of the DGE");
break;
}
ex = ex.getCause();
}
if (ex instanceof RuntimeException) {
throw new BadRequestException(ex);
} else {
throw new RuntimeException("Shouldn't be here", e);
}
}
}
/**
* Parses a query string legacy style query from the URI
* @param tsdb The TSDB we belong to
* @param query The HTTP Query for parsing
* @return A TSQuery if parsing was successful
* @throws BadRequestException if parsing was unsuccessful
* @since 2.3
*/
public static TSQuery parseQuery(final TSDB tsdb, final HttpQuery query) {
return parseQuery(tsdb, query, null);
}
/**
* Parses a query string legacy style query from the URI
* @param tsdb The TSDB we belong to
* @param query The HTTP Query for parsing
* @param expressions A list of parsed expression trees filled from the URI.
* If this is null, it means any expressions in the URI will be skipped.
* @return A TSQuery if parsing was successful
* @throws BadRequestException if parsing was unsuccessful
* @since 2.3
*/
public static TSQuery parseQuery(final TSDB tsdb, final HttpQuery query,
final List<ExpressionTree> expressions) {
final TSQuery data_query = new TSQuery();
data_query.setStart(query.getRequiredQueryStringParam("start"));
data_query.setEnd(query.getQueryStringParam("end"));
if (query.hasQueryStringParam("padding")) {
data_query.setPadding(true);
}
if (query.hasQueryStringParam("no_annotations")) {
data_query.setNoAnnotations(true);
}
if (query.hasQueryStringParam("global_annotations")) {
data_query.setGlobalAnnotations(true);
}
if (query.hasQueryStringParam("show_tsuids")) {
data_query.setShowTSUIDs(true);
}
if (query.hasQueryStringParam("ms")) {
data_query.setMsResolution(true);
}
if (query.hasQueryStringParam("show_query")) {
data_query.setShowQuery(true);
}
if (query.hasQueryStringParam("show_stats")) {
data_query.setShowStats(true);
}
if (query.hasQueryStringParam("show_summary")) {
data_query.setShowSummary(true);
}
// handle tsuid queries first
if (query.hasQueryStringParam("tsuid")) {
final List<String> tsuids = query.getQueryStringParams("tsuid");
for (String q : tsuids) {
parseTsuidTypeSubQuery(q, data_query);
}
}
if (query.hasQueryStringParam("m")) {
final List<String> legacy_queries = query.getQueryStringParams("m");
for (String q : legacy_queries) {
parseMTypeSubQuery(q, data_query);
}
}
// TODO - testing out the graphite style expressions here with the "exp"
// param that could stand for experimental or expression ;)
if (expressions != null) {
if (query.hasQueryStringParam("exp")) {
final List<String> uri_expressions = query.getQueryStringParams("exp");
final List<String> metric_queries = new ArrayList<String>(
uri_expressions.size());
// parse the expressions into their trees. If one or more expressions
// are improper then it will toss an exception up
expressions.addAll(Expressions.parseExpressions(
uri_expressions, data_query, metric_queries));
// iterate over each of the parsed metric queries and store it in the
// TSQuery list so that we fetch the data for them.
for (final String mq: metric_queries) {
parseMTypeSubQuery(mq, data_query);
}
}
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("Received a request with an expression but at the "
+ "wrong endpoint: " + query);
}
}
if (data_query.getQueries() == null || data_query.getQueries().size() < 1) {
throw new BadRequestException("Missing sub queries");
}
return data_query;
}
/**
* Parses a query string "m=..." type query and adds it to the TSQuery.
* This will generate a TSSubQuery and add it to the TSQuery if successful
* @param query_string The value of the m query string parameter, i.e. what
* comes after the equals sign
* @param data_query The query we're building
* @throws BadRequestException if we are unable to parse the query or it is
* missing components
*/
private static void parseMTypeSubQuery(final String query_string,
TSQuery data_query) {
if (query_string == null || query_string.isEmpty()) {
throw new BadRequestException("The query string was empty");
}
// m is of the following forms:
// agg:[interval-agg:][rate:]metric[{tag=value,...}]
// where the parts in square brackets `[' .. `]' are optional.
final String[] parts = Tags.splitString(query_string, ':');
int i = parts.length;
if (i < 2 || i > 5) {
throw new BadRequestException("Invalid parameter m=" + query_string + " ("
+ (i < 2 ? "not enough" : "too many") + " :-separated parts)");
}
final TSSubQuery sub_query = new TSSubQuery();
// the aggregator is first
sub_query.setAggregator(parts[0]);
i--; // Move to the last part (the metric name).
List<TagVFilter> filters = new ArrayList<TagVFilter>();
sub_query.setMetric(Tags.parseWithMetricAndFilters(parts[i], filters));
sub_query.setFilters(filters);
// parse out the rate and downsampler
for (int x = 1; x < parts.length - 1; x++) {
if (parts[x].toLowerCase().startsWith("rate")) {
sub_query.setRate(true);
if (parts[x].indexOf("{") >= 0) {
sub_query.setRateOptions(QueryRpc.parseRateOptions(true, parts[x]));
}
} else if (Character.isDigit(parts[x].charAt(0))) {
sub_query.setDownsample(parts[x]);
} else if (parts[x].toLowerCase().startsWith("explicit_tags")) {
sub_query.setExplicitTags(true);
}
}
if (data_query.getQueries() == null) {
final ArrayList<TSSubQuery> subs = new ArrayList<TSSubQuery>(1);
data_query.setQueries(subs);
}
data_query.getQueries().add(sub_query);
}
/**
* Parses a "tsuid=..." type query and adds it to the TSQuery.
* This will generate a TSSubQuery and add it to the TSQuery if successful
* @param query_string The value of the m query string parameter, i.e. what
* comes after the equals sign
* @param data_query The query we're building
* @throws BadRequestException if we are unable to parse the query or it is
* missing components
*/
private static void parseTsuidTypeSubQuery(final String query_string,
TSQuery data_query) {
if (query_string == null || query_string.isEmpty()) {
throw new BadRequestException("The tsuid query string was empty");
}
// tsuid queries are of the following forms:
// agg:[interval-agg:][rate:]tsuid[,s]
// where the parts in square brackets `[' .. `]' are optional.
final String[] parts = Tags.splitString(query_string, ':');
int i = parts.length;
if (i < 2 || i > 5) {
throw new BadRequestException("Invalid parameter m=" + query_string + " ("
+ (i < 2 ? "not enough" : "too many") + " :-separated parts)");
}
final TSSubQuery sub_query = new TSSubQuery();
// the aggregator is first
sub_query.setAggregator(parts[0]);
i--; // Move to the last part (the metric name).
final List<String> tsuid_array = Arrays.asList(parts[i].split(","));
sub_query.setTsuids(tsuid_array);
// parse out the rate and downsampler
for (int x = 1; x < parts.length - 1; x++) {
if (parts[x].toLowerCase().startsWith("rate")) {
sub_query.setRate(true);
if (parts[x].indexOf("{") >= 0) {
sub_query.setRateOptions(QueryRpc.parseRateOptions(true, parts[x]));
}
} else if (Character.isDigit(parts[x].charAt(0))) {
sub_query.setDownsample(parts[x]);
}
}
if (data_query.getQueries() == null) {
final ArrayList<TSSubQuery> subs = new ArrayList<TSSubQuery>(1);
data_query.setQueries(subs);
}
data_query.getQueries().add(sub_query);
}
/**
* Parses the "rate" section of the query string and returns an instance
* of the RateOptions class that contains the values found.
* <p/>
* The format of the rate specification is rate[{counter[,#[,#]]}].
* @param rate If true, then the query is set as a rate query and the rate
* specification will be parsed. If false, a default RateOptions instance
* will be returned and largely ignored by the rest of the processing
* @param spec The part of the query string that pertains to the rate
* @return An initialized RateOptions instance based on the specification
* @throws BadRequestException if the parameter is malformed
* @since 2.0
*/
static final public RateOptions parseRateOptions(final boolean rate,
final String spec) {
if (!rate || spec.length() == 4) {
return new RateOptions(false, Long.MAX_VALUE,
RateOptions.DEFAULT_RESET_VALUE);
}
if (spec.length() < 6) {
throw new BadRequestException("Invalid rate options specification: "
+ spec);
}
String[] parts = Tags
.splitString(spec.substring(5, spec.length() - 1), ',');
if (parts.length < 1 || parts.length > 3) {
throw new BadRequestException(
"Incorrect number of values in rate options specification, must be " +
"counter[,counter max value,reset value], recieved: "
+ parts.length + " parts");
}
final boolean counter = parts[0].endsWith("counter");
try {
final long max = (parts.length >= 2 && parts[1].length() > 0 ? Long
.parseLong(parts[1]) : Long.MAX_VALUE);
try {
final long reset = (parts.length >= 3 && parts[2].length() > 0 ? Long
.parseLong(parts[2]) : RateOptions.DEFAULT_RESET_VALUE);
final boolean drop_counter = parts[0].equals("dropcounter");
return new RateOptions(counter, max, reset, drop_counter);
} catch (NumberFormatException e) {
throw new BadRequestException(
"Reset value of counter was not a number, received '" + parts[2]
+ "'");
}
} catch (NumberFormatException e) {
throw new BadRequestException(
"Max value of counter was not a number, received '" + parts[1] + "'");
}
}
/**
* Parses a last point query from the URI string
* @param tsdb The TSDB to which we belong
* @param http_query The HTTP query to work with
* @return A LastPointQuery object to execute against
* @throws BadRequestException if parsing failed
*/
private LastPointQuery parseLastPointQuery(final TSDB tsdb,
final HttpQuery http_query) {
final LastPointQuery query = new LastPointQuery();
if (http_query.hasQueryStringParam("resolve")) {
query.setResolveNames(true);
}
if (http_query.hasQueryStringParam("back_scan")) {
try {
query.setBackScan(Integer.parseInt(http_query.getQueryStringParam("back_scan")));
} catch (NumberFormatException e) {
throw new BadRequestException("Unable to parse back_scan parameter");
}
}
final List<String> ts_queries = http_query.getQueryStringParams("timeseries");
final List<String> tsuid_queries = http_query.getQueryStringParams("tsuids");
final int num_queries =
(ts_queries != null ? ts_queries.size() : 0) +
(tsuid_queries != null ? tsuid_queries.size() : 0);
final List<LastPointSubQuery> sub_queries =
new ArrayList<LastPointSubQuery>(num_queries);
if (ts_queries != null) {
for (String ts_query : ts_queries) {
sub_queries.add(LastPointSubQuery.parseTimeSeriesQuery(ts_query));
}
}
if (tsuid_queries != null) {
for (String tsuid_query : tsuid_queries) {
sub_queries.add(LastPointSubQuery.parseTSUIDQuery(tsuid_query));
}
}
query.setQueries(sub_queries);
return query;
}
/** @param collector Populates the collector with statistics */
public static void collectStats(final StatsCollector collector) {
collector.record("http.query.invalid_requests", query_invalid);
collector.record("http.query.exceptions", query_exceptions);
collector.record("http.query.success", query_success);
}
public static class LastPointQuery {
private boolean resolve_names;
private int back_scan;
private List<LastPointSubQuery> sub_queries;
/**
* Default Constructor necessary for de/serialization
*/
public LastPointQuery() {
}
/** @return Whether or not to resolve the UIDs to names */
public boolean getResolveNames() {
return resolve_names;
}
/** @return Number of hours to scan back in time looking for data */
public int getBackScan() {
return back_scan;
}
/** @return A list of sub queries */
public List<LastPointSubQuery> getQueries() {
return sub_queries;
}
/** @param resolve_names Whether or not to resolve the UIDs to names */
public void setResolveNames(final boolean resolve_names) {
this.resolve_names = resolve_names;
}
/** @param back_scan Number of hours to scan back in time looking for data */
public void setBackScan(final int back_scan) {
this.back_scan = back_scan;
}
/** @param queries A list of sub queries to execute */
public void setQueries(final List<LastPointSubQuery> queries) {
this.sub_queries = queries;
}
}
public static class LastPointSubQuery {
private String metric;
private HashMap<String, String> tags;
private List<String> tsuids;
/**
* Default constructor necessary for de/serialization
*/
public LastPointSubQuery() {
}
public static LastPointSubQuery parseTimeSeriesQuery(final String query) {
final LastPointSubQuery sub_query = new LastPointSubQuery();
sub_query.tags = new HashMap<String, String>();
sub_query.metric = Tags.parseWithMetric(query, sub_query.tags);
return sub_query;
}
public static LastPointSubQuery parseTSUIDQuery(final String query) {
final LastPointSubQuery sub_query = new LastPointSubQuery();
final String[] tsuids = query.split(",");
sub_query.tsuids = new ArrayList<String>(tsuids.length);
for (String tsuid : tsuids) {
sub_query.tsuids.add(tsuid);
}
return sub_query;
}
/** @return The name of the metric to search for */
public String getMetric() {
return metric;
}
/** @return A map of tag names and values */
public Map<String, String> getTags() {
return tags;
}
/** @return A list of TSUIDs to get the last point for */
public List<String> getTSUIDs() {
return tsuids;
}
/** @param metric The metric to search for */
public void setMetric(final String metric) {
this.metric = metric;
}
/** @param tags A map of tag name/value pairs */
public void setTags(final Map<String, String> tags) {
this.tags = (HashMap<String, String>) tags;
}
/** @param tsuids A list of TSUIDs to get data for */
public void setTSUIDs(final List<String> tsuids) {
this.tsuids = tsuids;
}
}
}