diff --git a/agent/src/main/resources/profiles/local/pinpoint.config b/agent/src/main/resources/profiles/local/pinpoint.config
index b09bce41724ec..42f94b78ae82c 100644
--- a/agent/src/main/resources/profiles/local/pinpoint.config
+++ b/agent/src/main/resources/profiles/local/pinpoint.config
@@ -1247,4 +1247,14 @@ profiler.rocketmq.consumer.enable=false
# Comma separated list of package names
# eg) profiler.rocketmq.basePackage=com.company
# eg) profiler.rocketmq.basePackage=com.company.shopping.cart, com.company.payment
-profiler.rocketmq.basePackage=
\ No newline at end of file
+profiler.rocketmq.basePackage=
+
+###########################################################
+# Kotlin Corutines
+# v1.0.1 ~
+###########################################################
+profiler.kotlin.coroutines.enable=false
+# Only perfect string matching is supported. (If you do not include any value, it will not be tracked.)
+# Comma separated list of coroutines name
+# eg) profiler.kotlin.coroutines.name.include=CoroutineMyJob1,CoroutineMyJob2
+profiler.kotlin.coroutines.name.include=
\ No newline at end of file
diff --git a/agent/src/main/resources/profiles/release/pinpoint.config b/agent/src/main/resources/profiles/release/pinpoint.config
index 3e7522568b4c4..5d36cf55170dd 100644
--- a/agent/src/main/resources/profiles/release/pinpoint.config
+++ b/agent/src/main/resources/profiles/release/pinpoint.config
@@ -1270,4 +1270,14 @@ profiler.rocketmq.consumer.enable=false
# Comma separated list of package names
# eg) profiler.rocketmq.basePackage=com.company
# eg) profiler.rocketmq.basePackage=com.company.shopping.cart, com.company.payment
-profiler.rocketmq.basePackage=
\ No newline at end of file
+profiler.rocketmq.basePackage=
+
+###########################################################
+# Kotlin Corutines
+# v1.0.1 ~
+###########################################################
+profiler.kotlin.coroutines.enable=false
+# Only perfect string matching is supported. (If you do not include any value, it will not be tracked.)
+# Comma separated list of coroutines name
+# eg) profiler.kotlin.coroutines.name.include=CoroutineMyJob1,CoroutineMyJob2
+profiler.kotlin.coroutines.name.include=
\ No newline at end of file
diff --git a/plugins/assembly/pom.xml b/plugins/assembly/pom.xml
index d6275130cfb79..17f0169951cff 100644
--- a/plugins/assembly/pom.xml
+++ b/plugins/assembly/pom.xml
@@ -390,6 +390,14 @@
pinpoint-rocketmq-plugin
${project.version}
+
+
+ com.navercorp.pinpoint
+ pinpoint-kotlin-coroutines-plugin
+ ${project.version}
+
+
+
\ No newline at end of file
diff --git a/plugins/kotlin-coroutines/pom.xml b/plugins/kotlin-coroutines/pom.xml
new file mode 100644
index 0000000000000..bc4e3a8a8adf6
--- /dev/null
+++ b/plugins/kotlin-coroutines/pom.xml
@@ -0,0 +1,42 @@
+
+
+ 4.0.0
+
+ com.navercorp.pinpoint
+ pinpoint-plugins
+ 2.4.0-SNAPSHOT
+
+
+ pinpoint-kotlin-coroutines-plugin
+ pinpoint-kotlin-coroutines-plugin
+ jar
+
+
+ 1.8
+ ${env.JAVA_8_HOME}
+ java18
+
+
+
+
+ com.navercorp.pinpoint
+ pinpoint-bootstrap-core
+ provided
+
+
+
+ com.navercorp.pinpoint
+ pinpoint-annotations
+ provided
+
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-core
+ 1.5.2
+ provided
+
+
+
+
diff --git a/plugins/kotlin-coroutines/src/main/java/com/navercorp/pinpoint/plugin/kotlinx/coroutines/CoroutinesConfig.java b/plugins/kotlin-coroutines/src/main/java/com/navercorp/pinpoint/plugin/kotlinx/coroutines/CoroutinesConfig.java
new file mode 100644
index 0000000000000..b203a479fe577
--- /dev/null
+++ b/plugins/kotlin-coroutines/src/main/java/com/navercorp/pinpoint/plugin/kotlinx/coroutines/CoroutinesConfig.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2021 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.kotlinx.coroutines;
+
+import com.navercorp.pinpoint.bootstrap.config.ProfilerConfig;
+import com.navercorp.pinpoint.common.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Taejin Koo
+ */
+public class CoroutinesConfig {
+
+ private final boolean traceCoroutines;
+ private final List nameIncludeList;
+
+ public CoroutinesConfig(ProfilerConfig config) {
+ this.traceCoroutines = config.readBoolean("profiler.kotlin.coroutines.enable", false);
+ String nameIncludes = config.readString("profiler.kotlin.coroutines.name.include", "");
+
+ if (StringUtils.hasLength(nameIncludes)) {
+ String[] nameIncludeArray = nameIncludes.split(",");
+ List result = new ArrayList<>(nameIncludeArray.length);
+ for (String nameInclude : nameIncludeArray) {
+ if (StringUtils.hasLength(nameInclude)) {
+ result.add(nameInclude);
+ }
+ }
+ nameIncludeList = Collections.unmodifiableList(result);
+ } else {
+ nameIncludeList = Collections.emptyList();
+ }
+ }
+
+ public boolean isTraceCoroutines() {
+ return traceCoroutines;
+ }
+
+ public List getIncludedNameList() {
+ return nameIncludeList;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("CoroutinesConfig{");
+ sb.append("traceCoroutines=").append(traceCoroutines);
+ sb.append(", nameIncludeList=").append(nameIncludeList);
+ sb.append('}');
+ return sb.toString();
+ }
+}
diff --git a/plugins/kotlin-coroutines/src/main/java/com/navercorp/pinpoint/plugin/kotlinx/coroutines/CoroutinesConstants.java b/plugins/kotlin-coroutines/src/main/java/com/navercorp/pinpoint/plugin/kotlinx/coroutines/CoroutinesConstants.java
new file mode 100644
index 0000000000000..7125393e7ea58
--- /dev/null
+++ b/plugins/kotlin-coroutines/src/main/java/com/navercorp/pinpoint/plugin/kotlinx/coroutines/CoroutinesConstants.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2021 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.kotlinx.coroutines;
+
+import com.navercorp.pinpoint.common.trace.ServiceType;
+import com.navercorp.pinpoint.common.trace.ServiceTypeFactory;
+
+/**
+ * @author Taejin Koo
+ */
+public final class CoroutinesConstants {
+
+ private CoroutinesConstants() {
+ }
+
+ public static final ServiceType SERVICE_TYPE = ServiceTypeFactory.of(8901, "KT_COROUTINES");
+
+ public static final String SCOPE = "KT_COROUTINES_SCOPE";
+
+}
diff --git a/plugins/kotlin-coroutines/src/main/java/com/navercorp/pinpoint/plugin/kotlinx/coroutines/CoroutinesMetadataProvider.java b/plugins/kotlin-coroutines/src/main/java/com/navercorp/pinpoint/plugin/kotlinx/coroutines/CoroutinesMetadataProvider.java
new file mode 100644
index 0000000000000..03fdd7ef032d4
--- /dev/null
+++ b/plugins/kotlin-coroutines/src/main/java/com/navercorp/pinpoint/plugin/kotlinx/coroutines/CoroutinesMetadataProvider.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2021 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.kotlinx.coroutines;
+
+import com.navercorp.pinpoint.common.trace.TraceMetadataProvider;
+import com.navercorp.pinpoint.common.trace.TraceMetadataSetupContext;
+
+/**
+ * @author Taejin Koo
+ */
+public class CoroutinesMetadataProvider implements TraceMetadataProvider {
+
+ @Override
+ public void setup(TraceMetadataSetupContext context) {
+ context.addServiceType(CoroutinesConstants.SERVICE_TYPE);
+ }
+
+}
diff --git a/plugins/kotlin-coroutines/src/main/java/com/navercorp/pinpoint/plugin/kotlinx/coroutines/CoroutinesPlugin.java b/plugins/kotlin-coroutines/src/main/java/com/navercorp/pinpoint/plugin/kotlinx/coroutines/CoroutinesPlugin.java
new file mode 100644
index 0000000000000..61256e7b65d21
--- /dev/null
+++ b/plugins/kotlin-coroutines/src/main/java/com/navercorp/pinpoint/plugin/kotlinx/coroutines/CoroutinesPlugin.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2021 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.kotlinx.coroutines;
+
+import com.navercorp.pinpoint.bootstrap.async.AsyncContextAccessor;
+import com.navercorp.pinpoint.bootstrap.instrument.InstrumentClass;
+import com.navercorp.pinpoint.bootstrap.instrument.InstrumentException;
+import com.navercorp.pinpoint.bootstrap.instrument.InstrumentMethod;
+import com.navercorp.pinpoint.bootstrap.instrument.Instrumentor;
+import com.navercorp.pinpoint.bootstrap.instrument.matcher.Matcher;
+import com.navercorp.pinpoint.bootstrap.instrument.matcher.Matchers;
+import com.navercorp.pinpoint.bootstrap.instrument.matcher.operand.SuperClassInternalNameMatcherOperand;
+import com.navercorp.pinpoint.bootstrap.instrument.transformer.MatchableTransformTemplate;
+import com.navercorp.pinpoint.bootstrap.instrument.transformer.MatchableTransformTemplateAware;
+import com.navercorp.pinpoint.bootstrap.instrument.transformer.TransformCallback;
+import com.navercorp.pinpoint.bootstrap.logging.PLogger;
+import com.navercorp.pinpoint.bootstrap.logging.PLoggerFactory;
+import com.navercorp.pinpoint.bootstrap.plugin.ProfilerPlugin;
+import com.navercorp.pinpoint.bootstrap.plugin.ProfilerPluginSetupContext;
+import com.navercorp.pinpoint.common.util.ArrayUtils;
+import com.navercorp.pinpoint.plugin.kotlinx.coroutines.interceptor.CopyAsyncContextInterceptor;
+import com.navercorp.pinpoint.plugin.kotlinx.coroutines.interceptor.DispatchInterceptor;
+import com.navercorp.pinpoint.plugin.kotlinx.coroutines.interceptor.ExecuteTaskInterceptor;
+
+import java.security.ProtectionDomain;
+import java.util.List;
+
+import static com.navercorp.pinpoint.common.util.VarArgs.va;
+
+/**
+ * @author Taejin Koo
+ */
+public class CoroutinesPlugin implements ProfilerPlugin, MatchableTransformTemplateAware {
+ private final PLogger logger = PLoggerFactory.getLogger(this.getClass());
+
+ private MatchableTransformTemplate transformTemplate;
+
+ @Override
+ public void setup(ProfilerPluginSetupContext context) {
+ final CoroutinesConfig config = new CoroutinesConfig(context.getConfig());
+
+ final String simpleClazzName = this.getClass().getSimpleName();
+
+ if (!config.isTraceCoroutines()) {
+ logger.info("{} disabled", simpleClazzName);
+ return;
+ }
+ if (config.getIncludedNameList().isEmpty()) {
+ logger.info("{} could not find any included name.", simpleClazzName);
+ return;
+ }
+
+ logger.info("{} config:{}", simpleClazzName, config);
+
+ /**
+ * 1. Starts coroutine task
+ * 2. Creates DispatchedContinuation
+ * 3. Dispatches DispatchedContinuation
+ * L addCoroutineDispatcherTransformer()
+ * 4. Creates CancellableContinuation based on DispatchedContinuation
+ * L propagateAsyncContextTransformer
+ * 5. Dispatches CancellableContinuation
+ * 6. Executes task(actual implementation)
+ * L addExecuteTaskTransformer
+ */
+ addCoroutineDispatcherTransformer();
+ propagateAsyncContextTransformer();
+ addExecuteTaskTransformer();
+ }
+
+ private void addCoroutineDispatcherTransformer() {
+ Matcher dispatcherMatcher = Matchers.newPackageBasedMatcher("kotlinx.coroutines",
+ new SuperClassInternalNameMatcherOperand("kotlinx.coroutines.CoroutineDispatcher", true));
+ transformTemplate.transform(dispatcherMatcher, CoroutineDispatcherTransform.class);
+ }
+
+ private void propagateAsyncContextTransformer() {
+ // For adding AsyncContextAccessor
+ // > 1.4.0
+ transformTemplate.transform("kotlinx.coroutines.internal.DispatchedContinuation",
+ DispatchedContinuationTransform.class);
+ // < 1.4.0
+ transformTemplate.transform("kotlinx.coroutines.DispatchedContinuation",
+ DispatchedContinuationTransform.class);
+
+
+ // For adding AsyncContextAccessor to CancellableContinuation and propagation AsyncContext from DispatchedTask
+ transformTemplate.transform("kotlinx.coroutines.CancellableContinuationImpl",
+ CancellableContinuationTransform.class);
+ }
+
+ private void addExecuteTaskTransformer() {
+ // If below tracing makes problems, you could consider tracing the executeTask of CoroutineScheduler$Worker.
+ transformTemplate.transform("kotlinx.coroutines.scheduling.CoroutineScheduler", WorkerTransform.class);
+ }
+
+ public static class CoroutineDispatcherTransform implements TransformCallback {
+
+ @Override
+ public byte[] doInTransform(Instrumentor instrumentor, ClassLoader classLoader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws InstrumentException {
+ final CoroutinesConfig config = new CoroutinesConfig(instrumentor.getProfilerConfig());
+
+
+ InstrumentClass target = instrumentor.getInstrumentClass(classLoader, className, classfileBuffer);
+
+ InstrumentMethod dispatchMethod = target.getDeclaredMethod("dispatch", "kotlin.coroutines.CoroutineContext", "java.lang.Runnable");
+ if (dispatchMethod != null) {
+ dispatchMethod.addScopedInterceptor(DispatchInterceptor.class, va(config), CoroutinesConstants.SCOPE);
+ }
+
+ return target.toBytecode();
+ }
+
+ }
+
+ public static class DispatchedContinuationTransform implements TransformCallback {
+
+ @Override
+ public byte[] doInTransform(Instrumentor instrumentor, ClassLoader classLoader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws InstrumentException {
+ InstrumentClass target = instrumentor.getInstrumentClass(classLoader, className, classfileBuffer);
+ target.addField(AsyncContextAccessor.class);
+
+ return target.toBytecode();
+ }
+
+ }
+
+ public static class WorkerTransform implements TransformCallback {
+
+ @Override
+ public byte[] doInTransform(Instrumentor instrumentor, ClassLoader classLoader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws InstrumentException {
+ InstrumentClass target = instrumentor.getInstrumentClass(classLoader, className, classfileBuffer);
+ InstrumentMethod runSafelyMethod = target.getDeclaredMethod("runSafely", "kotlinx.coroutines.scheduling.Task");
+ if (runSafelyMethod != null) {
+ runSafelyMethod.addInterceptor(ExecuteTaskInterceptor.class);
+ }
+
+ return target.toBytecode();
+ }
+ }
+
+ public static class CancellableContinuationTransform implements TransformCallback {
+ @Override
+ public byte[] doInTransform(Instrumentor instrumentor, ClassLoader classLoader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws InstrumentException {
+ InstrumentClass target = instrumentor.getInstrumentClass(classLoader, className, classfileBuffer);
+ target.addField(AsyncContextAccessor.class);
+
+ List declaredConstructors = target.getDeclaredConstructors();
+ for (InstrumentMethod declaredConstructor : declaredConstructors) {
+ String[] parameterTypes = declaredConstructor.getParameterTypes();
+ if (ArrayUtils.hasLength(parameterTypes) && "kotlin.coroutines.Continuation".equals(parameterTypes[0])) {
+ declaredConstructor.addInterceptor(CopyAsyncContextInterceptor.class);
+ }
+ }
+
+ return target.toBytecode();
+ }
+ }
+
+ @Override
+ public void setTransformTemplate(MatchableTransformTemplate transformTemplate) {
+ this.transformTemplate = transformTemplate;
+ }
+
+}
diff --git a/plugins/kotlin-coroutines/src/main/java/com/navercorp/pinpoint/plugin/kotlinx/coroutines/interceptor/CopyAsyncContextInterceptor.java b/plugins/kotlin-coroutines/src/main/java/com/navercorp/pinpoint/plugin/kotlinx/coroutines/interceptor/CopyAsyncContextInterceptor.java
new file mode 100644
index 0000000000000..894c821536f9d
--- /dev/null
+++ b/plugins/kotlin-coroutines/src/main/java/com/navercorp/pinpoint/plugin/kotlinx/coroutines/interceptor/CopyAsyncContextInterceptor.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2021 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.kotlinx.coroutines.interceptor;
+
+import com.navercorp.pinpoint.bootstrap.async.AsyncContextAccessor;
+import com.navercorp.pinpoint.bootstrap.context.AsyncContext;
+import com.navercorp.pinpoint.bootstrap.interceptor.AroundInterceptor;
+import com.navercorp.pinpoint.bootstrap.logging.PLogger;
+import com.navercorp.pinpoint.bootstrap.logging.PLoggerFactory;
+import com.navercorp.pinpoint.common.util.ArrayUtils;
+
+/**
+ * @author Taejin Koo
+ */
+public class CopyAsyncContextInterceptor implements AroundInterceptor {
+
+ private final PLogger logger = PLoggerFactory.getLogger(CopyAsyncContextInterceptor.class);
+ private final boolean isDebug = logger.isDebugEnabled();
+
+ @Override
+ public void before(Object target, Object[] args) {
+ if (isDebug) {
+ logger.beforeInterceptor(target, args);
+ }
+ }
+
+ @Override
+ public void after(Object target, Object[] args, Object result, Throwable throwable) {
+ if (isDebug) {
+ logger.afterInterceptor(target, args, result, throwable);
+ }
+
+ if (ArrayUtils.isEmpty(args)) {
+ return;
+ }
+
+ AsyncContext originalAsyncContext = getDelegateAsyncContext(args[0]);
+ if (originalAsyncContext == null) {
+ logger.warn("Could not find delegate's AsyncContext");
+ return;
+ }
+
+ setAsyncContext(target, originalAsyncContext);
+ }
+
+ private AsyncContext getDelegateAsyncContext(Object asyncContextAccessor) {
+ if (asyncContextAccessor instanceof AsyncContextAccessor) {
+ return ((AsyncContextAccessor) asyncContextAccessor)._$PINPOINT$_getAsyncContext();
+ }
+ return null;
+ }
+
+ private void setAsyncContext(Object target, AsyncContext asyncContext) {
+ if (target instanceof AsyncContextAccessor) {
+ AsyncContext hasValue = ((AsyncContextAccessor) target)._$PINPOINT$_getAsyncContext();
+ if (hasValue == null) {
+ ((AsyncContextAccessor) target)._$PINPOINT$_setAsyncContext(asyncContext);
+ } else {
+ logger.warn("Target already has AsyncContext.");
+ }
+ }
+ }
+
+
+}
diff --git a/plugins/kotlin-coroutines/src/main/java/com/navercorp/pinpoint/plugin/kotlinx/coroutines/interceptor/DispatchInterceptor.java b/plugins/kotlin-coroutines/src/main/java/com/navercorp/pinpoint/plugin/kotlinx/coroutines/interceptor/DispatchInterceptor.java
new file mode 100644
index 0000000000000..8ebf0f043f1c8
--- /dev/null
+++ b/plugins/kotlin-coroutines/src/main/java/com/navercorp/pinpoint/plugin/kotlinx/coroutines/interceptor/DispatchInterceptor.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2021 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.kotlinx.coroutines.interceptor;
+
+import com.navercorp.pinpoint.bootstrap.async.AsyncContextAccessor;
+import com.navercorp.pinpoint.bootstrap.context.AsyncContext;
+import com.navercorp.pinpoint.bootstrap.context.MethodDescriptor;
+import com.navercorp.pinpoint.bootstrap.context.SpanEventRecorder;
+import com.navercorp.pinpoint.bootstrap.context.Trace;
+import com.navercorp.pinpoint.bootstrap.context.TraceContext;
+import com.navercorp.pinpoint.bootstrap.interceptor.AroundInterceptor;
+import com.navercorp.pinpoint.bootstrap.logging.PLogger;
+import com.navercorp.pinpoint.bootstrap.logging.PLoggerFactory;
+import com.navercorp.pinpoint.common.util.ArrayUtils;
+import com.navercorp.pinpoint.plugin.kotlinx.coroutines.CoroutinesConfig;
+import com.navercorp.pinpoint.plugin.kotlinx.coroutines.CoroutinesConstants;
+
+import kotlin.coroutines.Continuation;
+import kotlin.coroutines.CoroutineContext;
+import kotlinx.coroutines.CancellableContinuation;
+import kotlinx.coroutines.CoroutineName;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @author Taejin Koo
+ */
+public class DispatchInterceptor implements AroundInterceptor {
+
+ private final PLogger logger = PLoggerFactory.getLogger(this.getClass());
+ private final boolean isDebug = logger.isDebugEnabled();
+
+ private final TraceContext traceContext;
+ private final MethodDescriptor descriptor;
+ private final List includedNameList;
+
+ public DispatchInterceptor(TraceContext traceContext, MethodDescriptor descriptor, CoroutinesConfig config) {
+ this.traceContext = Objects.requireNonNull(traceContext, "traceContext");
+ this.descriptor = Objects.requireNonNull(descriptor, "descriptor");
+
+ Objects.requireNonNull(config, "config");
+ this.includedNameList = config.getIncludedNameList();
+ }
+
+ @Override
+ public void before(Object target, Object[] args) {
+ if (isDebug) {
+ logger.beforeInterceptor(target, args);
+ }
+
+ final Continuation continuation = getContinuation(args);
+ if (continuation == null) {
+ return;
+ }
+
+ if (isCompletedContinuation(continuation)) {
+ return;
+ }
+
+ if (!checkSupportCoroutinesName(continuation)) {
+ return;
+ }
+
+ final Trace trace = traceContext.currentTraceObject();
+ if (trace == null) {
+ return;
+ }
+
+ final SpanEventRecorder recorder = trace.traceBlockBegin();
+
+ final AsyncContextAccessor asyncContextAccessor = getAsyncContextAccessor(args);
+ if (asyncContextAccessor != null) {
+ // make asynchronous trace-id
+ final AsyncContext asyncContext = recorder.recordNextAsyncContext();
+ asyncContextAccessor._$PINPOINT$_setAsyncContext(asyncContext);
+ }
+ }
+
+ private Continuation getContinuation(final Object[] args) {
+ if (ArrayUtils.getLength(args) == 2 && args[1] instanceof Continuation) {
+ return (Continuation) args[1];
+ }
+ return null;
+ }
+
+ private boolean isCompletedContinuation(final Continuation continuation) {
+ if (continuation instanceof CancellableContinuation) {
+ return ((CancellableContinuation) continuation).isCompleted();
+ }
+ return false;
+ }
+
+ private boolean checkSupportCoroutinesName(final Continuation continuation) {
+ final CoroutineContext.Key key = CoroutineName.Key;
+
+ final CoroutineContext context = continuation.getContext();
+ if (context != null) {
+ Object element = context.get(key);
+ if (element instanceof CoroutineName) {
+ String name = ((CoroutineName) element).getName();
+ if (name != null) {
+ return includedNameList.contains(name);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private AsyncContextAccessor getAsyncContextAccessor(final Object[] args) {
+ if (ArrayUtils.getLength(args) != 2) {
+ if (isDebug) {
+ logger.debug("Invalid args object. args={}.", args);
+ }
+ return null;
+ }
+
+ if (args[1] instanceof AsyncContextAccessor) {
+ return (AsyncContextAccessor) args[1];
+ }
+
+ if (isDebug) {
+ logger.debug("Invalid args[1] object. Need metadata accessor({}).", AsyncContextAccessor.class.getName());
+ }
+
+ return null;
+ }
+
+ @Override
+ public void after(Object target, Object[] args, Object result, Throwable throwable) {
+ if (isDebug) {
+ logger.afterInterceptor(target, args, result, throwable);
+ }
+
+ final Continuation continuation = getContinuation(args);
+ if (continuation == null) {
+ return;
+ }
+
+ if (isCompletedContinuation(continuation)) {
+ return;
+ }
+
+ if (!checkSupportCoroutinesName(continuation)) {
+ return;
+ }
+
+ final Trace trace = traceContext.currentTraceObject();
+ if (trace == null) {
+ return;
+ }
+
+ try {
+ final SpanEventRecorder recorder = trace.currentSpanEventRecorder();
+ recorder.recordApi(descriptor);
+ recorder.recordServiceType(CoroutinesConstants.SERVICE_TYPE);
+ recorder.recordException(throwable);
+ } finally {
+ trace.traceBlockEnd();
+ }
+ }
+
+}
diff --git a/plugins/kotlin-coroutines/src/main/java/com/navercorp/pinpoint/plugin/kotlinx/coroutines/interceptor/ExecuteTaskInterceptor.java b/plugins/kotlin-coroutines/src/main/java/com/navercorp/pinpoint/plugin/kotlinx/coroutines/interceptor/ExecuteTaskInterceptor.java
new file mode 100644
index 0000000000000..f626c83b0475f
--- /dev/null
+++ b/plugins/kotlin-coroutines/src/main/java/com/navercorp/pinpoint/plugin/kotlinx/coroutines/interceptor/ExecuteTaskInterceptor.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2021 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.kotlinx.coroutines.interceptor;
+
+import com.navercorp.pinpoint.bootstrap.async.AsyncContextAccessor;
+import com.navercorp.pinpoint.bootstrap.context.AsyncContext;
+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.AsyncContextSpanEventSimpleAroundInterceptor;
+import com.navercorp.pinpoint.common.util.ArrayUtils;
+import com.navercorp.pinpoint.plugin.kotlinx.coroutines.CoroutinesConstants;
+
+/**
+ * @author Taejin Koo
+ */
+public class ExecuteTaskInterceptor extends AsyncContextSpanEventSimpleAroundInterceptor {
+
+ public ExecuteTaskInterceptor(TraceContext traceContext, MethodDescriptor methodDescriptor) {
+ super(traceContext, methodDescriptor);
+ }
+
+ @Override
+ protected void doInBeforeTrace(SpanEventRecorder recorder, AsyncContext asyncContext, Object target, Object[] args) {
+ }
+
+ @Override
+ protected AsyncContext getAsyncContext(Object target, Object[] args) {
+ if (ArrayUtils.getLength(args) != 1) {
+ return null;
+ }
+
+ Object asyncContextAccessor = args[0];
+ if (asyncContextAccessor instanceof AsyncContextAccessor) {
+ AsyncContext asyncContext = ((AsyncContextAccessor) asyncContextAccessor)._$PINPOINT$_getAsyncContext();
+ return asyncContext;
+ }
+
+ return null;
+ }
+
+ @Override
+ protected AsyncContext getAsyncContext(Object target, Object[] args, Object result, Throwable throwable) {
+ return getAsyncContext(target, args);
+ }
+
+ @Override
+ protected void doInAfterTrace(SpanEventRecorder recorder, Object target, Object[] args, Object result, Throwable throwable) {
+ recorder.recordApi(methodDescriptor);
+ recorder.recordServiceType(CoroutinesConstants.SERVICE_TYPE);
+ recorder.recordException(throwable);
+ }
+
+}
diff --git a/plugins/kotlin-coroutines/src/main/resources/META-INF/services/com.navercorp.pinpoint.bootstrap.plugin.ProfilerPlugin b/plugins/kotlin-coroutines/src/main/resources/META-INF/services/com.navercorp.pinpoint.bootstrap.plugin.ProfilerPlugin
new file mode 100644
index 0000000000000..8eb6c65fc3540
--- /dev/null
+++ b/plugins/kotlin-coroutines/src/main/resources/META-INF/services/com.navercorp.pinpoint.bootstrap.plugin.ProfilerPlugin
@@ -0,0 +1 @@
+com.navercorp.pinpoint.plugin.kotlinx.coroutines.CoroutinesPlugin
\ No newline at end of file
diff --git a/plugins/kotlin-coroutines/src/main/resources/META-INF/services/com.navercorp.pinpoint.common.trace.TraceMetadataProvider b/plugins/kotlin-coroutines/src/main/resources/META-INF/services/com.navercorp.pinpoint.common.trace.TraceMetadataProvider
new file mode 100644
index 0000000000000..52d82510a8b77
--- /dev/null
+++ b/plugins/kotlin-coroutines/src/main/resources/META-INF/services/com.navercorp.pinpoint.common.trace.TraceMetadataProvider
@@ -0,0 +1 @@
+com.navercorp.pinpoint.plugin.kotlinx.coroutines.CoroutinesMetadataProvider
\ No newline at end of file
diff --git a/plugins/pom.xml b/plugins/pom.xml
index 57128503ead60..38ac26a55e721 100644
--- a/plugins/pom.xml
+++ b/plugins/pom.xml
@@ -65,6 +65,7 @@
grpc
jetty
websphere
+ kotlin-coroutines
spring
spring-boot
spring-webflux