/*
* ApplicationInsights-Java
* Copyright (c) Microsoft Corporation
* All rights reserved.
*
* MIT License
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the ""Software""), to deal in the Software
* without restriction, including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software, and to permit
* persons to whom the Software is furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
* THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
* FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
package com.microsoft.applicationinsights.internal.agent;
import java.sql.Statement;
import java.sql.PreparedStatement;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.LinkedList;
import com.microsoft.applicationinsights.agent.internal.coresync.AgentNotificationsHandler;
import com.microsoft.applicationinsights.agent.internal.coresync.InstrumentedClassType;
import com.microsoft.applicationinsights.TelemetryClient;
import com.microsoft.applicationinsights.agent.internal.coresync.impl.ImplementationsCoordinator;
import com.microsoft.applicationinsights.internal.logger.InternalLogger;
import com.microsoft.applicationinsights.internal.schemav2.DependencyKind;
import com.microsoft.applicationinsights.internal.util.LocalStringsUtils;
import com.microsoft.applicationinsights.internal.util.ThreadLocalCleaner;
import com.microsoft.applicationinsights.telemetry.Duration;
import com.microsoft.applicationinsights.telemetry.ExceptionTelemetry;
import com.microsoft.applicationinsights.telemetry.RemoteDependencyTelemetry;
/**
* The Core's implementation: the methods are called for instrumented methods.
* The implementation can measure time in nano seconds, fetch Sql/Http data and report exceptions
*
* Created by gupele on 5/7/2015.
*/
final class CoreAgentNotificationsHandler implements AgentNotificationsHandler {
private final static String EXCEPTION_THROWN_ID = "__java_sdk__exceptionThrown__";
/**
* The class holds the data gathered on a method
*/
private static class MethodData {
public String name;
public Object[] arguments;
public long interval;
public String type;
public Object result;
}
private static class ThreadData {
public final LinkedList<MethodData> methods = new LinkedList<MethodData>();
}
static final class ThreadLocalData extends ThreadLocal<ThreadData> {
private ThreadData threadData;
@Override
protected ThreadData initialValue() {
threadData = new ThreadData();
return threadData;
}
};
private final ThreadLocalCleaner cleaner = new ThreadLocalCleaner() {
@Override
public void clean() {
threadDataThreadLocal.remove();
}
};
private ThreadLocalData threadDataThreadLocal = new ThreadLocalData();
private TelemetryClient telemetryClient = new TelemetryClient();
private final String name;
public ThreadLocalCleaner getCleaner() {
return cleaner;
}
public CoreAgentNotificationsHandler(String name) {
this.name = name;
}
@Override
public void exceptionCaught(String classAndMethodNames, Throwable throwable) {
try {
if (throwable instanceof Exception) {
telemetryClient.trackException((Exception)throwable);
}
} catch (Throwable t) {
}
}
@Override
public void httpMethodStarted(String classAndMethodNames, String url) {
startMethod(InstrumentedClassType.HTTP.toString(), name, url);
}
@Override
public void sqlStatementExecuteQueryPossibleQueryPlan(String name, Statement statement, String sqlStatement) {
startSqlMethod(statement, sqlStatement, null);
}
@Override
public void preparedStatementMethodStarted(String classAndMethodNames, PreparedStatement statement, String sqlStatement, Object[] args) {
startSqlMethod(statement, sqlStatement, args);
}
@Override
public void sqlStatementMethodStarted(String name, Statement statement, String sqlStatement) {
startSqlMethod(statement, sqlStatement, null);
}
@Override
public void preparedStatementExecuteBatchMethodStarted(String classAndMethodNames, PreparedStatement statement, String sqlStatement, int batchCounter) {
final String batchData = String.format("Batch of %d", batchCounter);
startSqlMethod(statement, sqlStatement, new Object[]{batchData});
}
public String getName() {
return name;
}
@Override
public void httpMethodFinished(String identifier, String method, String uri, int result, long delta) {
if (!LocalStringsUtils.isNullOrEmpty(uri) && uri.startsWith("https://dc.services.visualstudio.com")) {
return;
}
RemoteDependencyTelemetry telemetry = new RemoteDependencyTelemetry(identifier, null, new Duration(delta), true);
telemetry.setResultCode(Integer.toString(result));
telemetry.setType("HTTP");
telemetry.getContext().getProperties().put("URI", uri);
telemetry.getContext().getProperties().put("Method", method);
InternalLogger.INSTANCE.trace("'%s' sent an HTTP method: '%s', uri: '%s', duration=%s ms", identifier, method, uri, delta);
telemetryClient.track(telemetry);
}
@Override
public void methodStarted(String name) {
int index = name.lastIndexOf('#');
String classType;
if (index != -1) {
classType = name.substring(index + 1);
name = name.substring(0, index);
} else {
classType = InstrumentedClassType.OTHER.toString();
}
startMethod(classType, name, new String[]{});
}
@Override
public void methodFinished(String name, Throwable throwable) {
if (!finalizeMethod(0, null, throwable)) {
InternalLogger.INSTANCE.error("Agent has detected a 'Finish' method '%s' with exception '%s' event without a 'Start'",
name, throwable == null ? "unknown" : throwable.getClass().getName());
}
}
public void methodFinished(String name, String type, long thresholdInMS) {
if (!finalizeMethod(thresholdInMS, null, null)) {
InternalLogger.INSTANCE.error("Agent has detected a 'Finish' method ('%s') event without a 'Start'", name);
}
}
@Override
public void methodFinished(String name, long thresholdInMS) {
if (!finalizeMethod(thresholdInMS, null, null)) {
InternalLogger.INSTANCE.error("Agent has detected a 'Finish' method ('%s') event without a 'Start'", name);
}
}
@Override
public void methodFinished(String classAndMethodNames, long deltaInNS, Object[] args, Throwable throwable) {
long durationInMS = nanoToMilliseconds(deltaInNS);
Duration duration = new Duration(durationInMS);
RemoteDependencyTelemetry telemetry = new RemoteDependencyTelemetry(classAndMethodNames, null, duration, throwable == null);
telemetry.setDependencyKind(DependencyKind.Other);
if (args != null) {
String argsAsString = new ArgsFormatter().format(args);
telemetry.getContext().getProperties().put("Args", argsAsString);
}
InternalLogger.INSTANCE.trace("Sending RDD event for '%s', duration=%s ms", classAndMethodNames, durationInMS);
telemetryClient.track(telemetry);
if (throwable != null) {
ExceptionTelemetry exceptionTelemetry = new ExceptionTelemetry(throwable);
telemetryClient.track(exceptionTelemetry);
}
}
@Override
public void exceptionThrown(Exception e) {
throw new UnsupportedOperationException();
}
@Override
public void exceptionThrown(Exception e, int stackSize) {
ThreadData localData = threadDataThreadLocal.get();
MethodData methodData = null;
try {
if (localData.methods != null && !localData.methods.isEmpty()) {
for (MethodData m : localData.methods) {
if (EXCEPTION_THROWN_ID.equals(m.name)) {
return;
}
}
}
methodData = new MethodData();
methodData.interval = 0;
methodData.type = InstrumentedClassType.OTHER.toString();
methodData.arguments = null;
methodData.name = EXCEPTION_THROWN_ID;
localData.methods.addFirst(methodData);
ExceptionTelemetry et = new ExceptionTelemetry(e, stackSize);
telemetryClient.track(et);
} catch (Throwable t) {
}
if (methodData != null) {
localData.methods.remove(methodData);
}
}
private void startSqlMethod(Statement statement, String sqlStatement, Object[] additionalArgs) {
try {
Connection connection = null;
DatabaseMetaData metaData;
String url = null;
if (statement != null) {
try {
connection = statement.getConnection();
if (connection != null) {
metaData = connection.getMetaData();
if (metaData != null) {
url = metaData.getURL();
}
}
} catch (Throwable t) {
url = "jdbc:Unknown DB URL (failed to fetch from connection)";
}
}
Object[] sqlMetaData;
if (additionalArgs == null) {
sqlMetaData = new Object[] {url, sqlStatement, connection};
} else {
sqlMetaData = new Object[] {url, sqlStatement, connection, additionalArgs};
}
startSqlMethod(InstrumentedClassType.SQL.toString(), name, sqlMetaData);
ThreadData localData = threadDataThreadLocal.get();
} catch (Throwable e) {
}
}
private void startMethod(String type, String name, String... arguments) {
long start = System.nanoTime();
ThreadData localData = threadDataThreadLocal.get();
MethodData methodData = new MethodData();
methodData.interval = start;
methodData.type = type;
methodData.arguments = arguments;
methodData.name = name;
localData.methods.addFirst(methodData);
}
private void startSqlMethod(String type, String name, Object... arguments) {
long start = System.nanoTime();
ThreadData localData = threadDataThreadLocal.get();
MethodData methodData = new MethodData();
methodData.interval = start;
methodData.type = type;
methodData.arguments = arguments;
methodData.name = name;
localData.methods.addFirst(methodData);
}
private boolean finalizeMethod(long thresholdInMS, Object result, Throwable throwable) {
long finish = System.nanoTime();
ThreadData localData = threadDataThreadLocal.get();
if (localData.methods == null || localData.methods.isEmpty()) {
return false;
}
MethodData methodData = localData.methods.removeFirst();
if (methodData == null) {
return true;
}
methodData.interval = finish - methodData.interval;
if (throwable == null && thresholdInMS > 0) {
long asMS = nanoToMilliseconds(methodData.interval);
if (asMS < thresholdInMS){
return true;
}
}
methodData.result = result;
report(methodData, throwable);
return true;
}
private void report(MethodData methodData, Throwable throwable) {
if ("SQL".equalsIgnoreCase(methodData.type)) {
sendSQLTelemetry(methodData, throwable);
} else if ("HTTP".equalsIgnoreCase(methodData.type)) {
sendHTTPTelemetry(methodData, throwable);
} else {
sendInstrumentationTelemetry(methodData, throwable);
}
}
private void sendInstrumentationTelemetry(MethodData methodData, Throwable throwable) {
Duration duration = new Duration(nanoToMilliseconds(methodData.interval));
RemoteDependencyTelemetry telemetry = new RemoteDependencyTelemetry(methodData.name, null, duration, throwable == null);
telemetry.setType(methodData.type);
InternalLogger.INSTANCE.trace("Sending RDD event for '%s'", methodData.name);
telemetryClient.track(telemetry);
if (throwable != null) {
ExceptionTelemetry exceptionTelemetry = new ExceptionTelemetry(throwable);
telemetryClient.track(exceptionTelemetry);
}
}
private void sendHTTPTelemetry(MethodData methodData, Throwable throwable) {
if (methodData.arguments != null && methodData.arguments.length == 1) {
String url = methodData.arguments[0].toString();
long durationInMilliSeconds = nanoToMilliseconds(methodData.interval);
Duration duration = new Duration(durationInMilliSeconds);
InternalLogger.INSTANCE.trace("Sending HTTP RDD event, URL: '%s', duration=%s ms", url, durationInMilliSeconds);
RemoteDependencyTelemetry telemetry = new RemoteDependencyTelemetry(url, null, duration, throwable == null);
telemetry.setDependencyKind(DependencyKind.Http);
telemetryClient.trackDependency(telemetry);
if (throwable != null) {
ExceptionTelemetry exceptionTelemetry = new ExceptionTelemetry(throwable);
telemetryClient.track(exceptionTelemetry);
}
}
}
private void sendSQLTelemetry(MethodData methodData, Throwable throwable) {
if (methodData.arguments != null && methodData.arguments.length >= 3 && methodData.arguments[1] != null) {
try {
String dependencyName = null;
if (methodData.arguments[0] != null) {
dependencyName = methodData.arguments[0].toString();
}
String commandName = null;
if (methodData.arguments[1] == null) {
return;
}
commandName = methodData.arguments[1].toString();
long durationInMilliSeconds = nanoToMilliseconds(methodData.interval);
Duration duration = new Duration(durationInMilliSeconds);
RemoteDependencyTelemetry telemetry = new RemoteDependencyTelemetry(
dependencyName,
commandName,
duration,
throwable == null);
telemetry.setDependencyKind(DependencyKind.SQL);
StringBuilder sb = null;
if (methodData.arguments.length > 3) {
sb = formatAdditionalSqlArguments(methodData);
if (sb != null) {
telemetry.getContext().getProperties().put("Args", sb.toString());
}
} else {
if (durationInMilliSeconds > ImplementationsCoordinator.INSTANCE.getQueryPlanThresholdInMS()) {
sb = fetchExplainQuery(commandName, methodData.arguments[2]);
if (sb != null) {
telemetry.getContext().getProperties().put("Query Plan", sb.toString());
}
}
}
InternalLogger.INSTANCE.trace("Sending Sql RDD event for '%s', command: '%s', duration=%s ms", dependencyName, commandName, durationInMilliSeconds);
telemetryClient.track(telemetry);
if (throwable != null) {
InternalLogger.INSTANCE.trace("Sending Sql exception");
ExceptionTelemetry exceptionTelemetry = new ExceptionTelemetry(throwable);
telemetryClient.track(exceptionTelemetry);
}
} catch (Throwable t) {
t.printStackTrace();
}
}
}
private static long nanoToMilliseconds(long nanoSeconds) {
return nanoSeconds / 1000000;
}
private StringBuilder formatAdditionalSqlArguments(MethodData methodData) {
try {
StringBuilder sb = new StringBuilder();
sb.append(" [");
Object[] args = (Object[])methodData.arguments[3];
if (args != null && args.length > 0) {
for (Object arg : args) {
if (arg == null) {
sb.append("null,");
} else {
sb.append(arg.toString());
sb.append(',');
}
}
sb.deleteCharAt(sb.length() - 1);
}
sb.append(']');
return sb;
} catch (Throwable t) {
return null;
}
}
private StringBuilder fetchExplainQuery(String commandName, Object object) {
StringBuilder explainSB = null;
Statement explain = null;
ResultSet rs = null;
try {
if (commandName.startsWith("SELECT ")) {
Connection connection = (Connection)object;
if (connection == null) {
return explainSB;
}
explain = connection.createStatement();
rs = explain.executeQuery("EXPLAIN " + commandName);
explainSB = new StringBuilder();
while (rs.next()) {
explainSB.append('[');
int columns = rs.getMetaData().getColumnCount();
if (columns == 1) {
explainSB.append(rs.getString(1));
} else {
for (int i1 = 1; i1 < rs.getMetaData().getColumnCount(); ++i1) {
explainSB.append(rs.getMetaData().getColumnName(i1));
explainSB.append(':');
Object obj = rs.getObject(i1);
explainSB.append(obj == null ? "" : obj.toString());
explainSB.append(',');
}
explainSB.deleteCharAt(explainSB.length() - 1);
}
explainSB.append("],");
}
explainSB.deleteCharAt(explainSB.length() - 1);
}
} catch (Throwable t) {
} finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
}
}
if (explain != null) {
try {
explain.close();
} catch (SQLException e) {
}
}
}
return explainSB;
}
}