org.glowroot.central.repo.TraceDao.java Source code

Java tutorial

Introduction

Here is the source code for org.glowroot.central.repo.TraceDao.java

Source

/*
 * Copyright 2015-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.central.repo;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import javax.annotation.Nullable;

import com.datastax.driver.core.BoundStatement;
import com.datastax.driver.core.PreparedStatement;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.ResultSetFuture;
import com.datastax.driver.core.Row;
import com.datastax.driver.core.Session;
import com.google.common.base.Charsets;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.hash.Hashing;
import com.google.common.primitives.Ints;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.immutables.value.Value;

import org.glowroot.central.util.Messages;
import org.glowroot.central.util.MoreFutures;
import org.glowroot.central.util.Sessions;
import org.glowroot.common.config.StorageConfig;
import org.glowroot.common.live.ImmutableEntries;
import org.glowroot.common.live.ImmutableTracePoint;
import org.glowroot.common.live.LiveTraceRepository.Entries;
import org.glowroot.common.live.LiveTraceRepository.Existence;
import org.glowroot.common.live.LiveTraceRepository.TracePoint;
import org.glowroot.common.live.LiveTraceRepository.TracePointFilter;
import org.glowroot.common.model.Result;
import org.glowroot.common.repo.ConfigRepository;
import org.glowroot.common.repo.ImmutableErrorMessageCount;
import org.glowroot.common.repo.ImmutableErrorMessagePoint;
import org.glowroot.common.repo.ImmutableErrorMessageResult;
import org.glowroot.common.repo.ImmutableHeaderPlus;
import org.glowroot.common.repo.TraceRepository;
import org.glowroot.common.repo.Utils;
import org.glowroot.common.util.Clock;
import org.glowroot.common.util.Styles;
import org.glowroot.wire.api.model.ProfileOuterClass.Profile;
import org.glowroot.wire.api.model.Proto;
import org.glowroot.wire.api.model.Proto.StackTraceElement;
import org.glowroot.wire.api.model.TraceOuterClass.Trace;

import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.concurrent.TimeUnit.HOURS;

public class TraceDao implements TraceRepository {

    private final Session session;
    private final AgentDao agentDao;
    private final TransactionTypeDao transactionTypeDao;
    private final FullQueryTextDao fullQueryTextDao;
    private final TraceAttributeNameDao traceAttributeNameDao;
    private final ConfigRepository configRepository;
    private final Clock clock;

    private final PreparedStatement insertCheck;

    private final PreparedStatement insertOverallSlowPoint;
    private final PreparedStatement insertTransactionSlowPoint;

    private final PreparedStatement insertOverallSlowCount;
    private final PreparedStatement insertTransactionSlowCount;

    private final PreparedStatement insertOverallErrorPoint;
    private final PreparedStatement insertTransactionErrorPoint;

    private final PreparedStatement insertOverallErrorCount;
    private final PreparedStatement insertTransactionErrorCount;

    private final PreparedStatement insertOverallErrorMessage;
    private final PreparedStatement insertTransactionErrorMessage;

    private final PreparedStatement insertHeader;
    private final PreparedStatement insertEntry;
    private final PreparedStatement insertSharedQueryText;
    private final PreparedStatement insertMainThreadProfile;
    private final PreparedStatement insertAuxThreadProfile;

    private final PreparedStatement readCheck;

    private final PreparedStatement readOverallSlowPoint;
    private final PreparedStatement readTransactionSlowPoint;
    private final PreparedStatement readOverallErrorPoint;
    private final PreparedStatement readTransactionErrorPoint;

    private final PreparedStatement readOverallErrorMessage;
    private final PreparedStatement readTransactionErrorMessage;

    private final PreparedStatement readHeader;
    private final PreparedStatement readEntries;
    private final PreparedStatement readSharedQueryTexts;
    private final PreparedStatement readMainThreadProfile;
    private final PreparedStatement readAuxThreadProfile;

    private final PreparedStatement deletePartialOverallSlowPoint;
    private final PreparedStatement deletePartialTransactionSlowPoint;

    private final PreparedStatement deletePartialOverallSlowCount;
    private final PreparedStatement deletePartialTransactionSlowCount;

    public TraceDao(Session session, AgentDao agentDao, TransactionTypeDao transactionTypeDao,
            FullQueryTextDao fullQueryTextDao, TraceAttributeNameDao traceAttributeNameDao,
            ConfigRepository configRepository, Clock clock) throws Exception {
        this.session = session;
        this.agentDao = agentDao;
        this.transactionTypeDao = transactionTypeDao;
        this.fullQueryTextDao = fullQueryTextDao;
        this.traceAttributeNameDao = traceAttributeNameDao;
        this.configRepository = configRepository;
        this.clock = clock;

        int expirationHours = configRepository.getStorageConfig().traceExpirationHours();

        Sessions.createTableWithTWCS(session,
                "create table if not exists trace_check"
                        + " (agent_rollup varchar, agent_id varchar, trace_id varchar, primary key"
                        + " ((agent_rollup, agent_id), trace_id))",
                expirationHours);

        Sessions.createTableWithTWCS(session,
                "create table if not exists trace_tt_slow_point"
                        + " (agent_rollup varchar, transaction_type varchar, capture_time timestamp,"
                        + " agent_id varchar, trace_id varchar, duration_nanos bigint, partial boolean,"
                        + " error boolean, headline varchar, user varchar, attributes blob, primary key"
                        + " ((agent_rollup, transaction_type), capture_time, agent_id, trace_id))",
                expirationHours);

        Sessions.createTableWithTWCS(session,
                "create table if not exists trace_tn_slow_point"
                        + " (agent_rollup varchar, transaction_type varchar, transaction_name varchar,"
                        + " capture_time timestamp, agent_id varchar, trace_id varchar,"
                        + " duration_nanos bigint, partial boolean, error boolean, headline varchar,"
                        + " user varchar, attributes blob, primary key ((agent_rollup, transaction_type,"
                        + " transaction_name), capture_time, agent_id, trace_id))",
                expirationHours);

        Sessions.createTableWithTWCS(session,
                "create table if not exists trace_tt_error_point"
                        + " (agent_rollup varchar, transaction_type varchar, capture_time timestamp,"
                        + " agent_id varchar, trace_id varchar, duration_nanos bigint, partial boolean,"
                        + " error_message varchar, headline varchar, user varchar, attributes blob,"
                        + " primary key ((agent_rollup, transaction_type), capture_time, agent_id," + " trace_id))",
                expirationHours);

        Sessions.createTableWithTWCS(session,
                "create table if not exists trace_tn_error_point"
                        + " (agent_rollup varchar, transaction_type varchar, transaction_name varchar,"
                        + " capture_time timestamp, agent_id varchar, trace_id varchar,"
                        + " duration_nanos bigint, partial boolean, error_message varchar,"
                        + " headline varchar, user varchar, attributes blob, primary key ((agent_rollup,"
                        + " transaction_type, transaction_name), capture_time, agent_id, trace_id))",
                expirationHours);

        Sessions.createTableWithTWCS(session,
                "create table if not exists trace_tt_error_message"
                        + " (agent_rollup varchar, transaction_type varchar, capture_time timestamp,"
                        + " agent_id varchar, trace_id varchar, error_message varchar, primary key"
                        + " ((agent_rollup, transaction_type), capture_time, agent_id, trace_id))",
                expirationHours);

        Sessions.createTableWithTWCS(session,
                "create table if not exists trace_tn_error_message"
                        + " (agent_rollup varchar, transaction_type varchar, transaction_name varchar,"
                        + " capture_time timestamp, agent_id varchar, trace_id varchar,"
                        + " error_message varchar, primary key ((agent_rollup, transaction_type,"
                        + " transaction_name), capture_time, agent_id, trace_id))",
                expirationHours);

        Sessions.createTableWithTWCS(session, "create table if not exists trace_header"
                + " (agent_id varchar, trace_id varchar, header blob, primary key (agent_id," + " trace_id))",
                expirationHours);

        // index_ is just to provide uniqueness
        Sessions.createTableWithTWCS(session,
                "create table if not exists trace_entry"
                        + " (agent_id varchar, trace_id varchar, index_ int, depth int,"
                        + " start_offset_nanos bigint, duration_nanos bigint, active boolean,"
                        + " message varchar, shared_query_text_index int, query_message_prefix varchar,"
                        + " query_message_suffix varchar, detail blob, location_stack_trace blob,"
                        + " error blob, primary key (agent_id, trace_id, index_))",
                expirationHours);

        // index_ is just to provide uniqueness
        Sessions.createTableWithTWCS(session,
                "create table if not exists trace_shared_query_text"
                        + " (agent_id varchar, trace_id varchar, index_ int, truncated_text varchar,"
                        + " truncated_end_text varchar, full_text_sha1 varchar, primary key (agent_id,"
                        + " trace_id, index_))",
                expirationHours);

        Sessions.createTableWithTWCS(session, "create table if not exists trace_main_thread_profile"
                + " (agent_id varchar, trace_id varchar, profile blob, primary key (agent_id," + " trace_id))",
                expirationHours);

        Sessions.createTableWithTWCS(session, "create table if not exists trace_aux_thread_profile"
                + " (agent_id varchar, trace_id varchar, profile blob, primary key (agent_id," + " trace_id))",
                expirationHours);

        // agent_rollup/capture_time is not necessarily unique
        // using a counter would be nice since only need sum over capture_time range
        // but counter has no TTL, see https://issues.apache.org/jira/browse/CASSANDRA-2103
        // so adding trace_id to provide uniqueness
        Sessions.createTableWithTWCS(session,
                "create table if not exists trace_tt_slow_count"
                        + " (agent_rollup varchar, transaction_type varchar, capture_time timestamp,"
                        + " agent_id varchar, trace_id varchar, primary key ((agent_rollup,"
                        + " transaction_type), capture_time, agent_id, trace_id))",
                expirationHours);

        Sessions.createTableWithTWCS(session, "create table if not exists trace_tn_slow_count"
                + " (agent_rollup varchar, transaction_type varchar, transaction_name varchar,"
                + " capture_time timestamp, agent_id varchar, trace_id varchar, primary key"
                + " ((agent_rollup, transaction_type, transaction_name), capture_time, agent_id," + " trace_id))",
                expirationHours);

        Sessions.createTableWithTWCS(session,
                "create table if not exists trace_tt_error_count"
                        + " (agent_rollup varchar, transaction_type varchar, capture_time timestamp,"
                        + " agent_id varchar, trace_id varchar, primary key ((agent_rollup,"
                        + " transaction_type), capture_time, agent_id, trace_id))",
                expirationHours);

        Sessions.createTableWithTWCS(session, "create table if not exists trace_tn_error_count"
                + " (agent_rollup varchar, transaction_type varchar, transaction_name varchar,"
                + " capture_time timestamp, agent_id varchar, trace_id varchar, primary key"
                + " ((agent_rollup, transaction_type, transaction_name), capture_time, agent_id," + " trace_id))",
                expirationHours);

        insertCheck = session.prepare(
                "insert into trace_check (agent_rollup, agent_id, trace_id)" + " values (?, ?, ?) using ttl ?");

        insertOverallSlowPoint = session.prepare("insert into trace_tt_slow_point (agent_rollup,"
                + " transaction_type, capture_time, agent_id, trace_id, duration_nanos, partial,"
                + " error, headline, user, attributes) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + " using ttl ?");

        insertTransactionSlowPoint = session.prepare("insert into trace_tn_slow_point"
                + " (agent_rollup, transaction_type, transaction_name, capture_time, agent_id,"
                + " trace_id, duration_nanos, partial, error, headline, user, attributes) values"
                + " (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) using ttl ?");

        insertOverallSlowCount = session.prepare("insert into trace_tt_slow_count (agent_rollup,"
                + " transaction_type, capture_time, agent_id, trace_id) values (?, ?, ?, ?, ?)" + " using ttl ?");

        insertTransactionSlowCount = session.prepare("insert into trace_tn_slow_count"
                + " (agent_rollup, transaction_type, transaction_name, capture_time, agent_id,"
                + " trace_id) values (?, ?, ?, ?, ?, ?) using ttl ?");

        insertOverallErrorPoint = session.prepare("insert into trace_tt_error_point (agent_rollup,"
                + " transaction_type, capture_time, agent_id, trace_id, duration_nanos, partial,"
                + " error_message, headline, user, attributes) values"
                + " (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) using ttl ?");

        insertTransactionErrorPoint = session.prepare("insert into trace_tn_error_point"
                + " (agent_rollup, transaction_type, transaction_name, capture_time, agent_id,"
                + " trace_id, duration_nanos, partial, error_message, headline, user, attributes)"
                + " values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) using ttl ?");

        insertOverallErrorCount = session.prepare("insert into trace_tt_error_count (agent_rollup,"
                + " transaction_type, capture_time, agent_id, trace_id) values (?, ?, ?, ?, ?)" + " using ttl ?");

        insertTransactionErrorCount = session.prepare("insert into trace_tn_error_count"
                + " (agent_rollup, transaction_type, transaction_name, capture_time, agent_id,"
                + " trace_id) values (?, ?, ?, ?, ?, ?) using ttl ?");

        insertOverallErrorMessage = session.prepare("insert into trace_tt_error_message"
                + " (agent_rollup, transaction_type, capture_time, agent_id, trace_id,"
                + " error_message) values (?, ?, ?, ?, ?, ?) using ttl ?");

        insertTransactionErrorMessage = session.prepare("insert into trace_tn_error_message"
                + " (agent_rollup, transaction_type, transaction_name, capture_time, agent_id,"
                + " trace_id, error_message) values (?, ?, ?, ?, ?, ?, ?) using ttl ?");

        insertHeader = session
                .prepare("insert into trace_header (agent_id, trace_id, header)" + " values (?, ?, ?) using ttl ?");

        insertEntry = session.prepare("insert into trace_entry (agent_id, trace_id, index_, depth,"
                + " start_offset_nanos, duration_nanos, active, message, shared_query_text_index,"
                + " query_message_prefix, query_message_suffix, detail, location_stack_trace,"
                + " error) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) using ttl ?");

        insertSharedQueryText = session.prepare("insert into trace_shared_query_text (agent_id,"
                + " trace_id, index_, truncated_text, truncated_end_text, full_text_sha1) values"
                + " (?, ?, ?, ?, ?, ?) using ttl ?");

        insertMainThreadProfile = session.prepare("insert into trace_main_thread_profile"
                + " (agent_id, trace_id, profile) values (?, ?, ?) using ttl ?");

        insertAuxThreadProfile = session.prepare("insert into trace_aux_thread_profile"
                + " (agent_id, trace_id, profile) values (?, ?, ?) using ttl ?");

        readCheck = session.prepare(
                "select trace_id from trace_check where agent_rollup = ?" + " and agent_id = ? and trace_id = ?");

        readOverallSlowPoint = session.prepare("select agent_id, trace_id, capture_time,"
                + " duration_nanos, partial, error, headline, user, attributes"
                + " from trace_tt_slow_point where agent_rollup = ? and transaction_type = ?"
                + " and capture_time > ? and capture_time <= ?");

        readTransactionSlowPoint = session.prepare("select agent_id, trace_id, capture_time,"
                + " duration_nanos, partial, error, headline, user, attributes"
                + " from trace_tn_slow_point where agent_rollup = ? and transaction_type = ?"
                + " and transaction_name = ? and capture_time > ? and capture_time <= ?");

        readOverallErrorPoint = session.prepare("select agent_id, trace_id, capture_time,"
                + " duration_nanos, partial, error_message, headline, user, attributes"
                + " from trace_tt_error_point where agent_rollup = ? and transaction_type = ?"
                + " and capture_time > ? and capture_time <= ?");

        readTransactionErrorPoint = session.prepare("select agent_id, trace_id, capture_time,"
                + " duration_nanos, partial, error_message, headline, user, attributes"
                + " from trace_tn_error_point where agent_rollup = ? and transaction_type = ?"
                + " and transaction_name = ? and capture_time > ? and capture_time <= ?");

        readOverallErrorMessage = session.prepare("select capture_time, error_message"
                + " from trace_tt_error_message where agent_rollup = ? and transaction_type = ?"
                + " and capture_time > ? and capture_time <= ?");

        readTransactionErrorMessage = session.prepare("select capture_time, error_message"
                + " from trace_tn_error_message where agent_rollup = ? and transaction_type = ?"
                + " and transaction_name = ? and capture_time > ? and capture_time <= ?");

        readHeader = session.prepare("select header from trace_header where agent_id = ? and trace_id = ?");

        readEntries = session.prepare("select depth, start_offset_nanos, duration_nanos,"
                + " active, message, shared_query_text_index, query_message_prefix,"
                + " query_message_suffix, detail, location_stack_trace, error from trace_entry"
                + " where agent_id = ? and trace_id = ?");

        readSharedQueryTexts = session.prepare("select truncated_text, truncated_end_text,"
                + " full_text_sha1 from trace_shared_query_text where agent_id = ?" + " and trace_id = ?");

        readMainThreadProfile = session
                .prepare("select profile from trace_main_thread_profile" + " where agent_id = ? and trace_id = ?");

        readAuxThreadProfile = session
                .prepare("select profile from trace_aux_thread_profile" + " where agent_id = ? and trace_id = ?");

        deletePartialOverallSlowPoint = session.prepare("delete from trace_tt_slow_point"
                + " where agent_rollup = ? and transaction_type = ? and capture_time = ?"
                + " and agent_id = ? and trace_id = ?");

        deletePartialTransactionSlowPoint = session.prepare("delete from trace_tn_slow_point"
                + " where agent_rollup = ? and transaction_type = ? and transaction_name = ?"
                + " and capture_time = ? and agent_id = ? and trace_id = ?");

        deletePartialOverallSlowCount = session.prepare("delete from trace_tt_slow_count"
                + " where agent_rollup = ? and transaction_type = ? and capture_time = ?"
                + " and agent_id = ? and trace_id = ?");

        deletePartialTransactionSlowCount = session.prepare("delete from trace_tn_slow_count"
                + " where agent_rollup = ? and transaction_type = ? and transaction_name = ?"
                + " and capture_time = ? and agent_id = ? and trace_id = ?");
    }

    public void store(String agentId, Trace trace) throws Exception {
        String traceId = trace.getId();
        Trace.Header priorHeader = trace.getUpdate() ? readHeader(agentId, traceId) : null;
        Trace.Header header = trace.getHeader();

        List<String> agentRollupIds = agentDao.readAgentRollupIds(agentId);

        List<ResultSetFuture> futures = Lists.newArrayList();

        List<Trace.SharedQueryText> sharedQueryTexts = Lists.newArrayList();
        for (Trace.SharedQueryText sharedQueryText : trace.getSharedQueryTextList()) {
            String fullTextSha1 = sharedQueryText.getFullTextSha1();
            if (fullTextSha1.isEmpty()) {
                String fullText = sharedQueryText.getFullText();
                if (fullText.length() > 2 * StorageConfig.TRACE_QUERY_TEXT_TRUNCATE) {
                    fullTextSha1 = Hashing.sha1().hashString(fullText, Charsets.UTF_8).toString();
                    futures.addAll(fullQueryTextDao.store(agentId, fullTextSha1, fullText));
                    for (int i = 1; i < agentRollupIds.size(); i++) {
                        futures.addAll(fullQueryTextDao.updateCheckTTL(agentRollupIds.get(i), fullTextSha1));
                    }
                    sharedQueryTexts.add(Trace.SharedQueryText.newBuilder()
                            .setTruncatedText(fullText.substring(0, StorageConfig.TRACE_QUERY_TEXT_TRUNCATE))
                            .setTruncatedEndText(fullText.substring(
                                    fullText.length() - StorageConfig.TRACE_QUERY_TEXT_TRUNCATE, fullText.length()))
                            .setFullTextSha1(fullTextSha1).build());
                } else {
                    sharedQueryTexts.add(sharedQueryText);
                }
            } else {
                futures.addAll(fullQueryTextDao.updateTTL(agentId, fullTextSha1));
                for (int i = 1; i < agentRollupIds.size(); i++) {
                    futures.addAll(fullQueryTextDao.updateCheckTTL(agentRollupIds.get(i), fullTextSha1));
                }
                sharedQueryTexts.add(sharedQueryText);
            }
        }

        // wait for success before proceeding in order to ensure cannot end up with orphaned
        // fullTextSha1
        MoreFutures.waitForAll(futures);
        futures.clear();

        int adjustedTTL = AggregateDao.getAdjustedTTL(getTTL(), header.getCaptureTime(), clock);
        for (String agentRollupId : agentRollupIds) {

            if (!agentRollupId.equals(agentId)) {
                BoundStatement boundStatement = insertCheck.bind();
                int i = 0;
                boundStatement.setString(i++, agentRollupId);
                boundStatement.setString(i++, agentId);
                boundStatement.setString(i++, traceId);
                boundStatement.setInt(i++, adjustedTTL);
                futures.add(session.executeAsync(boundStatement));
            }

            if (header.getSlow()) {
                BoundStatement boundStatement = insertOverallSlowPoint.bind();
                bindSlowPoint(boundStatement, agentRollupId, agentId, traceId, header, adjustedTTL, true);
                futures.add(session.executeAsync(boundStatement));

                boundStatement = insertTransactionSlowPoint.bind();
                bindSlowPoint(boundStatement, agentRollupId, agentId, traceId, header, adjustedTTL, false);
                futures.add(session.executeAsync(boundStatement));

                boundStatement = insertOverallSlowCount.bind();
                bindCount(boundStatement, agentRollupId, agentId, traceId, header, adjustedTTL, true);
                futures.add(session.executeAsync(boundStatement));

                boundStatement = insertTransactionSlowCount.bind();
                bindCount(boundStatement, agentRollupId, agentId, traceId, header, adjustedTTL, false);
                futures.add(session.executeAsync(boundStatement));

                if (priorHeader != null) {
                    boundStatement = deletePartialOverallSlowPoint.bind();
                    bind(boundStatement, agentRollupId, agentId, traceId, priorHeader, true);
                    futures.add(session.executeAsync(boundStatement));

                    boundStatement = deletePartialTransactionSlowPoint.bind();
                    bind(boundStatement, agentRollupId, agentId, traceId, priorHeader, false);
                    futures.add(session.executeAsync(boundStatement));

                    boundStatement = deletePartialOverallSlowCount.bind();
                    bind(boundStatement, agentRollupId, agentId, traceId, priorHeader, true);
                    futures.add(session.executeAsync(boundStatement));

                    boundStatement = deletePartialTransactionSlowCount.bind();
                    bind(boundStatement, agentRollupId, agentId, traceId, priorHeader, false);
                    futures.add(session.executeAsync(boundStatement));
                }
            }
            // seems unnecessary to insert error info for partial traces
            // and this avoids having to clean up partial trace data when trace is complete
            if (header.hasError() && !header.getPartial()) {
                BoundStatement boundStatement = insertOverallErrorMessage.bind();
                bindErrorMessage(boundStatement, agentRollupId, agentId, traceId, header, adjustedTTL, true);
                futures.add(session.executeAsync(boundStatement));

                boundStatement = insertTransactionErrorMessage.bind();
                bindErrorMessage(boundStatement, agentRollupId, agentId, traceId, header, adjustedTTL, false);
                futures.add(session.executeAsync(boundStatement));

                boundStatement = insertOverallErrorPoint.bind();
                bindErrorPoint(boundStatement, agentRollupId, agentId, traceId, header, adjustedTTL, true);
                futures.add(session.executeAsync(boundStatement));

                boundStatement = insertTransactionErrorPoint.bind();
                bindErrorPoint(boundStatement, agentRollupId, agentId, traceId, header, adjustedTTL, false);
                futures.add(session.executeAsync(boundStatement));

                boundStatement = insertOverallErrorCount.bind();
                bindCount(boundStatement, agentRollupId, agentId, traceId, header, adjustedTTL, true);
                futures.add(session.executeAsync(boundStatement));

                boundStatement = insertTransactionErrorCount.bind();
                bindCount(boundStatement, agentRollupId, agentId, traceId, header, adjustedTTL, false);
                futures.add(session.executeAsync(boundStatement));
            }
            for (Trace.Attribute attributeName : header.getAttributeList()) {
                traceAttributeNameDao.store(agentRollupId, header.getTransactionType(), attributeName.getName(),
                        futures);
            }
        }

        BoundStatement boundStatement = insertHeader.bind();
        int i = 0;
        boundStatement.setString(i++, agentId);
        boundStatement.setString(i++, traceId);
        boundStatement.setBytes(i++, ByteBuffer.wrap(header.toByteArray()));
        boundStatement.setInt(i++, adjustedTTL);
        futures.add(session.executeAsync(boundStatement));

        int index = 0;
        for (Trace.Entry entry : trace.getEntryList()) {
            boundStatement = insertEntry.bind();
            i = 0;
            boundStatement.setString(i++, agentId);
            boundStatement.setString(i++, traceId);
            boundStatement.setInt(i++, index++);
            boundStatement.setInt(i++, entry.getDepth());
            boundStatement.setLong(i++, entry.getStartOffsetNanos());
            boundStatement.setLong(i++, entry.getDurationNanos());
            boundStatement.setBool(i++, entry.getActive());
            if (entry.hasQueryEntryMessage()) {
                boundStatement.setToNull(i++);
                boundStatement.setInt(i++, entry.getQueryEntryMessage().getSharedQueryTextIndex());
                boundStatement.setString(i++, Strings.emptyToNull(entry.getQueryEntryMessage().getPrefix()));
                boundStatement.setString(i++, Strings.emptyToNull(entry.getQueryEntryMessage().getSuffix()));
            } else {
                // message is empty for trace entries added using addErrorEntry()
                boundStatement.setString(i++, Strings.emptyToNull(entry.getMessage()));
                boundStatement.setToNull(i++);
                boundStatement.setToNull(i++);
                boundStatement.setToNull(i++);
            }
            List<Trace.DetailEntry> detailEntries = entry.getDetailEntryList();
            if (detailEntries.isEmpty()) {
                boundStatement.setToNull(i++);
            } else {
                boundStatement.setBytes(i++, Messages.toByteBuffer(detailEntries));
            }
            List<StackTraceElement> location = entry.getLocationStackTraceElementList();
            if (location.isEmpty()) {
                boundStatement.setToNull(i++);
            } else {
                boundStatement.setBytes(i++, Messages.toByteBuffer(location));
            }
            if (entry.hasError()) {
                boundStatement.setBytes(i++, ByteBuffer.wrap(entry.getError().toByteArray()));
            } else {
                boundStatement.setToNull(i++);
            }
            boundStatement.setInt(i++, adjustedTTL);
            futures.add(session.executeAsync(boundStatement));
        }

        index = 0;
        for (Trace.SharedQueryText sharedQueryText : sharedQueryTexts) {
            boundStatement = insertSharedQueryText.bind();
            i = 0;
            boundStatement.setString(i++, agentId);
            boundStatement.setString(i++, traceId);
            boundStatement.setInt(i++, index++);
            String fullText = sharedQueryText.getFullText();
            if (fullText.isEmpty()) {
                boundStatement.setString(i++, sharedQueryText.getTruncatedText());
                boundStatement.setString(i++, sharedQueryText.getTruncatedEndText());
                boundStatement.setString(i++, sharedQueryText.getFullTextSha1());
            } else {
                boundStatement.setString(i++, fullText);
                boundStatement.setToNull(i++);
                boundStatement.setToNull(i++);
            }
            boundStatement.setInt(i++, adjustedTTL);
            futures.add(session.executeAsync(boundStatement));
        }

        if (trace.hasMainThreadProfile()) {
            boundStatement = insertMainThreadProfile.bind();
            bindThreadProfile(boundStatement, agentId, traceId, trace.getMainThreadProfile(), adjustedTTL);
            futures.add(session.executeAsync(boundStatement));
        }

        if (trace.hasAuxThreadProfile()) {
            boundStatement = insertAuxThreadProfile.bind();
            bindThreadProfile(boundStatement, agentId, traceId, trace.getAuxThreadProfile(), adjustedTTL);
            futures.add(session.executeAsync(boundStatement));
        }
        futures.addAll(transactionTypeDao.store(agentRollupIds, header.getTransactionType()));
        MoreFutures.waitForAll(futures);
    }

    @Override
    public Result<TracePoint> readSlowPoints(String agentRollupId, TraceQuery query, TracePointFilter filter,
            int limit) throws IOException {
        BoundStatement boundStatement;
        String transactionName = query.transactionName();
        if (transactionName == null) {
            boundStatement = readOverallSlowPoint.bind();
            bindTraceQuery(boundStatement, agentRollupId, query, true);
        } else {
            boundStatement = readTransactionSlowPoint.bind();
            bindTraceQuery(boundStatement, agentRollupId, query, false);
        }
        ResultSet results = session.execute(boundStatement);
        return processPoints(results, filter, limit, false);
    }

    @Override
    public Result<TracePoint> readErrorPoints(String agentRollupId, TraceQuery query, TracePointFilter filter,
            int limit) throws IOException {
        BoundStatement boundStatement;
        String transactionName = query.transactionName();
        if (transactionName == null) {
            boundStatement = readOverallErrorPoint.bind();
            bindTraceQuery(boundStatement, agentRollupId, query, true);
        } else {
            boundStatement = readTransactionErrorPoint.bind();
            bindTraceQuery(boundStatement, agentRollupId, query, false);
        }
        ResultSet results = session.execute(boundStatement);
        return processPoints(results, filter, limit, true);
    }

    @Override
    public long readSlowCount(String agentRollupId, TraceQuery query) {
        String transactionName = query.transactionName();
        if (transactionName == null) {
            ResultSet results = session.execute(
                    "select count(*) from trace_tt_slow_count where agent_rollup = ?"
                            + " and transaction_type = ? and capture_time > ?" + " and capture_time <= ?",
                    agentRollupId, query.transactionType(), query.from(), query.to());
            return results.one().getLong(0);
        } else {
            ResultSet results = session.execute(
                    "select count(*) from trace_tn_slow_count where agent_rollup = ?"
                            + " and transaction_type = ? and transaction_name = ?"
                            + " and capture_time > ? and capture_time <= ?",
                    agentRollupId, query.transactionType(), transactionName, query.from(), query.to());
            return results.one().getLong(0);
        }
    }

    @Override
    public long readErrorCount(String agentRollupId, TraceQuery query) {
        String transactionName = query.transactionName();
        if (transactionName == null) {
            ResultSet results = session.execute(
                    "select count(*) from trace_tt_error_count where agent_rollup = ?"
                            + " and transaction_type = ? and capture_time > ?" + " and capture_time <= ?",
                    agentRollupId, query.transactionType(), query.from(), query.to());
            return results.one().getLong(0);
        } else {
            ResultSet results = session.execute(
                    "select count(*) from trace_tn_error_count where agent_rollup = ?"
                            + " and transaction_type = ? and transaction_name = ?"
                            + " and capture_time > ? and capture_time <= ?",
                    agentRollupId, query.transactionType(), transactionName, query.from(), query.to());
            return results.one().getLong(0);
        }
    }

    @Override
    public ErrorMessageResult readErrorMessages(String agentRollupId, TraceQuery query, ErrorMessageFilter filter,
            long resolutionMillis, int limit) throws Exception {
        BoundStatement boundStatement;
        String transactionName = query.transactionName();
        if (transactionName == null) {
            boundStatement = readOverallErrorMessage.bind();
            bindTraceQuery(boundStatement, agentRollupId, query, true);
        } else {
            boundStatement = readTransactionErrorMessage.bind();
            bindTraceQuery(boundStatement, agentRollupId, query, false);
        }
        ResultSet results = session.execute(boundStatement);
        // rows are already in order by captureTime, so saving sort step by using linked hash map
        Map<Long, MutableLong> pointCounts = Maps.newLinkedHashMap();
        Map<String, MutableLong> messageCounts = Maps.newHashMap();
        for (Row row : results) {
            long captureTime = checkNotNull(row.getTimestamp(0)).getTime();
            String errorMessage = checkNotNull(row.getString(1));
            if (!matches(filter, errorMessage)) {
                continue;
            }
            long rollupCaptureTime = Utils.getRollupCaptureTime(captureTime, resolutionMillis);
            pointCounts.computeIfAbsent(rollupCaptureTime, k -> new MutableLong()).increment();
            messageCounts.computeIfAbsent(errorMessage, k -> new MutableLong()).increment();
        }
        List<ErrorMessagePoint> points = pointCounts.entrySet().stream()
                .map(e -> ImmutableErrorMessagePoint.of(e.getKey(), e.getValue().value))
                .sorted(Comparator.comparingLong(ErrorMessagePoint::captureTime))
                // explicit type on this line is needed for Checker Framework
                // see https://github.com/typetools/checker-framework/issues/531
                .collect(Collectors.<ErrorMessagePoint>toList());
        List<ErrorMessageCount> counts = messageCounts.entrySet().stream()
                .map(e -> ImmutableErrorMessageCount.of(e.getKey(), e.getValue().value))
                // points above are already ordered by cassandra, but need to reverse sort counts
                .sorted(Comparator.comparing(ErrorMessageCount::count).reversed())
                // explicit type on this line is needed for Checker Framework
                // see https://github.com/typetools/checker-framework/issues/531
                .collect(Collectors.<ErrorMessageCount>toList());

        if (counts.size() <= limit) {
            return ImmutableErrorMessageResult.builder().addAllPoints(points).counts(new Result<>(counts, false))
                    .build();
        } else {
            return ImmutableErrorMessageResult.builder().addAllPoints(points)
                    .counts(new Result<>(counts.subList(0, limit), true)).build();
        }
    }

    @Override
    public @Nullable HeaderPlus readHeaderPlus(String agentRollupId, String agentId, String traceId)
            throws InvalidProtocolBufferException {
        checkValidAgentIdForRequest(agentRollupId, agentId, traceId);
        Trace.Header header = readHeader(agentId, traceId);
        if (header == null) {
            return null;
        }
        Existence entriesExistence = header.getEntryCount() == 0 ? Existence.NO : Existence.YES;
        Existence profileExistence = header.getMainThreadProfileSampleCount() == 0
                && header.getAuxThreadProfileSampleCount() == 0 ? Existence.NO : Existence.YES;
        return ImmutableHeaderPlus.of(header, entriesExistence, profileExistence);
    }

    @Override
    public Entries readEntries(String agentRollupId, String agentId, String traceId) throws IOException {
        checkValidAgentIdForRequest(agentRollupId, agentId, traceId);
        return ImmutableEntries.builder().addAllEntries(readEntriesInternal(agentId, traceId))
                .addAllSharedQueryTexts(readSharedQueryTexts(agentId, traceId)).build();
    }

    // since this is only used by export, SharedQueryTexts are always returned with fullTrace
    // (never with truncatedText/truncatedEndText/fullTraceSha1) @Override
    @Override
    public Entries readEntriesForExport(String agentRollupId, String agentId, String traceId) throws IOException {
        checkValidAgentIdForRequest(agentRollupId, agentId, traceId);
        ImmutableEntries.Builder entries = ImmutableEntries.builder()
                .addAllEntries(readEntriesInternal(agentId, traceId));
        List<Trace.SharedQueryText> sharedQueryTexts = Lists.newArrayList();
        for (Trace.SharedQueryText sharedQueryText : readSharedQueryTexts(agentId, traceId)) {
            String fullTextSha1 = sharedQueryText.getFullTextSha1();
            if (fullTextSha1.isEmpty()) {
                sharedQueryTexts.add(sharedQueryText);
            } else {
                String fullText = fullQueryTextDao.getFullText(agentId, fullTextSha1);
                if (fullText == null) {
                    sharedQueryTexts
                            .add(Trace.SharedQueryText.newBuilder()
                                    .setFullText(sharedQueryText.getTruncatedText()
                                            + " ... [full query text has expired] ... "
                                            + sharedQueryText.getTruncatedEndText())
                                    .build());
                } else {
                    sharedQueryTexts.add(Trace.SharedQueryText.newBuilder().setFullText(fullText).build());
                }
            }
        }
        return entries.addAllSharedQueryTexts(sharedQueryTexts).build();
    }

    @Override
    public @Nullable Profile readMainThreadProfile(String agentRollupId, String agentId, String traceId)
            throws InvalidProtocolBufferException {
        checkValidAgentIdForRequest(agentRollupId, agentId, traceId);
        BoundStatement boundStatement = readMainThreadProfile.bind();
        boundStatement.setString(0, agentId);
        boundStatement.setString(1, traceId);
        ResultSet results = session.execute(boundStatement);
        Row row = results.one();
        if (row == null) {
            return null;
        }
        ByteBuffer bytes = checkNotNull(row.getBytes(0));
        return Profile.parseFrom(ByteString.copyFrom(bytes));
    }

    @Override
    public @Nullable Profile readAuxThreadProfile(String agentRollupId, String agentId, String traceId)
            throws InvalidProtocolBufferException {
        checkValidAgentIdForRequest(agentRollupId, agentId, traceId);
        BoundStatement boundStatement = readAuxThreadProfile.bind();
        boundStatement.setString(0, agentId);
        boundStatement.setString(1, traceId);
        ResultSet results = session.execute(boundStatement);
        Row row = results.one();
        if (row == null) {
            return null;
        }
        ByteBuffer bytes = checkNotNull(row.getBytes(0));
        return Profile.parseFrom(ByteString.copyFrom(bytes));
    }

    private void checkValidAgentIdForRequest(String agentRollupId, String agentId, String traceId) {
        if (agentId.equals(agentRollupId)) {
            return;
        }
        BoundStatement boundStatement = readCheck.bind();
        int i = 0;
        boundStatement.setString(i++, agentRollupId);
        boundStatement.setString(i++, agentId);
        boundStatement.setString(i++, traceId);
        if (session.execute(boundStatement).isExhausted()) {
            throw new IllegalArgumentException("Agent " + agentId + " was not a child of rollup " + agentRollupId
                    + " at the time of trace " + traceId);
        }
    }

    private @Nullable Trace.Header readHeader(String agentId, String traceId)
            throws InvalidProtocolBufferException {
        BoundStatement boundStatement = readHeader.bind();
        boundStatement.setString(0, agentId);
        boundStatement.setString(1, traceId);
        ResultSet results = session.execute(boundStatement);
        Row row = results.one();
        if (row == null) {
            return null;
        }
        ByteBuffer bytes = checkNotNull(row.getBytes(0));
        return Trace.Header.parseFrom(ByteString.copyFrom(bytes));
    }

    private List<Trace.Entry> readEntriesInternal(String agentId, String traceId) throws IOException {
        BoundStatement boundStatement = readEntries.bind();
        boundStatement.setString(0, agentId);
        boundStatement.setString(1, traceId);
        ResultSet results = session.execute(boundStatement);
        List<Trace.Entry> entries = Lists.newArrayList();
        while (!results.isExhausted()) {
            Row row = results.one();
            int i = 0;
            Trace.Entry.Builder entry = Trace.Entry.newBuilder().setDepth(row.getInt(i++))
                    .setStartOffsetNanos(row.getLong(i++)).setDurationNanos(row.getLong(i++))
                    .setActive(row.getBool(i++));
            if (row.isNull(i + 1)) { // shared_query_text_index
                // message is null for trace entries added using addErrorEntry()
                entry.setMessage(Strings.nullToEmpty(row.getString(i++)));
                i++; // shared_query_text_index
                i++; // query_message_prefix
                i++; // query_message_suffix
            } else {
                i++; // message
                Trace.QueryEntryMessage queryEntryMessage = Trace.QueryEntryMessage.newBuilder()
                        .setSharedQueryTextIndex(row.getInt(i++)).setPrefix(Strings.nullToEmpty(row.getString(i++)))
                        .setSuffix(Strings.nullToEmpty(row.getString(i++))).build();
                entry.setQueryEntryMessage(queryEntryMessage);
            }
            ByteBuffer detailBytes = row.getBytes(i++);
            if (detailBytes != null) {
                entry.addAllDetailEntry(Messages.parseDelimitedFrom(detailBytes, Trace.DetailEntry.parser()));
            }
            ByteBuffer locationBytes = row.getBytes(i++);
            if (locationBytes != null) {
                entry.addAllLocationStackTraceElement(
                        Messages.parseDelimitedFrom(locationBytes, Proto.StackTraceElement.parser()));
            }
            ByteBuffer errorBytes = row.getBytes(i++);
            if (errorBytes != null) {
                entry.setError(Trace.Error.parseFrom(ByteString.copyFrom(errorBytes)));
            }
            entries.add(entry.build());
        }
        return entries;
    }

    private List<Trace.SharedQueryText> readSharedQueryTexts(String agentId, String traceId) throws IOException {
        BoundStatement boundStatement = readSharedQueryTexts.bind();
        boundStatement.setString(0, agentId);
        boundStatement.setString(1, traceId);
        ResultSet results = session.execute(boundStatement);
        List<Trace.SharedQueryText> sharedQueryTexts = Lists.newArrayList();
        while (!results.isExhausted()) {
            Row row = results.one();
            int i = 0;
            String truncatedText = checkNotNull(row.getString(i++));
            String truncatedEndText = row.getString(i++);
            String fullTextSha1 = row.getString(i++);
            Trace.SharedQueryText.Builder sharedQueryText = Trace.SharedQueryText.newBuilder();
            if (fullTextSha1 == null) {
                sharedQueryText.setFullText(truncatedText);
            } else {
                sharedQueryText.setFullTextSha1(fullTextSha1).setTruncatedText(truncatedText)
                        .setTruncatedEndText(checkNotNull(truncatedEndText));
            }
            sharedQueryTexts.add(sharedQueryText.build());
        }
        return sharedQueryTexts;
    }

    private int getTTL() throws Exception {
        return Ints.saturatedCast(HOURS.toSeconds(configRepository.getStorageConfig().traceExpirationHours()));
    }

    private static void bindSlowPoint(BoundStatement boundStatement, String agentRollupId, String agentId,
            String traceId, Trace.Header header, int adjustedTTL, boolean overall) throws IOException {
        int i = bind(boundStatement, agentRollupId, agentId, traceId, header, overall);
        boundStatement.setLong(i++, header.getDurationNanos());
        boundStatement.setBool(i++, header.getPartial());
        boundStatement.setBool(i++, header.hasError());
        boundStatement.setString(i++, header.getHeadline());
        boundStatement.setString(i++, Strings.emptyToNull(header.getUser()));
        List<Trace.Attribute> attributes = header.getAttributeList();
        if (attributes.isEmpty()) {
            boundStatement.setToNull(i++);
        } else {
            boundStatement.setBytes(i++, Messages.toByteBuffer(attributes));
        }
        boundStatement.setInt(i++, adjustedTTL);
    }

    private static void bindCount(BoundStatement boundStatement, String agentRollupId, String agentId,
            String traceId, Trace.Header header, int adjustedTTL, boolean overall) {
        int i = bind(boundStatement, agentRollupId, agentId, traceId, header, overall);
        boundStatement.setInt(i++, adjustedTTL);
    }

    private static void bindErrorMessage(BoundStatement boundStatement, String agentRollupId, String agentId,
            String traceId, Trace.Header header, int adjustedTTL, boolean overall) {
        int i = bind(boundStatement, agentRollupId, agentId, traceId, header, overall);
        boundStatement.setString(i++, header.getError().getMessage());
        boundStatement.setInt(i++, adjustedTTL);
    }

    private static void bindErrorPoint(BoundStatement boundStatement, String agentRollupId, String agentId,
            String traceId, Trace.Header header, int adjustedTTL, boolean overall) throws IOException {
        int i = bind(boundStatement, agentRollupId, agentId, traceId, header, overall);
        boundStatement.setLong(i++, header.getDurationNanos());
        boundStatement.setBool(i++, header.getPartial());
        boundStatement.setString(i++, header.getError().getMessage());
        boundStatement.setString(i++, header.getHeadline());
        boundStatement.setString(i++, Strings.emptyToNull(header.getUser()));
        List<Trace.Attribute> attributes = header.getAttributeList();
        if (attributes.isEmpty()) {
            boundStatement.setToNull(i++);
        } else {
            boundStatement.setBytes(i++, Messages.toByteBuffer(attributes));
        }
        boundStatement.setInt(i++, adjustedTTL);
    }

    private static int bind(BoundStatement boundStatement, String agentRollupId, String agentId, String traceId,
            Trace.Header header, boolean overall) {
        int i = 0;
        boundStatement.setString(i++, agentRollupId);
        boundStatement.setString(i++, header.getTransactionType());
        if (!overall) {
            boundStatement.setString(i++, header.getTransactionName());
        }
        boundStatement.setTimestamp(i++, new Date(header.getCaptureTime()));
        boundStatement.setString(i++, agentId);
        boundStatement.setString(i++, traceId);
        return i;
    }

    private static void bindThreadProfile(BoundStatement boundStatement, String agentId, String traceId,
            Profile profile, int adjustedTTL) {
        int i = 0;
        boundStatement.setString(i++, agentId);
        boundStatement.setString(i++, traceId);
        boundStatement.setBytes(i++, ByteBuffer.wrap(profile.toByteArray()));
        boundStatement.setInt(i++, adjustedTTL);
    }

    private static void bindTraceQuery(BoundStatement boundStatement, String agentRollupId, TraceQuery query,
            boolean overall) {
        int i = 0;
        boundStatement.setString(i++, agentRollupId);
        boundStatement.setString(i++, query.transactionType());
        String transactionName = query.transactionName();
        if (!overall) {
            boundStatement.setString(i++, transactionName);
        }
        boundStatement.setTimestamp(i++, new Date(query.from()));
        boundStatement.setTimestamp(i++, new Date(query.to()));
    }

    private static Result<TracePoint> processPoints(ResultSet results, TracePointFilter filter, int limit,
            boolean errorPoints) throws IOException {
        List<TracePoint> tracePoints = Lists.newArrayList();
        for (Row row : results) {
            int i = 0;
            String agentId = checkNotNull(row.getString(i++));
            String traceId = checkNotNull(row.getString(i++));
            long captureTime = checkNotNull(row.getTimestamp(i++)).getTime();
            long durationNanos = row.getLong(i++);
            boolean partial = row.getBool(i++);
            boolean error = errorPoints ? true : row.getBool(i++);
            // error points are defined by having an error message, so safe to checkNotNull
            String errorMessage = errorPoints ? checkNotNull(row.getString(i++)) : "";
            // headline is null for data inserted prior to 0.9.7
            String headline = Strings.nullToEmpty(row.getString(i++));
            String user = Strings.nullToEmpty(row.getString(i++));
            ByteBuffer attributeBytes = row.getBytes(i++);
            List<Trace.Attribute> attrs = Messages.parseDelimitedFrom(attributeBytes, Trace.Attribute.parser());
            Map<String, List<String>> attributes = attrs.stream()
                    .collect(Collectors.toMap(Trace.Attribute::getName, Trace.Attribute::getValueList));
            if (filter.matchesHeadline(headline) && filter.matchesError(errorMessage) && filter.matchesUser(user)
                    && filter.matchesAttributes(attributes)) {
                tracePoints.add(
                        ImmutableTracePoint.builder().agentId(agentId).traceId(traceId).captureTime(captureTime)
                                .durationNanos(durationNanos).partial(partial).error(error).build());
            }
        }
        // remove duplicates (partially stored traces) since there is (small) window between updated
        // insert (with new capture time) and the delete of prior insert (with prior capture time)
        Set<TraceKey> traceKeys = Sets.newHashSet();
        ListIterator<TracePoint> i = tracePoints.listIterator(tracePoints.size());
        while (i.hasPrevious()) {
            TracePoint trace = i.previous();
            TraceKey traceKey = ImmutableTraceKey.of(trace.agentId(), trace.traceId());
            if (!traceKeys.add(traceKey)) {
                i.remove();
            }
        }
        // apply limit and re-sort if needed
        if (tracePoints.size() > limit) {
            tracePoints = tracePoints.stream()
                    .sorted(Comparator.comparingLong(TracePoint::durationNanos).reversed()).limit(limit)
                    .sorted(Comparator.comparingLong(TracePoint::captureTime))
                    // explicit type on this line is needed for Checker Framework
                    // see https://github.com/typetools/checker-framework/issues/531
                    .collect(Collectors.<TracePoint>toList());
            return new Result<>(tracePoints, true);
        } else {
            return new Result<>(tracePoints, false);
        }
    }

    private static boolean matches(ErrorMessageFilter filter, String errorMessage) {
        String upper = errorMessage.toUpperCase(Locale.ENGLISH);
        for (String include : filter.includes()) {
            if (!upper.contains(include.toUpperCase(Locale.ENGLISH))) {
                return false;
            }
        }
        for (String exclude : filter.excludes()) {
            if (upper.contains(exclude.toUpperCase(Locale.ENGLISH))) {
                return false;
            }
        }
        return true;
    }

    @Value.Immutable
    @Styles.AllParameters
    interface TraceKey {
        String agentId();

        String traceId();
    }

    private static class MutableLong {
        private long value;

        private void increment() {
            value++;
        }
    }
}