Skip to content

Commit e8795e9

Browse files
authored
Add MongoDB 5.x plugin, Jedis 5.x support, fix Spring MVC javax/jakarta detection (#803)
## Summary - Add MongoDB driver 5.x support (new plugin module for 5.2+, extend 4.x for 5.0-5.1) - Extend Jedis 4.x plugin to support Jedis 5.x (witness method fix) - Fix Spring MVC plugin javax/jakarta servlet detection when both exist on classpath - Document package-private class access pattern for plugins ## MongoDB 5.0-5.1 (4.x plugin extended) Added versions to support-version.list. Same MongoClientDelegate API. ## MongoDB 5.2+ (new mongodb-5.x-plugin module) MongoClusterImpl replaces MongoClientDelegate. New interceptors with remotePeer propagation via setAccessible reflection (MongoClusterImpl is package-private). ## Jedis 5.x (4.x plugin witness fix) All intercepted classes unchanged in 5.x. Plugin was silently not activating because witness Pipeline.persist(1 arg) moved to parent PipeliningBase. Changed witness to Connection.executeCommand(1 arg). Resolves apache/skywalking#11747 ## Spring MVC javax/jakarta fix When both javax.servlet and jakarta.servlet exist on classpath, the exclusive if/else set IS_JAVAX=true and skipped Jakarta check. Fix: check both independently. Ref: apache/skywalking#13425
1 parent 1aabdb6 commit e8795e9

File tree

29 files changed

+1090
-14
lines changed

29 files changed

+1090
-14
lines changed

.claude/skills/new-plugin/SKILL.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@ Pick interception points based on these principles:
5757
**Principle 1: Data accessibility without reflection.**
5858
Choose methods where the information you need (peer address, operation name, request/response details, headers for inject/extract) is directly available as method arguments, return values, or accessible through the `this` object's public API. **Never use reflection to read private fields.** If the data is not accessible at one method, look at a different point in the execution flow.
5959

60+
If the target class is **package-private** (e.g., `final class` without `public`), you cannot import or cast to it. **Same-package helper classes do NOT work** because the agent and application use different classloaders — Java treats them as different runtime packages even with the same package name (`IllegalAccessError`). Use `setAccessible` reflection to call public methods:
61+
```java
62+
try {
63+
java.lang.reflect.Method method = objInst.getClass().getMethod("publicMethodName");
64+
method.setAccessible(true); // Required for package-private class
65+
Object result = method.invoke(objInst);
66+
} catch (Exception e) {
67+
LOGGER.warn("Failed to access method", e);
68+
}
69+
```
70+
6071
**Principle 2: Use `EnhancedInstance` dynamic field to propagate context inside the library.**
6172
This is the primary mechanism for passing data between interception points. The agent adds a dynamic field to every enhanced class via `EnhancedInstance`. Use it to:
6273
- Store server address (peer) at connection/client creation time, retrieve it at command execution time

.github/workflows/plugins-test.1.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ jobs:
9191
- lettuce-webflux-5x-scenario
9292
- mongodb-3.x-scenario
9393
- mongodb-4.x-scenario
94+
- mongodb-5.x-scenario
9495
- netty-socketio-scenario
9596
- postgresql-above9.4.1207-scenario
9697
- mssql-jtds-scenario

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ packages/
2222
/test/jacoco/classes
2323
/test/jacoco/*.exec
2424
test/jacoco
25+
.claude/settings.local.json

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Release Notes.
2020
* Extend Spring Kafka plugin to support Spring Kafka 2.4 -> 2.9 and 3.0 -> 3.3.
2121
* Enhance test/plugin/run.sh to support extra Maven properties per version in support-version.list (format: version,key=value).
2222
* Add MariaDB 3.x plugin (all classes renamed in 3.x).
23+
* Extend Jedis 4.x plugin to support Jedis 5.x (fix witness method for 5.x compatibility).
2324

2425
All issues and pull requests are [here](https://github.com/apache/skywalking/milestone/249?closed=1)
2526

apm-sniffer/apm-sdk-plugin/CLAUDE.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,25 @@ public class MyPluginConfig {
145145
```
146146
Config key becomes: `plugin.myplugin.some_setting`
147147

148+
### Accessing Package-Private Classes
149+
150+
When a plugin needs to call methods on a **package-private** class in the target library (e.g., `MongoClusterImpl` which is `final class` without `public`), you cannot import or cast to it from the plugin package.
151+
152+
**Same-package helper classes do NOT work** because the agent and application use different classloaders. Even though the package names match, Java considers them different runtime packages, so package-private access is denied (`IllegalAccessError`).
153+
154+
**Solution: use `setAccessible` reflection** to call public methods on package-private classes:
155+
```java
156+
try {
157+
java.lang.reflect.Method method = objInst.getClass().getMethod("publicMethodName");
158+
method.setAccessible(true); // Required for package-private class
159+
Object result = method.invoke(objInst);
160+
} catch (Exception e) {
161+
LOGGER.warn("Failed to access method", e);
162+
}
163+
```
164+
165+
**When to use:** Only when the target class is package-private and you need to call its public methods. Prefer normal casting when the class is public.
166+
148167
### Dependency Management
149168

150169
**Plugin dependencies must use `provided` scope:**

apm-sniffer/apm-sdk-plugin/jedis-plugins/jedis-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jedis/v4/define/AbstractWitnessInstrumentation.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,11 @@ protected String[] witnessClasses() {
3535

3636
@Override
3737
protected List<WitnessMethod> witnessMethods() {
38+
// Connection.executeCommand(CommandObject) exists in Jedis 4.x+ and 5.x,
39+
// but not in 3.x. Previous witness Pipeline.persist(1) broke in 5.x
40+
// because persist moved from Pipeline to PipeliningBase parent class.
3841
return Collections.singletonList(new WitnessMethod(
39-
"redis.clients.jedis.Pipeline",
40-
named("persist").and(takesArguments(1))));
42+
"redis.clients.jedis.Connection",
43+
named("executeCommand").and(takesArguments(1))));
4144
}
4245
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
~ Licensed to the Apache Software Foundation (ASF) under one or more
4+
~ contributor license agreements. See the NOTICE file distributed with
5+
~ this work for additional information regarding copyright ownership.
6+
~ The ASF licenses this file to You under the Apache License, Version 2.0
7+
~ (the "License"); you may not use this file except in compliance with
8+
~ the License. You may obtain a copy of the License at
9+
~
10+
~ http://www.apache.org/licenses/LICENSE-2.0
11+
~
12+
~ Unless required by applicable law or agreed to in writing, software
13+
~ distributed under the License is distributed on an "AS IS" BASIS,
14+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
~ See the License for the specific language governing permissions and
16+
~ limitations under the License.
17+
~
18+
-->
19+
20+
<project xmlns="http://maven.apache.org/POM/4.0.0"
21+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
22+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
23+
<modelVersion>4.0.0</modelVersion>
24+
25+
<parent>
26+
<artifactId>apm-sdk-plugin</artifactId>
27+
<groupId>org.apache.skywalking</groupId>
28+
<version>9.7.0-SNAPSHOT</version>
29+
</parent>
30+
31+
<artifactId>apm-mongodb-5.x-plugin</artifactId>
32+
<packaging>jar</packaging>
33+
34+
<name>mongodb-5.x-plugin</name>
35+
36+
<dependencies>
37+
<dependency>
38+
<groupId>org.mongodb</groupId>
39+
<artifactId>mongodb-driver-sync</artifactId>
40+
<version>5.2.0</version>
41+
<scope>provided</scope>
42+
</dependency>
43+
<dependency>
44+
<groupId>org.apache.skywalking</groupId>
45+
<artifactId>apm-mongodb-4.x-plugin</artifactId>
46+
<version>${project.version}</version>
47+
<scope>provided</scope>
48+
</dependency>
49+
</dependencies>
50+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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.mongodb.v5.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 org.apache.skywalking.apm.agent.core.plugin.bytebuddy.ArgumentTypeNameMatch.takesArgumentWithType;
30+
import static org.apache.skywalking.apm.agent.core.plugin.match.NameMatch.byName;
31+
32+
/**
33+
* Enhance {@code com.mongodb.client.internal.MongoClusterImpl} which replaces
34+
* {@code MongoClientDelegate} in MongoDB driver 5.2+.
35+
* Extract remotePeer from Cluster (constructor arg[1]) and store in dynamic field.
36+
*/
37+
public class MongoClusterImplInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
38+
39+
private static final String ENHANCE_CLASS = "com.mongodb.client.internal.MongoClusterImpl";
40+
41+
private static final String CONSTRUCTOR_INTERCEPTOR =
42+
"org.apache.skywalking.apm.plugin.mongodb.v5.interceptor.MongoClusterImplConstructorInterceptor";
43+
44+
@Override
45+
protected String[] witnessClasses() {
46+
return new String[] {ENHANCE_CLASS};
47+
}
48+
49+
@Override
50+
protected ClassMatch enhanceClass() {
51+
return byName(ENHANCE_CLASS);
52+
}
53+
54+
@Override
55+
public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
56+
return new ConstructorInterceptPoint[] {
57+
new ConstructorInterceptPoint() {
58+
@Override
59+
public ElementMatcher<MethodDescription> getConstructorMatcher() {
60+
return takesArgumentWithType(1, "com.mongodb.internal.connection.Cluster");
61+
}
62+
63+
@Override
64+
public String getConstructorInterceptor() {
65+
return CONSTRUCTOR_INTERCEPTOR;
66+
}
67+
}
68+
};
69+
}
70+
71+
@Override
72+
public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
73+
return new InstanceMethodsInterceptPoint[] {
74+
new InstanceMethodsInterceptPoint() {
75+
@Override
76+
public ElementMatcher<MethodDescription> getMethodsMatcher() {
77+
return named("getOperationExecutor");
78+
}
79+
80+
@Override
81+
public String getMethodsInterceptor() {
82+
return CONSTRUCTOR_INTERCEPTOR;
83+
}
84+
85+
@Override
86+
public boolean isOverrideArgs() {
87+
return false;
88+
}
89+
}
90+
};
91+
}
92+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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.mongodb.v5.define;
20+
21+
import net.bytebuddy.description.method.MethodDescription;
22+
import net.bytebuddy.matcher.ElementMatcher;
23+
import net.bytebuddy.matcher.ElementMatchers;
24+
import org.apache.skywalking.apm.agent.core.plugin.bytebuddy.ArgumentTypeNameMatch;
25+
import org.apache.skywalking.apm.agent.core.plugin.interceptor.ConstructorInterceptPoint;
26+
import org.apache.skywalking.apm.agent.core.plugin.interceptor.InstanceMethodsInterceptPoint;
27+
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.ClassInstanceMethodsEnhancePluginDefine;
28+
import org.apache.skywalking.apm.agent.core.plugin.match.ClassMatch;
29+
import org.apache.skywalking.apm.agent.core.plugin.match.NameMatch;
30+
31+
/**
32+
* Enhance {@code MongoClusterImpl$OperationExecutorImpl} in MongoDB driver 5.2+.
33+
* <p>
34+
* Constructor interception: propagate remotePeer from enclosing MongoClusterImpl
35+
* (synthetic arg[0] for non-static inner class).
36+
* <p>
37+
* Method interception: create exit spans on execute() calls.
38+
* Reuses the 4.x MongoDBOperationExecutorInterceptor.
39+
*/
40+
public class MongoClusterOperationExecutorInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
41+
42+
private static final String ENHANCE_CLASS =
43+
"com.mongodb.client.internal.MongoClusterImpl$OperationExecutorImpl";
44+
45+
private static final String CONSTRUCTOR_INTERCEPTOR =
46+
"org.apache.skywalking.apm.plugin.mongodb.v5.interceptor.OperationExecutorImplConstructorInterceptor";
47+
48+
private static final String EXECUTE_INTERCEPTOR =
49+
"org.apache.skywalking.apm.plugin.mongodb.v4.interceptor.MongoDBOperationExecutorInterceptor";
50+
51+
private static final String METHOD_NAME = "execute";
52+
53+
private static final String ARGUMENT_TYPE = "com.mongodb.client.ClientSession";
54+
55+
@Override
56+
protected String[] witnessClasses() {
57+
return new String[] {"com.mongodb.client.internal.MongoClusterImpl"};
58+
}
59+
60+
@Override
61+
protected ClassMatch enhanceClass() {
62+
return NameMatch.byName(ENHANCE_CLASS);
63+
}
64+
65+
@Override
66+
public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
67+
return new ConstructorInterceptPoint[] {
68+
new ConstructorInterceptPoint() {
69+
@Override
70+
public ElementMatcher<MethodDescription> getConstructorMatcher() {
71+
return ElementMatchers.any();
72+
}
73+
74+
@Override
75+
public String getConstructorInterceptor() {
76+
return CONSTRUCTOR_INTERCEPTOR;
77+
}
78+
}
79+
};
80+
}
81+
82+
@Override
83+
public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
84+
return new InstanceMethodsInterceptPoint[] {
85+
new InstanceMethodsInterceptPoint() {
86+
@Override
87+
public ElementMatcher<MethodDescription> getMethodsMatcher() {
88+
return ElementMatchers
89+
.named(METHOD_NAME)
90+
.and(ArgumentTypeNameMatch.takesArgumentWithType(2, ARGUMENT_TYPE))
91+
.or(ElementMatchers.<MethodDescription>named(METHOD_NAME)
92+
.and(ArgumentTypeNameMatch.takesArgumentWithType(3, ARGUMENT_TYPE)));
93+
}
94+
95+
@Override
96+
public String getMethodsInterceptor() {
97+
return EXECUTE_INTERCEPTOR;
98+
}
99+
100+
@Override
101+
public boolean isOverrideArgs() {
102+
return false;
103+
}
104+
}
105+
};
106+
}
107+
}

0 commit comments

Comments
 (0)