// 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.core; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TimeZone; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.common.base.Objects; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; import net.opentsdb.stats.QueryStats; import net.opentsdb.utils.DateTime; /** * Parameters and state to query the underlying storage system for * timeseries data points. When setting up a query, use the setter methods to * store user information such as the start time and list of queries. After * setting the proper values, call the {@link #validateAndSetQuery()} method to * validate the request. If required information is missing or cannot be parsed * it will throw an exception. If validation passes, use * {@link #buildQueries(TSDB)} to compile the query into {@link Query} objects * for processing. * <b>Note:</b> If using POJO deserialization, make sure to avoid setting the * {@code start_time} and {@code end_time} fields. * @since 2.0 */ @JsonIgnoreProperties(ignoreUnknown = true) public final class TSQuery { /** User given start date/time, could be relative or absolute */ private String start; /** User given end date/time, could be relative, absolute or empty */ private String end; /** User's timezone used for converting absolute human readable dates */ private String timezone; /** Options for serializers, graphs, etc */ private HashMap<String, ArrayList<String>> options; /** * Whether or not to include padding, i.e. data to either side of the start/ * end dates */ private boolean padding; /** Whether or not to suppress annotation output */ private boolean no_annotations; /** Whether or not to scan for global annotations in the same time range */ private boolean with_global_annotations; /** Whether or not to show TSUIDs when returning data */ private boolean show_tsuids; /** A list of parsed sub queries, must have one or more to fetch data */ private ArrayList<TSSubQuery> queries; /** The parsed start time value * <b>Do not set directly</b> */ private long start_time; /** The parsed end time value * <b>Do not set directly</b> */ private long end_time; /** Whether or not the user wasn't millisecond resolution */ private boolean ms_resolution; /** Whether or not to show the sub query with the results */ private boolean show_query; /** Whether or not to include stats in the output */ private boolean show_stats; /** Whether or not to include stats summary in the output */ private boolean show_summary; /** Whether or not to delete the queried data */ private boolean delete = false; /** A flag denoting whether or not to align intervals based on the calendar */ private boolean use_calendar; /** The query status for tracking over all performance of this query */ private QueryStats query_stats; /** * Default constructor necessary for POJO de/serialization */ public TSQuery() { } @Override public int hashCode() { // NOTE: Do not add any non-user submitted variables to the hash. We don't // want the hash to change after validation. // We also don't care about stats or summary return Objects.hashCode(start, end, timezone, use_calendar, options, padding, no_annotations, with_global_annotations, show_tsuids, queries, ms_resolution); } @Override public boolean equals(final Object obj) { if (obj == null) { return false; } if (!(obj instanceof TSQuery)) { return false; } if (obj == this) { return true; } // NOTE: Do not add any non-user submitted variables to the comparator. We // don't want the value to change after validation. // We also don't care about stats or summary final TSQuery query = (TSQuery)obj; return Objects.equal(start, query.start) && Objects.equal(end, query.end) && Objects.equal(timezone, query.timezone) && Objects.equal(use_calendar,query.use_calendar) && Objects.equal(options, query.options) && Objects.equal(padding, query.padding) && Objects.equal(no_annotations, query.no_annotations) && Objects.equal(with_global_annotations, query.with_global_annotations) && Objects.equal(show_tsuids, query.show_tsuids) && Objects.equal(queries, query.queries) && Objects.equal(ms_resolution, query.ms_resolution); } /** * Runs through query parameters to make sure it's a valid request. * This includes parsing relative timestamps, verifying that the end time is * later than the start time (or isn't set), that one or more metrics or * TSUIDs are present, etc. If no exceptions are thrown, the query is * considered valid. * <b>Warning:</b> You must call this before passing it on for processing as * it sets the {@code start_time} and {@code end_time} fields as well as * sets the {@link TSSubQuery} fields necessary for execution. * @throws IllegalArgumentException if something is wrong with the query */ public void validateAndSetQuery() { if (start == null || start.isEmpty()) { throw new IllegalArgumentException("Missing start time"); } start_time = DateTime.parseDateTimeString(start, timezone); if (end != null && !end.isEmpty()) { end_time = DateTime.parseDateTimeString(end, timezone); } else { end_time = System.currentTimeMillis(); } if (end_time <= start_time) { throw new IllegalArgumentException( "End time [" + end_time + "] must be greater than the start time [" + start_time +"]"); } if (queries == null || queries.isEmpty()) { throw new IllegalArgumentException("Missing queries"); } // validate queries int i = 0; for (TSSubQuery sub : queries) { sub.validateAndSetQuery(); final DownsamplingSpecification ds = sub.downsamplingSpecification(); if (ds != null && timezone != null && !timezone.isEmpty() && ds != DownsamplingSpecification.NO_DOWNSAMPLER) { final TimeZone tz = DateTime.timezones.get(timezone); if (tz == null) { throw new IllegalArgumentException( "The timezone specification could not be found"); } ds.setTimezone(tz); } if (ds != null && use_calendar && ds != DownsamplingSpecification.NO_DOWNSAMPLER) { ds.setUseCalendar(true); } sub.setIndex(i++); } } /** * Compiles the TSQuery into an array of Query objects for execution. * If the user has not set a down sampler explicitly, and they don't want * millisecond resolution, then we set the down sampler to 1 second to handle * situations where storage may have multiple data points per second. * @param tsdb The tsdb to use for {@link TSDB#newQuery} * @return An array of queries */ public Query[] buildQueries(final TSDB tsdb) { try { return buildQueriesAsync(tsdb).joinUninterruptibly(); } catch (final Exception e) { throw new RuntimeException("Unexpected exception", e); } } /** * Compiles the TSQuery into an array of Query objects for execution. * If the user has not set a down sampler explicitly, and they don't want * millisecond resolution, then we set the down sampler to 1 second to handle * situations where storage may have multiple data points per second. * @param tsdb The tsdb to use for {@link TSDB#newQuery} * @return A deferred array of queries to wait on for compilation. * @since 2.2 */ public Deferred<Query[]> buildQueriesAsync(final TSDB tsdb) { final Query[] tsdb_queries = new Query[queries.size()]; final List<Deferred<Object>> deferreds = new ArrayList<Deferred<Object>>(queries.size()); for (int i = 0; i < queries.size(); i++) { final Query query = tsdb.newQuery(); deferreds.add(query.configureFromQuery(this, i)); tsdb_queries[i] = query; } class GroupFinished implements Callback<Query[], ArrayList<Object>> { @Override public Query[] call(final ArrayList<Object> deferreds) { return tsdb_queries; } @Override public String toString() { return "Query compile group callback"; } } return Deferred.group(deferreds).addCallback(new GroupFinished()); } public String toString() { final StringBuilder buf = new StringBuilder(); buf.append("TSQuery(start_time=") .append(start) .append(", end_time=") .append(end) .append(", subQueries["); if (queries != null && !queries.isEmpty()) { int counter = 0; for (TSSubQuery sub : queries) { if (counter > 0) { buf.append(", "); } buf.append(sub); counter++; } } buf.append("] padding=") .append(padding) .append(", no_annotations=") .append(no_annotations) .append(", with_global_annotations=") .append(with_global_annotations) .append(", show_tsuids=") .append(show_tsuids) .append(", ms_resolution=") .append(ms_resolution) .append(", options=["); if (options != null && !options.isEmpty()) { int counter = 0; for (Map.Entry<String, ArrayList<String>> entry : options.entrySet()) { if (counter > 0) { buf.append(", "); } buf.append(entry.getKey()) .append("=["); final ArrayList<String> values = entry.getValue(); for (int i = 0; i < values.size(); i++) { if (i > 0) { buf.append(", "); } buf.append(values.get(i)); } } } buf.append("])"); return buf.toString(); } /** @return the parsed start time for all queries */ public long startTime() { return this.start_time; } /** @return the parsed end time for all queries */ public long endTime() { return this.end_time; } /** @return the user given, raw start time */ public String getStart() { return start; } /** @return the user given, raw end time */ public String getEnd() { return end; } /** @return the user supplied timezone */ public String getTimezone() { return timezone; } /** @return a map of serializer options */ public Map<String, ArrayList<String>> getOptions() { return options; } /** @return whether or not the user wants padding */ public boolean getPadding() { return padding; } /** @return whether or not to supress annotatino output */ public boolean getNoAnnotations() { return no_annotations; } /** @return whether or not to load global annotations for the time range */ public boolean getGlobalAnnotations() { return with_global_annotations; } /** @return whether or not to display TSUIDs with the results */ public boolean getShowTSUIDs() { return show_tsuids; } /** @return the list of sub queries */ public List<TSSubQuery> getQueries() { return queries; } /** @return whether or not the requestor wants millisecond resolution */ public boolean getMsResolution() { return ms_resolution; } /** @return whether or not to show the query with the results */ public boolean getShowQuery() { return show_query; } /** @return whether or not to return stats per query */ public boolean getShowStats() { return show_stats; } /** @return Whether or not to show the query summary */ public boolean getShowSummary() { return this.show_summary; } /** @return Whether or not to delete the queried data @since 2.2 */ public boolean getDelete() { return this.delete; } /** @return the flag denoting whether intervals should be aligned based on * the calendar * @since 2.3 */ public boolean getUseCalendar() { return use_calendar; } /** @return the query stats object. Ignored during JSON serialization */ @JsonIgnore public QueryStats getQueryStats() { return query_stats; } /** * Sets the start time for further parsing. This can be an absolute or * relative value. See {@link DateTime#parseDateTimeString} for details. * @param start A start time from the user */ public void setStart(String start) { this.start = start; } /** * Optionally sets the end time for all queries. If not set, the current * system time will be used. This can be an absolute or relative value. See * {@link DateTime#parseDateTimeString} for details. * @param end An end time from the user */ public void setEnd(String end) { this.end = end; } /** @param timezone an optional timezone for date parsing */ public void setTimezone(String timezone) { this.timezone = timezone; } /** @param options a map of options to pass on to the serializer */ public void setOptions(HashMap<String, ArrayList<String>> options) { this.options = options; } /** @param padding whether or not the query should include padding */ public void setPadding(boolean padding) { this.padding = padding; } /** @param no_annotations whether or not to suppress annotation output */ public void setNoAnnotations(boolean no_annotations) { this.no_annotations = no_annotations; } /** @param with_global whether or not to load global annotations */ public void setGlobalAnnotations(boolean with_global) { with_global_annotations = with_global; } /** @param show_tsuids whether or not to show TSUIDs in output */ public void setShowTSUIDs(boolean show_tsuids) { this.show_tsuids = show_tsuids; } /** @param queries a list of {@link TSSubQuery} objects to store*/ public void setQueries(ArrayList<TSSubQuery> queries) { this.queries = queries; } /** @param ms_resolution whether or not the user wants millisecond resolution */ public void setMsResolution(boolean ms_resolution) { this.ms_resolution = ms_resolution; } /** @param show_query whether or not to show the query with the serialization */ public void setShowQuery(boolean show_query) { this.show_query = show_query; } /** @param show_stats whether or not to show stats in the serialization */ public void setShowStats(boolean show_stats) { this.show_stats = show_stats; } /** @param show_summary whether or not to show the query summary */ public void setShowSummary(boolean show_summary) { this.show_summary = show_summary; } /** @param delete whether or not to delete the queried data @since 2.2 */ public void setDelete(boolean delete) { this.delete = delete; } /** @param use_calendar a flag denoting whether or not to align intervals * based on the calendar @since 2.3 */ public void setUseCalendar(boolean use_calendar) { this.use_calendar = use_calendar; } /** @param query_stats the query stats object to associate with this query */ public void setQueryStats(final QueryStats query_stats) { this.query_stats = query_stats; } }