Skip to content

Commit 5750c33

Browse files
amarzialidevflow.devflow-routing-intake
andauthored
Add tracing support for native method calls via Java FFM API (#10718)
wip wip2 Trace native methods calls using FFM APIs Add more tests try to add linux tests cleanup Add config stuff Add measured methods Improve the implementation refactorise some code leftover Narrow allowed packages to reduce startup time Restrict allowed packages to jdk.internal.loader.NativeLibraries better trie Better instrumentation name Merge branch 'master' into andrea.marziali/ffm-instrument Merge branch 'master' into andrea.marziali/ffm-instrument Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
1 parent b7fb6fc commit 5750c33

File tree

20 files changed

+859
-2
lines changed

20 files changed

+859
-2
lines changed
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
package datadog.trace.bootstrap.instrumentation.ffm;
2+
3+
import datadog.context.ContextScope;
4+
import datadog.trace.api.InstrumenterConfig;
5+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
6+
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
7+
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
8+
import datadog.trace.bootstrap.instrumentation.decorator.BaseDecorator;
9+
import java.lang.invoke.MethodHandle;
10+
import java.lang.invoke.MethodHandles;
11+
import java.lang.invoke.MethodType;
12+
import java.util.Set;
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
15+
16+
public final class FFMNativeMethodDecorator extends BaseDecorator {
17+
private static final Logger LOGGER = LoggerFactory.getLogger(FFMNativeMethodDecorator.class);
18+
private static final CharSequence TRACE_FFM = UTF8BytesString.create("trace-ffm");
19+
private static final CharSequence OPERATION_NAME = UTF8BytesString.create("trace.native");
20+
21+
private static final MethodHandle START_SPAN_MH =
22+
safeFindStatic(
23+
"startSpan",
24+
MethodType.methodType(ContextScope.class, CharSequence.class, boolean.class));
25+
private static final MethodHandle END_SPAN_MH =
26+
safeFindStatic(
27+
"endSpan",
28+
MethodType.methodType(Object.class, Throwable.class, ContextScope.class, Object.class));
29+
30+
public static final FFMNativeMethodDecorator DECORATE = new FFMNativeMethodDecorator();
31+
32+
private static MethodHandle safeFindStatic(String name, MethodType methodType) {
33+
try {
34+
return MethodHandles.lookup().findStatic(FFMNativeMethodDecorator.class, name, methodType);
35+
} catch (Throwable t) {
36+
LOGGER.debug("Cannot find method {} in NativeMethodHandleWrapper", name, t);
37+
return null;
38+
}
39+
}
40+
41+
public static MethodHandle wrap(
42+
final MethodHandle original, final String libraryName, final String methodName) {
43+
if (START_SPAN_MH == null || END_SPAN_MH == null) {
44+
return original;
45+
}
46+
try {
47+
MethodType originalType = original.type();
48+
boolean isVoid = originalType.returnType() == void.class;
49+
50+
// We need the ContextScope to be visible to the finally block.
51+
// Easiest way is to artificially prepend it to the target signature.
52+
// The added parameter is ignored by the original handle.
53+
// originalWithScope: (ContextScope, args...) -> R
54+
MethodHandle originalWithScope = MethodHandles.dropArguments(original, 0, ContextScope.class);
55+
56+
/*
57+
* Build the cleanup handle used by MethodHandles.tryFinally.
58+
*
59+
* tryFinally has a strict calling convention:
60+
* - void target -> cleanup(Throwable, ContextScope, args...)
61+
* - non-void -> cleanup(Throwable, R, ContextScope, args...)
62+
*
63+
* END_SPAN_MH is (Throwable, ContextScope, Object) -> Object,
64+
* so we need to reshape it to match what tryFinally expects.
65+
*/
66+
MethodHandle cleanup;
67+
68+
if (isVoid) {
69+
// No return value: bind `null` as the result argument.
70+
MethodHandle endWithNull = MethodHandles.insertArguments(END_SPAN_MH, 2, (Object) null);
71+
72+
// Make it accept the original arguments even though they are unused.
73+
MethodHandle endDropped =
74+
MethodHandles.dropArguments(endWithNull, 2, originalType.parameterList());
75+
76+
// tryFinally requires void return for void targets.
77+
cleanup = endDropped.asType(endDropped.type().changeReturnType(void.class));
78+
79+
} else {
80+
/*
81+
* Non-void case:
82+
* tryFinally will call cleanup as:
83+
* (Throwable, returnValue, ContextScope, args...)
84+
*
85+
* END_SPAN_MH expects:
86+
* (Throwable, ContextScope, result)
87+
*
88+
* So we first permute parameters to swap returnValue and ContextScope.
89+
*/
90+
MethodHandle endPermuted =
91+
MethodHandles.permuteArguments(
92+
END_SPAN_MH,
93+
MethodType.methodType(
94+
Object.class, Throwable.class, Object.class, ContextScope.class),
95+
0,
96+
2,
97+
1);
98+
99+
// Accept original arguments (unused) after the required ones.
100+
MethodHandle endDropped =
101+
MethodHandles.dropArguments(endPermuted, 3, originalType.parameterList());
102+
103+
// Adapt return and result parameter types to match the original signature.
104+
MethodType cleanupType =
105+
endDropped
106+
.type()
107+
.changeParameterType(1, originalType.returnType())
108+
.changeReturnType(originalType.returnType());
109+
110+
cleanup = endDropped.asType(cleanupType);
111+
}
112+
113+
// Wrap the original in try/finally semantics.
114+
// Resulting handle:
115+
// (ContextScope, args...) -> R
116+
MethodHandle withFinally = MethodHandles.tryFinally(originalWithScope, cleanup);
117+
118+
// Precompute span metadata so we don't redo the lookup per invocation.
119+
final CharSequence resourceName = resourceNameFor(libraryName, methodName);
120+
final boolean methodMeasured = isMethodMeasured(libraryName, methodName);
121+
122+
// Bind both arguments to startSpan.
123+
// After binding: () -> ContextScope
124+
MethodHandle boundStart =
125+
MethodHandles.insertArguments(START_SPAN_MH, 0, resourceName, methodMeasured);
126+
127+
// Make it look like it takes the same arguments as the original,
128+
// even though they are ignored.
129+
// (args...) -> ContextScope
130+
MethodHandle startCombiner =
131+
MethodHandles.dropArguments(boundStart, 0, originalType.parameterList());
132+
133+
/*
134+
* foldArguments wires it all together:
135+
*
136+
* scope = startCombiner(args...)
137+
* return withFinally(scope, args...)
138+
*
139+
* Final shape matches the original:
140+
* (args...) -> R
141+
*/
142+
return MethodHandles.foldArguments(withFinally, startCombiner);
143+
144+
} catch (Throwable t) {
145+
LOGGER.debug(
146+
"Cannot wrap method handle for library {} and method {}", libraryName, methodName, t);
147+
return original;
148+
}
149+
}
150+
151+
@SuppressWarnings("unused")
152+
public static ContextScope startSpan(CharSequence resourceName, boolean methodMeasured) {
153+
AgentSpan span = AgentTracer.startSpan(TRACE_FFM.toString(), OPERATION_NAME);
154+
DECORATE.afterStart(span);
155+
span.setResourceName(resourceName);
156+
if (methodMeasured) {
157+
span.setMeasured(true);
158+
}
159+
return AgentTracer.activateSpan(span);
160+
}
161+
162+
@SuppressWarnings("unused")
163+
public static Object endSpan(Throwable t, ContextScope scope, Object result) {
164+
try {
165+
if (scope != null) {
166+
final AgentSpan span = AgentSpan.fromContext(scope.context());
167+
scope.close();
168+
169+
if (span != null) {
170+
if (t != null) {
171+
DECORATE.onError(span, t);
172+
span.addThrowable(t);
173+
}
174+
span.finish();
175+
}
176+
}
177+
} catch (Throwable ignored) {
178+
179+
}
180+
return result;
181+
}
182+
183+
public static boolean isMethodTraced(final String library, final String method) {
184+
return matches(InstrumenterConfig.get().getTraceNativeMethods().get(library), method);
185+
}
186+
187+
public static boolean isMethodMeasured(final String library, final String method) {
188+
return matches(InstrumenterConfig.get().getMeasureNativeMethods().get(library), method);
189+
}
190+
191+
public static CharSequence resourceNameFor(final String library, final String method) {
192+
return UTF8BytesString.create(library + "." + method);
193+
}
194+
195+
private static boolean matches(final Set<String> allows, final String method) {
196+
if (allows == null) {
197+
return false;
198+
}
199+
return allows.contains(method) || allows.contains("*");
200+
}
201+
202+
@Override
203+
protected String[] instrumentationNames() {
204+
return new String[] {TRACE_FFM.toString()};
205+
}
206+
207+
@Override
208+
protected CharSequence spanType() {
209+
return null;
210+
}
211+
212+
@Override
213+
protected CharSequence component() {
214+
return TRACE_FFM;
215+
}
216+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package datadog.trace.bootstrap.instrumentation.ffm;
2+
3+
import datadog.trace.api.Pair;
4+
import java.io.File;
5+
import java.util.Locale;
6+
import java.util.concurrent.ConcurrentHashMap;
7+
8+
public final class NativeLibraryHelper {
9+
// this map is unlimited. However, the number of entries depends on the configured methods we want
10+
// to trace.
11+
private static final ConcurrentHashMap<Long, Pair<String, String>> SYMBOLS_MAP =
12+
new ConcurrentHashMap<>();
13+
14+
private NativeLibraryHelper() {}
15+
16+
public static void onSymbolLookup(
17+
final String libraryName, final String symbol, final long address) {
18+
if (libraryName != null && !libraryName.isEmpty()) {
19+
if (FFMNativeMethodDecorator.isMethodTraced(libraryName, symbol)) {
20+
SYMBOLS_MAP.put(address, Pair.of(libraryName, symbol));
21+
}
22+
}
23+
}
24+
25+
public static Pair<String, String> reverseResolveLibraryAndSymbol(long address) {
26+
return SYMBOLS_MAP.get(address);
27+
}
28+
29+
public static String extractLibraryName(String fullPath) {
30+
String libraryName = new File(fullPath).getName().toLowerCase(Locale.ROOT);
31+
int dot = libraryName.lastIndexOf('.');
32+
libraryName = (dot > 0) ? libraryName.substring(0, dot) : libraryName;
33+
return libraryName;
34+
}
35+
}

dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/context/FieldBackedContextInjector.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public ClassVisitor wrap(
106106
final MethodList<?> methods,
107107
final int writerFlags,
108108
final int readerFlags) {
109-
return new ClassVisitor(Opcodes.ASM8, classVisitor) {
109+
return new ClassVisitor(Opcodes.ASM9, classVisitor) {
110110

111111
private final boolean frames =
112112
implementationContext.getClassFileVersion().isAtLeast(ClassFileVersion.JAVA_V6);

dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/context/FieldBackedContextRequestRewriter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public ClassVisitor wrap(
8484
final MethodList<?> methods,
8585
final int writerFlags,
8686
final int readerFlags) {
87-
return new ClassVisitor(Opcodes.ASM8, classVisitor) {
87+
return new ClassVisitor(Opcodes.ASM9, classVisitor) {
8888
@Override
8989
public MethodVisitor visitMethod(
9090
final int access,

dd-java-agent/agent-tooling/src/main/resources/datadog/trace/agent/tooling/bytebuddy/matcher/ignored_class_name.trie

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@
4444
1 io.opentelemetry.javaagent.*
4545
1 java.*
4646
0 java.lang.ClassLoader
47+
0 jdk.internal.foreign.abi.AbstractLinker
48+
0 jdk.internal.loader.NativeLibrary
49+
0 jdk.internal.loader.RawNativeLibraries$*
50+
0 jdk.internal.loader.NativeLibraries$*
4751
# allow exception profiling instrumentation
4852
0 java.lang.Exception
4953
0 java.lang.Error
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
plugins {
2+
id 'idea'
3+
}
4+
5+
muzzle {
6+
pass {
7+
coreJdk('25')
8+
}
9+
}
10+
11+
apply from: "$rootDir/gradle/java.gradle"
12+
apply from: "$rootDir/gradle/slf4j-simple.gradle"
13+
14+
tracerJava {
15+
addSourceSetFor(JavaVersion.VERSION_25)
16+
}
17+
18+
testJvmConstraints {
19+
minJavaVersion = JavaVersion.VERSION_25
20+
}
21+
22+
idea {
23+
module {
24+
jdkName = '25'
25+
}
26+
}
27+
28+
29+
tasks.named("compileMain_java25Java", JavaCompile) {
30+
configureCompiler(it, 25, JavaVersion.VERSION_1_8)
31+
}
32+
33+
tasks.named("compileTestGroovy", GroovyCompile) {
34+
configureCompiler(it, 25)
35+
}
36+
dependencies {
37+
implementation project(':dd-java-agent:instrumentation:datadog:tracing:trace-annotation')
38+
}

0 commit comments

Comments
 (0)