All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.glowroot.agent.fat.storage.TraceDao Maven / Gradle / Ivy

There is a newer version: 0.9.24
Show newest version
/*
 * Copyright 2011-2016 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.agent.fat.storage;

import java.io.IOException;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.List;
import java.util.Locale;

import javax.annotation.Nullable;

import org.glowroot.agent.shaded.google.common.base.Strings;
import org.glowroot.agent.shaded.google.common.collect.ImmutableList;
import org.checkerframework.checker.tainting.qual.Untainted;

import org.glowroot.agent.fat.storage.TracePointQueryBuilder.ParameterizedSql;
import org.glowroot.agent.fat.storage.util.CappedDatabase;
import org.glowroot.agent.fat.storage.util.DataSource;
import org.glowroot.agent.fat.storage.util.DataSource.JdbcRowQuery;
import org.glowroot.agent.fat.storage.util.DataSource.JdbcUpdate;
import org.glowroot.agent.fat.storage.util.ImmutableColumn;
import org.glowroot.agent.fat.storage.util.ImmutableIndex;
import org.glowroot.agent.fat.storage.util.RowMappers;
import org.glowroot.agent.fat.storage.util.Schemas.Column;
import org.glowroot.agent.fat.storage.util.Schemas.ColumnType;
import org.glowroot.agent.fat.storage.util.Schemas.Index;
import org.glowroot.agent.shaded.glowroot.common.live.ImmutableTracePoint;
import org.glowroot.agent.shaded.glowroot.common.live.LiveTraceRepository.Existence;
import org.glowroot.agent.shaded.glowroot.common.live.LiveTraceRepository.TraceKind;
import org.glowroot.agent.shaded.glowroot.common.live.LiveTraceRepository.TracePoint;
import org.glowroot.agent.shaded.glowroot.common.live.LiveTraceRepository.TracePointFilter;
import org.glowroot.agent.shaded.glowroot.common.model.Result;
import org.glowroot.agent.shaded.glowroot.storage.repo.ImmutableErrorMessageCount;
import org.glowroot.agent.shaded.glowroot.storage.repo.ImmutableErrorMessagePoint;
import org.glowroot.agent.shaded.glowroot.storage.repo.ImmutableErrorMessageResult;
import org.glowroot.agent.shaded.glowroot.storage.repo.ImmutableHeaderPlus;
import org.glowroot.agent.shaded.glowroot.storage.repo.TraceRepository;
import org.glowroot.agent.shaded.glowroot.wire.api.model.ProfileOuterClass.Profile;
import org.glowroot.agent.shaded.glowroot.wire.api.model.TraceOuterClass.Trace;

import static org.glowroot.agent.shaded.google.common.base.Preconditions.checkNotNull;
import static org.glowroot.agent.fat.storage.util.Checkers.castUntainted;

public class TraceDao implements TraceRepository {

    private static final String AGENT_ID = "";

    private static final ImmutableList traceColumns = ImmutableList.of(
            ImmutableColumn.of("id", ColumnType.VARCHAR),
            ImmutableColumn.of("partial", ColumnType.BOOLEAN),
            ImmutableColumn.of("slow", ColumnType.BOOLEAN),
            ImmutableColumn.of("error", ColumnType.BOOLEAN),
            ImmutableColumn.of("start_time", ColumnType.BIGINT),
            ImmutableColumn.of("capture_time", ColumnType.BIGINT),
            ImmutableColumn.of("duration_nanos", ColumnType.BIGINT), // nanoseconds
            ImmutableColumn.of("transaction_type", ColumnType.VARCHAR),
            ImmutableColumn.of("transaction_name", ColumnType.VARCHAR),
            ImmutableColumn.of("headline", ColumnType.VARCHAR),
            ImmutableColumn.of("user", ColumnType.VARCHAR),
            ImmutableColumn.of("error_message", ColumnType.VARCHAR),
            ImmutableColumn.of("header", ColumnType.VARBINARY), // protobuf
            ImmutableColumn.of("entries_capped_id", ColumnType.BIGINT),
            ImmutableColumn.of("main_thread_profile_capped_id", ColumnType.BIGINT),
            ImmutableColumn.of("aux_thread_profile_capped_id", ColumnType.BIGINT));

    // capture_time column is used for expiring records without using FK with on delete cascade
    private static final ImmutableList traceAttributeColumns =
            ImmutableList.of(ImmutableColumn.of("trace_id", ColumnType.VARCHAR),
                    ImmutableColumn.of("name", ColumnType.VARCHAR),
                    ImmutableColumn.of("value", ColumnType.VARCHAR),
                    ImmutableColumn.of("capture_time", ColumnType.BIGINT));

    private static final ImmutableList traceIndexes = ImmutableList.of(
            // duration_nanos, id and error columns are included so database can return the
            // result set directly from the index without having to reference the table for each row
            //
            // trace_overall_slow_idx is for readSlowCount() and readSlowPoints()
            ImmutableIndex.of("trace_overall_slow_idx",
                    ImmutableList.of("transaction_type", "slow", "capture_time", "duration_nanos",
                            "error", "id")),
            // trace_transaction_slow_idx is for readSlowCount() and readSlowPoints()
            ImmutableIndex.of("trace_transaction_slow_idx",
                    ImmutableList.of("transaction_type", "transaction_name", "slow", "capture_time",
                            "duration_nanos", "error", "id")),
            // trace_overall_error_idx is for readErrorCount() and readErrorPoints()
            ImmutableIndex.of("trace_error_idx",
                    ImmutableList.of("transaction_type", "error", "capture_time", "duration_nanos",
                            "error", "id")),
            // trace_transaction_error_idx is for readErrorCount() and readErrorPoints()
            ImmutableIndex.of("trace_transaction_error_idx",
                    ImmutableList.of("transaction_type", "transaction_name", "error",
                            "capture_time", "duration_nanos", "id")),
            // trace_capture_time_idx is for reaper, this is very important when trace table is huge
            // e.g. after leaving slow threshold at 0 for a while
            ImmutableIndex.of("trace_capture_time_idx", ImmutableList.of("capture_time")),
            // trace_idx is for trace header lookup
            ImmutableIndex.of("trace_idx", ImmutableList.of("id")));

    private static final ImmutableList traceAttributeIndexes = ImmutableList.of(
            ImmutableIndex.of("trace_attribute_idx", ImmutableList.of("trace_id")));

    private final DataSource dataSource;
    private final CappedDatabase traceCappedDatabase;
    private final TraceAttributeNameDao traceAttributeNameDao;
    private final TransactionTypeDao transactionTypeDao;

    TraceDao(DataSource dataSource, CappedDatabase traceCappedDatabase,
            TransactionTypeDao transactionTypeDao) throws Exception {
        this.dataSource = dataSource;
        this.traceCappedDatabase = traceCappedDatabase;
        traceAttributeNameDao = new TraceAttributeNameDao(dataSource);
        this.transactionTypeDao = transactionTypeDao;
        dataSource.syncTable("trace", traceColumns);
        dataSource.syncIndexes("trace", traceIndexes);
        dataSource.syncTable("trace_attribute", traceAttributeColumns);
        dataSource.syncIndexes("trace_attribute", traceAttributeIndexes);
    }

    @Override
    public void collect(final String agentId, final Trace trace) throws Exception {
        final Trace.Header header = trace.getHeader();
        boolean exists =
                dataSource.queryForExists("select 1 from trace where id = ?", trace.getId());
        dataSource.update(new TraceUpsert(trace, exists));
        if (header.getAttributeCount() > 0) {
            if (exists) {
                dataSource.update("delete from trace_attribute where trace_id = ?", trace.getId());
            }
            dataSource.batchUpdate(new TraceAttributeInsert(trace));
            for (Trace.Attribute attribute : header.getAttributeList()) {
                traceAttributeNameDao.updateLastCaptureTime(header.getTransactionType(),
                        attribute.getName(), header.getCaptureTime());
            }
        }
        transactionTypeDao.updateLastCaptureTime(trace.getHeader().getTransactionType(),
                trace.getHeader().getCaptureTime());
    }

    @Override
    public List readTraceAttributeNames(String agentRollup, String transactionType)
            throws Exception {
        return traceAttributeNameDao.readTraceAttributeNames(transactionType);
    }

    @Override
    public Result readSlowPoints(TraceQuery query, TracePointFilter filter, int limit)
            throws Exception {
        return readPoints(TraceKind.SLOW, query, filter, limit);
    }

    @Override
    public Result readErrorPoints(TraceQuery query, TracePointFilter filter, int limit)
            throws Exception {
        return readPoints(TraceKind.ERROR, query, filter, limit);
    }

    @Override
    public long readSlowCount(TraceQuery query) throws Exception {
        String transactionName = query.transactionName();
        if (transactionName == null) {
            return dataSource.queryForLong(
                    "select count(*) from trace where transaction_type = ? and capture_time > ?"
                            + " and capture_time <= ? and slow = ?",
                    query.transactionType(), query.from(), query.to(), true);
        } else {
            return dataSource.queryForLong(
                    "select count(*) from trace where transaction_type = ? and transaction_name = ?"
                            + " and capture_time > ? and capture_time <= ? and slow = ?",
                    query.transactionType(), transactionName, query.from(), query.to(), true);
        }
    }

    @Override
    public long readErrorCount(TraceQuery query) throws Exception {
        String transactionName = query.transactionName();
        if (transactionName == null) {
            return dataSource.queryForLong(
                    "select count(*) from trace where transaction_type = ? and capture_time > ?"
                            + " and capture_time <= ? and error = ?",
                    query.transactionType(), query.from(), query.to(), true);
        } else {
            return dataSource.queryForLong(
                    "select count(*) from trace where transaction_type = ? and transaction_name = ?"
                            + " and capture_time > ? and capture_time <= ? and error = ?",
                    query.transactionType(), transactionName, query.from(), query.to(), true);
        }
    }

    @Override
    public ErrorMessageResult readErrorMessages(TraceQuery query, ErrorMessageFilter filter,
            long resolutionMillis, int limit) throws Exception {
        List points =
                dataSource.query(new ErrorPointQuery(query, filter, resolutionMillis));
        List counts =
                dataSource.query(new ErrorMessageCountQuery(query, filter, limit + 1));
        // one extra record over the limit is fetched above to identify if the limit was hit
        return ImmutableErrorMessageResult.builder()
                .addAllPoints(points)
                .counts(Result.from(counts, limit))
                .build();
    }

    @Override
    public @Nullable HeaderPlus readHeaderPlus(String agentId, String traceId) throws Exception {
        return dataSource.queryAtMostOne(new TraceHeaderQuery(traceId));
    }

    @Override
    public List readEntries(String agentId, String traceId) throws Exception {
        Long cappedId = dataSource
                .queryForOptionalLong("select entries_capped_id from trace where id = ?", traceId);
        if (cappedId == null) {
            // trace must have just expired while user was viewing it, or data source is closing
            return ImmutableList.of();
        }
        return traceCappedDatabase.readMessages(cappedId, Trace.Entry.parser());
    }

    @Override
    public @Nullable Profile readMainThreadProfile(String agentId, String traceId)
            throws Exception {
        Long cappedId = dataSource.queryForOptionalLong(
                "select main_thread_profile_capped_id from trace where id = ?", traceId);
        if (cappedId == null) {
            // trace must have just expired while user was viewing it, or data source is closing
            return null;
        }
        return traceCappedDatabase.readMessage(cappedId, Profile.parser());
    }

    @Override
    public @Nullable Profile readAuxThreadProfile(String agentId, String traceId)
            throws Exception {
        Long cappedId = dataSource.queryForOptionalLong(
                "select aux_thread_profile_capped_id from trace where id = ?", traceId);
        if (cappedId == null) {
            // trace must have just expired while user was viewing it, or data source is closing
            return null;
        }
        return traceCappedDatabase.readMessage(cappedId, Profile.parser());
    }

    @Override
    public void deleteAll(String agentRollup) throws Exception {
        traceAttributeNameDao.deleteAll();
        dataSource.execute("truncate table trace");
        dataSource.execute("truncate table trace_attribute");
    }

    void deleteBefore(long captureTime) throws Exception {
        traceAttributeNameDao.deleteBefore(captureTime);
        dataSource.deleteBefore("trace", captureTime);
        dataSource.deleteBefore("trace_attribute", captureTime);
    }

    private Result readPoints(TraceKind traceKind, TraceQuery query,
            TracePointFilter filter, int limit) throws Exception {
        ParameterizedSql parameterizedSql =
                new TracePointQueryBuilder(traceKind, query, filter, limit).getParameterizedSql();
        List points = dataSource.query(new TracePointQuery(parameterizedSql));
        // one extra record over the limit is fetched above to identify if the limit was hit
        return Result.from(points, limit);
    }

    private static void appendQueryAndFilter(StringBuilder sql, TraceQuery query,
            ErrorMessageFilter filter) {
        sql.append(" and transaction_type = ?");
        String transactionName = query.transactionName();
        if (transactionName != null) {
            sql.append(" and transaction_name = ?");
        }
        sql.append(" and capture_time > ? and capture_time <= ?");
        for (int i = 0; i < filter.includes().size(); i++) {
            sql.append(" and upper(error_message) like ?");
        }
        for (int i = 0; i < filter.excludes().size(); i++) {
            sql.append(" and upper(error_message) not like ?");
        }
    }

    private static int bindQueryAndFilter(PreparedStatement preparedStatement, int startIndex,
            TraceQuery query, ErrorMessageFilter filter) throws SQLException {
        int i = startIndex;
        preparedStatement.setString(i++, query.transactionType());
        String transactionName = query.transactionName();
        if (transactionName != null) {
            preparedStatement.setString(i++, transactionName);
        }
        preparedStatement.setLong(i++, query.from());
        preparedStatement.setLong(i++, query.to());
        for (String include : filter.includes()) {
            preparedStatement.setString(i++, '%' + include.toUpperCase(Locale.ENGLISH) + '%');
        }
        for (String exclude : filter.excludes()) {
            preparedStatement.setString(i++, '%' + exclude.toUpperCase(Locale.ENGLISH) + '%');
        }
        return i;
    }

    private class TraceUpsert implements JdbcUpdate {

        private final boolean update;
        private final String traceId;
        private final Trace.Header header;
        private final @Nullable Long entriesId;
        private final @Nullable Long mainThreadProfileId;
        private final @Nullable Long auxThreadProfileId;

        private TraceUpsert(Trace trace, boolean update) throws IOException {
            this.update = update;
            this.traceId = trace.getId();
            this.header = trace.getHeader();

            List entries = trace.getEntryList();
            if (entries.isEmpty()) {
                entriesId = null;
            } else {
                entriesId = traceCappedDatabase.writeMessages(entries,
                        TraceCappedDatabaseStats.TRACE_ENTRIES);
            }
            if (trace.hasMainThreadProfile()) {
                mainThreadProfileId = traceCappedDatabase.writeMessage(trace.getMainThreadProfile(),
                        TraceCappedDatabaseStats.TRACE_PROFILES);
            } else {
                mainThreadProfileId = null;
            }
            if (trace.hasMainThreadProfile()) {
                auxThreadProfileId = traceCappedDatabase.writeMessage(trace.getAuxThreadProfile(),
                        TraceCappedDatabaseStats.TRACE_PROFILES);
            } else {
                auxThreadProfileId = null;
            }
        }

        @Override
        public @Untainted String getSql() {
            if (update) {
                return "update trace set partial = ?, slow = ?, error = ?, start_time = ?,"
                        + " capture_time = ?, duration_nanos = ?, transaction_type = ?,"
                        + " transaction_name = ?, headline = ?, user = ?, error_message = ?,"
                        + " header = ?, entries_capped_id = ?, main_thread_profile_capped_id = ?,"
                        + " aux_thread_profile_capped_id = ? where id = ?";
            } else {
                return "insert into trace (partial, slow, error, start_time, capture_time,"
                        + " duration_nanos, transaction_type, transaction_name, headline,"
                        + " user, error_message, header, entries_capped_id,"
                        + " main_thread_profile_capped_id, aux_thread_profile_capped_id, id)"
                        + " values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
            }
        }

        // minimal work inside this method as it is called with active connection
        @Override
        public void bind(PreparedStatement preparedStatement) throws SQLException {
            int i = 1;
            preparedStatement.setBoolean(i++, header.getPartial());
            preparedStatement.setBoolean(i++, header.getSlow());
            preparedStatement.setBoolean(i++, header.hasError());
            preparedStatement.setLong(i++, header.getStartTime());
            preparedStatement.setLong(i++, header.getCaptureTime());
            preparedStatement.setLong(i++, header.getDurationNanos());
            preparedStatement.setString(i++, header.getTransactionType());
            preparedStatement.setString(i++, header.getTransactionName());
            preparedStatement.setString(i++, header.getHeadline());
            preparedStatement.setString(i++, Strings.emptyToNull(header.getUser()));
            if (header.hasError()) {
                preparedStatement.setString(i++, header.getError().getMessage());
            } else {
                preparedStatement.setNull(i++, Types.VARCHAR);
            }
            preparedStatement.setBytes(i++, header.toByteArray());
            RowMappers.setLong(preparedStatement, i++, entriesId);
            RowMappers.setLong(preparedStatement, i++, mainThreadProfileId);
            RowMappers.setLong(preparedStatement, i++, auxThreadProfileId);
            preparedStatement.setString(i++, traceId);
        }
    }

    private static class TraceAttributeInsert implements JdbcUpdate {

        private final Trace trace;

        private TraceAttributeInsert(Trace trace) {
            this.trace = trace;
        }

        @Override
        public @Untainted String getSql() {
            return "insert into trace_attribute (trace_id, name, value, capture_time)"
                    + " values (?, ?, ?, ?)";
        }

        @Override
        public void bind(PreparedStatement preparedStatement) throws SQLException {
            Trace.Header header = trace.getHeader();
            for (Trace.Attribute attribute : header.getAttributeList()) {
                for (String value : attribute.getValueList()) {
                    preparedStatement.setString(1, trace.getId());
                    preparedStatement.setString(2, attribute.getName());
                    preparedStatement.setString(3, value);
                    preparedStatement.setLong(4, header.getCaptureTime());
                    preparedStatement.addBatch();
                }
            }

        }
    }

    private static class TracePointQuery implements JdbcRowQuery {

        private final ParameterizedSql parameterizedSql;

        private TracePointQuery(ParameterizedSql parameterizedSql) {
            this.parameterizedSql = parameterizedSql;
        }

        @Override
        public @Untainted String getSql() {
            return castUntainted(parameterizedSql.sql());
        }

        @Override
        public void bind(PreparedStatement preparedStatement) throws SQLException {
            int i = 1;
            for (Object obj : parameterizedSql.args()) {
                preparedStatement.setObject(i++, obj);
            }
        }

        @Override
        public TracePoint mapRow(ResultSet resultSet) throws SQLException {
            String traceId = checkNotNull(resultSet.getString(1));
            return ImmutableTracePoint.builder()
                    .agentId(AGENT_ID)
                    .traceId(traceId)
                    .captureTime(resultSet.getLong(2))
                    .durationNanos(resultSet.getLong(3))
                    .error(resultSet.getBoolean(4))
                    .build();
        }
    }

    private class TraceHeaderQuery implements JdbcRowQuery {

        private final String traceId;

        private TraceHeaderQuery(String traceId) {
            this.traceId = traceId;
        }

        @Override
        public @Untainted String getSql() {
            return "select header, entries_capped_id, main_thread_profile_capped_id,"
                    + " aux_thread_profile_capped_id from trace where id = ?";
        }

        @Override
        public void bind(PreparedStatement preparedStatement) throws SQLException {
            preparedStatement.setString(1, traceId);
        }

        @Override
        public HeaderPlus mapRow(ResultSet resultSet) throws Exception {
            byte[] header = checkNotNull(resultSet.getBytes(1));
            Existence entriesExistence = RowMappers.getExistence(resultSet, 2, traceCappedDatabase);
            Existence mainThreadProfileExistence =
                    RowMappers.getExistence(resultSet, 3, traceCappedDatabase);
            Existence auxThreadProfileExistence =
                    RowMappers.getExistence(resultSet, 3, traceCappedDatabase);
            Existence profileExistence;
            if (mainThreadProfileExistence == Existence.EXPIRED
                    || auxThreadProfileExistence == Existence.EXPIRED) {
                profileExistence = Existence.EXPIRED;
            } else if (mainThreadProfileExistence == Existence.YES
                    || auxThreadProfileExistence == Existence.YES) {
                profileExistence = Existence.YES;
            } else {
                profileExistence = Existence.NO;
            }
            return ImmutableHeaderPlus.builder()
                    .header(Trace.Header.parseFrom(header))
                    .entriesExistence(entriesExistence)
                    .profileExistence(profileExistence)
                    .build();
        }
    }

    private static class ErrorPointQuery implements JdbcRowQuery {

        private final TraceQuery query;
        private final ErrorMessageFilter filter;
        private final long resolutionMillis;

        private ErrorPointQuery(TraceQuery query, ErrorMessageFilter filter,
                long resolutionMillis) {
            this.query = query;
            this.filter = filter;
            this.resolutionMillis = resolutionMillis;
        }

        @Override
        public @Untainted String getSql() {
            // need ".0" to force double result
            String captureTimeSql = castUntainted(
                    "ceil(capture_time / " + resolutionMillis + ".0) * " + resolutionMillis);
            StringBuilder sql = new StringBuilder();
            sql.append("select " + captureTimeSql + ", count(*) from trace where error = ?");
            appendQueryAndFilter(sql, query, filter);
            sql.append(" group by " + captureTimeSql + " order by " + captureTimeSql);
            return castUntainted(sql.toString());
        }

        @Override
        public void bind(PreparedStatement preparedStatement) throws SQLException {
            int i = 1;
            preparedStatement.setBoolean(i++, true);
            bindQueryAndFilter(preparedStatement, i, query, filter);
        }

        @Override
        public ErrorMessagePoint mapRow(ResultSet resultSet) throws SQLException {
            long captureTime = resultSet.getLong(1);
            long errorCount = resultSet.getLong(2);
            return ImmutableErrorMessagePoint.of(captureTime, errorCount);
        }
    }

    private static class ErrorMessageCountQuery implements JdbcRowQuery {

        private final TraceQuery query;
        private final ErrorMessageFilter filter;
        private final int limit;

        private ErrorMessageCountQuery(TraceQuery query, ErrorMessageFilter filter, int limit) {
            this.query = query;
            this.filter = filter;
            this.limit = limit;
        }

        @Override
        public @Untainted String getSql() {
            StringBuilder sql = new StringBuilder();
            sql.append("select error_message, count(*) from trace where error = ?");
            appendQueryAndFilter(sql, query, filter);
            sql.append(" group by error_message order by count(*) desc limit ?");
            return castUntainted(sql.toString());
        }

        @Override
        public void bind(PreparedStatement preparedStatement) throws SQLException {
            int i = 1;
            preparedStatement.setBoolean(i++, true);
            i = bindQueryAndFilter(preparedStatement, i, query, filter);
            preparedStatement.setInt(i++, limit);
        }

        @Override
        public ErrorMessageCount mapRow(ResultSet resultSet) throws SQLException {
            return ImmutableErrorMessageCount.builder()
                    .message(Strings.nullToEmpty(resultSet.getString(1)))
                    .count(resultSet.getLong(2))
                    .build();
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy