Repository: pinpoint-apm/pinpoint
Issue: Add trace support when using JDBC CallableStatements
Labels: enhancement

Current JDBC tracers do not propagate server information when using `CallableStatement`, and results in rendering an _unknown_db_ node. Adding an interceptor for `CallableStatement` similar to what's done for `PrepareStatement` should fix the issue.


Issue comments:

Pull request review comments:

Patch that resolved the issue:
diff --git a/bootstrap-core/src/main/java/com/navercorp/pinpoint/bootstrap/plugin/jdbc/interceptor/CallableStatementBindVariableInterceptor.java b/bootstrap-core/src/main/java/com/navercorp/pinpoint/bootstrap/plugin/jdbc/interceptor/CallableStatementBindVariableInterceptor.java
new file mode 100644
index 000000000000..1471633ebfa0
--- /dev/null
+++ b/bootstrap-core/src/main/java/com/navercorp/pinpoint/bootstrap/plugin/jdbc/interceptor/CallableStatementBindVariableInterceptor.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2016 NAVER Corp.
+ *
+ * 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 com.navercorp.pinpoint.bootstrap.plugin.jdbc.interceptor;
+
+import com.navercorp.pinpoint.bootstrap.context.TraceContext;
+import com.navercorp.pinpoint.bootstrap.interceptor.annotation.TargetFilter;
+
+/**
+ * @author HyunGil Jeong
+ */
+@TargetFilter(type = "com.navercorp.pinpoint.bootstrap.plugin.jdbc.PreparedStatementBindingMethodFilter", singleton = true)
+public class CallableStatementBindVariableInterceptor extends PreparedStatementBindVariableInterceptor {
+
+    public CallableStatementBindVariableInterceptor(TraceContext traceContext) {
+        super(traceContext);
+    }
+
+}
diff --git a/bootstrap-core/src/main/java/com/navercorp/pinpoint/bootstrap/plugin/jdbc/interceptor/CallableStatementExecuteQueryInterceptor.java b/bootstrap-core/src/main/java/com/navercorp/pinpoint/bootstrap/plugin/jdbc/interceptor/CallableStatementExecuteQueryInterceptor.java
new file mode 100644
index 000000000000..0f1bad6654d2
--- /dev/null
+++ b/bootstrap-core/src/main/java/com/navercorp/pinpoint/bootstrap/plugin/jdbc/interceptor/CallableStatementExecuteQueryInterceptor.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2016 NAVER Corp.
+ *
+ * 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 com.navercorp.pinpoint.bootstrap.plugin.jdbc.interceptor;
+
+import com.navercorp.pinpoint.bootstrap.context.MethodDescriptor;
+import com.navercorp.pinpoint.bootstrap.context.TraceContext;
+import com.navercorp.pinpoint.bootstrap.interceptor.annotation.TargetMethod;
+import com.navercorp.pinpoint.bootstrap.interceptor.annotation.TargetMethods;
+
+/**
+ * @author HyunGil Jeong
+ */
+@TargetMethods({
+        @TargetMethod(name = "execute"),
+        @TargetMethod(name = "executeQuery"),
+        @TargetMethod(name = "executeUpdate")
+})
+public class CallableStatementExecuteQueryInterceptor extends PreparedStatementExecuteQueryInterceptor {
+
+    public CallableStatementExecuteQueryInterceptor(TraceContext traceContext, MethodDescriptor descriptor) {
+        super(traceContext, descriptor);
+    }
+
+    public CallableStatementExecuteQueryInterceptor(TraceContext traceContext, MethodDescriptor descriptor, int maxSqlBindValue) {
+        super(traceContext, descriptor, maxSqlBindValue);
+    }
+
+}
diff --git a/bootstrap-core/src/main/java/com/navercorp/pinpoint/bootstrap/plugin/jdbc/interceptor/CallableStatementRegisterOutParameterInterceptor.java b/bootstrap-core/src/main/java/com/navercorp/pinpoint/bootstrap/plugin/jdbc/interceptor/CallableStatementRegisterOutParameterInterceptor.java
new file mode 100644
index 000000000000..e21d6a264ebf
--- /dev/null
+++ b/bootstrap-core/src/main/java/com/navercorp/pinpoint/bootstrap/plugin/jdbc/interceptor/CallableStatementRegisterOutParameterInterceptor.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2016 NAVER Corp.
+ *
+ * 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 com.navercorp.pinpoint.bootstrap.plugin.jdbc.interceptor;
+
+import com.navercorp.pinpoint.bootstrap.context.DatabaseInfo;
+import com.navercorp.pinpoint.bootstrap.context.MethodDescriptor;
+import com.navercorp.pinpoint.bootstrap.context.SpanEventRecorder;
+import com.navercorp.pinpoint.bootstrap.context.TraceContext;
+import com.navercorp.pinpoint.bootstrap.interceptor.SpanEventSimpleAroundInterceptorForPlugin;
+import com.navercorp.pinpoint.bootstrap.interceptor.annotation.TargetMethod;
+import com.navercorp.pinpoint.bootstrap.interceptor.annotation.TargetMethods;
+import com.navercorp.pinpoint.bootstrap.plugin.jdbc.DatabaseInfoAccessor;
+import com.navercorp.pinpoint.bootstrap.plugin.jdbc.UnKnownDatabaseInfo;
+
+/**
+ * @author HyunGil Jeong
+ */
+@TargetMethods({
+        @TargetMethod(name = "registerOutParameter", paramTypes = {"int", "int"}),
+        @TargetMethod(name = "registerOutParameter", paramTypes = {"int", "int", "int"}),
+        @TargetMethod(name = "registerOutParameter", paramTypes = {"int", "int", "java.lang.String"})
+})
+public class CallableStatementRegisterOutParameterInterceptor extends SpanEventSimpleAroundInterceptorForPlugin {
+
+    public CallableStatementRegisterOutParameterInterceptor(TraceContext traceContext, MethodDescriptor descriptor) {
+        super(traceContext, descriptor);
+    }
+
+    @Override
+    protected void doInBeforeTrace(SpanEventRecorder recorder, Object target, Object[] args) {
+    }
+
+    @Override
+    protected void doInAfterTrace(SpanEventRecorder recorder, Object target, Object[] args, Object result, Throwable throwable) {
+        DatabaseInfo databaseInfo = (target instanceof DatabaseInfoAccessor) ? ((DatabaseInfoAccessor)target)._$PINPOINT$_getDatabaseInfo() : null;
+
+        if (databaseInfo == null) {
+            databaseInfo = UnKnownDatabaseInfo.INSTANCE;
+        }
+
+        recorder.recordServiceType(databaseInfo.getType());
+        recorder.recordEndPoint(databaseInfo.getMultipleHost());
+        recorder.recordDestinationId(databaseInfo.getDatabaseId());
+
+        recorder.recordApi(methodDescriptor, args);
+        recorder.recordException(throwable);
+    }
+}
diff --git a/bootstrap-core/src/main/java/com/navercorp/pinpoint/bootstrap/plugin/jdbc/interceptor/PreparedStatementCreateInterceptor.java b/bootstrap-core/src/main/java/com/navercorp/pinpoint/bootstrap/plugin/jdbc/interceptor/PreparedStatementCreateInterceptor.java
index 47b317fbbda9..5cae250b0df2 100644
--- a/bootstrap-core/src/main/java/com/navercorp/pinpoint/bootstrap/plugin/jdbc/interceptor/PreparedStatementCreateInterceptor.java
+++ b/bootstrap-core/src/main/java/com/navercorp/pinpoint/bootstrap/plugin/jdbc/interceptor/PreparedStatementCreateInterceptor.java
@@ -38,7 +38,10 @@
         @TargetMethod(name="prepareStatement", paramTypes={ "java.lang.String", "int[]" }),
         @TargetMethod(name="prepareStatement", paramTypes={ "java.lang.String", "java.lang.String[]" }),
         @TargetMethod(name="prepareStatement", paramTypes={ "java.lang.String", "int", "int" }),
-        @TargetMethod(name="prepareStatement", paramTypes={ "java.lang.String", "int", "int", "int" })
+        @TargetMethod(name="prepareStatement", paramTypes={ "java.lang.String", "int", "int", "int" }),
+        @TargetMethod(name="prepareCall", paramTypes={ "java.lang.String" }),
+        @TargetMethod(name="prepareCall", paramTypes={ "java.lang.String", "int", "int" }),
+        @TargetMethod(name="prepareCall", paramTypes={ "java.lang.String", "int", "int", "int" })
 })
 public class PreparedStatementCreateInterceptor extends SpanEventSimpleAroundInterceptorForPlugin {
 
diff --git a/plugins/cubrid-jdbc/src/main/java/com/navercorp/pinpoint/plugin/jdbc/cubrid/CubridPlugin.java b/plugins/cubrid-jdbc/src/main/java/com/navercorp/pinpoint/plugin/jdbc/cubrid/CubridPlugin.java
index ee1b31a7a930..1af6bd882248 100644
--- a/plugins/cubrid-jdbc/src/main/java/com/navercorp/pinpoint/plugin/jdbc/cubrid/CubridPlugin.java
+++ b/plugins/cubrid-jdbc/src/main/java/com/navercorp/pinpoint/plugin/jdbc/cubrid/CubridPlugin.java
@@ -43,6 +43,7 @@ public void setup(ProfilerPluginSetupContext context) {
         addCUBRIDConnectionTransformer(config);
         addCUBRIDDriverTransformer();
         addCUBRIDPreparedStatementTransformer(config);
+        addCUBRIDCallableStatementTransformer();
         addCUBRIDStatementTransformer();
     }
 
@@ -113,6 +114,24 @@ public byte[] doInTransform(Instrumentor instrumentor, ClassLoader loader, Strin
             }
         });
     }
+
+    private void addCUBRIDCallableStatementTransformer() {
+        transformTemplate.transform("cubrid.jdbc.driver.CUBRIDCallableStatement", new TransformCallback() {
+
+            @Override
+            public byte[] doInTransform(Instrumentor instrumentor, ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws InstrumentException {
+                InstrumentClass target = instrumentor.getInstrumentClass(loader, className, classfileBuffer);
+
+                target.addField("com.navercorp.pinpoint.bootstrap.plugin.jdbc.DatabaseInfoAccessor");
+                target.addField("com.navercorp.pinpoint.bootstrap.plugin.jdbc.ParsingResultAccessor");
+                target.addField("com.navercorp.pinpoint.bootstrap.plugin.jdbc.BindValueAccessor");
+
+                target.addScopedInterceptor("com.navercorp.pinpoint.bootstrap.plugin.jdbc.interceptor.CallableStatementRegisterOutParameterInterceptor", CubridConstants.CUBRID_SCOPE);
+
+                return target.toBytecode();
+            }
+        });
+    }
     
     private void addCUBRIDStatementTransformer() {
         transformTemplate.transform("cubrid.jdbc.driver.CUBRIDStatement", new TransformCallback() {
diff --git a/plugins/jtds/src/main/java/com/navercorp/pinpoint/plugin/jdbc/jtds/JtdsPlugin.java b/plugins/jtds/src/main/java/com/navercorp/pinpoint/plugin/jdbc/jtds/JtdsPlugin.java
index ff21ffd6a2d4..47739759252c 100644
--- a/plugins/jtds/src/main/java/com/navercorp/pinpoint/plugin/jdbc/jtds/JtdsPlugin.java
+++ b/plugins/jtds/src/main/java/com/navercorp/pinpoint/plugin/jdbc/jtds/JtdsPlugin.java
@@ -42,6 +42,7 @@ public void setup(ProfilerPluginSetupContext context) {
         addConnectionTransformer(config);
         addDriverTransformer();
         addPreparedStatementTransformer(config);
+        addCallableStatementTransformer();
         addStatementTransformer();
     }
 
@@ -115,6 +116,24 @@ public byte[] doInTransform(Instrumentor instrumentor, ClassLoader loader, Strin
         });
     }
 
+    private void addCallableStatementTransformer() {
+        transformTemplate.transform("net.sourceforge.jtds.jdbc.JtdsCallableStatement", new TransformCallback() {
+
+            @Override
+            public byte[] doInTransform(Instrumentor instrumentor, ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws InstrumentException {
+                InstrumentClass target = instrumentor.getInstrumentClass(loader, className, classfileBuffer);
+
+                target.addField("com.navercorp.pinpoint.bootstrap.plugin.jdbc.DatabaseInfoAccessor");
+                target.addField("com.navercorp.pinpoint.bootstrap.plugin.jdbc.ParsingResultAccessor");
+                target.addField("com.navercorp.pinpoint.bootstrap.plugin.jdbc.BindValueAccessor");
+
+                target.addScopedInterceptor("com.navercorp.pinpoint.bootstrap.plugin.jdbc.interceptor.CallableStatementRegisterOutParameterInterceptor", JtdsConstants.JTDS_SCOPE);
+
+                return target.toBytecode();
+            }
+        });
+    }
+
     private void addStatementTransformer() {
         transformTemplate.transform("net.sourceforge.jtds.jdbc.JtdsStatement", new TransformCallback() {
 
diff --git a/plugins/mariadb-jdbc/src/main/java/com/navercorp/pinpoint/plugin/jdbc/mariadb/MariaDBPlugin.java b/plugins/mariadb-jdbc/src/main/java/com/navercorp/pinpoint/plugin/jdbc/mariadb/MariaDBPlugin.java
index b594f217f812..590c4abc5502 100644
--- a/plugins/mariadb-jdbc/src/main/java/com/navercorp/pinpoint/plugin/jdbc/mariadb/MariaDBPlugin.java
+++ b/plugins/mariadb-jdbc/src/main/java/com/navercorp/pinpoint/plugin/jdbc/mariadb/MariaDBPlugin.java
@@ -44,7 +44,12 @@ public void setup(ProfilerPluginSetupContext context) {
         addDriverTransformer();
         addPreparedStatementTransformer(config);
         addPreparedStatementBindVariableTransformer(config);
+        addCallableStatementTransformer();
         addStatementTransformer();
+
+        // MariaDb 1.3.x's CallableStatements are completely separated from PreparedStatements (similar to MySQL)
+        // Separate interceptors must be injected.
+        add_1_3_x_CallableStatementTransformer(config);
     }
 
     private void addConnectionTransformer(final MariaDBConfig config) {
@@ -206,6 +211,54 @@ public byte[] doInTransform(Instrumentor instrumentor, ClassLoader loader, Strin
 
     }
 
+    private void addCallableStatementTransformer() {
+        TransformCallback transformer = new TransformCallback() {
+            @Override
+            public byte[] doInTransform(Instrumentor instrumentor, ClassLoader loader, String className,
+                                        Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
+                                        byte[] classfileBuffer) throws InstrumentException {
+                InstrumentClass target = instrumentor.getInstrumentClass(loader, className, classfileBuffer);
+
+                target.addField("com.navercorp.pinpoint.bootstrap.plugin.jdbc.DatabaseInfoAccessor");
+                target.addField("com.navercorp.pinpoint.bootstrap.plugin.jdbc.ParsingResultAccessor");
+                target.addField("com.navercorp.pinpoint.bootstrap.plugin.jdbc.BindValueAccessor");
+
+                target.addScopedInterceptor("com.navercorp.pinpoint.bootstrap.plugin.jdbc.interceptor.CallableStatementRegisterOutParameterInterceptor", MariaDBConstants.MARIADB_SCOPE);
+
+                return target.toBytecode();
+            }
+        };
+
+        transformTemplate.transform("org.mariadb.jdbc.AbstractCallableProcedureStatement", transformer);
+        transformTemplate.transform("org.mariadb.jdbc.AbstractCallableFunctionStatement", transformer);
+    }
+
+    private void add_1_3_x_CallableStatementTransformer(final MariaDBConfig config) {
+        transformTemplate.transform("org.mariadb.jdbc.MariaDbCallableStatement", new TransformCallback() {
+            @Override
+            public byte[] doInTransform(Instrumentor instrumentor, ClassLoader loader, String className,
+                                        Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
+                                        byte[] classfileBuffer) throws InstrumentException {
+                InstrumentClass target = instrumentor.getInstrumentClass(loader, className, classfileBuffer);
+
+                target.addField("com.navercorp.pinpoint.bootstrap.plugin.jdbc.DatabaseInfoAccessor");
+                target.addField("com.navercorp.pinpoint.bootstrap.plugin.jdbc.ParsingResultAccessor");
+                target.addField("com.navercorp.pinpoint.bootstrap.plugin.jdbc.BindValueAccessor");
+
+                int maxBindValueSize = config.getMaxSqlBindValueSize();
+
+                target.addScopedInterceptor("com.navercorp.pinpoint.bootstrap.plugin.jdbc.interceptor.CallableStatementExecuteQueryInterceptor", va(maxBindValueSize), MariaDBConstants.MARIADB_SCOPE);
+                target.addScopedInterceptor("com.navercorp.pinpoint.bootstrap.plugin.jdbc.interceptor.CallableStatementRegisterOutParameterInterceptor", MariaDBConstants.MARIADB_SCOPE);
+
+                if (config.isTraceSqlBindValue()) {
+                    target.addScopedInterceptor("com.navercorp.pinpoint.bootstrap.plugin.jdbc.interceptor.CallableStatementBindVariableInterceptor", MariaDBConstants.MARIADB_SCOPE);
+                }
+
+                return target.toBytecode();
+            }
+        });
+    }
+
     @Override
     public void setTransformTemplate(TransformTemplate transformTemplate) {
         this.transformTemplate = transformTemplate;
diff --git a/plugins/mysql-jdbc/src/main/java/com/navercorp/pinpoint/plugin/jdbc/mysql/MySqlPlugin.java b/plugins/mysql-jdbc/src/main/java/com/navercorp/pinpoint/plugin/jdbc/mysql/MySqlPlugin.java
index ba730858995a..58d668c4c7b8 100644
--- a/plugins/mysql-jdbc/src/main/java/com/navercorp/pinpoint/plugin/jdbc/mysql/MySqlPlugin.java
+++ b/plugins/mysql-jdbc/src/main/java/com/navercorp/pinpoint/plugin/jdbc/mysql/MySqlPlugin.java
@@ -44,10 +44,12 @@ public void setup(ProfilerPluginSetupContext context) {
         addDriverTransformer();
         addStatementTransformer();
         addPreparedStatementTransformer(config);
+        addCallableStatementTransformer(config);
 
         // From MySQL driver 5.1.x, backward compatibility is broken.
         // Driver returns not com.mysql.jdbc.Connection but com.mysql.jdbc.JDBC4Connection which extends com.mysql.jdbc.ConnectionImpl from 5.1.x
         addJDBC4PreparedStatementTransformer(config);
+        addJDBC4CallableStatementTransformer(config);
     }
 
     private void addConnectionTransformer(final MySqlConfig config) {
@@ -127,6 +129,32 @@ public byte[] doInTransform(Instrumentor instrumentor, ClassLoader loader, Strin
         });
     }
 
+    private void addCallableStatementTransformer(final MySqlConfig config) {
+        transformTemplate.transform("com.mysql.jdbc.CallableStatement", new TransformCallback() {
+
+            @Override
+            public byte[] doInTransform(Instrumentor instrumentor, ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws InstrumentException {
+                InstrumentClass target = instrumentor.getInstrumentClass(loader, className, classfileBuffer);
+
+                target.addField("com.navercorp.pinpoint.bootstrap.plugin.jdbc.DatabaseInfoAccessor");
+                target.addField("com.navercorp.pinpoint.bootstrap.plugin.jdbc.ParsingResultAccessor");
+                target.addField("com.navercorp.pinpoint.bootstrap.plugin.jdbc.BindValueAccessor");
+
+                int maxBindValueSize = config.getMaxSqlBindValueSize();
+
+                target.addScopedInterceptor("com.navercorp.pinpoint.bootstrap.plugin.jdbc.interceptor.CallableStatementExecuteQueryInterceptor", va(maxBindValueSize), MySqlConstants.MYSQL_SCOPE);
+                target.addScopedInterceptor("com.navercorp.pinpoint.bootstrap.plugin.jdbc.interceptor.CallableStatementRegisterOutParameterInterceptor", MySqlConstants.MYSQL_SCOPE);
+
+                if (config.isTraceSqlBindValue()) {
+                    final PreparedStatementBindingMethodFilter excludes = PreparedStatementBindingMethodFilter.excludes("setRowId", "setNClob", "setSQLXML");
+                    target.addScopedInterceptor(excludes, "com.navercorp.pinpoint.bootstrap.plugin.jdbc.interceptor.CallableStatementBindVariableInterceptor", MySqlConstants.MYSQL_SCOPE, ExecutionPolicy.BOUNDARY);
+                }
+
+                return target.toBytecode();
+            }
+        });
+    }
+
     private void addJDBC4PreparedStatementTransformer(final MySqlConfig config) {
         transformTemplate.transform("com.mysql.jdbc.JDBC4PreparedStatement", new TransformCallback() {
 
@@ -144,6 +172,22 @@ public byte[] doInTransform(Instrumentor instrumentor, ClassLoader loader, Strin
         });
     }
 
+    private void addJDBC4CallableStatementTransformer(final MySqlConfig config) {
+        transformTemplate.transform("com.mysql.jdbc.JDBC4CallableStatement", new TransformCallback() {
+
+            @Override
+            public byte[] doInTransform(Instrumentor instrumentor, ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws InstrumentException {
+                InstrumentClass target = instrumentor.getInstrumentClass(loader, className, classfileBuffer);
+
+                if (config.isTraceSqlBindValue()) {
+                    final PreparedStatementBindingMethodFilter includes = PreparedStatementBindingMethodFilter.includes("setRowId", "setNClob", "setSQLXML");
+                    target.addScopedInterceptor(includes, "com.navercorp.pinpoint.bootstrap.plugin.jdbc.interceptor.CallableStatementBindVariableInterceptor", MySqlConstants.MYSQL_SCOPE, ExecutionPolicy.BOUNDARY);
+                }
+
+                return target.toBytecode();
+            }
+        });
+    }
 
     private void addStatementTransformer() {
         TransformCallback transformer = new TransformCallback() {
diff --git a/plugins/oracle-jdbc/src/main/java/com/navercorp/pinpoint/plugin/jdbc/oracle/OraclePlugin.java b/plugins/oracle-jdbc/src/main/java/com/navercorp/pinpoint/plugin/jdbc/oracle/OraclePlugin.java
index acd14910e520..452a53a51b32 100644
--- a/plugins/oracle-jdbc/src/main/java/com/navercorp/pinpoint/plugin/jdbc/oracle/OraclePlugin.java
+++ b/plugins/oracle-jdbc/src/main/java/com/navercorp/pinpoint/plugin/jdbc/oracle/OraclePlugin.java
@@ -37,6 +37,8 @@ public class OraclePlugin implements ProfilerPlugin, TransformTemplateAware {
     private static final String CLASS_STATEMENT = "oracle.jdbc.driver.OracleStatement";
     private static final String CLASS_PREPARED_STATEMENT_WRAPPER = "oracle.jdbc.driver.OraclePreparedStatementWrapper";
     private static final String CLASS_PREPARED_STATEMENT = "oracle.jdbc.driver.OraclePreparedStatement";
+    private static final String CLASS_CALLABLE_STATEMENT_WRAPPER = "oracle.jdbc.driver.OracleCallableStatementWrapper";
+    private static final String CLASS_CALLABLE_STATEMENT = "oracle.jdbc.driver.OracleCallableStatement";
 
     private TransformTemplate transformTemplate;
 
@@ -47,6 +49,7 @@ public void setup(ProfilerPluginSetupContext context) {
         addConnectionTransformer(config);
         addDriverTransformer();
         addPreparedStatementTransformer(config);
+        addCallableStatementTransformer();
         addStatementTransformer();
     }
 
@@ -128,6 +131,32 @@ public byte[] doInTransform(Instrumentor instrumentor, ClassLoader loader, Strin
         transformTemplate.transform(CLASS_PREPARED_STATEMENT_WRAPPER, transformer);
     }
 
+    private void addCallableStatementTransformer() {
+        TransformCallback transformer = new TransformCallback() {
+
+            @Override
+            public byte[] doInTransform(Instrumentor instrumentor, ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws InstrumentException {
+                InstrumentClass target = instrumentor.getInstrumentClass(loader, className, classfileBuffer);
+                if (className.equals(CLASS_CALLABLE_STATEMENT)) {
+                    if (instrumentor.exist(loader, CLASS_CALLABLE_STATEMENT_WRAPPER)) {
+                        return null;
+                    }
+                }
+
+                target.addField("com.navercorp.pinpoint.bootstrap.plugin.jdbc.DatabaseInfoAccessor");
+                target.addField("com.navercorp.pinpoint.bootstrap.plugin.jdbc.ParsingResultAccessor");
+                target.addField("com.navercorp.pinpoint.bootstrap.plugin.jdbc.BindValueAccessor");
+
+                target.addScopedInterceptor("com.navercorp.pinpoint.bootstrap.plugin.jdbc.interceptor.CallableStatementRegisterOutParameterInterceptor", OracleConstants.ORACLE_SCOPE);
+
+                return target.toBytecode();
+            }
+        };
+
+        transformTemplate.transform(CLASS_CALLABLE_STATEMENT, transformer);
+        transformTemplate.transform(CLASS_CALLABLE_STATEMENT_WRAPPER, transformer);
+    }
+
     private void addStatementTransformer() {
         TransformCallback transformer = new TransformCallback() {
 


Test patch:
diff --git a/agent/src/test/java/com/navercorp/pinpoint/plugin/jdbc/mariadb/MariaDBIT.java b/agent/src/test/java/com/navercorp/pinpoint/plugin/jdbc/mariadb/MariaDBIT.java
deleted file mode 100644
index 11748fe5c665..000000000000
--- a/agent/src/test/java/com/navercorp/pinpoint/plugin/jdbc/mariadb/MariaDBIT.java
+++ /dev/null
@@ -1,86 +0,0 @@
-package com.navercorp.pinpoint.plugin.jdbc.mariadb;
-
-import static com.navercorp.pinpoint.bootstrap.plugin.test.Expectations.*;
-import static org.junit.Assert.*;
-
-import java.sql.Connection;
-import java.sql.DriverManager;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.Statement;
-import java.util.Properties;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mariadb.jdbc.Driver;
-import org.mariadb.jdbc.MariaDbConnection;
-import org.mariadb.jdbc.MariaDbServerPreparedStatement;
-import org.mariadb.jdbc.MariaDbStatement;
-
-import ch.vorburger.mariadb4j.DB;
-
-import com.navercorp.pinpoint.bootstrap.plugin.test.PluginTestVerifier;
-import com.navercorp.pinpoint.bootstrap.plugin.test.PluginTestVerifierHolder;
-import com.navercorp.pinpoint.test.plugin.Dependency;
-import com.navercorp.pinpoint.test.plugin.JvmVersion;
-import com.navercorp.pinpoint.test.plugin.PinpointPluginTestSuite;
-import com.navercorp.pinpoint.test.plugin.Repository;
-
-@RunWith(PinpointPluginTestSuite.class)
-@JvmVersion(7)
-@Repository("http://jcenter.bintray.com")
-@Dependency({ "org.mariadb.jdbc:mariadb-java-client:[1.3.4,)", "ch.vorburger.mariaDB4j:mariaDB4j:2.1.3" })
-public class MariaDBIT {
-
-    private static final String JDBC_URL = "jdbc:mariadb://127.0.0.1:13306/test";
-    private static final String PREPARED_STATEMENT = "SELECT * FROM playground where id = ?";
-
-    private DB db;
-
-    @Before
-    public void init() throws Exception {
-        db = DB.newEmbeddedDB(13306);
-        db.start();
-        db.createDB("test");
-        db.source("jdbc/mariadb/init.sql");
-    }
-
-    @Test
-    public void testConnection() throws Exception {
-        PluginTestVerifier verifier = PluginTestVerifierHolder.getInstance();
-
-        Connection connection = DriverManager.getConnection(JDBC_URL, "root", null);
-
-//        verifier.verifyTraceCount(1);
-
-        Statement statement = connection.createStatement();
-        ResultSet result = statement.executeQuery("SELECT count(1) FROM playground");
-        result.first();
-        assertEquals(3, result.getInt(1));
-
-        verifier.printCache();
-        
-        verifier.verifyTraceCount(2);
-        verifier.verifyTrace(event("MARIADB", Driver.class.getMethod("connect", String.class, Properties.class), cachedArgs(JDBC_URL)));
-        verifier.verifyTrace(event("MARIADB_EXECUTE_QUERY", MariaDbStatement.class.getMethod("executeQuery", String.class), sql("SELECT count(0#) FROM playground", "1")));
-
-        PreparedStatement ps = connection.prepareStatement(PREPARED_STATEMENT);
-        ps.setInt(1, 3);
-        result = ps.executeQuery();
-        result.first();
-        assertEquals("THREE", result.getString(2));
-
-        verifier.printCache();
-        verifier.verifyTraceCount(2);
-        verifier.verifyTrace(event("MARIADB", MariaDbConnection.class.getMethod("prepareStatement", String.class), sql(PREPARED_STATEMENT, null)));
-        verifier.verifyTrace(event("MARIADB_EXECUTE_QUERY", MariaDbServerPreparedStatement.class.getMethod("executeQuery"), sql(PREPARED_STATEMENT, null, "3")));
-    }
-
-    @After
-    public void cleanUp() throws Exception {
-        db.stop();
-    }
-
-}
diff --git a/agent/src/test/java/com/navercorp/pinpoint/plugin/jdbc/mariadb/MariaDB_1_3_x_IT.java b/agent/src/test/java/com/navercorp/pinpoint/plugin/jdbc/mariadb/MariaDB_1_3_x_IT.java
new file mode 100644
index 000000000000..8ceeee24d745
--- /dev/null
+++ b/agent/src/test/java/com/navercorp/pinpoint/plugin/jdbc/mariadb/MariaDB_1_3_x_IT.java
@@ -0,0 +1,104 @@
+package com.navercorp.pinpoint.plugin.jdbc.mariadb;
+
+import static com.navercorp.pinpoint.bootstrap.plugin.test.Expectations.*;
+
+import java.lang.reflect.Method;
+import java.util.Properties;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import com.navercorp.pinpoint.bootstrap.plugin.test.PluginTestVerifier;
+import com.navercorp.pinpoint.bootstrap.plugin.test.PluginTestVerifierHolder;
+import com.navercorp.pinpoint.test.plugin.Dependency;
+import com.navercorp.pinpoint.test.plugin.JvmVersion;
+import com.navercorp.pinpoint.test.plugin.PinpointPluginTestSuite;
+import com.navercorp.pinpoint.test.plugin.Repository;
+
+@RunWith(PinpointPluginTestSuite.class)
+@JvmVersion(7)
+@Repository("http://jcenter.bintray.com")
+@Dependency({"org.mariadb.jdbc:mariadb-java-client:[1.3.0,1.3.max]", "ch.vorburger.mariaDB4j:mariaDB4j:2.1.3"})
+public class MariaDB_1_3_x_IT extends MariaDB_IT_Base {
+
+    // see CallableParameterMetaData#queryMetaInfos
+    private static final String CALLABLE_QUERY_META_INFOS_QUERY = "select param_list,returns from mysql.proc where db=database() and name='0$'";
+
+    @Test
+    public void testStatement() throws Exception {
+        super.executeStatement();
+
+        PluginTestVerifier verifier = PluginTestVerifierHolder.getInstance();
+        verifier.printCache();
+        verifier.verifyTraceCount(2);
+
+        // Driver#connect(String, Properties)
+        Class<?> driverClass = Class.forName("org.mariadb.jdbc.Driver");
+        Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
+        verifier.verifyTrace(event("MARIADB", connect, null, URL, DATABASE_NAME, cachedArgs(JDBC_URL)));
+
+        // MariaDbStatement#executeQuery(String)
+        Class<?> mariaDbStatementClass = Class.forName("org.mariadb.jdbc.MariaDbStatement");
+        Method executeQuery = mariaDbStatementClass.getDeclaredMethod("executeQuery", String.class);
+        verifier.verifyTrace(event("MARIADB_EXECUTE_QUERY", executeQuery, null, URL, DATABASE_NAME, sql(STATEMENT_NORMALIZED_QUERY, "1")));
+    }
+
+    @Test
+    public void testPreparedStatement() throws Exception {
+        super.executePreparedStatement();
+
+        PluginTestVerifier verifier = PluginTestVerifierHolder.getInstance();
+        verifier.printCache();
+        verifier.verifyTraceCount(3);
+
+        // Driver#connect(String, Properties)
+        Class<?> driverClass = Class.forName("org.mariadb.jdbc.Driver");
+        Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
+        verifier.verifyTrace(event("MARIADB", connect, null, URL, DATABASE_NAME, cachedArgs(JDBC_URL)));
+
+        // MariaDbConnection#prepareStatement(String)
+        Class<?> mariaDbConnectionClass = Class.forName("org.mariadb.jdbc.MariaDbConnection");
+        Method prepareStatement = mariaDbConnectionClass.getDeclaredMethod("prepareStatement", String.class);
+        verifier.verifyTrace(event("MARIADB", prepareStatement, null, URL, DATABASE_NAME, sql(PREPARED_STATEMENT_QUERY, null)));
+
+        // MariaDbServerPreparedStatement#executeQuery
+        Class<?> mariaDbServerPreparedStatementClass = Class.forName("org.mariadb.jdbc.MariaDbServerPreparedStatement");
+        Method executeQuery = mariaDbServerPreparedStatementClass.getDeclaredMethod("executeQuery");
+        verifier.verifyTrace(event("MARIADB_EXECUTE_QUERY", executeQuery, null, URL, DATABASE_NAME, sql(PREPARED_STATEMENT_QUERY, null, "3")));
+    }
+
+    @Test
+    public void testCallableStatement() throws Exception {
+        super.executeCallableStatement();
+
+        PluginTestVerifier verifier = PluginTestVerifierHolder.getInstance();
+        verifier.printCache();
+        verifier.verifyTraceCount(5);
+
+        // Driver#connect(String, Properties)
+        Class<?> driverClass = Class.forName("org.mariadb.jdbc.Driver");
+        Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
+        verifier.verifyTrace(event("MARIADB", connect, null, URL, DATABASE_NAME, cachedArgs(JDBC_URL)));
+
+        // MariaDbConnection#prepareCall(String)
+        Class<?> mariaDbConnectionClass = Class.forName("org.mariadb.jdbc.MariaDbConnection");
+        Method prepareCall = mariaDbConnectionClass.getDeclaredMethod("prepareCall", String.class);
+        verifier.verifyTrace(event("MARIADB", prepareCall, null, URL, DATABASE_NAME, sql(CALLABLE_STATEMENT_QUERY, null)));
+
+        // MariaDbClientCallableStatement#registerOutParameter(int, int)
+        Class<?> mariaDbClientCallableStatementClass = Class.forName("org.mariadb.jdbc.MariaDbCallableStatement");
+        Method registerOutParameter = mariaDbClientCallableStatementClass.getDeclaredMethod("registerOutParameter", int.class, int.class);
+        verifier.verifyTrace(event("MARIADB", registerOutParameter, null, URL, DATABASE_NAME, args(2, CALLABLE_STATMENT_OUTPUT_PARAM_TYPE)));
+
+        // MariaDbCallableStatement#executeQuery
+        Class<?> mariaDbCallableStatementClass = Class.forName("org.mariadb.jdbc.MariaDbCallableStatement");
+        Method callableStatementExecuteQuery = mariaDbCallableStatementClass.getDeclaredMethod("executeQuery");
+        verifier.verifyTrace(event("MARIADB_EXECUTE_QUERY", callableStatementExecuteQuery, null, URL, DATABASE_NAME, sql(CALLABLE_STATEMENT_QUERY, null, CALLABLE_STATEMENT_INPUT_PARAM)));
+
+        // MariaDbStatement#executeQuery
+        Class<?> mariaDbStatementClass = Class.forName("org.mariadb.jdbc.MariaDbStatement");
+        Method statementExecuteQuery = mariaDbStatementClass.getDeclaredMethod("executeQuery", String.class);
+        verifier.verifyTrace(event("MARIADB_EXECUTE_QUERY", statementExecuteQuery, null, URL, DATABASE_NAME, sql(CALLABLE_QUERY_META_INFOS_QUERY, PROCEDURE_NAME)));
+    }
+
+}
diff --git a/agent/src/test/java/com/navercorp/pinpoint/plugin/jdbc/mariadb/MariaDB_1_4_x_IT.java b/agent/src/test/java/com/navercorp/pinpoint/plugin/jdbc/mariadb/MariaDB_1_4_x_IT.java
new file mode 100644
index 000000000000..76fd14be8c25
--- /dev/null
+++ b/agent/src/test/java/com/navercorp/pinpoint/plugin/jdbc/mariadb/MariaDB_1_4_x_IT.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2016 NAVER Corp.
+ *
+ * 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 com.navercorp.pinpoint.plugin.jdbc.mariadb;
+
+import com.navercorp.pinpoint.bootstrap.plugin.test.PluginTestVerifier;
+import com.navercorp.pinpoint.bootstrap.plugin.test.PluginTestVerifierHolder;
+import com.navercorp.pinpoint.test.plugin.Dependency;
+import com.navercorp.pinpoint.test.plugin.JvmVersion;
+import com.navercorp.pinpoint.test.plugin.PinpointPluginTestSuite;
+import com.navercorp.pinpoint.test.plugin.Repository;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Method;
+import java.util.Properties;
+
+import static com.navercorp.pinpoint.bootstrap.plugin.test.Expectations.*;
+
+/**
+ * @author HyunGil Jeong
+ */
+
+@RunWith(PinpointPluginTestSuite.class)
+@JvmVersion(7)
+@Repository("http://jcenter.bintray.com")
+@Dependency({ "org.mariadb.jdbc:mariadb-java-client:[1.4.min,1.4.max]", "ch.vorburger.mariaDB4j:mariaDB4j:2.1.3" })
+public class MariaDB_1_4_x_IT extends MariaDB_IT_Base {
+
+    // see CallableParameterMetaData#queryMetaInfos
+    private  static final String CALLABLE_QUERY_META_INFOS_QUERY = "select param_list, returns, db, type from mysql.proc where db=DATABASE() and name=?";
+
+    @Test
+    public void testStatement() throws Exception {
+        super.executeStatement();
+
+        PluginTestVerifier verifier = PluginTestVerifierHolder.getInstance();
+        verifier.printCache();
+
+        // Driver#connect(String, Properties)
+        Class<?> driverClass = Class.forName("org.mariadb.jdbc.Driver");
+        Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
+        verifier.verifyTrace(event("MARIADB", connect, null, URL, DATABASE_NAME, cachedArgs(JDBC_URL)));
+
+        // MariaDbStatement#executeQuery(String)
+        Class<?> mariaDbStatementClass = Class.forName("org.mariadb.jdbc.MariaDbStatement");
+        Method executeQuery = mariaDbStatementClass.getDeclaredMethod("executeQuery", String.class);
+        verifier.verifyTrace(event("MARIADB_EXECUTE_QUERY", executeQuery, null, URL, DATABASE_NAME, sql(STATEMENT_NORMALIZED_QUERY, "1")));
+    }
+
+    @Test
+    public void testPreparedStatement() throws Exception {
+        super.executePreparedStatement();
+
+        PluginTestVerifier verifier = PluginTestVerifierHolder.getInstance();
+        verifier.printCache();
+        verifier.verifyTraceCount(3);
+
+        // Driver#connect(String, Properties)
+        Class<?> driverClass = Class.forName("org.mariadb.jdbc.Driver");
+        Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
+        verifier.verifyTrace(event("MARIADB", connect, null, URL, DATABASE_NAME, cachedArgs(JDBC_URL)));
+
+        // MariaDbConnection#prepareStatement(String)
+        Class<?> mariaDbConnectionClass = Class.forName("org.mariadb.jdbc.MariaDbConnection");
+        Method prepareStatement = mariaDbConnectionClass.getDeclaredMethod("prepareStatement", String.class);
+        verifier.verifyTrace(event("MARIADB", prepareStatement, null, URL, DATABASE_NAME, sql(PREPARED_STATEMENT_QUERY, null)));
+
+        // MariaDbServerPreparedStatement#executeQuery
+        Class<?> mariaDbServerPreparedStatementClass = Class.forName("org.mariadb.jdbc.MariaDbServerPreparedStatement");
+        Method executeQuery = mariaDbServerPreparedStatementClass.getDeclaredMethod("executeQuery");
+        verifier.verifyTrace(event("MARIADB_EXECUTE_QUERY", executeQuery, null, URL, DATABASE_NAME, sql(PREPARED_STATEMENT_QUERY, null, "3")));
+    }
+
+    @Test
+    public void testCallableStatement() throws Exception {
+        super.executeCallableStatement();
+
+        PluginTestVerifier verifier = PluginTestVerifierHolder.getInstance();
+        verifier.printCache();
+        verifier.verifyTraceCount(6);
+
+        // Driver#connect(String, Properties)
+        Class<?> driverClass = Class.forName("org.mariadb.jdbc.Driver");
+        Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
+        verifier.verifyTrace(event("MARIADB", connect, null, URL, DATABASE_NAME, cachedArgs(JDBC_URL)));
+
+        // MariaDbConnection#prepareCall(String)
+        Class<?> mariaDbConnectionClass = Class.forName("org.mariadb.jdbc.MariaDbConnection");
+        Method prepareCall = mariaDbConnectionClass.getDeclaredMethod("prepareCall", String.class);
+        verifier.verifyTrace(event("MARIADB", prepareCall, null, URL, DATABASE_NAME, sql(CALLABLE_STATEMENT_QUERY, null)));
+
+        // AbstractCallableProcedureStatement#registerOutParameter
+        Class<?> abstractCallableProcedureStatementClass = Class.forName("org.mariadb.jdbc.AbstractCallableProcedureStatement");
+        Method registerOutParameter = abstractCallableProcedureStatementClass.getMethod("registerOutParameter", int.class, int.class);
+        verifier.verifyTrace(event("MARIADB", registerOutParameter, null, URL, DATABASE_NAME, args(2, CALLABLE_STATMENT_OUTPUT_PARAM_TYPE)));
+
+        // MariaDbServerPreparedStatement#executeQuery
+        Class<?> mariaDbServerPreparedStatementClass = Class.forName("org.mariadb.jdbc.MariaDbServerPreparedStatement");
+        Method executeQuery = mariaDbServerPreparedStatementClass.getDeclaredMethod("executeQuery");
+        verifier.verifyTrace(event("MARIADB_EXECUTE_QUERY", executeQuery, null, URL, DATABASE_NAME, sql(CALLABLE_STATEMENT_QUERY, null, CALLABLE_STATEMENT_INPUT_PARAM)));
+
+        // MariaDbConnection#prepareStatement(String)
+        Method prepareStatement = mariaDbConnectionClass.getDeclaredMethod("prepareStatement", String.class);
+        verifier.verifyTrace(event("MARIADB", prepareStatement, null, URL, DATABASE_NAME, sql(CALLABLE_QUERY_META_INFOS_QUERY, null)));
+
+        // MariaDbServerPreparedStatement#executeQuery
+        verifier.verifyTrace(event("MARIADB_EXECUTE_QUERY", executeQuery, null, URL, DATABASE_NAME, sql(CALLABLE_QUERY_META_INFOS_QUERY, null, PROCEDURE_NAME)));
+    }
+}
diff --git a/agent/src/test/java/com/navercorp/pinpoint/plugin/jdbc/mariadb/MariaDB_IT_Base.java b/agent/src/test/java/com/navercorp/pinpoint/plugin/jdbc/mariadb/MariaDB_IT_Base.java
new file mode 100644
index 000000000000..f0fb3c4f00dc
--- /dev/null
+++ b/agent/src/test/java/com/navercorp/pinpoint/plugin/jdbc/mariadb/MariaDB_IT_Base.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2016 NAVER Corp.
+ *
+ * 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 com.navercorp.pinpoint.plugin.jdbc.mariadb;
+
+import ch.vorburger.mariadb4j.DB;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+import java.sql.CallableStatement;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Types;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * @author HyunGil Jeong
+ */
+public class MariaDB_IT_Base {
+
+    private static final int PORT = 13306;
+    protected static final String URL = "127.0.0.1:" + PORT;
+    protected static final String DATABASE_NAME = "test";
+
+    protected static final String JDBC_URL = "jdbc:mariadb://" + URL + "/" + DATABASE_NAME;
+
+    // for Statement
+    protected static final String STATEMENT_QUERY = "SELECT count(1) FROM playground";
+    protected static final String STATEMENT_NORMALIZED_QUERY = "SELECT count(0#) FROM playground";
+
+    // for Prepared Statement
+    protected static final String PREPARED_STATEMENT_QUERY = "SELECT * FROM playground where id = ?";
+
+    // for Callable Statement
+    protected static final String PROCEDURE_NAME = "getPlaygroundByName";
+    protected static final String CALLABLE_STATEMENT_QUERY = "{ CALL " + PROCEDURE_NAME + "(?, ?) }";
+    protected static final String CALLABLE_STATEMENT_INPUT_PARAM = "TWO";
+    protected static final int CALLABLE_STATMENT_OUTPUT_PARAM_TYPE = Types.INTEGER;
+
+    private static DB TEST_DATABASE;
+
+    @BeforeClass
+    public static void setUpBeforeClass() throws Exception {
+        TEST_DATABASE = DB.newEmbeddedDB(PORT);
+        TEST_DATABASE.start();
+        TEST_DATABASE.createDB("test");
+        TEST_DATABASE.source("jdbc/mariadb/init.sql");
+    }
+
+    protected final void executeStatement() throws Exception {
+        Connection connection = null;
+        try {
+            connection = DriverManager.getConnection(JDBC_URL, "root", null);
+
+            Statement statement = null;
+            try {
+                statement = connection.createStatement();
+
+                ResultSet result = null;
+                try {
+                    result = statement.executeQuery(STATEMENT_QUERY);
+                    result.first();
+                    assertEquals(3, result.getInt(1));
+                } finally {
+                    closeResultSet(result);
+                }
+            } finally {
+                closeStatement(statement);
+            }
+        } finally {
+            closeConnection(connection);
+        }
+    }
+
+    protected final void executePreparedStatement() throws Exception {
+        Connection connection = null;
+        try {
+            connection = DriverManager.getConnection(JDBC_URL, "root", null);
+
+            PreparedStatement ps = null;
+            try {
+                ps = connection.prepareStatement(PREPARED_STATEMENT_QUERY);
+                ps.setInt(1, 3);
+
+                ResultSet result = null;
+                try {
+                    result = ps.executeQuery();
+                    result.first();
+                    assertEquals("THREE", result.getString(2));
+                } finally {
+                    closeResultSet(result);
+                }
+            } finally {
+                closeStatement(ps);
+            }
+        } finally {
+            closeConnection(connection);
+        }
+    }
+
+    protected final void executeCallableStatement() throws Exception {
+
+        final int expectedTotalCount = 3;
+        final int expectedMatchingCount = 1;
+        final int expectedMatchingId = 2;
+        final String outputParamCountName = "outputParamCount";
+
+        Connection conn = null;
+        try {
+            conn = DriverManager.getConnection(JDBC_URL, "root", null);
+
+            CallableStatement cs = null;
+            try {
+                cs = conn.prepareCall(CALLABLE_STATEMENT_QUERY);
+                cs.setString(1, CALLABLE_STATEMENT_INPUT_PARAM);
+                cs.registerOutParameter(2, CALLABLE_STATMENT_OUTPUT_PARAM_TYPE);
+
+                ResultSet rs = null;
+                try {
+                    rs = cs.executeQuery();
+                    int matchingCount = 0;
+                    while (rs.next()) {
+                        ++matchingCount;
+                        if (matchingCount > expectedMatchingCount) {
+                            fail();
+                        }
+                        assertEquals(expectedMatchingId, rs.getInt(1));
+                        assertEquals(CALLABLE_STATEMENT_INPUT_PARAM, rs.getString(2));
+                    }
+                    assertEquals(expectedMatchingCount, matchingCount);
+                } finally {
+                    closeResultSet(rs);
+                }
+                final int totalCount = cs.getInt(outputParamCountName);
+                assertEquals(expectedTotalCount, totalCount);
+            } finally {
+                closeStatement(cs);
+            }
+        } finally {
+            closeConnection(conn);
+        }
+    }
+
+    private void closeConnection(Connection conn) throws SQLException {
+        if (conn != null) {
+            conn.close();
+        }
+    }
+
+    private void closeResultSet(ResultSet rs) throws SQLException {
+        if (rs != null) {
+            rs.close();
+        }
+    }
+
+    private void closeStatement(Statement statement) throws SQLException {
+        if (statement != null) {
+            statement.close();
+        }
+    }
+
+    @AfterClass
+    public static void tearDownAfterClass() throws Exception {
+        TEST_DATABASE.stop();
+    }
+}
diff --git a/agent/src/test/resources/jdbc/mariadb/init.sql b/agent/src/test/resources/jdbc/mariadb/init.sql
index 975d93386f07..0833e4865210 100644
--- a/agent/src/test/resources/jdbc/mariadb/init.sql
+++ b/agent/src/test/resources/jdbc/mariadb/init.sql
@@ -2,4 +2,13 @@ USE test;
 CREATE TABLE IF NOT EXISTS playground (id int(5) NOT NULL, name varchar(50) DEFAULT NULL);
 INSERT INTO playground VALUES (1, 'ONE');
 INSERT INTO playground VALUES (2, 'TWO');
-INSERT INTO playground VALUES (3, 'THREE');
\ No newline at end of file
+INSERT INTO playground VALUES (3, 'THREE');
+
+DELIMITER //
+CREATE PROCEDURE getPlaygroundByName (IN inputParamName VARCHAR(50), OUT outputParamCount INT)
+    BEGIN
+        SELECT count(*) INTO outputParamCount FROM playground;
+        SELECT id, name FROM playground WHERE name = inputParamName ORDER BY id ASC;
+    END;
+//
+DELIMITER ;
\ No newline at end of file