org.glowroot.local.ui.TransactionJsonService.java Source code

Java tutorial

Introduction

Here is the source code for org.glowroot.local.ui.TransactionJsonService.java

Source

/*
 * Copyright 2011-2015 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.local.ui;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.annotation.Nullable;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Strings;
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.collector.Aggregate;
import org.glowroot.collector.LazyHistogram;
import org.glowroot.collector.TransactionCollectorImpl;
import org.glowroot.collector.TransactionSummary;
import org.glowroot.common.Clock;
import org.glowroot.common.ObjectMappers;
import org.glowroot.local.store.AggregateDao;
import org.glowroot.local.store.AggregateDao.TransactionSummarySortOrder;
import org.glowroot.local.store.AggregateTimer;
import org.glowroot.local.store.QueryResult;
import org.glowroot.local.store.TraceDao;
import org.glowroot.local.store.TransactionSummaryQuery;
import org.glowroot.transaction.TransactionRegistry;
import org.glowroot.transaction.model.ProfileNode;
import org.glowroot.transaction.model.Transaction;

import static com.google.common.base.Preconditions.checkNotNull;

@JsonService
class TransactionJsonService {

    private static final ObjectMapper mapper = ObjectMappers.create();
    private static final double MICROSECONDS_PER_MILLISECOND = 1000.0;

    private final TransactionCommonService transactionCommonService;
    private final TraceDao traceDao;
    private final TransactionRegistry transactionRegistry;
    private final TransactionCollectorImpl transactionCollector;
    private final Clock clock;

    private final long fixedAggregateIntervalMillis;
    private final long fixedAggregateRollupMillis;

    TransactionJsonService(TransactionCommonService transactionCommonService, TraceDao traceDao,
            TransactionRegistry transactionRegistry, TransactionCollectorImpl transactionCollector, Clock clock,
            long fixedAggregateIntervalSeconds, long fixedAggregateRollupSeconds) {
        this.transactionCommonService = transactionCommonService;
        this.traceDao = traceDao;
        this.transactionRegistry = transactionRegistry;
        this.transactionCollector = transactionCollector;
        this.clock = clock;
        this.fixedAggregateIntervalMillis = fixedAggregateIntervalSeconds * 1000;
        this.fixedAggregateRollupMillis = fixedAggregateRollupSeconds * 1000;
    }

    @GET("/backend/transaction/overview")
    String getOverview(String queryString) throws Exception {
        TransactionDataRequest request = QueryStrings.decode(queryString, TransactionDataRequest.class);

        List<Aggregate> aggregates = transactionCommonService.getAggregates(request.transactionType(),
                request.transactionName(), request.from(), request.to());
        List<DataSeries> dataSeriesList = getDataSeriesForOverviewChart(request, aggregates);
        Map<Long, Long> transactionCounts = getTransactionCounts(aggregates);
        if (!aggregates.isEmpty() && aggregates.get(0).captureTime() == request.from()) {
            // the left most aggregate is not really in the requested interval since it is for
            // prior capture times
            aggregates = aggregates.subList(1, aggregates.size());
        }
        HistogramMergedAggregate histogramMergedAggregate = AggregateMerging
                .getHistogramMergedAggregate(aggregates);

        StringBuilder sb = new StringBuilder();
        JsonGenerator jg = mapper.getFactory().createGenerator(CharStreams.asWriter(sb));
        jg.writeStartObject();
        jg.writeObjectField("dataSeries", dataSeriesList);
        jg.writeObjectField("transactionCounts", transactionCounts);
        jg.writeObjectField("mergedAggregate", histogramMergedAggregate);
        jg.writeEndObject();
        jg.close();
        return sb.toString();
    }

    private Map<Long, Long> getTransactionCounts(List<Aggregate> aggregates) {
        Map<Long, Long> transactionCounts = Maps.newHashMap();
        for (Aggregate aggregate : aggregates) {
            transactionCounts.put(aggregate.captureTime(), aggregate.transactionCount());
        }
        return transactionCounts;
    }

    @GET("/backend/transaction/metrics")
    String getMetrics(String queryString) throws Exception {
        TransactionDataRequest request = QueryStrings.decode(queryString, TransactionDataRequest.class);

        List<Aggregate> aggregates = transactionCommonService.getAggregates(request.transactionType(),
                request.transactionName(), request.from(), request.to());
        List<DataSeries> dataSeriesList = getDataSeriesForMetricsChart(request, aggregates);
        Map<Long, Long> transactionCounts = getTransactionCounts(aggregates);
        if (!aggregates.isEmpty() && aggregates.get(0).captureTime() == request.from()) {
            // the left most aggregate is not really in the requested interval since it is for
            // prior capture times
            aggregates = aggregates.subList(1, aggregates.size());
        }
        TimerMergedAggregate timerMergedAggregate = AggregateMerging.getTimerMergedAggregate(aggregates);
        ThreadInfoAggregate threadInfoAggregate = AggregateMerging.getThreadInfoAggregate(aggregates);

        StringBuilder sb = new StringBuilder();
        JsonGenerator jg = mapper.getFactory().createGenerator(CharStreams.asWriter(sb));
        jg.writeStartObject();
        jg.writeObjectField("dataSeries", dataSeriesList);
        jg.writeObjectField("transactionCounts", transactionCounts);
        jg.writeObjectField("mergedAggregate", timerMergedAggregate);
        if (!threadInfoAggregate.isEmpty()) {
            jg.writeObjectField("threadInfoAggregate", threadInfoAggregate);
        }
        jg.writeEndObject();
        jg.close();
        return sb.toString();
    }

    @GET("/backend/transaction/profile")
    String getProfile(String queryString) throws Exception {
        TransactionProfileRequest request = QueryStrings.decode(queryString, TransactionProfileRequest.class);
        ProfileNode profile = transactionCommonService.getProfile(request.transactionType(),
                request.transactionName(), request.from(), request.to(), request.truncateLeafPercentage());
        StringBuilder sb = new StringBuilder();
        JsonGenerator jg = mapper.getFactory().createGenerator(CharStreams.asWriter(sb));
        jg.writeObject(profile);
        jg.close();
        return sb.toString();
    }

    @GET("/backend/transaction/summaries")
    String getSummaries(String queryString) throws Exception {
        TransactionSummaryRequest request = QueryStrings.decode(queryString, TransactionSummaryRequest.class);

        TransactionSummary overallSummary = transactionCommonService.readOverallSummary(request.transactionType(),
                request.from() + 1, request.to());

        TransactionSummaryQuery query = TransactionSummaryQuery.builder().transactionType(request.transactionType())
                .from(request.from() + 1).to(request.to()).sortOrder(request.sortOrder()).limit(request.limit())
                .build();
        QueryResult<TransactionSummary> queryResult = transactionCommonService.readTransactionSummaries(query);

        StringBuilder sb = new StringBuilder();
        JsonGenerator jg = mapper.getFactory().createGenerator(CharStreams.asWriter(sb));
        jg.writeStartObject();
        jg.writeFieldName("overall");
        jg.writeObject(overallSummary);
        jg.writeFieldName("transactions");
        jg.writeObject(queryResult.records());
        jg.writeBooleanField("moreAvailable", queryResult.moreAvailable());
        jg.writeEndObject();
        jg.close();
        return sb.toString();
    }

    @GET("/backend/transaction/tab-bar-data")
    String getTabBarData(String queryString) throws Exception {
        TransactionDataRequest request = QueryStrings.decode(queryString, TransactionDataRequest.class);

        String transactionName = request.transactionName();
        long profileSampleCount = transactionCommonService.getProfileSampleCount(request.transactionType(),
                transactionName, request.from(), request.to());
        boolean profileExpired = false;
        if (profileSampleCount == 0) {
            profileExpired = transactionCommonService.shouldHaveProfiles(request.transactionType(), transactionName,
                    request.from(), request.to());
        }
        long traceCount;
        if (transactionName == null) {
            traceCount = traceDao.readOverallCount(request.transactionType(), request.from(), request.to());
        } else {
            traceCount = traceDao.readTransactionCount(request.transactionType(), transactionName, request.from(),
                    request.to());
        }
        boolean includeActiveTraces = shouldIncludeActiveTraces(request);
        if (includeActiveTraces) {
            // include active traces, this is mostly for the case where there is just a single very
            // long running active trace and it would be misleading to display Traces (0) on the tab
            for (Transaction transaction : transactionRegistry.getTransactions()) {
                // don't include partially stored traces since those are already counted above
                if (matchesActive(transaction, request) && !transaction.isPartiallyStored()) {
                    traceCount++;
                }
            }
        }
        boolean tracesExpired = false;
        if (traceCount == 0) {
            tracesExpired = transactionCommonService.shouldHaveTraces(request.transactionType(), transactionName,
                    request.from(), request.to());
        }

        StringBuilder sb = new StringBuilder();
        JsonGenerator jg = mapper.getFactory().createGenerator(CharStreams.asWriter(sb));
        jg.writeStartObject();
        jg.writeNumberField("profileSampleCount", profileSampleCount);
        jg.writeBooleanField("profileExpired", profileExpired);
        jg.writeNumberField("traceCount", traceCount);
        jg.writeBooleanField("tracesExpired", tracesExpired);
        jg.writeEndObject();
        jg.close();
        return sb.toString();
    }

    @GET("/backend/transaction/flame-graph")
    String getFlameGraph(String queryString) throws Exception {
        FlameGraphRequest request = QueryStrings.decode(queryString, FlameGraphRequest.class);
        ProfileNode profile = transactionCommonService.getProfile(request.transactionType(),
                request.transactionName(), request.from(), request.to(), request.truncateLeafPercentage());
        ProfileNode interestingNode = profile;
        while (interestingNode.getChildNodes().size() == 1) {
            interestingNode = interestingNode.getChildNodes().get(0);
        }
        if (interestingNode.getChildNodes().isEmpty()) {
            // only a single branch through entire tree
            interestingNode = profile;
        }
        StringBuilder sb = new StringBuilder();
        JsonGenerator jg = mapper.getFactory().createGenerator(CharStreams.asWriter(sb));
        jg.writeStartObject();
        jg.writeObjectFieldStart("");
        jg.writeNumberField("svUnique", 0);
        jg.writeNumberField("svTotal", interestingNode.getSampleCount());
        jg.writeObjectFieldStart("svChildren");
        writeFlameGraphNode(interestingNode, jg);
        jg.writeEndObject();
        jg.writeEndObject();
        jg.close();
        return sb.toString();
    }

    private List<DataSeries> getDataSeriesForOverviewChart(TransactionDataRequest request,
            List<Aggregate> aggregates) throws Exception {
        if (aggregates.isEmpty()) {
            return Lists.newArrayList();
        }
        DataSeriesHelper dataSeriesHelper = new DataSeriesHelper(clock, getDataPointIntervalMillis(request));
        List<DataSeries> dataSeriesList = Lists.newArrayList();
        DataSeries dataSeries1 = new DataSeries("50th percentile");
        DataSeries dataSeries2 = new DataSeries("95th percentile");
        DataSeries dataSeries3 = new DataSeries("99th percentile");
        dataSeriesList.add(dataSeries1);
        dataSeriesList.add(dataSeries2);
        dataSeriesList.add(dataSeries3);
        Aggregate lastAggregate = null;
        for (Aggregate aggregate : aggregates) {
            if (lastAggregate == null) {
                // first aggregate
                dataSeriesHelper.addInitialUpslopeIfNeeded(request.from(), aggregate.captureTime(), dataSeriesList,
                        null);
            } else {
                dataSeriesHelper.addGapIfNeeded(lastAggregate.captureTime(), aggregate.captureTime(),
                        dataSeriesList, null);
            }
            lastAggregate = aggregate;
            LazyHistogram histogram = new LazyHistogram();
            histogram.decodeFromByteBuffer(ByteBuffer.wrap(aggregate.histogram()));
            dataSeries1.add(aggregate.captureTime(),
                    histogram.getValueAtPercentile(50) / MICROSECONDS_PER_MILLISECOND);
            dataSeries2.add(aggregate.captureTime(),
                    histogram.getValueAtPercentile(95) / MICROSECONDS_PER_MILLISECOND);
            dataSeries3.add(aggregate.captureTime(),
                    histogram.getValueAtPercentile(99) / MICROSECONDS_PER_MILLISECOND);
        }
        if (lastAggregate != null) {
            dataSeriesHelper.addFinalDownslopeIfNeeded(request.to(), dataSeriesList, null,
                    lastAggregate.captureTime());
        }
        return dataSeriesList;
    }

    private List<DataSeries> getDataSeriesForMetricsChart(TransactionDataRequest request,
            List<Aggregate> aggregates) throws IOException {
        if (aggregates.isEmpty()) {
            return Lists.newArrayList();
        }
        List<StackedPoint> stackedPoints = Lists.newArrayList();
        for (Aggregate aggregate : aggregates) {
            stackedPoints.add(StackedPoint.create(aggregate));
        }
        return getMetricDataSeries(request, stackedPoints);
    }

    private List<DataSeries> getMetricDataSeries(TransactionDataRequest request, List<StackedPoint> stackedPoints) {
        DataSeriesHelper dataSeriesHelper = new DataSeriesHelper(clock, getDataPointIntervalMillis(request));
        final int topX = 5;
        List<String> timerNames = getTopTimerNames(stackedPoints, topX + 1);
        List<DataSeries> dataSeriesList = Lists.newArrayList();
        for (int i = 0; i < Math.min(timerNames.size(), topX); i++) {
            dataSeriesList.add(new DataSeries(timerNames.get(i)));
        }
        // need 'other' data series even if < topX timers in order to capture root timers,
        // e.g. time spent in 'servlet' timer but not in any nested timer
        DataSeries otherDataSeries = new DataSeries(null);
        Aggregate lastAggregate = null;
        for (StackedPoint stackedPoint : stackedPoints) {
            Aggregate aggregate = stackedPoint.getAggregate();
            if (lastAggregate == null) {
                // first aggregate
                dataSeriesHelper.addInitialUpslopeIfNeeded(request.from(), aggregate.captureTime(), dataSeriesList,
                        otherDataSeries);
            } else {
                dataSeriesHelper.addGapIfNeeded(lastAggregate.captureTime(), aggregate.captureTime(),
                        dataSeriesList, otherDataSeries);
            }
            lastAggregate = aggregate;
            MutableLongMap<String> stackedTimers = stackedPoint.getStackedTimers();
            long totalOtherMicros = aggregate.totalMicros();
            for (DataSeries dataSeries : dataSeriesList) {
                MutableLong totalMicros = stackedTimers.get(dataSeries.getName());
                if (totalMicros == null) {
                    dataSeries.add(aggregate.captureTime(), 0);
                } else {
                    // convert to average seconds
                    dataSeries.add(aggregate.captureTime(),
                            (totalMicros.longValue() / (double) aggregate.transactionCount())
                                    / MICROSECONDS_PER_MILLISECOND);
                    totalOtherMicros -= totalMicros.longValue();
                }
            }
            if (aggregate.transactionCount() == 0) {
                otherDataSeries.add(aggregate.captureTime(), 0);
            } else {
                // convert to average seconds
                otherDataSeries.add(aggregate.captureTime(),
                        (totalOtherMicros / (double) aggregate.transactionCount()) / MICROSECONDS_PER_MILLISECOND);
            }
        }
        if (lastAggregate != null) {
            dataSeriesHelper.addFinalDownslopeIfNeeded(request.to(), dataSeriesList, otherDataSeries,
                    lastAggregate.captureTime());
        }
        dataSeriesList.add(otherDataSeries);
        return dataSeriesList;
    }

    private long getDataPointIntervalMillis(TransactionDataRequest request) {
        if (request.to() - request.from() > AggregateDao.ROLLUP_THRESHOLD_MILLIS) {
            return fixedAggregateRollupMillis;
        } else {
            return fixedAggregateIntervalMillis;
        }
    }

    // calculate top 5 timers
    private static List<String> getTopTimerNames(List<StackedPoint> stackedPoints, int topX) {
        MutableLongMap<String> timerTotals = new MutableLongMap<String>();
        for (StackedPoint stackedPoint : stackedPoints) {
            for (Entry<String, MutableLong> entry : stackedPoint.getStackedTimers().entrySet()) {
                timerTotals.add(entry.getKey(), entry.getValue().longValue());
            }
        }
        Ordering<Entry<String, MutableLong>> valueOrdering = Ordering.natural()
                .onResultOf(new Function<Entry<String, MutableLong>, Long>() {
                    @Override
                    public Long apply(@Nullable Entry<String, MutableLong> entry) {
                        checkNotNull(entry);
                        return entry.getValue().longValue();
                    }
                });
        List<String> timerNames = Lists.newArrayList();
        @SuppressWarnings("assignment.type.incompatible")
        List<Entry<String, MutableLong>> topTimerTotals = valueOrdering.greatestOf(timerTotals.entrySet(), topX);
        for (Entry<String, MutableLong> entry : topTimerTotals) {
            timerNames.add(entry.getKey());
        }
        return timerNames;
    }

    private boolean shouldIncludeActiveTraces(TransactionDataRequest request) {
        long currentTimeMillis = clock.currentTimeMillis();
        return (request.to() == 0 || request.to() > currentTimeMillis) && request.from() < currentTimeMillis;
    }

    @VisibleForTesting
    boolean matchesActive(Transaction transaction, TransactionDataRequest request) {
        if (!transactionCollector.shouldStore(transaction)) {
            return false;
        }
        if (!request.transactionType().equals(transaction.getTransactionType())) {
            return false;
        }
        String transactionName = request.transactionName();
        if (transactionName != null && !transactionName.equals(transaction.getTransactionName())) {
            return false;
        }
        return true;
    }

    private static void writeFlameGraphNode(ProfileNode node, JsonGenerator jg) throws IOException {
        jg.writeObjectFieldStart(Strings.nullToEmpty(node.getStackTraceElement()));
        int svUnique = node.getSampleCount();
        for (ProfileNode childNode : node.getChildNodes()) {
            svUnique -= childNode.getSampleCount();
        }
        jg.writeNumberField("svUnique", svUnique);
        jg.writeNumberField("svTotal", node.getSampleCount());
        jg.writeObjectFieldStart("svChildren");
        for (ProfileNode childNode : node.getChildNodes()) {
            writeFlameGraphNode(childNode, jg);
        }
        jg.writeEndObject();
        jg.writeEndObject();
    }

    private static class StackedPoint {

        private final Aggregate aggregate;
        // stacked timer values only include time spent as a leaf node in the timer tree
        private final MutableLongMap<String> stackedTimers;

        private static StackedPoint create(Aggregate aggregate) throws IOException {
            String timers = aggregate.timers();
            MutableLongMap<String> stackedTimers = new MutableLongMap<String>();
            AggregateTimer syntheticRootTimer = mapper.readValue(timers, AggregateTimer.class);
            // skip synthetic root timer
            for (AggregateTimer realRootTimer : syntheticRootTimer.getNestedTimers()) {
                // skip real root timers
                for (AggregateTimer topLevelTimer : realRootTimer.getNestedTimers()) {
                    // traverse tree starting at top-level (under root) timers
                    addToStackedTimer(topLevelTimer, stackedTimers);
                }
            }
            return new StackedPoint(aggregate, stackedTimers);
        }

        private StackedPoint(Aggregate aggregate, MutableLongMap<String> stackedTimers) {
            this.aggregate = aggregate;
            this.stackedTimers = stackedTimers;
        }

        private Aggregate getAggregate() {
            return aggregate;
        }

        private MutableLongMap<String> getStackedTimers() {
            return stackedTimers;
        }

        private static void addToStackedTimer(AggregateTimer timer, MutableLongMap<String> stackedTimers) {
            long totalNestedMicros = 0;
            for (AggregateTimer nestedTimer : timer.getNestedTimers()) {
                totalNestedMicros += nestedTimer.getTotalMicros();
                addToStackedTimer(nestedTimer, stackedTimers);
            }
            // timer name is only null for synthetic root timer which is never passed to this method
            String timerName = checkNotNull(timer.getName());
            stackedTimers.add(timerName, timer.getTotalMicros() - totalNestedMicros);
        }
    }

    // by using MutableLong, two operations (get/put) are not required for each increment,
    // instead just a single get is needed (except for first delta)
    //
    // not thread safe, for thread safety use guava's AtomicLongMap
    @SuppressWarnings("serial")
    private static class MutableLongMap<K> extends HashMap<K, MutableLong> {
        private void add(K key, long delta) {
            MutableLong existing = get(key);
            if (existing == null) {
                put(key, new MutableLong(delta));
            } else {
                existing.value += delta;
            }
        }
    }

    private static class MutableLong {
        private long value;

        private MutableLong(long value) {
            this.value = value;
        }

        private long longValue() {
            return value;
        }
    }

    @Value.Immutable
    @JsonSerialize
    abstract static class TransactionSummaryRequestBase {
        abstract long from();

        abstract long to();

        abstract String transactionType();

        abstract TransactionSummarySortOrder sortOrder();

        abstract int limit();
    }

    @Value.Immutable
    @JsonSerialize
    abstract static class TransactionDataRequestBase {
        abstract long from();

        abstract long to();

        abstract String transactionType();

        abstract @Nullable String transactionName();
    }

    @Value.Immutable
    @JsonSerialize
    abstract static class TransactionProfileRequestBase {
        abstract long from();

        abstract long to();

        abstract String transactionType();

        abstract @Nullable String transactionName();

        abstract double truncateLeafPercentage();
    }

    @Value.Immutable
    @JsonSerialize
    abstract static class FlameGraphRequestBase {
        abstract long from();

        abstract long to();

        abstract String transactionType();

        abstract @Nullable String transactionName();

        abstract double truncateLeafPercentage();
    }
}