/* * Copyright 2016-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.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Set; import java.util.TimeZone; import javax.annotation.Nullable; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.io.CharStreams; import io.netty.handler.codec.http.HttpResponseStatus; import org.immutables.value.Value; import org.glowroot.common.live.ImmutableOverviewAggregate; import org.glowroot.common.live.ImmutablePercentileAggregate; import org.glowroot.common.live.ImmutableThroughputAggregate; import org.glowroot.common.live.ImmutableTransactionQuery; import org.glowroot.common.live.LiveAggregateRepository.OverviewAggregate; import org.glowroot.common.live.LiveAggregateRepository.PercentileAggregate; import org.glowroot.common.live.LiveAggregateRepository.ThroughputAggregate; import org.glowroot.common.live.LiveAggregateRepository.TransactionQuery; import org.glowroot.common.model.LazyHistogram; import org.glowroot.common.repo.AgentRepository; import org.glowroot.common.repo.AggregateRepository; import org.glowroot.common.repo.GaugeValueRepository; import org.glowroot.common.repo.GaugeValueRepository.Gauge; import org.glowroot.common.repo.Utils; import org.glowroot.common.util.ObjectMappers; import org.glowroot.ui.GaugeValueJsonService.GaugeOrdering; import org.glowroot.ui.HttpSessionManager.Authentication; import org.glowroot.wire.api.model.CollectorServiceOuterClass.GaugeValue; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static java.util.concurrent.TimeUnit.DAYS; import static java.util.concurrent.TimeUnit.HOURS; @JsonService class ReportJsonService { private static final String RESPONSE_TIME_AVG = "transaction:response-time-avg"; private static final String RESPONSE_TIME_PERCENTILE = "transaction:response-time-percentile"; private static final String THROUGHPUT = "transaction:throughput"; private static final double NANOSECONDS_PER_MILLISECOND = 1000000.0; private static final ObjectMapper mapper = ObjectMappers.create(); private final AggregateRepository aggregateRepository; private final AgentRepository agentRepository; private final GaugeValueRepository gaugeValueRepository; ReportJsonService(AggregateRepository aggregateRepository, AgentRepository agentRepository, GaugeValueRepository gaugeValueRepository) { this.aggregateRepository = aggregateRepository; this.agentRepository = agentRepository; this.gaugeValueRepository = gaugeValueRepository; } // permission is checked based on agentRollupIds in the request @GET(path = "/backend/report/all-gauges", permission = "") String getGauges(@BindRequest RequestWithAgentRollupIds request, @BindAuthentication Authentication authentication) throws Exception { checkPermissions(request.agentRollupIds(), "agent:jvm:gauges", authentication); Set<Gauge> gauges = Sets.newHashSet(); for (String agentRollupId : request.agentRollupIds()) { gauges.addAll(gaugeValueRepository.getGauges(agentRollupId)); } ImmutableList<Gauge> sortedGauges = new GaugeOrdering().immutableSortedCopy(gauges); return mapper.writeValueAsString(sortedGauges); } // permission is checked based on agentRollupIds in the request @GET(path = "/backend/report", permission = "") String getReport(@BindRequest ReportRequest request, @BindAuthentication Authentication authentication) throws Exception { String metricId = request.metricId(); if (metricId.startsWith("transaction:")) { checkPermissions(request.agentRollupIds(), "agent:transaction:overview", authentication); } else if (metricId.startsWith("gauge:")) { checkPermissions(request.agentRollupIds(), "agent:jvm:gauges", authentication); } else { throw new IllegalStateException("Unexpected metric id: " + metricId); } TimeZone timeZone = TimeZone.getTimeZone(request.timeZoneId()); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd"); simpleDateFormat.setTimeZone(timeZone); Date from = simpleDateFormat.parse(request.fromDate()); Date to = simpleDateFormat.parse(request.toDate()); Calendar cal = Calendar.getInstance(); cal.setTime(to); cal.add(Calendar.DATE, 1); to = cal.getTime(); RollupCaptureTimeFn rollupCaptureTimeFn = new RollupCaptureTimeFn(request.rollup(), timeZone, request.fromDate()); double gapMillis; switch (request.rollup()) { case HOURLY: gapMillis = HOURS.toMillis(1) * 1.5; break; case DAILY: gapMillis = DAYS.toMillis(1) * 1.5; break; case WEEKLY: gapMillis = DAYS.toMillis(1) * 7 * 1.5; break; case MONTHLY: gapMillis = DAYS.toMillis(1) * 30 * 1.5; break; default: throw new IllegalStateException("Unexpected rollup: " + request.rollup()); } List<DataSeries> dataSeriesList; if (metricId.startsWith("transaction:")) { dataSeriesList = getTransactionReport(request, timeZone, from, to, rollupCaptureTimeFn, gapMillis, metricId); } else if (metricId.startsWith("gauge:")) { String gaugeName = metricId.substring("gauge:".length()); dataSeriesList = Lists.newArrayList(); for (String agentRollupId : request.agentRollupIds()) { // FIXME, rollup level 2 is nice since 30 min intervals // but need level 3 for long time periods int rollupLevel = 2; dataSeriesList.add(getDataSeriesForGauge(agentRollupId, gaugeName, from, to, rollupLevel, rollupCaptureTimeFn, request.rollup(), timeZone, gapMillis)); } } else { throw new IllegalStateException("Unexpected metric id: " + metricId); } 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(); } private List<DataSeries> getTransactionReport(ReportRequest request, TimeZone timeZone, Date from, Date to, RollupCaptureTimeFn rollupCaptureTimeFn, double gapMillis, String metricId) throws Exception { TransactionQuery query = ImmutableTransactionQuery.builder() .transactionType(checkNotNull(request.transactionType())) // + 1 to make from non-inclusive, since data points are displayed as midpoint of // time range .from(from.getTime() + 1) .to(to.getTime()) .rollupLevel(2) // FIXME, level 2 is nice since 30 min intervals // but need level 3 for long time periods .build(); List<DataSeries> dataSeriesList = Lists.newArrayList(); for (String agentRollupId : request.agentRollupIds()) { if (metricId.equals(RESPONSE_TIME_AVG)) { dataSeriesList.add(getDataSeriesForAverage(agentRollupId, query, rollupCaptureTimeFn, request.rollup(), timeZone, gapMillis)); } else if (metricId.equals(RESPONSE_TIME_PERCENTILE)) { dataSeriesList.add(getDataSeriesForPercentile(agentRollupId, query, checkNotNull(request.metricPercentile()), rollupCaptureTimeFn, request.rollup(), timeZone, gapMillis)); } else if (metricId.equals(THROUGHPUT)) { dataSeriesList.add(getDataSeriesForThroughput(agentRollupId, query, rollupCaptureTimeFn, request.rollup(), timeZone, gapMillis)); } else { throw new IllegalStateException("Unexpected metric id: " + metricId); } } return dataSeriesList; } private DataSeries getDataSeriesForAverage(String agentRollupId, TransactionQuery query, RollupCaptureTimeFn rollupCaptureTimeFn, ROLLUP rollup, TimeZone timeZone, double gapMillis) throws Exception { DataSeries dataSeries = new DataSeries(agentRepository.readAgentRollupDisplay(agentRollupId)); List<OverviewAggregate> aggregates = aggregateRepository.readOverviewAggregates(agentRollupId, query); aggregates = TransactionCommonService.rollUpOverviewAggregates(aggregates, rollupCaptureTimeFn); if (aggregates.isEmpty()) { return dataSeries; } OverviewAggregate lastAggregate = aggregates.get(aggregates.size() - 1); long lastCaptureTime = lastAggregate.captureTime(); long lastRollupCaptureTime = rollupCaptureTimeFn.apply(lastCaptureTime); if (lastCaptureTime != lastRollupCaptureTime) { aggregates.set(aggregates.size() - 1, ImmutableOverviewAggregate.builder() .copyFrom(lastAggregate) .captureTime(lastRollupCaptureTime) .build()); } OverviewAggregate priorAggregate = null; for (OverviewAggregate aggregate : aggregates) { if (priorAggregate != null && aggregate.captureTime() - priorAggregate.captureTime() > gapMillis) { dataSeries.addNull(); } dataSeries.add(getIntervalAverage(rollup, timeZone, aggregate.captureTime()), aggregate.totalDurationNanos() / (aggregate.transactionCount() * NANOSECONDS_PER_MILLISECOND)); priorAggregate = aggregate; } double totalDurationNanos = 0; long transactionCount = 0; for (OverviewAggregate aggregate : aggregates) { totalDurationNanos += aggregate.totalDurationNanos(); transactionCount += aggregate.transactionCount(); } // individual aggregate transaction counts cannot be zero, and aggregates is non-empty // (see above conditional), so transactionCount is guaranteed non-zero checkState(transactionCount != 0); dataSeries .setOverall(totalDurationNanos / (transactionCount * NANOSECONDS_PER_MILLISECOND)); return dataSeries; } private DataSeries getDataSeriesForPercentile(String agentRollupId, TransactionQuery query, double percentile, RollupCaptureTimeFn rollupCaptureTimeFn, ROLLUP rollup, TimeZone timeZone, double gapMillis) throws Exception { DataSeries dataSeries = new DataSeries(agentRepository.readAgentRollupDisplay(agentRollupId)); List<PercentileAggregate> aggregates = aggregateRepository.readPercentileAggregates(agentRollupId, query); aggregates = TransactionCommonService.rollUpPercentileAggregates(aggregates, rollupCaptureTimeFn); if (aggregates.isEmpty()) { return dataSeries; } PercentileAggregate lastAggregate = aggregates.get(aggregates.size() - 1); long lastCaptureTime = lastAggregate.captureTime(); long lastRollupCaptureTime = rollupCaptureTimeFn.apply(lastCaptureTime); if (lastCaptureTime != lastRollupCaptureTime) { aggregates.set(aggregates.size() - 1, ImmutablePercentileAggregate.builder() .copyFrom(lastAggregate) .captureTime(lastRollupCaptureTime) .build()); } PercentileAggregate priorAggregate = null; for (PercentileAggregate aggregate : aggregates) { if (priorAggregate != null && aggregate.captureTime() - priorAggregate.captureTime() > gapMillis) { dataSeries.addNull(); } LazyHistogram durationNanosHistogram = new LazyHistogram(aggregate.durationNanosHistogram()); dataSeries.add(getIntervalAverage(rollup, timeZone, aggregate.captureTime()), durationNanosHistogram.getValueAtPercentile(percentile) / NANOSECONDS_PER_MILLISECOND); priorAggregate = aggregate; } LazyHistogram mergedHistogram = new LazyHistogram(); for (PercentileAggregate aggregate : aggregates) { mergedHistogram.merge(aggregate.durationNanosHistogram()); } dataSeries.setOverall( mergedHistogram.getValueAtPercentile(percentile) / NANOSECONDS_PER_MILLISECOND); return dataSeries; } private DataSeries getDataSeriesForThroughput(String agentRollupId, TransactionQuery query, RollupCaptureTimeFn rollupCaptureTimeFn, ROLLUP rollup, TimeZone timeZone, double gapMillis) throws Exception { DataSeries dataSeries = new DataSeries(agentRepository.readAgentRollupDisplay(agentRollupId)); List<ThroughputAggregate> aggregates = aggregateRepository.readThroughputAggregates(agentRollupId, query); aggregates = TransactionCommonService.rollUpThroughputAggregates(aggregates, rollupCaptureTimeFn); if (aggregates.isEmpty()) { return dataSeries; } ThroughputAggregate lastAggregate = aggregates.get(aggregates.size() - 1); long lastCaptureTime = lastAggregate.captureTime(); long lastRollupCaptureTime = rollupCaptureTimeFn.apply(lastCaptureTime); if (lastCaptureTime != lastRollupCaptureTime) { aggregates.set(aggregates.size() - 1, ImmutableThroughputAggregate.builder() .copyFrom(lastAggregate) .captureTime(lastRollupCaptureTime) .build()); } long transactionCount = 0; long totalIntervalMillis = 0; ThroughputAggregate priorAggregate = null; for (ThroughputAggregate aggregate : aggregates) { if (priorAggregate != null && aggregate.captureTime() - priorAggregate.captureTime() > gapMillis) { dataSeries.addNull(); } long rollupIntervalMillis = getRollupIntervalMillis(rollup, timeZone, aggregate.captureTime()); dataSeries.add(getIntervalAverage(rollup, timeZone, aggregate.captureTime()), 60000.0 * aggregate.transactionCount() / rollupIntervalMillis); transactionCount += aggregate.transactionCount(); totalIntervalMillis += rollupIntervalMillis; priorAggregate = aggregate; } // individual aggregate intervals are non-zero, and aggregates is non-empty // (see above conditional), so totalIntervalMillis is guaranteed non-zero checkState(totalIntervalMillis != 0); dataSeries.setOverall(60000.0 * transactionCount / totalIntervalMillis); return dataSeries; } private DataSeries getDataSeriesForGauge(String agentRollupId, String gaugeName, Date from, Date to, int rollupLevel, RollupCaptureTimeFn rollupCaptureTimeFn, ROLLUP rollup, TimeZone timeZone, double gapMillis) throws Exception { DataSeries dataSeries = new DataSeries(agentRepository.readAgentRollupDisplay(agentRollupId)); // from + 1 to make from non-inclusive, since data points are displayed as midpoint of // time range List<GaugeValue> gaugeValues = gaugeValueRepository.readGaugeValues(agentRollupId, gaugeName, from.getTime() + 1, to.getTime(), rollupLevel); gaugeValues = GaugeValueJsonService.rollUpGaugeValues(gaugeValues, gaugeName, rollupCaptureTimeFn); if (gaugeValues.isEmpty()) { return dataSeries; } GaugeValue lastGaugeValue = gaugeValues.get(gaugeValues.size() - 1); long lastCaptureTime = lastGaugeValue.getCaptureTime(); long lastRollupCaptureTime = rollupCaptureTimeFn.apply(lastCaptureTime); if (lastCaptureTime != lastRollupCaptureTime) { gaugeValues.set(gaugeValues.size() - 1, lastGaugeValue.toBuilder() .setCaptureTime(lastRollupCaptureTime) .build()); } GaugeValue priorGaugeValue = null; for (GaugeValue gaugeValue : gaugeValues) { if (priorGaugeValue != null && gaugeValue.getCaptureTime() - priorGaugeValue.getCaptureTime() > gapMillis) { dataSeries.addNull(); } dataSeries.add(getIntervalAverage(rollup, timeZone, gaugeValue.getCaptureTime()), gaugeValue.getValue()); priorGaugeValue = gaugeValue; } double total = 0; long weight = 0; for (GaugeValue gaugeValue : gaugeValues) { total += gaugeValue.getValue() * gaugeValue.getWeight(); weight += gaugeValue.getWeight(); } // individual gauge value weights cannot be zero, and gaugeValues is non-empty // (see above conditional), so weight is guaranteed non-zero checkState(weight != 0); dataSeries.setOverall(total / weight); return dataSeries; } private static void checkPermissions(List<String> agentRollupIds, String permission, Authentication authentication) throws Exception { for (String agentRollupId : agentRollupIds) { if (!authentication.isAgentPermitted(agentRollupId, permission)) { throw new JsonServiceException(HttpResponseStatus.FORBIDDEN); } } } private static long getIntervalAverage(ROLLUP rollup, TimeZone timeZone, long captureTime) { return captureTime - getRollupIntervalMillis(rollup, timeZone, captureTime) / 2; } @VisibleForTesting static long getRollupIntervalMillis(ROLLUP rollup, TimeZone timeZone, long captureTime) { Calendar calendar; switch (rollup) { case HOURLY: return HOURS.toMillis(1); case DAILY: calendar = Calendar.getInstance(timeZone); calendar.setTimeInMillis(captureTime); calendar.add(Calendar.DATE, -1); return captureTime - calendar.getTimeInMillis(); case WEEKLY: calendar = Calendar.getInstance(timeZone); calendar.setTimeInMillis(captureTime); calendar.add(Calendar.DATE, -7); return captureTime - calendar.getTimeInMillis(); case MONTHLY: calendar = Calendar.getInstance(timeZone); calendar.setTimeInMillis(captureTime); calendar.add(Calendar.MONTH, -1); return captureTime - calendar.getTimeInMillis(); default: throw new IllegalStateException("Unexpected rollup: " + rollup); } } @Value.Immutable interface RequestWithAgentRollupIds { List<String> agentRollupIds(); } @Value.Immutable interface ReportRequest { List<String> agentRollupIds(); String metricId(); @Nullable Double metricPercentile(); @Nullable String transactionType(); String fromDate(); String toDate(); ROLLUP rollup(); String timeZoneId(); } enum ROLLUP { HOURLY, DAILY, WEEKLY, MONTHLY } @VisibleForTesting static class RollupCaptureTimeFn implements Function<Long, Long> { private final ROLLUP rollup; private final TimeZone timeZone; private final int baseDayOfWeek; @VisibleForTesting RollupCaptureTimeFn(ROLLUP rollup, TimeZone timeZone, String baseCaptureTime) throws ParseException { this.rollup = rollup; this.timeZone = timeZone; if (rollup == ROLLUP.WEEKLY) { // SimpleDateFormat and Calendar both need to be in same timezone // (in this case default) SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd"); Calendar calendar = Calendar.getInstance(); calendar.setTime(simpleDateFormat.parse(baseCaptureTime)); baseDayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); } else { baseDayOfWeek = -1; } } @Override public Long apply(Long captureTime) { Calendar calendar; switch (rollup) { case HOURLY: return Utils.getRollupCaptureTime(captureTime, HOURS.toMillis(1), timeZone); case DAILY: return getDailyRollupCaptureTime(captureTime).getTimeInMillis(); case WEEKLY: calendar = getDailyRollupCaptureTime(captureTime); int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); int diff = baseDayOfWeek - dayOfWeek; if (diff < 0) { diff += 7; } calendar.add(Calendar.DATE, diff); return calendar.getTimeInMillis(); case MONTHLY: calendar = Calendar.getInstance(timeZone); calendar.setTimeInMillis(captureTime); calendar.set(Calendar.DAY_OF_MONTH, 1); calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); if (calendar.getTimeInMillis() == captureTime) { return captureTime; } else { calendar.add(Calendar.MONTH, 1); return calendar.getTimeInMillis(); } default: throw new IllegalStateException("Unexpected rollup: " + rollup); } } private Calendar getDailyRollupCaptureTime(Long captureTime) { Calendar calendar; calendar = Calendar.getInstance(timeZone); calendar.setTimeInMillis(captureTime); calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); if (calendar.getTimeInMillis() == captureTime) { return calendar; } else { calendar.add(Calendar.DATE, 1); return calendar; } } } }