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