Skip to content

Commit ad13893

Browse files
authored
Enhance kotlin coroutine plugin for stack tracing (#451)
1 parent 6466b27 commit ad13893

9 files changed

Lines changed: 312 additions & 12 deletions

File tree

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Release Notes.
1212
* Remove Powermock entirely from the test cases.
1313
* Fix H2 instrumentation point
1414
* Refactor pipeline in jedis-plugin.
15+
* Enhance kotlin coroutine plugin for stack tracing.
1516

1617
#### Documentation
1718
* Update docs of Tracing APIs, reorganize the API docs into six parts.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package org.apache.skywalking.apm.plugin.kotlin.coroutine;
20+
21+
import org.apache.skywalking.apm.agent.core.context.ContextManager;
22+
import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan;
23+
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
24+
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor;
25+
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
26+
27+
import java.lang.reflect.Method;
28+
import java.util.HashMap;
29+
import java.util.Map;
30+
31+
public class DispatchedTaskExceptionInterceptor implements InstanceMethodsAroundInterceptor {
32+
33+
@Override
34+
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, MethodInterceptResult result) {
35+
}
36+
37+
@Override
38+
public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Object ret) {
39+
if (!(ret instanceof Throwable)) return ret;
40+
Throwable exception = (Throwable) ret;
41+
42+
if (ContextManager.isActive() && objInst.getSkyWalkingDynamicField() instanceof AbstractSpan) {
43+
AbstractSpan span = (AbstractSpan) objInst.getSkyWalkingDynamicField();
44+
String[] elements = Utils.getCoroutineStackTraceElements(objInst);
45+
if (elements.length > 0) {
46+
Map<String, String> eventMap = new HashMap<>();
47+
eventMap.put("coroutine.stack", String.join("\n", elements));
48+
span.log(System.currentTimeMillis(), eventMap);
49+
}
50+
51+
objInst.setSkyWalkingDynamicField(exception);
52+
span.errorOccurred().log(exception);
53+
}
54+
return ret;
55+
}
56+
57+
@Override
58+
public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Throwable t) {
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package org.apache.skywalking.apm.plugin.kotlin.coroutine;
20+
21+
import org.apache.skywalking.apm.agent.core.context.ContextManager;
22+
import org.apache.skywalking.apm.agent.core.context.ContextSnapshot;
23+
import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan;
24+
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
25+
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor;
26+
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
27+
import org.apache.skywalking.apm.network.trace.component.ComponentsDefine;
28+
29+
import java.lang.reflect.Method;
30+
31+
public class DispatchedTaskRunInterceptor implements InstanceMethodsAroundInterceptor {
32+
33+
@Override
34+
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, MethodInterceptResult result) {
35+
if (objInst.getSkyWalkingDynamicField() instanceof ContextSnapshot) {
36+
ContextSnapshot snapshot = (ContextSnapshot) objInst.getSkyWalkingDynamicField();
37+
38+
if (ContextManager.isActive() && snapshot.isFromCurrent()) {
39+
// Thread not switched, skip restore snapshot.
40+
return;
41+
}
42+
43+
// Create local coroutine span
44+
AbstractSpan span = ContextManager.createLocalSpan(TracingRunnable.COROUTINE);
45+
span.setComponent(ComponentsDefine.KT_COROUTINE);
46+
objInst.setSkyWalkingDynamicField(span);
47+
48+
// Recover with snapshot
49+
ContextManager.continued(snapshot);
50+
}
51+
}
52+
53+
@Override
54+
public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Object ret) {
55+
if (ContextManager.isActive() && objInst.getSkyWalkingDynamicField() instanceof AbstractSpan) {
56+
AbstractSpan span = (AbstractSpan) objInst.getSkyWalkingDynamicField();
57+
if (span != null) {
58+
ContextManager.stopSpan(span);
59+
}
60+
}
61+
return ret;
62+
}
63+
64+
@Override
65+
public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Throwable t) {
66+
if (ContextManager.isActive() && objInst.getSkyWalkingDynamicField() instanceof AbstractSpan) {
67+
AbstractSpan span = (AbstractSpan) objInst.getSkyWalkingDynamicField();
68+
if (span != null) {
69+
ContextManager.stopSpan(span.errorOccurred().log(t));
70+
}
71+
}
72+
}
73+
}

apm-sniffer/optional-plugins/kotlin-coroutine-plugin/src/main/java/org/apache/skywalking/apm/plugin/kotlin/coroutine/DispatcherInterceptor.java

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,39 @@
1818

1919
package org.apache.skywalking.apm.plugin.kotlin.coroutine;
2020

21+
import org.apache.skywalking.apm.agent.core.context.ContextManager;
2122
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
2223
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor;
2324
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
2425

2526
import java.lang.reflect.Method;
2627

2728
public class DispatcherInterceptor implements InstanceMethodsAroundInterceptor {
29+
2830
@Override
29-
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
30-
MethodInterceptResult result) {
31-
// Wrapping runnable with current context snapshot
32-
allArguments[1] = TracingRunnable.wrapOrNot((Runnable) allArguments[1]);
31+
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, MethodInterceptResult result) {
32+
if (!ContextManager.isActive()) {
33+
return;
34+
}
35+
36+
Runnable runnable = (Runnable) allArguments[1];
37+
38+
if (Utils.isDispatchedTask(runnable)) {
39+
// Using instrumentation for DispatchedContinuation
40+
EnhancedInstance enhancedRunnable = (EnhancedInstance) runnable;
41+
enhancedRunnable.setSkyWalkingDynamicField(ContextManager.capture());
42+
} else {
43+
// Wrapping runnable with current context snapshot
44+
allArguments[1] = TracingRunnable.wrapOrNot(runnable);
45+
}
3346
}
3447

3548
@Override
36-
public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
37-
Object ret) {
49+
public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Object ret) {
3850
return ret;
3951
}
4052

4153
@Override
42-
public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments,
43-
Class<?>[] argumentsTypes, Throwable t) {
54+
public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Throwable t) {
4455
}
4556
}

apm-sniffer/optional-plugins/kotlin-coroutine-plugin/src/main/java/org/apache/skywalking/apm/plugin/kotlin/coroutine/TracingRunnable.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
* A class implementation will be cheaper cost than lambda with captured variables implementation.
3131
*/
3232
class TracingRunnable implements Runnable {
33-
private static final String COROUTINE = "/Kotlin/Coroutine";
33+
public static final String COROUTINE = "Kotlin/Coroutine";
3434

3535
private ContextSnapshot snapshot;
3636
private Runnable delegate;
@@ -48,7 +48,7 @@ private TracingRunnable(ContextSnapshot snapshot, Runnable delegate) {
4848
*/
4949
public static Runnable wrapOrNot(Runnable delegate) {
5050
// Just wrap continuation with active trace context
51-
if (ContextManager.isActive()) {
51+
if (ContextManager.isActive() && !(delegate instanceof TracingRunnable)) {
5252
return new TracingRunnable(ContextManager.capture(), delegate);
5353
} else {
5454
return delegate;
@@ -72,6 +72,9 @@ public void run() {
7272

7373
try {
7474
delegate.run();
75+
} catch (Throwable e) {
76+
span.errorOccurred().log(e);
77+
throw e;
7578
} finally {
7679
ContextManager.stopSpan(span);
7780
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package org.apache.skywalking.apm.plugin.kotlin.coroutine;
20+
21+
import kotlin.coroutines.jvm.internal.CoroutineStackFrame;
22+
import org.apache.skywalking.apm.plugin.kotlin.coroutine.define.DispatchedTaskInstrumentation;
23+
24+
import java.util.ArrayList;
25+
26+
public class Utils {
27+
private static Class<?> DISPATCHED_TASK_CLASS = null;
28+
private static Boolean IS_DISPATCHED_TASK_CLASS_LOADED = false;
29+
30+
private static void loadDispatchedTaskClass() {
31+
if (IS_DISPATCHED_TASK_CLASS_LOADED) return;
32+
try {
33+
DISPATCHED_TASK_CLASS = Class.forName(DispatchedTaskInstrumentation.ENHANCE_CLASS);
34+
} catch (ClassNotFoundException ignored) {
35+
} finally {
36+
IS_DISPATCHED_TASK_CLASS_LOADED = true;
37+
}
38+
}
39+
40+
public static boolean isDispatchedTask(Runnable runnable) {
41+
loadDispatchedTaskClass();
42+
if (DISPATCHED_TASK_CLASS == null) return false;
43+
return DISPATCHED_TASK_CLASS.isAssignableFrom(runnable.getClass());
44+
}
45+
46+
public static String[] getCoroutineStackTraceElements(Object runnable) {
47+
if (!(runnable instanceof CoroutineStackFrame)) {
48+
return new String[0];
49+
}
50+
51+
ArrayList<String> elements = new ArrayList<>();
52+
CoroutineStackFrame frame = (CoroutineStackFrame) runnable;
53+
while (frame != null) {
54+
StackTraceElement element = frame.getStackTraceElement();
55+
frame = frame.getCallerFrame();
56+
57+
if (element != null) {
58+
elements.add(element.toString());
59+
} else {
60+
elements.add("Unknown Source");
61+
}
62+
}
63+
return elements.toArray(new String[0]);
64+
}
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package org.apache.skywalking.apm.plugin.kotlin.coroutine.define;
20+
21+
import net.bytebuddy.description.method.MethodDescription;
22+
import net.bytebuddy.matcher.ElementMatcher;
23+
import org.apache.skywalking.apm.agent.core.plugin.interceptor.ConstructorInterceptPoint;
24+
import org.apache.skywalking.apm.agent.core.plugin.interceptor.InstanceMethodsInterceptPoint;
25+
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.ClassInstanceMethodsEnhancePluginDefine;
26+
import org.apache.skywalking.apm.agent.core.plugin.match.ClassMatch;
27+
28+
import static net.bytebuddy.matcher.ElementMatchers.named;
29+
import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments;
30+
import static org.apache.skywalking.apm.agent.core.plugin.match.NameMatch.byName;
31+
32+
public class DispatchedTaskInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
33+
public static final String ENHANCE_CLASS = "kotlinx.coroutines.DispatchedTask";
34+
public static final String RUN_INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.kotlin.coroutine.DispatchedTaskRunInterceptor";
35+
public static final String ENHANCE_METHOD_RUN = "run";
36+
public static final String EXCEPTION_INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.kotlin.coroutine.DispatchedTaskExceptionInterceptor";
37+
public static final String ENHANCE_METHOD_GET_EXCEPTIONAL_RESULT = "getExceptionalResult$kotlinx_coroutines_core";
38+
39+
@Override
40+
protected ClassMatch enhanceClass() {
41+
return byName(ENHANCE_CLASS);
42+
}
43+
44+
@Override
45+
public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
46+
return new ConstructorInterceptPoint[0];
47+
}
48+
49+
@Override
50+
public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
51+
return new InstanceMethodsInterceptPoint[]{
52+
new InstanceMethodsInterceptPoint() {
53+
@Override
54+
public ElementMatcher<MethodDescription> getMethodsMatcher() {
55+
return named(ENHANCE_METHOD_RUN).and(takesNoArguments());
56+
}
57+
58+
@Override
59+
public String getMethodsInterceptor() {
60+
return RUN_INTERCEPTOR_CLASS;
61+
}
62+
63+
@Override
64+
public boolean isOverrideArgs() {
65+
return true;
66+
}
67+
},
68+
new InstanceMethodsInterceptPoint() {
69+
@Override
70+
public ElementMatcher<MethodDescription> getMethodsMatcher() {
71+
return named(ENHANCE_METHOD_GET_EXCEPTIONAL_RESULT);
72+
}
73+
74+
@Override
75+
public String getMethodsInterceptor() {
76+
return EXCEPTION_INTERCEPTOR_CLASS;
77+
}
78+
79+
@Override
80+
public boolean isOverrideArgs() {
81+
return false;
82+
}
83+
}
84+
};
85+
}
86+
}

apm-sniffer/optional-plugins/kotlin-coroutine-plugin/src/main/resources/skywalking-plugin.def

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616

17-
kotlin-coroutine=org.apache.skywalking.apm.plugin.kotlin.coroutine.define.DispatcherInstrumentation
17+
kotlin-coroutine=org.apache.skywalking.apm.plugin.kotlin.coroutine.define.DispatcherInstrumentation
18+
kotlin-coroutine=org.apache.skywalking.apm.plugin.kotlin.coroutine.define.DispatchedTaskInstrumentation

test/plugin/scenarios/kotlin-coroutine-scenario/config/expectedData.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ segmentItems:
4949
- {key: db.instance, value: demo}
5050
- {key: db.statement, value: ''}
5151
skipAnalysis: 'false'
52-
- operationName: /Kotlin/Coroutine
52+
- operationName: Kotlin/Coroutine
5353
parentSpanId: -1
5454
spanId: 0
5555
startTime: nq 0

0 commit comments

Comments
 (0)