package ru.yandex.market.graphouse.data;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.gson.stream.JsonWriter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.jetty.io.RuntimeIOException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowCallbackHandler;
import ru.yandex.market.graphouse.search.MetricSearch;
import ru.yandex.market.graphouse.search.tree.MetricName;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* @author Dmitry Andreev <a href="mailto:AndreevDm@yandex-team.ru"></a>
* @date 03/04/2017
*/
public class MetricDataService {
private static final Logger log = LogManager.getLogger();
private final MetricSearch metricSearch;
private final JdbcTemplate clickHouseJdbcTemplate;
private final String graphiteTable;
public MetricDataService(MetricSearch metricSearch, JdbcTemplate clickHouseJdbcTemplate, String graphiteTable) {
this.metricSearch = metricSearch;
this.clickHouseJdbcTemplate = clickHouseJdbcTemplate;
this.graphiteTable = graphiteTable;
}
public void getData(List<String> metricStrings, int startTimeSeconds, int endTimeSeconds,
PrintWriter writer) throws Exception {
JsonWriter jsonWriter = new JsonWriter(writer);
ListMultimap<String, MetricName> functionToMetrics = getFunctionToMetrics(metricStrings);
jsonWriter.beginObject();
for (String function : functionToMetrics.keySet()) {
appendData(functionToMetrics.get(function), function, startTimeSeconds, endTimeSeconds, jsonWriter);
}
jsonWriter.endObject();
}
private void appendData(List<MetricName> metrics, String function,
int startTimeSeconds, int endTimeSeconds, JsonWriter jsonWriter) {
int timeSeconds = endTimeSeconds - startTimeSeconds;
int stepSeconds = selectStep(metrics, startTimeSeconds);
int dataPoints = timeSeconds / stepSeconds;
startTimeSeconds = startTimeSeconds / stepSeconds * stepSeconds;
endTimeSeconds = startTimeSeconds + (dataPoints * stepSeconds);
MetricDataRowCallbackHandler handler = new MetricDataRowCallbackHandler(
jsonWriter, startTimeSeconds, endTimeSeconds, stepSeconds
);
clickHouseJdbcTemplate.query(
"SELECT metric, ts, " + function + "(value) as value FROM (" +
" SELECT metric, ts, argMax(value, updated) as value FROM " + graphiteTable +
" WHERE metric IN (" + toMetricString(metrics) + ")" +
" AND ts >= ? AND ts < ? AND date >= toDate(?) AND date <= toDate(?)" +
" GROUP BY metric, timestamp as ts" +
") GROUP BY metric, intDiv(toUInt32(ts), ?) * ? as ts ORDER BY metric, ts",
handler,
startTimeSeconds, endTimeSeconds, startTimeSeconds, endTimeSeconds, stepSeconds, stepSeconds
);
handler.finish();
}
@VisibleForTesting
protected static class MetricDataRowCallbackHandler implements RowCallbackHandler {
private final JsonWriter jsonWriter;
private final int start;
private final int end;
private final int step;
private String currentMetric = null;
private int nextTs;
public MetricDataRowCallbackHandler(JsonWriter jsonWriter, int start, int end, int step) {
this.jsonWriter = jsonWriter;
this.start = start;
this.end = end;
this.step = step;
}
@Override
public void processRow(ResultSet rs) throws SQLException {
try {
String metric = rs.getString("metric");
int ts = rs.getInt("ts");
double value = rs.getDouble("value");
checkNewMetric(metric);
fillNulls(ts);
if (Double.isFinite(value)) {
jsonWriter.value(value);
nextTs = ts + step;
}
} catch (IOException e) {
log.error("Failed to read data from CH", e);
throw new RuntimeIOException(e);
}
}
public void finish() {
try {
endMetric();
} catch (IOException e) {
log.error("Failed to read data from CH", e);
throw new RuntimeIOException(e);
}
}
private void fillNulls(int max) throws IOException {
for (; nextTs < max; nextTs += step) {
jsonWriter.nullValue();
}
}
private void checkNewMetric(String metric) throws IOException {
if (metric.equals(currentMetric)) {
return;
}
if (currentMetric != null) {
endMetric();
}
startMetric(metric);
}
private void endMetric() throws IOException {
if (currentMetric == null) {
return;
}
fillNulls(end);
jsonWriter.endArray().endObject();
currentMetric = null;
}
private void startMetric(String metric) throws IOException {
nextTs = start;
currentMetric = metric;
jsonWriter.name(metric).beginObject();
jsonWriter.name("start").value(start);
jsonWriter.name("end").value(end);
jsonWriter.name("step").value(step);
jsonWriter.name("points").beginArray();
}
}
private static String toMetricString(List<MetricName> metrics) {
return metrics.stream()
.map(MetricName::getName)
.collect(Collectors.joining("','", "'", "'"));
}
private int selectStep(List<MetricName> metrics, int startTimeSeconds) {
int ageSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) - startTimeSeconds;
return metrics.stream()
.mapToInt(m -> m.getRetention().getStepSize(ageSeconds))
.max()
.orElse(1);
}
private ListMultimap<String, MetricName> getFunctionToMetrics(List<String> metricStrings) throws IOException {
ListMultimap<String, MetricName> functionToMetrics = ArrayListMultimap.create();
for (String metricString : metricStrings) {
metricSearch.search(metricString, metric -> {
if (metric instanceof MetricName) {
MetricName metricName = (MetricName) metric;
functionToMetrics.put(metricName.getRetention().getFunction(), metricName);
}
});
}
return functionToMetrics;
}
}