// This file is part of OpenTSDB.
// Copyright (C) 2015 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.stats;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Objects;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import net.opentsdb.core.Const;
import net.opentsdb.core.QueryException;
import net.opentsdb.core.TSQuery;
import net.opentsdb.utils.DateTime;
import net.opentsdb.utils.JSON;
import net.opentsdb.utils.Pair;
/**
* This class stores information about OpenTSDB queries executed through the
* HTTP API. It maintains a list of running queries as well as a cache of the
* last {@code COMPLETED_QUERY_CACHE_SIZE} number of queries executed. The
* stats can be observed via /api/query/stats.
*
* When a query is executed, it should instantiate an object of this class.
* Once the query is completed, make sure to call {@link markComplete}.
*
* The cache will store each query based on the combination of the client, query
* and the result code. If the same query was executed multiple times then it
* will increment the "executed" counter for the query in the cache.
*
* NOTE: Record everything in nano seconds, then convert to floating millis for
* serialization.
* @since 2.2
*/
public class QueryStats {
private static final Logger LOG = LoggerFactory.getLogger(QueryStats.class);
private static final Logger QUERY_LOG = LoggerFactory.getLogger("QueryLog");
/** Determines how many query stats to keep in the cache */
private static int COMPLETED_QUERY_CACHE_SIZE = 256;
/** Whether or not to allow duplicate queries from the same endpoint to
* run simultaneously. */
private static boolean ENABLE_DUPLICATES = true;
/** Stores queries currently executing. If a thread doesn't call into
* markComplete then it's possible for this map to fill up.
* Hash is the remote + query */
private static ConcurrentHashMap<Integer, QueryStats> running_queries =
new ConcurrentHashMap<Integer, QueryStats>();
/** Size limited cache of queries from the past.
* Hash is the remote + query + response code */
private static Cache<Integer, QueryStats> completed_queries =
CacheBuilder.newBuilder().maximumSize(COMPLETED_QUERY_CACHE_SIZE).build();
/** Start time for the query in nano seconds. Can be set post construction
* if necessary */
private final long query_start_ns;
/** Start timestamp for the query in millis for printing */
private final long query_start_ms;
/** When the query was marked completed in nanoseconds */
private long query_completed_ts;
/** The remote address as <IP>:<port>, may be ipv6 */
private final String remote_address;
/** The TSQuery object that contains the query specification */
private final TSQuery query;
/** HTTP response when the query was completed, either successfully or failed */
private HttpResponseStatus response;
/** Set if the query terminated with an exception */
private Throwable exception;
/** How many times this exact query was executed. Only updated on completion */
private long executed;
/** The users (if known) who executed this query (could be pulled from a header) */
private String user;
/** Stats for the entire query */
private final Map<QueryStat, Long> overall_stats;
/** Hold a list of stats for the sub queries */
private final Map<Integer, Map<QueryStat, Long>> query_stats;
/** Holds a list of stats for each scanner */
private final Map<Integer, Map<Integer, Map<QueryStat, Long>>> scanner_stats;
/** Hold a list of the region servers encountered for each scanner */
private final Map<Integer, Map<Integer, Set<String>>> scanner_servers;
/** Holds a lis tof the scanner IDs for each scanner */
private final Map<Integer, Map<Integer, String>> scanner_ids;
/** Holds a copy of the headers from the request */
private final Map<String, String> headers;
/** Whether or not the data was successfully sent to the client */
private boolean sent_to_client;
/**
* A list of statistics surrounding individual queries
*/
public enum QueryStat {
// Query Setup stats
STRING_TO_UID_TIME ("stringToUidTime", true),
// Storage stats
COLUMNS_FROM_STORAGE ("columnsFromStorage", false),
ROWS_FROM_STORAGE ("rowsFromStorage", false),
BYTES_FROM_STORAGE ("bytesFromStorage", false),
SUCCESSFUL_SCAN ("successfulScan", false),
// Single Scanner stats
DPS_PRE_FILTER ("dpsPreFilter", false),
ROWS_PRE_FILTER ("rowsPreFilter", false),
DPS_POST_FILTER ("dpsPostFilter", false),
ROWS_POST_FILTER ("rowsPostFilter", false),
SCANNER_UID_TO_STRING_TIME ("scannerUidToStringTime", true),
COMPACTION_TIME ("compactionTime", true),
HBASE_TIME ("hbaseTime", true),
UID_PAIRS_RESOLVED ("uidPairsResolved", false),
SCANNER_TIME ("scannerTime", true),
// Overall Salt Scanner stats
SCANNER_MERGE_TIME ("saltScannerMergeTime", true),
// Post Scan stats
QUERY_SCAN_TIME ("queryScanTime", true),
GROUP_BY_TIME ("groupByTime", true),
// Serialization time stats
UID_TO_STRING_TIME ("uidToStringTime", true),
AGGREGATED_SIZE ("emittedDPs", false),
NAN_DPS ("nanDPs", false),
AGGREGATION_TIME ("aggregationTime", true),
SERIALIZATION_TIME ("serializationTime", true),
// Final stats
PROCESSING_PRE_WRITE_TIME ("processingPreWriteTime", true),
TOTAL_TIME ("totalTime", true),
// MAX and Agg Times
MAX_HBASE_TIME ("maxHBaseTime", true),
AVG_HBASE_TIME ("avgHBaseTime", true),
MAX_SALT_SCANNER_TIME ("maxScannerTime", true),
AVG_SALT_SCANNER_TIME ("avgScannerTime", true),
MAX_UID_TO_STRING ("maxUidToStringTime", true),
AVG_UID_TO_STRING ("avgUidToStringTime", true),
MAX_COMPACTION_TIME ("maxCompactionTime", true),
AVG_COMPACTION_TIME ("avgCompactionTime", true),
MAX_SCANNER_UID_TO_STRING_TIME ("maxScannerUidtoStringTime", true),
AVG_SCANNER_UID_TO_STRING_TIME ("avgScannerUidToStringTime", true),
MAX_SCANNER_MERGE_TIME ("maxSaltScannerMergeTime", true),
AVG_SCANNER_MERGE_TIME ("avgSaltScannerMergeTime", true),
MAX_SCAN_TIME ("maxQueryScanTime", true),
AVG_SCAN_TIME ("avgQueryScanTime", true),
MAX_AGGREGATION_TIME ("maxAggregationTime", true),
AVG_AGGREGATION_TIME ("avgAggregationTime", true),
MAX_SERIALIZATION_TIME ("maxSerializationTime", true),
AVG_SERIALIZATION_TIME ("avgSerializationTime", true)
;
/** The serializable name for this enum */
private final String stat_name;
/** Whether or not the stat is time based */
private final boolean is_time;
private QueryStat(final String stat_name, final boolean is_time) {
this.stat_name = stat_name;
this.is_time = is_time;
}
@Override
public String toString() {
return stat_name;
}
}
// always AVG, MAX in the pair order
static final Map<QueryStat, Pair<QueryStat, QueryStat>> AGG_MAP =
new HashMap<QueryStat, Pair<QueryStat, QueryStat>>();
static {
AGG_MAP.put(QueryStat.HBASE_TIME, new Pair<QueryStat, QueryStat>(
QueryStat.AVG_HBASE_TIME, QueryStat.MAX_HBASE_TIME));
AGG_MAP.put(QueryStat.SCANNER_TIME, new Pair<QueryStat, QueryStat>(
QueryStat.AVG_SALT_SCANNER_TIME, QueryStat.MAX_HBASE_TIME));
AGG_MAP.put(QueryStat.UID_TO_STRING_TIME, new Pair<QueryStat, QueryStat>(
QueryStat.MAX_UID_TO_STRING, QueryStat.MAX_UID_TO_STRING));
AGG_MAP.put(QueryStat.SCANNER_UID_TO_STRING_TIME, new Pair<QueryStat, QueryStat>(
QueryStat.MAX_SCANNER_UID_TO_STRING_TIME,
QueryStat.AVG_SCANNER_UID_TO_STRING_TIME));
AGG_MAP.put(QueryStat.QUERY_SCAN_TIME, new Pair<QueryStat, QueryStat>(
QueryStat.MAX_SCAN_TIME, QueryStat.AVG_SCAN_TIME));
AGG_MAP.put(QueryStat.AGGREGATION_TIME, new Pair<QueryStat, QueryStat>(
QueryStat.MAX_AGGREGATION_TIME, QueryStat.AVG_AGGREGATION_TIME));
AGG_MAP.put(QueryStat.SERIALIZATION_TIME, new Pair<QueryStat, QueryStat>(
QueryStat.MAX_SERIALIZATION_TIME,
QueryStat.AVG_SERIALIZATION_TIME));
}
/**
* Default CTor
* @param remote_address Remote address of the client
* @param query Query being executed
* @param headers The HTTP headers passed with the query
* @throws QueryException if the exact query is already running, e.g if the
* client submitted the same query twice
*/
public QueryStats(final String remote_address, final TSQuery query,
final Map<String, String> headers) {
if (remote_address == null || remote_address.isEmpty()) {
throw new IllegalArgumentException("Remote address was null or empty");
}
if (query == null) {
throw new IllegalArgumentException("Query object was null");
}
this.remote_address = remote_address;
this.query = query;
this.headers = headers; // can be null
executed = 1;
query_start_ns = DateTime.nanoTime();
query_start_ms = DateTime.currentTimeMillis();
overall_stats = new HashMap<QueryStat, Long>();
query_stats = new ConcurrentHashMap<Integer, Map<QueryStat, Long>>(1);
scanner_stats = new ConcurrentHashMap<Integer,
Map<Integer, Map<QueryStat, Long>>>(1);
scanner_servers = new ConcurrentHashMap<Integer, Map<Integer, Set<String>>>(1);
scanner_ids = new ConcurrentHashMap<Integer, Map<Integer, String>>(1);
if (LOG.isDebugEnabled()) {
LOG.debug("New query for remote " + remote_address + " with hash " +
hashCode() + " on thread " + Thread.currentThread().getId());
}
if (running_queries.putIfAbsent(this.hashCode(), this) != null) {
if (ENABLE_DUPLICATES) {
LOG.warn("Query " + query + " is already executing for endpoint: " +
remote_address);
} else {
throw new QueryException("Query is already executing for endpoint: " +
remote_address);
}
}
if (LOG.isDebugEnabled()) {
LOG.debug("Successfully put new query for remote " + remote_address +
" with hash " + hashCode() + " on thread " +
Thread.currentThread().getId() + " w q " + query.toString());
}
LOG.info("Executing new query=" + JSON.serializeToString(this));
}
/**
* Returns the hash based on the remote address and the query
*/
@Override
public int hashCode() {
return remote_address.hashCode() ^ query.hashCode();
}
/**
* Equals is based solely on the endpoint and the original query
*/
@Override
public boolean equals(final Object obj) {
if (obj == null) {
return false;
}
if (!(obj instanceof QueryStats)) {
return false;
}
if (obj == this) {
return true;
}
final QueryStats stats = (QueryStats)obj;
return Objects.equal(remote_address, stats.remote_address)
&& Objects.equal(query, stats.query);
}
@Override
public String toString() {
// have to hack it to get the details. By default we dump just the highest
// level of stats.
final Map<String, Object> details = new HashMap<String, Object>();
details.put("queryStartTimestamp", getQueryStartTimestamp());
details.put("queryCompletedTimestamp", getQueryCompletedTimestamp());
details.put("exception", getException());
details.put("httpResponse", getHttpResponse());
details.put("numRunningQueries", getNumRunningQueries());
details.put("query", getQuery());
details.put("user", getUser());
details.put("requestHeaders", getRequestHeaders());
details.put("executed", getExecuted());
details.put("stats", getStats(true, true));
return JSON.serializeToString(details);
}
/**
* Marks a query as completed successfully with the 200 HTTP response code
* without an exception.
* Moves it from the running map to the cache, updating the cache if it already
* existed.
*/
public void markSerializationSuccessful() {
markSerialized(HttpResponseStatus.OK, null);
}
/**
* Marks a query as completed with the given HTTP code with exception and
* moves it from the running map to the cache, updating the cache if it
* already existed.
* @param response The HTTP response to log
* @param exception The exception thrown
*/
public void markSerialized(final HttpResponseStatus response,
final Throwable exception) {
this.exception = exception;
this.response = response;
query_completed_ts = DateTime.currentTimeMillis();
overall_stats.put(QueryStat.PROCESSING_PRE_WRITE_TIME, DateTime.nanoTime() - query_start_ns);
synchronized (running_queries) {
if (!running_queries.containsKey(this.hashCode())) {
if (!ENABLE_DUPLICATES) {
LOG.warn("Query was already marked as complete: " + this);
}
}
running_queries.remove(hashCode());
if (LOG.isDebugEnabled()) {
LOG.debug("Removed completed query " + remote_address + " with hash " +
hashCode() + " on thread " + Thread.currentThread().getId());
}
}
aggQueryStats();
final int cache_hash = this.hashCode() ^ response.toString().hashCode();
synchronized (completed_queries) {
final QueryStats old_query = completed_queries.getIfPresent(cache_hash);
if (old_query == null) {
completed_queries.put(cache_hash, this);
} else {
old_query.executed++;
}
}
}
/**
* Marks the query as complete and logs it to the proper logs. This is called
* after the data has been sent to the client.
*/
public void markSent() {
sent_to_client = true;
overall_stats.put(QueryStat.TOTAL_TIME, DateTime.nanoTime() - query_start_ns);
LOG.info("Completing query=" + JSON.serializeToString(this));
QUERY_LOG.info(this.toString());
}
/** Leaves the sent_to_client field as false when we were unable to write to
* the client end point. */
public void markSendFailed() {
overall_stats.put(QueryStat.TOTAL_TIME, DateTime.nanoTime() - query_start_ns);
LOG.info("Completing query=" + JSON.serializeToString(this));
QUERY_LOG.info(this.toString());
}
/**
* Builds a serializable map from the running and cached query maps to be
* returned to a caller.
* @return A map for serialization
*/
public static Map<String, Object> getRunningAndCompleteStats() {
Map<String, Object> root = new TreeMap<String, Object>();
if (running_queries.isEmpty()) {
root.put("running", Collections.emptyList());
} else {
final List<Object> running = new ArrayList<Object>(running_queries.size());
root.put("running", running);
// don't need to lock the map beyond what the iterator will do implicitly
for (final QueryStats stats : running_queries.values()) {
final Map<String, Object> obj = new HashMap<String, Object>(10);
obj.put("query", stats.query);
obj.put("remote", stats.remote_address);
obj.put("user", stats.user);
obj.put("headers", stats.headers);;
obj.put("queryStart", DateTime.msFromNano(stats.query_start_ns));
obj.put("elapsed", DateTime.msFromNanoDiff(DateTime.nanoTime(),
stats.query_start_ns));
running.add(obj);
}
}
final Map<Integer, QueryStats> completed = completed_queries.asMap();
if (completed.isEmpty()) {
root.put("completed", Collections.emptyList());
} else {
root.put("completed", completed.values());
}
return root;
}
/**
* Fetches data about the running queries and status of queries in the cache
* @param collector The collector to write to
*/
public static void collectStats(final StatsCollector collector) {
collector.record("query.count", running_queries.size(), "type=running");
}
/**
* Add an overall statistic for the query (i.e. not associated with a sub
* query or scanner)
* @param name The name of the stat
* @param value The value to store
*/
public void addStat(final QueryStat name, final long value) {
overall_stats.put(name, value);
}
/**
* Adds a stat for a sub query, replacing it if it exists. Times must be
* in nanoseconds.
* @param query_index The index of the sub query to update
* @param name The name of the stat to update
* @param value The value to set
*/
public void addStat(final int query_index, final QueryStat name,
final long value) {
Map<QueryStat, Long> qs = query_stats.get(query_index);
if (qs == null) {
qs = new HashMap<QueryStat, Long>();
query_stats.put(query_index, qs);
}
qs.put(name, value);
}
/**
* Aggregates the various stats from the lower to upper levels. This includes
* calculating max and average time values for stats marked as time based.
*/
public void aggQueryStats() {
// These are overall aggregations
final Map<QueryStat, Pair<Long, Long>> overall_cumulations =
new HashMap<QueryStat, Pair<Long, Long>>();
// scanner aggs
for (final Entry<Integer, Map<Integer, Map<QueryStat, Long>>> entry :
scanner_stats.entrySet()) {
final int query_index = entry.getKey();
final Map<QueryStat, Pair<Long, Long>> cumulations =
new HashMap<QueryStat, Pair<Long, Long>>();
for (final Entry<Integer, Map<QueryStat, Long>> scanner :
entry.getValue().entrySet()) {
for (final Entry<QueryStat, Long> stat : scanner.getValue().entrySet()) {
if (stat.getKey().is_time) {
if (!AGG_MAP.containsKey(stat.getKey())) {
// we're not aggregating this value
continue;
}
// per query aggs
Pair<Long, Long> pair = cumulations.get(stat.getKey());
if (pair == null) {
pair = new Pair<Long, Long>(0L, Long.MIN_VALUE);
cumulations.put(stat.getKey(), pair);
}
pair.setKey(pair.getKey() + stat.getValue());
if (stat.getValue() > pair.getValue()) {
pair.setValue(stat.getValue());
}
// overall aggs required here for proper time averaging
pair = overall_cumulations.get(stat.getKey());
if (pair == null) {
pair = new Pair<Long, Long>(0L, Long.MIN_VALUE);
overall_cumulations.put(stat.getKey(), pair);
}
pair.setKey(pair.getKey() + stat.getValue());
if (stat.getValue() > pair.getValue()) {
pair.setValue(stat.getValue());
}
} else {
// only add counters for the per query maps as they'll be rolled
// up below into the overall
updateStat(query_index, stat.getKey(), stat.getValue());
}
}
}
// per query aggs
for (final Entry<QueryStat, Pair<Long, Long>> cumulation :
cumulations.entrySet()) {
// names can't be null as we validate above that it exists
final Pair<QueryStat, QueryStat> names = AGG_MAP.get(cumulation.getKey());
addStat(query_index, names.getKey(),
(cumulation.getValue().getKey() / entry.getValue().size()));
addStat(query_index, names.getValue(), cumulation.getValue().getValue());
}
}
// handle the per scanner aggs
for (final Entry<QueryStat, Pair<Long, Long>> cumulation :
overall_cumulations.entrySet()) {
// names can't be null as we validate above that it exists
final Pair<QueryStat, QueryStat> names = AGG_MAP.get(cumulation.getKey());
addStat(names.getKey(),
(cumulation.getValue().getKey() /
(scanner_stats.size() *
Const.SALT_WIDTH() > 0 ? Const.SALT_BUCKETS() : 1)));
addStat(names.getValue(), cumulation.getValue().getValue());
}
overall_cumulations.clear();
// aggregate counters from the sub queries
for (final Map<QueryStat, Long> sub_query : query_stats.values()) {
for (final Entry<QueryStat, Long> stat : sub_query.entrySet()) {
if (stat.getKey().is_time) {
if (!AGG_MAP.containsKey(stat.getKey())) {
// we're not aggregating this value
continue;
}
Pair<Long, Long> pair = overall_cumulations.get(stat.getKey());
if (pair == null) {
pair = new Pair<Long, Long>(0L, Long.MIN_VALUE);
overall_cumulations.put(stat.getKey(), pair);
}
pair.setKey(pair.getKey() + stat.getValue());
if (stat.getValue() > pair.getValue()) {
pair.setValue(stat.getValue());
}
} else if (overall_stats.containsKey(stat.getKey())) {
overall_stats.put(stat.getKey(),
overall_stats.get(stat.getKey()) + stat.getValue());
} else {
overall_stats.put(stat.getKey(), stat.getValue());
}
}
}
for (final Entry<QueryStat, Pair<Long, Long>> cumulation :
overall_cumulations.entrySet()) {
// names can't be null as we validate above that it exists
final Pair<QueryStat, QueryStat> names = AGG_MAP.get(cumulation.getKey());
overall_stats.put(names.getKey(),
(cumulation.getValue().getKey() / query_stats.size()));
overall_stats.put(names.getValue(), cumulation.getValue().getValue());
}
}
/**
* Increments the cumulative value for a cumulative stat. If it's a time then
* it must be in nanoseconds
* @param query_index The index of the sub query
* @param name The name of the stat
* @param value The value to add to the existing value
*/
public void updateStat(final int query_index, final QueryStat name,
final long value) {
Map<QueryStat, Long> qs = query_stats.get(query_index);
long cum_time = value;
if (qs == null) {
qs = new HashMap<QueryStat, Long>();
query_stats.put(query_index, qs);
}
if (qs.containsKey(name)) {
cum_time += qs.get(name);
}
qs.put(name, cum_time);
}
/**
* Adds a value for a specific scanner for a specific sub query. If it's a time
* then it must be in nanoseconds.
* @param query_index The index of the sub query
* @param id The numeric ID of the scanner
* @param name The name of the stat
* @param value The value to add to the map
*/
public void addScannerStat(final int query_index, final int id,
final QueryStat name, final long value) {
Map<Integer, Map<QueryStat, Long>> qs = scanner_stats.get(query_index);
if (qs == null) {
qs = new ConcurrentHashMap<Integer, Map<QueryStat, Long>>(
Const.SALT_WIDTH() > 0 ? Const.SALT_BUCKETS() : 1);
scanner_stats.put(query_index, qs);
}
Map<QueryStat, Long> scanner_stat_map = qs.get(id);
if (scanner_stat_map == null) {
scanner_stat_map = new HashMap<QueryStat, Long>();
qs.put(id, scanner_stat_map);
}
scanner_stat_map.put(name, value);
}
/**
* Adds or overwrites the list of servers scanned by a scanner
* @param query_index The index of the sub query
* @param id The numeric ID of the scanner
* @param servers The list of servers encountered
*/
public void addScannerServers(final int query_index, final int id,
final Set<String> servers) {
Map<Integer, Set<String>> query_servers = scanner_servers.get(query_index);
if (query_servers == null) {
query_servers = new ConcurrentHashMap<Integer, Set<String>>(
Const.SALT_WIDTH() > 0 ? Const.SALT_BUCKETS() : 1);
scanner_servers.put(query_index, query_servers);
}
query_servers.put(id, servers);
}
/**
* Updates or adds a stat for a specific scanner. IF it's a time it must
* be in nanoseconds
* @param query_index The index of the sub query
* @param id The numeric ID of the scanner
* @param name The name of the stat
* @param value The value to update to the map
*/
public void updateScannerStat(final int query_index, final int id,
final QueryStat name, final long value) {
Map<Integer, Map<QueryStat, Long>> qs = scanner_stats.get(query_index);
long cum_time = value;
if (qs == null) {
qs = new ConcurrentHashMap<Integer, Map<QueryStat, Long>>();
scanner_stats.put(query_index, qs);
}
Map<QueryStat, Long> scanner_stat_map = qs.get(id);
if (scanner_stat_map == null) {
scanner_stat_map = new HashMap<QueryStat, Long>();
qs.put(id, scanner_stat_map);
}
if (scanner_stat_map.containsKey(name)) {
cum_time += scanner_stat_map.get(name);
}
scanner_stat_map.put(name, cum_time);
}
/**
* Adds a scanner for a sub query to the stats along with the description of
* the scanner.
* @param query_index The index of the sub query
* @param id The numeric ID of the scanner
* @param string_id The description of the scanner
*/
public void addScannerId(final int query_index, final int id,
final String string_id) {
Map<Integer, String> scanners = scanner_ids.get(query_index);
if (scanners == null) {
scanners = new ConcurrentHashMap<Integer, String>();
scanner_ids.put(query_index, scanners);
}
scanners.put(id, string_id);
}
/** @return the start time of the query in nano seconds */
public long queryStart() {
return query_start_ns;
}
/** @param user The user who executed the query */
public void setUser(final String user) {
this.user = user;
}
/** @return The user who executed the query if known */
public String getUser() {
return user;
}
/** @return The multi-mapped set of request headers associated with the query */
public Map<String, String> getRequestHeaders() {
return headers;
}
/** @return The number of currently running queries */
public int getNumRunningQueries() {
return running_queries.size();
}
/** @return An exception associated with the query or null if the query
* returned successfully. */
public String getException() {
if (exception == null) {
return "null";
}
return exception.getMessage() +
(exception.getStackTrace() != null && exception.getStackTrace().length > 0
? "\n" + exception.getStackTrace()[0].toString() : "");
}
/** @return The HTTP status response for the query */
public HttpResponseStatus getHttpResponse() {
return response;
}
/** @return The number of times this query has been executed from the same
* endpoint. */
public long getExecuted() {
return executed;
}
/** @return The full query */
public TSQuery getQuery() {
return query;
}
/** @return When the query was received and started executing, in ms */
public long getQueryStartTimestamp() {
return query_start_ms;
}
/** @return When the query was marked as completed, in ms */
public long getQueryCompletedTimestamp() {
return query_completed_ts;
}
/** @return Whether or not the data was successfully sent to the client */
public boolean getSentToClient() {
return sent_to_client;
}
/** @return A map with the subset of query measurements, not including scanners
* or sub queries */
public Map<String, Object> getStats() {
return getStats(false, false);
}
/**
* Returns measurements of the given query
* @param with_scanners Whether or not to dump individual scanner stats
* @return A map with stats for the query
*/
public Map<String, Object> getStats(final boolean with_sub_queries,
final boolean with_scanners) {
final Map<String, Object> map = new TreeMap<String, Object>();
for (final Entry<QueryStat, Long> entry : overall_stats.entrySet()) {
if (entry.getKey().is_time) {
map.put(entry.getKey().toString(), DateTime.msFromNano(entry.getValue()));
} else {
map.put(entry.getKey().toString(), entry.getValue());
}
}
if (with_sub_queries) {
final Iterator<Entry<Integer, Map<QueryStat, Long>>> it =
query_stats.entrySet().iterator();
while (it.hasNext()) {
final Entry<Integer, Map<QueryStat, Long>> entry = it.next();
final Map<String, Object> qs = new HashMap<String, Object>(1);
qs.put(String.format("queryIdx_%02d", entry.getKey()),
getQueryStats(entry.getKey(), with_scanners));
map.putAll(qs);
}
}
return map;
}
/**
* Returns a map of stats for a single sub query
* @param index The sub query to fetch
* @param with_scanners Whether or not to print detailed stats for each scanner
* @return A map with stats to print or null if the sub query didn't have any
* data.
*/
public Map<String, Object> getQueryStats(final int index,
final boolean with_scanners) {
final Map<QueryStat, Long> qs = query_stats.get(index);
if (qs == null) {
return null;
}
final Map<String, Object> query_map = new TreeMap<String, Object>();
query_map.put("queryIndex", index);
final Iterator<Entry<QueryStat, Long>> stats_it =
qs.entrySet().iterator();
while (stats_it.hasNext()) {
final Entry<QueryStat, Long> stat = stats_it.next();
if (stat.getKey().is_time) {
query_map.put(stat.getKey().toString(),
DateTime.msFromNano(stat.getValue()));
} else {
query_map.put(stat.getKey().toString(), stat.getValue());
}
}
if (with_scanners) {
final Map<Integer, Map<QueryStat, Long>> scanner_stats_map =
scanner_stats.get(index);
final Map<String, Object> scanner_maps = new TreeMap<String, Object>();
query_map.put("scannerStats", scanner_maps);
if (scanner_stats_map != null) {
final Map<Integer, String> scanners = scanner_ids.get(index);
final Iterator<Entry<Integer, Map<QueryStat, Long>>> scanner_it =
scanner_stats_map.entrySet().iterator();
while (scanner_it.hasNext()) {
final Entry<Integer, Map<QueryStat, Long>> scanner = scanner_it.next();
final Map<String, Object> scanner_map = new TreeMap<String, Object>();
scanner_maps.put(String.format("scannerIdx_%02d", scanner.getKey()),
scanner_map);
final String id;
if (scanners != null) {
id = scanners.get(scanner.getKey());
} else {
id = null;
}
scanner_map.put("scannerId", id);
/* Uncomment when AsyncHBase supports this
final Map<Integer, Set<String>> servers = scanner_servers.get(index);
if (servers != null) {
scanner_map.put("regionServers", servers.get(scanner.getKey()));
} else {
scanner_map.put("regionServers", null);
}
*/
final Iterator<Entry<QueryStat, Long>> scanner_stats_it =
scanner.getValue().entrySet().iterator();
while (scanner_stats_it.hasNext()) {
final Entry<QueryStat, Long> scanner_stats = scanner_stats_it.next();
if (!scanner_stats.getKey().is_time) {
scanner_map.put(scanner_stats.getKey().toString(),
scanner_stats.getValue());
} else {
scanner_map.put(scanner_stats.getKey().toString(),
DateTime.msFromNano(scanner_stats.getValue()));
}
}
}
}
}
return query_map;
}
/** @return A stat for the overall query or -1 if the stat didn't exist. */
public long getStat(final QueryStat stat) {
if (!overall_stats.containsKey(stat)) {
return -1;
}
return overall_stats.get(stat);
}
/** @return a timed stat for the overall query or NaN if the stat didn't exist */
public double getTimeStat(final QueryStat stat) {
if (!stat.is_time) {
throw new IllegalArgumentException("The stat is not a time stat");
}
if (!overall_stats.containsKey(stat)) {
return Double.NaN;
}
return DateTime.msFromNano(overall_stats.get(stat));
}
/** @param whether or not to allow duplicate queries to run */
public static void setEnableDuplicates(final boolean enable_dupes) {
ENABLE_DUPLICATES = enable_dupes;
}
}