/*
* Copyright 2015-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.plugin.cassandra;
import java.util.ArrayList;
import java.util.Collection;
import javax.annotation.Nullable;
import org.glowroot.agent.plugin.api.Agent;
import org.glowroot.agent.plugin.api.AsyncQueryEntry;
import org.glowroot.agent.plugin.api.QueryEntry;
import org.glowroot.agent.plugin.api.QueryMessageSupplier;
import org.glowroot.agent.plugin.api.ThreadContext;
import org.glowroot.agent.plugin.api.Timer;
import org.glowroot.agent.plugin.api.TimerName;
import org.glowroot.agent.plugin.api.config.ConfigListener;
import org.glowroot.agent.plugin.api.config.ConfigService;
import org.glowroot.agent.plugin.api.weaving.BindParameter;
import org.glowroot.agent.plugin.api.weaving.BindReturn;
import org.glowroot.agent.plugin.api.weaving.BindThrowable;
import org.glowroot.agent.plugin.api.weaving.BindTraveler;
import org.glowroot.agent.plugin.api.weaving.OnAfter;
import org.glowroot.agent.plugin.api.weaving.OnBefore;
import org.glowroot.agent.plugin.api.weaving.OnReturn;
import org.glowroot.agent.plugin.api.weaving.OnThrow;
import org.glowroot.agent.plugin.api.weaving.Pointcut;
import org.glowroot.agent.plugin.api.weaving.Shim;
import org.glowroot.agent.plugin.cassandra.ResultSetAspect.ResultSet;
import org.glowroot.agent.plugin.cassandra.ResultSetFutureAspect.ResultSetFutureMixin;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
public class SessionAspect {
private static final String QUERY_TYPE = "CQL";
private static final ConfigService configService = Agent.getConfigService("cassandra");
// visibility is provided by memoryBarrier in org.glowroot.config.ConfigService
private static int stackTraceThresholdMillis;
static {
configService.registerConfigListener(new ConfigListener() {
@Override
public void onChange() {
Double value = configService.getDoubleProperty("stackTraceThresholdMillis").value();
stackTraceThresholdMillis = value == null ? Integer.MAX_VALUE : value.intValue();
}
});
}
@Shim("com.datastax.driver.core.Statement")
public interface Statement {}
@Shim("com.datastax.driver.core.RegularStatement")
public interface RegularStatement extends Statement {
@Nullable
String getQueryString();
}
@Shim("com.datastax.driver.core.BoundStatement")
public interface BoundStatement extends Statement {
@Shim("com.datastax.driver.core.PreparedStatement preparedStatement()")
@Nullable
PreparedStatement glowroot$preparedStatement();
}
@Shim("com.datastax.driver.core.BatchStatement")
public interface BatchStatement extends Statement {
@Nullable
Collection<Statement> getStatements();
}
@Shim("com.datastax.driver.core.PreparedStatement")
public interface PreparedStatement {
@Nullable
String getQueryString();
}
@Pointcut(className = "com.datastax.driver.core.Session", methodName = "execute",
methodParameterTypes = {"com.datastax.driver.core.Statement"},
nestingGroup = "cassandra", timerName = "cql execute",
suppressionKey = "wait-on-future")
public static class ExecuteAdvice {
private static final TimerName timerName = Agent.getTimerName(ExecuteAdvice.class);
@OnBefore
public static @Nullable QueryEntry onBefore(ThreadContext context,
@BindParameter @Nullable Object arg) {
QueryEntryInfo queryEntryInfo = getQueryEntryInfo(arg);
if (queryEntryInfo == null) {
return null;
}
return context.startQueryEntry(QUERY_TYPE, queryEntryInfo.queryText,
queryEntryInfo.queryMessageSupplier, timerName);
}
@OnReturn
public static void onReturn(@BindReturn @Nullable ResultSet resultSet,
@BindTraveler @Nullable QueryEntry queryEntry) {
if (queryEntry != null) {
if (resultSet != null) {
resultSet.glowroot$setLastQueryEntry(queryEntry);
}
queryEntry.endWithStackTrace(stackTraceThresholdMillis, MILLISECONDS);
}
}
@OnThrow
public static void onThrow(@BindThrowable Throwable t,
@BindTraveler @Nullable QueryEntry queryEntry) {
if (queryEntry != null) {
queryEntry.endWithError(t);
}
}
}
@Pointcut(className = "com.datastax.driver.core.Session", methodName = "prepare",
methodParameterTypes = {"*"}, timerName = "cql prepare",
suppressionKey = "wait-on-future")
public static class PrepareAdvice {
private static final TimerName timerName = Agent.getTimerName(PrepareAdvice.class);
@OnBefore
public static Timer onBefore(ThreadContext context) {
return context.startTimer(timerName);
}
@OnAfter
public static void onAfter(@BindTraveler Timer timer) {
timer.stop();
}
}
@Pointcut(className = "com.datastax.driver.core.Session", methodName = "executeAsync",
methodParameterTypes = {"com.datastax.driver.core.Statement"},
nestingGroup = "cassandra", timerName = "cql execute")
public static class ExecuteAsyncAdvice {
private static final TimerName timerName = Agent.getTimerName(ExecuteAsyncAdvice.class);
@OnBefore
public static @Nullable AsyncQueryEntry onBefore(ThreadContext context,
@BindParameter @Nullable Object arg) {
QueryEntryInfo queryEntryInfo = getQueryEntryInfo(arg);
if (queryEntryInfo == null) {
return null;
}
return context.startAsyncQueryEntry(QUERY_TYPE, queryEntryInfo.queryText,
queryEntryInfo.queryMessageSupplier, timerName);
}
@OnReturn
public static void onReturn(@BindReturn @Nullable ResultSetFutureMixin future,
final @BindTraveler @Nullable AsyncQueryEntry asyncQueryEntry) {
if (asyncQueryEntry == null) {
return;
}
asyncQueryEntry.stopSyncTimer();
if (future == null) {
asyncQueryEntry.end();
return;
}
// to prevent race condition, setting async query entry before getting completed status,
// and the converse is done when getting async query entry
// ok if end() happens to get called twice
future.glowroot$setAsyncQueryEntry(asyncQueryEntry);
if (future.glowroot$isCompleted()) {
// ResultSetFuture completed really fast, prior to @OnReturn
Throwable exception = future.glowroot$getException();
if (exception == null) {
asyncQueryEntry.end();
} else {
asyncQueryEntry.endWithError(exception);
}
return;
}
}
@OnThrow
public static void onThrow(@BindThrowable Throwable t,
@BindTraveler @Nullable AsyncQueryEntry asyncQueryEntry) {
if (asyncQueryEntry != null) {
asyncQueryEntry.stopSyncTimer();
asyncQueryEntry.endWithError(t);
}
}
}
private static @Nullable QueryEntryInfo getQueryEntryInfo(@Nullable Object arg) {
if (arg == null) {
// seems nothing sensible to do here other than ignore
return null;
}
String queryText;
if (arg instanceof String) {
queryText = (String) arg;
} else if (arg instanceof RegularStatement) {
queryText = nullToEmpty(((RegularStatement) arg).getQueryString());
} else if (arg instanceof BoundStatement) {
PreparedStatement preparedStatement =
((BoundStatement) arg).glowroot$preparedStatement();
queryText = preparedStatement == null ? ""
: nullToEmpty(preparedStatement.getQueryString());
} else if (arg instanceof BatchStatement) {
Collection<Statement> statements = ((BatchStatement) arg).getStatements();
if (statements == null) {
statements = new ArrayList<Statement>();
}
queryText = concatenate(statements);
} else {
return null;
}
return new QueryEntryInfo(queryText, QueryMessageSupplier.create("cql execution: "));
}
private static String concatenate(Collection<Statement> statements) {
if (statements.isEmpty()) {
return "[empty batch]";
}
StringBuilder sb = new StringBuilder("[batch] ");
String currQuery = null;
int currCount = 0;
boolean first = true;
for (Statement statement : statements) {
String query = getQuery(statement);
if (currQuery == null) {
currQuery = query;
currCount = 1;
} else if (!query.equals(currQuery)) {
if (!first) {
sb.append(", ");
}
first = false;
if (currCount == 1) {
sb.append(currQuery);
} else {
sb.append(currCount + " x " + currQuery);
}
currQuery = query;
currCount = 1;
} else {
currCount++;
}
}
if (currQuery != null) {
if (!first) {
sb.append(", ");
}
if (currCount == 1) {
sb.append(currQuery);
} else {
sb.append(currCount + " x " + currQuery);
}
}
return sb.toString();
}
private static String getQuery(Statement statement) {
if (statement instanceof RegularStatement) {
String qs = ((RegularStatement) statement).getQueryString();
return nullToEmpty(qs);
} else if (statement instanceof BoundStatement) {
PreparedStatement preparedStatement =
((BoundStatement) statement).glowroot$preparedStatement();
String qs = preparedStatement == null ? "" : preparedStatement.getQueryString();
return nullToEmpty(qs);
} else if (statement instanceof BatchStatement) {
return "[nested batch statement]";
} else {
return "[unexpected statement type: " + statement.getClass().getName() + "]";
}
}
private static String nullToEmpty(@Nullable String string) {
return string == null ? "" : string;
}
private static class QueryEntryInfo {
private final String queryText;
private final QueryMessageSupplier queryMessageSupplier;
private QueryEntryInfo(String queryText, QueryMessageSupplier messageSupplier) {
this.queryText = queryText;
this.queryMessageSupplier = messageSupplier;
}
}
}