/*
* Copyright 2013-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.glowroot.ui;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.common.io.CharStreams;
import org.immutables.value.Value;
import org.glowroot.common.repo.AgentRepository;
import org.glowroot.common.repo.ConfigRepository;
import org.glowroot.common.repo.GaugeValueRepository;
import org.glowroot.common.repo.GaugeValueRepository.Gauge;
import org.glowroot.common.repo.Utils;
import org.glowroot.common.repo.util.RollupLevelService;
import org.glowroot.common.util.ObjectMappers;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.GaugeValue;
import static com.google.common.base.Preconditions.checkNotNull;
@JsonService
class GaugeValueJsonService {
private static final ObjectMapper mapper = ObjectMappers.create();
private final GaugeValueRepository gaugeValueRepository;
private final RollupLevelService rollupLevelService;
private final AgentRepository agentRepository;
private final ConfigRepository configRepository;
GaugeValueJsonService(GaugeValueRepository gaugeValueRepository,
RollupLevelService rollupLevelService, AgentRepository agentRepository,
ConfigRepository configRepository) {
this.gaugeValueRepository = gaugeValueRepository;
this.rollupLevelService = rollupLevelService;
this.agentRepository = agentRepository;
this.configRepository = configRepository;
}
@GET(path = "/backend/jvm/gauges", permission = "agent:jvm:gauges")
String getGaugeValues(@BindAgentRollupId String agentRollupId,
@BindRequest GaugeValueRequest request) throws Exception {
int rollupLevel =
rollupLevelService.getGaugeRollupLevelForView(request.from(), request.to());
if (rollupLevel == 0 && !agentRepository.isAgent(agentRollupId)) {
// agent rollups from children do not have level-0 data
rollupLevel = 1;
}
long intervalMillis;
if (rollupLevel == 0) {
intervalMillis = configRepository.getGaugeCollectionIntervalMillis();
} else {
intervalMillis =
configRepository.getRollupConfigs().get(rollupLevel - 1).intervalMillis();
}
double gapMillis = intervalMillis * 1.5;
long revisedFrom = request.from() - intervalMillis;
long revisedTo = request.to() + intervalMillis;
Map<String, List<GaugeValue>> map = Maps.newLinkedHashMap();
for (String gaugeName : request.gaugeName()) {
map.put(gaugeName,
getGaugeValues(agentRollupId, revisedFrom, revisedTo, gaugeName, rollupLevel));
}
if (rollupLevel != 0) {
syncManualRollupCaptureTimes(map, rollupLevel);
}
List<DataSeries> dataSeriesList = Lists.newArrayList();
for (Entry<String, List<GaugeValue>> entry : map.entrySet()) {
dataSeriesList
.add(convertToDataSeriesWithGaps(entry.getKey(), entry.getValue(), gapMillis));
}
StringBuilder sb = new StringBuilder();
JsonGenerator jg = mapper.getFactory().createGenerator(CharStreams.asWriter(sb));
jg.writeStartObject();
jg.writeObjectField("dataSeries", dataSeriesList);
jg.writeEndObject();
jg.close();
return sb.toString();
}
@GET(path = "/backend/jvm/all-gauges", permission = "agent:jvm:gauges")
String getAllGaugeNames(@BindAgentRollupId String agentRollupId) throws Exception {
List<Gauge> gauges = gaugeValueRepository.getGauges(agentRollupId);
ImmutableList<Gauge> sortedGauges = new GaugeOrdering().immutableSortedCopy(gauges);
return mapper.writeValueAsString(sortedGauges);
}
private List<GaugeValue> getGaugeValues(String agentRollupId, long from, long to,
String gaugeName, int rollupLevel) throws Exception {
List<GaugeValue> gaugeValues = gaugeValueRepository.readGaugeValues(agentRollupId,
gaugeName, from, to, rollupLevel);
if (rollupLevel == 0) {
return gaugeValues;
}
long nonRolledUpFrom = from;
if (!gaugeValues.isEmpty()) {
long lastRolledUpTime = gaugeValues.get(gaugeValues.size() - 1).getCaptureTime();
nonRolledUpFrom = Math.max(nonRolledUpFrom, lastRolledUpTime + 1);
}
List<GaugeValue> orderedNonRolledUpGaugeValues = Lists.newArrayList();
orderedNonRolledUpGaugeValues.addAll(gaugeValueRepository.readGaugeValues(agentRollupId,
gaugeName, nonRolledUpFrom, to, 0));
gaugeValues = Lists.newArrayList(gaugeValues);
long fixedIntervalMillis =
configRepository.getRollupConfigs().get(rollupLevel - 1).intervalMillis();
gaugeValues.addAll(rollUpGaugeValues(orderedNonRolledUpGaugeValues, gaugeName,
new RollupCaptureTimeFn(fixedIntervalMillis)));
return gaugeValues;
}
private <K> void syncManualRollupCaptureTimes(Map<K, List<GaugeValue>> map, int rollupLevel) {
long fixedIntervalMillis =
configRepository.getRollupConfigs().get(rollupLevel - 1).intervalMillis();
Map<K, Long> manualRollupCaptureTimes = Maps.newHashMap();
long maxCaptureTime = Long.MIN_VALUE;
for (Entry<K, List<GaugeValue>> entry : map.entrySet()) {
List<GaugeValue> gaugeValues = entry.getValue();
if (gaugeValues.isEmpty()) {
continue;
}
GaugeValue lastGaugeValue = gaugeValues.get(gaugeValues.size() - 1);
long lastCaptureTime = lastGaugeValue.getCaptureTime();
maxCaptureTime = Math.max(maxCaptureTime, lastCaptureTime);
if (lastCaptureTime % fixedIntervalMillis != 0) {
manualRollupCaptureTimes.put(entry.getKey(), lastCaptureTime);
}
}
if (maxCaptureTime == Long.MIN_VALUE) {
// nothing to sync
return;
}
long maxRollupCaptureTime = Utils.getRollupCaptureTime(maxCaptureTime, fixedIntervalMillis);
long maxDiffToSync = Math.min(fixedIntervalMillis / 5, 60000);
for (Entry<K, Long> entry : manualRollupCaptureTimes.entrySet()) {
Long captureTime = entry.getValue();
if (Utils.getRollupCaptureTime(captureTime,
fixedIntervalMillis) != maxRollupCaptureTime) {
continue;
}
if (maxCaptureTime - captureTime > maxDiffToSync) {
// only sync up times that are close to each other
continue;
}
K key = entry.getKey();
List<GaugeValue> gaugeValues = checkNotNull(map.get(key));
// make copy in case ImmutableList
gaugeValues = Lists.newArrayList(gaugeValues);
GaugeValue lastGaugeValue = gaugeValues.get(gaugeValues.size() - 1);
gaugeValues.set(gaugeValues.size() - 1, lastGaugeValue.toBuilder()
.setCaptureTime(maxCaptureTime)
.build());
map.put(key, gaugeValues);
}
}
static List<GaugeValue> rollUpGaugeValues(List<GaugeValue> orderedNonRolledUpGaugeValues,
String gaugeName, Function<Long, Long> rollupCaptureTimeFn) {
List<GaugeValue> rolledUpGaugeValues = Lists.newArrayList();
double currTotal = 0;
long currWeight = 0;
long currRollupCaptureTime = Long.MIN_VALUE;
for (GaugeValue nonRolledUpGaugeValue : orderedNonRolledUpGaugeValues) {
long captureTime = nonRolledUpGaugeValue.getCaptureTime();
long rollupCaptureTime = rollupCaptureTimeFn.apply(captureTime);
if (rollupCaptureTime != currRollupCaptureTime && currWeight > 0) {
rolledUpGaugeValues.add(GaugeValue.newBuilder()
.setGaugeName(gaugeName)
.setCaptureTime(currRollupCaptureTime)
.setValue(currTotal / currWeight)
.setWeight(currWeight)
.build());
currTotal = 0;
currWeight = 0;
}
currRollupCaptureTime = rollupCaptureTime;
currTotal += nonRolledUpGaugeValue.getValue() * nonRolledUpGaugeValue.getWeight();
currWeight += nonRolledUpGaugeValue.getWeight();
}
if (currWeight > 0) {
// roll up final one
long lastCaptureTime = orderedNonRolledUpGaugeValues
.get(orderedNonRolledUpGaugeValues.size() - 1).getCaptureTime();
rolledUpGaugeValues.add(GaugeValue.newBuilder()
.setGaugeName(gaugeName)
.setCaptureTime(lastCaptureTime)
.setValue(currTotal / currWeight)
.setWeight(currWeight)
.build());
}
return rolledUpGaugeValues;
}
private static DataSeries convertToDataSeriesWithGaps(String dataSeriesName,
List<GaugeValue> gaugeValues, double gapMillis) {
DataSeries dataSeries = new DataSeries(dataSeriesName);
GaugeValue lastGaugeValue = null;
for (GaugeValue gaugeValue : gaugeValues) {
if (lastGaugeValue != null
&& gaugeValue.getCaptureTime() - lastGaugeValue.getCaptureTime() > gapMillis) {
dataSeries.addNull();
}
dataSeries.add(gaugeValue.getCaptureTime(), gaugeValue.getValue());
lastGaugeValue = gaugeValue;
}
return dataSeries;
}
@Value.Immutable
interface GaugeValueRequest {
long from();
long to();
// singular because this is used in query string
ImmutableList<String> gaugeName();
}
static class GaugeOrdering extends Ordering<Gauge> {
@Override
public int compare(Gauge left, Gauge right) {
return left.display().compareToIgnoreCase(right.display());
}
}
private static class RollupCaptureTimeFn implements Function<Long, Long> {
private final long fixedIntervalMillis;
private RollupCaptureTimeFn(long fixedIntervalMillis) {
this.fixedIntervalMillis = fixedIntervalMillis;
}
@Override
public Long apply(Long captureTime) {
return Utils.getRollupCaptureTime(captureTime, fixedIntervalMillis);
}
}
}