// 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.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; import com.stumbleupon.async.DeferredGroupException; import net.opentsdb.core.RowKey; import net.opentsdb.core.TSDB; import net.opentsdb.core.Tags; import net.opentsdb.search.SearchQuery; import net.opentsdb.search.TimeSeriesLookup; import net.opentsdb.search.SearchQuery.SearchType; import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; import net.opentsdb.uid.UniqueId.UniqueIdType; import net.opentsdb.utils.Exceptions; import net.opentsdb.utils.Pair; /** * Handles very basic search calls by passing the user's query to the configured * search plugin and pushing the response back through the serializers. * Also allows for time series lookups given a metric, tag name, tag value or * combination thereof using the tsdb-meta table. * @since 2.0 */ final class SearchRpc implements HttpRpc { /** * Handles the /api/search/<type> endpoint * @param tsdb The TSDB to which we belong * @param query The HTTP query to work with */ @Override public void execute(TSDB tsdb, HttpQuery query) { final HttpMethod method = query.getAPIMethod(); if (method != HttpMethod.GET && method != HttpMethod.POST) { throw new BadRequestException("Unsupported method: " + method.getName()); } // the uri will be /api/vX/search/<type> or /api/search/<type> final String[] uri = query.explodeAPIPath(); final String endpoint = uri.length > 1 ? uri[1] : ""; final SearchType type; final SearchQuery search_query; try { type = SearchQuery.parseSearchType(endpoint); } catch (IllegalArgumentException e) { throw new BadRequestException("Invalid search query type supplied", e); } if (query.hasContent()) { search_query = query.serializer().parseSearchQueryV1(); } else { search_query = parseQueryString(query, type); } search_query.setType(type); if (type == SearchType.LOOKUP) { processLookup(tsdb, query, search_query); return; } try { final SearchQuery results = tsdb.executeSearch(search_query).joinUninterruptibly(); query.sendReply(query.serializer().formatSearchResultsV1(results)); } catch (IllegalStateException e) { throw new BadRequestException("Searching is not enabled", e); } catch (Exception e) { throw new RuntimeException(e); } } /** * Parses required search values from the query string * @param query The HTTP query to work with * @param type The type of search query requested * @return A parsed SearchQuery object */ private final SearchQuery parseQueryString(final HttpQuery query, final SearchType type) { final SearchQuery search_query = new SearchQuery(); if (type == SearchType.LOOKUP) { final String query_string = query.getRequiredQueryStringParam("m"); search_query.setTags(new ArrayList<Pair<String, String>>()); try { search_query.setMetric(Tags.parseWithMetric(query_string, search_query.getTags())); } catch (IllegalArgumentException e) { throw new BadRequestException("Unable to parse query", e); } if (query.hasQueryStringParam("limit")) { final String limit = query.getQueryStringParam("limit"); try { search_query.setLimit(Integer.parseInt(limit)); } catch (NumberFormatException e) { throw new BadRequestException( "Unable to convert 'limit' to a valid number"); } } return search_query; } // process a regular search query search_query.setQuery(query.getRequiredQueryStringParam("query")); if (query.hasQueryStringParam("limit")) { final String limit = query.getQueryStringParam("limit"); try { search_query.setLimit(Integer.parseInt(limit)); } catch (NumberFormatException e) { throw new BadRequestException( "Unable to convert 'limit' to a valid number"); } } if (query.hasQueryStringParam("start_index")) { final String idx = query.getQueryStringParam("start_index"); try { search_query.setStartIndex(Integer.parseInt(idx)); } catch (NumberFormatException e) { throw new BadRequestException( "Unable to convert 'start_index' to a valid number"); } } return search_query; } /** * Processes a lookup query against the tsdb-meta table, returning (and * resolving) the TSUIDs of any series that matched the query. * @param tsdb The TSDB to which we belong * @param query The HTTP query to work with * @param search_query A search query configured with at least a metric * or a list of tag pairs. If neither are set, the method will throw an error. * @throws BadRequestException if the metric and tags are null or empty or * a UID fails to resolve. * @since 2.1 */ private void processLookup(final TSDB tsdb, final HttpQuery query, final SearchQuery search_query) { if (search_query.getMetric() == null && (search_query.getTags() == null || search_query.getTags().size() < 1)) { throw new BadRequestException( "Missing metric and tags. Please supply at least one value."); } final long start = System.currentTimeMillis(); class MetricCB implements Callback<Object, String> { final Map<String, Object> series; MetricCB(final Map<String, Object> series) { this.series = series; } @Override public Object call(final String name) throws Exception { series.put("metric", name); return null; } } class TagsCB implements Callback<Object, HashMap<String, String>> { final Map<String, Object> series; TagsCB(final Map<String, Object> series) { this.series = series; } @Override public Object call(final HashMap<String, String> names) throws Exception { series.put("tags", names); return null; } } class Serialize implements Callback<Object, ArrayList<Object>> { final List<Object> results; Serialize(final List<Object> results) { this.results = results; } @Override public Object call(final ArrayList<Object> ignored) throws Exception { search_query.setResults(results); search_query.setTime(System.currentTimeMillis() - start); query.sendReply(query.serializer().formatSearchResultsV1(search_query)); return null; } } class LookupCB implements Callback<Deferred<Object>, List<byte[]>> { @Override public Deferred<Object> call(final List<byte[]> tsuids) throws Exception { final List<Object> results = new ArrayList<Object>(tsuids.size()); search_query.setTotalResults(tsuids.size()); final ArrayList<Deferred<Object>> deferreds = new ArrayList<Deferred<Object>>(tsuids.size()); for (final byte[] tsuid : tsuids) { // has to be concurrent if the uid table is split across servers final Map<String, Object> series = new ConcurrentHashMap<String, Object>(3); results.add(series); series.put("tsuid", UniqueId.uidToString(tsuid)); byte[] metric_uid = Arrays.copyOfRange(tsuid, 0, TSDB.metrics_width()); deferreds.add(tsdb.getUidName(UniqueIdType.METRIC, metric_uid) .addCallback(new MetricCB(series))); final List<byte[]> tag_ids = UniqueId.getTagPairsFromTSUID(tsuid); deferreds.add(Tags.resolveIdsAsync(tsdb, tag_ids) .addCallback(new TagsCB(series))); } return Deferred.group(deferreds).addCallback(new Serialize(results)); } } class ErrCB implements Callback<Object, Exception> { @Override public Object call(final Exception e) throws Exception { if (e instanceof NoSuchUniqueId) { query.sendReply(HttpResponseStatus.NOT_FOUND, query.serializer().formatErrorV1( new BadRequestException(HttpResponseStatus.NOT_FOUND, "Unable to resolve one or more TSUIDs", (NoSuchUniqueId)e))); } else if (e instanceof NoSuchUniqueName) { query.sendReply(HttpResponseStatus.NOT_FOUND, query.serializer().formatErrorV1( new BadRequestException(HttpResponseStatus.NOT_FOUND, "Unable to resolve one or more UIDs", (NoSuchUniqueName)e))); } else if (e instanceof DeferredGroupException) { final Throwable ex = Exceptions.getCause((DeferredGroupException)e); if (ex instanceof NoSuchUniqueId) { query.sendReply(HttpResponseStatus.NOT_FOUND, query.serializer().formatErrorV1( new BadRequestException(HttpResponseStatus.NOT_FOUND, "Unable to resolve one or more TSUIDs", (NoSuchUniqueId)ex))); } else if (ex instanceof NoSuchUniqueName) { query.sendReply(HttpResponseStatus.NOT_FOUND, query.serializer().formatErrorV1( new BadRequestException(HttpResponseStatus.NOT_FOUND, "Unable to resolve one or more UIDs", (NoSuchUniqueName)ex))); } else { query.sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR, query.serializer().formatErrorV1( new BadRequestException(HttpResponseStatus.INTERNAL_SERVER_ERROR, "Unexpected exception", ex))); } } else { query.sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR, query.serializer().formatErrorV1( new BadRequestException(HttpResponseStatus.INTERNAL_SERVER_ERROR, "Unexpected exception", e))); } return null; } } new TimeSeriesLookup(tsdb, search_query).lookupAsync() .addCallback(new LookupCB()) .addErrback(new ErrCB()); } }