/*
* This file is part of a module with proprietary Enterprise Features.
*
* Licensed to Crate.io Inc. ("Crate.io") under one or more contributor
* license agreements. See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*
* Unauthorized copying of this file, via any medium is strictly prohibited.
*
* To use this file, Crate.io must have given you permission to enable and
* use such Enterprise Features and you must have a valid Enterprise or
* Subscription Agreement with Crate.io. If you enable or use the Enterprise
* Features, you represent and warrant that you have a valid Enterprise or
* Subscription Agreement with Crate.io. Your use of the Enterprise Features
* if governed by the terms and conditions of your Enterprise or Subscription
* Agreement with Crate.io.
*/
package io.crate.beans;
import com.google.common.annotations.VisibleForTesting;
import io.crate.action.sql.Option;
import io.crate.action.sql.ResultReceiver;
import io.crate.action.sql.SQLOperations;
import io.crate.data.Row;
import io.crate.exceptions.SQLExceptions;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.lucene.BytesRefs;
import org.elasticsearch.common.settings.Settings;
import javax.annotation.Nonnull;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import static io.crate.action.sql.SQLOperations.Session.UNNAMED;
import static io.crate.beans.QueryStats.MetricType.AVERAGE_DURATION;
import static io.crate.beans.QueryStats.MetricType.FREQUENCY;
public class QueryStats implements QueryStatsMBean {
private final Logger logger;
enum MetricType {
FREQUENCY,
AVERAGE_DURATION
}
public static final String NAME = "io.crate.monitoring:type=QueryStats";
private static final String QUERY_PATTERN = "^\\s*(%s).*";
private static final String STMT = "SELECT COUNT(*) / ((CURRENT_TIMESTAMP - ?) / 1000.0), " +
"AVG(ended - started), REGEXP_MATCHES(LOWER(stmt), ?)[1] " +
"FROM sys.jobs_log " +
"WHERE started BETWEEN ? AND CURRENT_TIMESTAMP " +
" AND REGEXP_MATCHES(LOWER(stmt), ?)[1] IS NOT NULL " +
"GROUP BY 3";
private final ConcurrentMap<String, Double> metrics;
private final SQLOperations.Session session;
@VisibleForTesting
final ConcurrentMap<String, Long> lastQueried;
public QueryStats(SQLOperations sqlOperations, Settings settings) {
logger = Loggers.getLogger(QueryStats.class, settings);
session = sqlOperations.createSession("sys", null, Option.NONE, 10000);
session.parse(NAME, STMT, Collections.emptyList());
lastQueried = new ConcurrentHashMap<>();
metrics = new ConcurrentHashMap<>();
}
@Override
public double getSelectQueryFrequency() {
return updateAndGetLastSingleMetricValue("select", FREQUENCY);
}
@Override
public double getInsertQueryFrequency() {
return updateAndGetLastSingleMetricValue("insert", FREQUENCY);
}
@Override
public double getUpdateQueryFrequency() {
return updateAndGetLastSingleMetricValue("update", FREQUENCY);
}
@Override
public double getDeleteQueryFrequency() {
return updateAndGetLastSingleMetricValue("delete", FREQUENCY);
}
@Override
public double getSelectQueryAverageDuration() {
return updateAndGetLastSingleMetricValue("select", AVERAGE_DURATION);
}
@Override
public double getInsertQueryAverageDuration() {
return updateAndGetLastSingleMetricValue("insert", AVERAGE_DURATION);
}
@Override
public double getUpdateQueryAverageDuration() {
return updateAndGetLastSingleMetricValue("update", AVERAGE_DURATION);
}
@Override
public double getDeleteQueryAverageDuration() {
return updateAndGetLastSingleMetricValue("delete", AVERAGE_DURATION);
}
@Override
public double getOverallQueryFrequency() {
return updateAndGetLastOverallMetricValue("select|insert|delete|update", FREQUENCY);
}
@Override
public double getOverallQueryAverageDuration() {
return updateAndGetLastOverallMetricValue("select|insert|delete|update", AVERAGE_DURATION);
}
private double updateAndGetLastOverallMetricValue(String query, MetricType type) {
return updateAndLastGetMetricValue(query, type, true);
}
private double updateAndGetLastSingleMetricValue(String query, MetricType type) {
return updateAndLastGetMetricValue(query, type, false);
}
private double updateAndLastGetMetricValue(String query, MetricType type, boolean overall) {
try {
String queryUID = query + type;
String queryPattern = String.format(Locale.ENGLISH, QUERY_PATTERN, query);
long lastTs = updateAndGetLastExecutedTsFor(queryUID);
session.bind(UNNAMED, NAME, Arrays.asList(lastTs, queryPattern, lastTs, queryPattern), null);
session.execute(UNNAMED, 0, new ResultReceiver() {
private final CompletableFuture<Boolean> completionFuture = new CompletableFuture<>();
private final List<Row> rows = new ArrayList<>();
@Override
public void setNextRow(Row row) {
rows.add(row);
}
@Override
public void allFinished(boolean interrupted) {
double value = overall ?
getTotalMetricValue(rows, type.ordinal()) :
getMetricValue(rows, query, type.ordinal());
metrics.put(queryUID, value);
completionFuture.complete(interrupted);
}
@Override
public void fail(@Nonnull Throwable t) {
logger.error("Failed to process metric results!", t);
completionFuture.completeExceptionally(t);
}
@Override
public CompletableFuture<?> completionFuture() {
return completionFuture;
}
@Override
public void batchFinished() {
}
});
session.sync();
return metrics.getOrDefault(queryUID, .0);
} catch (Throwable t) {
throw SQLExceptions.createSQLActionException(t);
}
}
@VisibleForTesting
double getMetricValue(List<Row> rows, String query, int metricIdx) {
return rows.stream()
.filter(row -> query.equalsIgnoreCase(BytesRefs.toString(row.get(2))))
.mapToDouble(row -> (double) row.get(metricIdx))
.findAny().orElse(.0);
}
@VisibleForTesting
double getTotalMetricValue(List<Row> rows, int metricIdx) {
return rows.stream()
.mapToDouble(row -> (double) row.get(metricIdx))
.sum();
}
@VisibleForTesting
long updateAndGetLastExecutedTsFor(String queryUID) {
long currentTs = System.currentTimeMillis();
long lastTs = lastQueried.getOrDefault(queryUID, currentTs);
lastQueried.put(queryUID, currentTs);
return lastTs;
}
}