From bb404a23f8a48ed399b69a820668a50d5ea2c3dc Mon Sep 17 00:00:00 2001 From: Wu Sheng Date: Thu, 9 Apr 2026 15:53:45 +0800 Subject: [PATCH 1/5] Extend MongoDB 4.x plugin test to cover driver 5.0-5.1 (same MongoClientDelegate API) --- .../scenarios/mongodb-4.x-scenario/support-version.list | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/plugin/scenarios/mongodb-4.x-scenario/support-version.list b/test/plugin/scenarios/mongodb-4.x-scenario/support-version.list index 42c7e89fb5..ec4ce41a71 100644 --- a/test/plugin/scenarios/mongodb-4.x-scenario/support-version.list +++ b/test/plugin/scenarios/mongodb-4.x-scenario/support-version.list @@ -25,4 +25,7 @@ 4.8.2 4.9.1 4.10.2 -4.11.5 \ No newline at end of file +4.11.5 +# 5.0-5.1 still uses MongoClientDelegate (same as 4.x) +5.0.1 +5.1.4 \ No newline at end of file From 28f1a11aadb120b8f813f2de2fea7b73c8576596 Mon Sep 17 00:00:00 2001 From: Wu Sheng Date: Thu, 9 Apr 2026 16:38:24 +0800 Subject: [PATCH 2/5] Add MongoDB 5.x plugin and extend 4.x to cover 5.0-5.1 MongoDB 5.2+ replaced MongoClientDelegate with package-private MongoClusterImpl. New mongodb-5.x-plugin module with: - MongoClusterImplInstrumentation: intercept constructor (Cluster at arg[1]) and getOperationExecutor() to propagate remotePeer - MongoClusterOperationExecutorInstrumentation: intercept inner class OperationExecutorImpl constructor (enclosing instance at synthetic arg[0]) and execute() methods (reuses 4.x MongoDBOperationExecutorInterceptor) Key challenge: OperationExecutorImpl is created inside MongoClusterImpl's constructor before onConstruct fires. Solved by calling getOperationExecutor() via setAccessible reflection in onConstruct to set peer on the stored executor. TODO: Add @InternalAccessor annotation to core framework to allow plugin helper classes in target library packages, bypassing checkstyle package rules. Locally verified: 5.0.1, 5.1.4 (4.x plugin), 5.2.0, 5.5.1 (5.x plugin) all passed. --- .github/workflows/plugins-test.1.yaml | 1 + .../apm-sdk-plugin/mongodb-5.x-plugin/pom.xml | 50 ++++++ .../MongoClusterImplInstrumentation.java | 92 ++++++++++ ...usterOperationExecutorInstrumentation.java | 107 ++++++++++++ ...ongoClusterImplConstructorInterceptor.java | 87 ++++++++++ ...ionExecutorImplConstructorInterceptor.java | 53 ++++++ .../src/main/resources/skywalking-plugin.def | 19 ++ apm-sniffer/apm-sdk-plugin/pom.xml | 1 + .../service-agent/java-agent/Plugin-list.md | 1 + .../java-agent/Supported-list.md | 2 +- .../mongodb-5.x-scenario/bin/startup.sh | 21 +++ .../config/expectedData.yaml | 162 ++++++++++++++++++ .../mongodb-5.x-scenario/configuration.yml | 24 +++ .../scenarios/mongodb-5.x-scenario/pom.xml | 148 ++++++++++++++++ .../src/main/assembly/assembly.xml | 41 +++++ .../apm/testcase/mongodb/Application.java | 34 ++++ .../mongodb/controller/CaseController.java | 95 ++++++++++ .../src/main/resources/application.yaml | 25 +++ .../src/main/resources/log4j2.xml | 30 ++++ .../mongodb-5.x-scenario/support-version.list | 19 ++ 20 files changed, 1011 insertions(+), 1 deletion(-) create mode 100644 apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/pom.xml create mode 100644 apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/mongodb/v5/define/MongoClusterImplInstrumentation.java create mode 100644 apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/mongodb/v5/define/MongoClusterOperationExecutorInstrumentation.java create mode 100644 apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/mongodb/v5/interceptor/MongoClusterImplConstructorInterceptor.java create mode 100644 apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/mongodb/v5/interceptor/OperationExecutorImplConstructorInterceptor.java create mode 100644 apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/resources/skywalking-plugin.def create mode 100644 test/plugin/scenarios/mongodb-5.x-scenario/bin/startup.sh create mode 100644 test/plugin/scenarios/mongodb-5.x-scenario/config/expectedData.yaml create mode 100644 test/plugin/scenarios/mongodb-5.x-scenario/configuration.yml create mode 100644 test/plugin/scenarios/mongodb-5.x-scenario/pom.xml create mode 100644 test/plugin/scenarios/mongodb-5.x-scenario/src/main/assembly/assembly.xml create mode 100644 test/plugin/scenarios/mongodb-5.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/mongodb/Application.java create mode 100644 test/plugin/scenarios/mongodb-5.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/mongodb/controller/CaseController.java create mode 100644 test/plugin/scenarios/mongodb-5.x-scenario/src/main/resources/application.yaml create mode 100644 test/plugin/scenarios/mongodb-5.x-scenario/src/main/resources/log4j2.xml create mode 100644 test/plugin/scenarios/mongodb-5.x-scenario/support-version.list diff --git a/.github/workflows/plugins-test.1.yaml b/.github/workflows/plugins-test.1.yaml index 21d5f04eab..19e20deb5c 100644 --- a/.github/workflows/plugins-test.1.yaml +++ b/.github/workflows/plugins-test.1.yaml @@ -91,6 +91,7 @@ jobs: - lettuce-webflux-5x-scenario - mongodb-3.x-scenario - mongodb-4.x-scenario + - mongodb-5.x-scenario - netty-socketio-scenario - postgresql-above9.4.1207-scenario - mssql-jtds-scenario diff --git a/apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/pom.xml b/apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/pom.xml new file mode 100644 index 0000000000..141dbd464a --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/pom.xml @@ -0,0 +1,50 @@ + + + + + 4.0.0 + + + apm-sdk-plugin + org.apache.skywalking + 9.7.0-SNAPSHOT + + + apm-mongodb-5.x-plugin + jar + + mongodb-5.x-plugin + + + + org.mongodb + mongodb-driver-sync + 5.2.0 + provided + + + org.apache.skywalking + apm-mongodb-4.x-plugin + ${project.version} + provided + + + diff --git a/apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/mongodb/v5/define/MongoClusterImplInstrumentation.java b/apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/mongodb/v5/define/MongoClusterImplInstrumentation.java new file mode 100644 index 0000000000..96b834fbca --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/mongodb/v5/define/MongoClusterImplInstrumentation.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.skywalking.apm.plugin.mongodb.v5.define; + +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.ConstructorInterceptPoint; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.InstanceMethodsInterceptPoint; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.ClassInstanceMethodsEnhancePluginDefine; +import org.apache.skywalking.apm.agent.core.plugin.match.ClassMatch; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static org.apache.skywalking.apm.agent.core.plugin.bytebuddy.ArgumentTypeNameMatch.takesArgumentWithType; +import static org.apache.skywalking.apm.agent.core.plugin.match.NameMatch.byName; + +/** + * Enhance {@code com.mongodb.client.internal.MongoClusterImpl} which replaces + * {@code MongoClientDelegate} in MongoDB driver 5.2+. + * Extract remotePeer from Cluster (constructor arg[1]) and store in dynamic field. + */ +public class MongoClusterImplInstrumentation extends ClassInstanceMethodsEnhancePluginDefine { + + private static final String ENHANCE_CLASS = "com.mongodb.client.internal.MongoClusterImpl"; + + private static final String CONSTRUCTOR_INTERCEPTOR = + "org.apache.skywalking.apm.plugin.mongodb.v5.interceptor.MongoClusterImplConstructorInterceptor"; + + @Override + protected String[] witnessClasses() { + return new String[] {ENHANCE_CLASS}; + } + + @Override + protected ClassMatch enhanceClass() { + return byName(ENHANCE_CLASS); + } + + @Override + public ConstructorInterceptPoint[] getConstructorsInterceptPoints() { + return new ConstructorInterceptPoint[] { + new ConstructorInterceptPoint() { + @Override + public ElementMatcher getConstructorMatcher() { + return takesArgumentWithType(1, "com.mongodb.internal.connection.Cluster"); + } + + @Override + public String getConstructorInterceptor() { + return CONSTRUCTOR_INTERCEPTOR; + } + } + }; + } + + @Override + public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() { + return new InstanceMethodsInterceptPoint[] { + new InstanceMethodsInterceptPoint() { + @Override + public ElementMatcher getMethodsMatcher() { + return named("getOperationExecutor"); + } + + @Override + public String getMethodsInterceptor() { + return CONSTRUCTOR_INTERCEPTOR; + } + + @Override + public boolean isOverrideArgs() { + return false; + } + } + }; + } +} diff --git a/apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/mongodb/v5/define/MongoClusterOperationExecutorInstrumentation.java b/apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/mongodb/v5/define/MongoClusterOperationExecutorInstrumentation.java new file mode 100644 index 0000000000..53bdb6da93 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/mongodb/v5/define/MongoClusterOperationExecutorInstrumentation.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.skywalking.apm.plugin.mongodb.v5.define; + +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatchers; +import org.apache.skywalking.apm.agent.core.plugin.bytebuddy.ArgumentTypeNameMatch; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.ConstructorInterceptPoint; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.InstanceMethodsInterceptPoint; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.ClassInstanceMethodsEnhancePluginDefine; +import org.apache.skywalking.apm.agent.core.plugin.match.ClassMatch; +import org.apache.skywalking.apm.agent.core.plugin.match.NameMatch; + +/** + * Enhance {@code MongoClusterImpl$OperationExecutorImpl} in MongoDB driver 5.2+. + *

+ * Constructor interception: propagate remotePeer from enclosing MongoClusterImpl + * (synthetic arg[0] for non-static inner class). + *

+ * Method interception: create exit spans on execute() calls. + * Reuses the 4.x MongoDBOperationExecutorInterceptor. + */ +public class MongoClusterOperationExecutorInstrumentation extends ClassInstanceMethodsEnhancePluginDefine { + + private static final String ENHANCE_CLASS = + "com.mongodb.client.internal.MongoClusterImpl$OperationExecutorImpl"; + + private static final String CONSTRUCTOR_INTERCEPTOR = + "org.apache.skywalking.apm.plugin.mongodb.v5.interceptor.OperationExecutorImplConstructorInterceptor"; + + private static final String EXECUTE_INTERCEPTOR = + "org.apache.skywalking.apm.plugin.mongodb.v4.interceptor.MongoDBOperationExecutorInterceptor"; + + private static final String METHOD_NAME = "execute"; + + private static final String ARGUMENT_TYPE = "com.mongodb.client.ClientSession"; + + @Override + protected String[] witnessClasses() { + return new String[] {"com.mongodb.client.internal.MongoClusterImpl"}; + } + + @Override + protected ClassMatch enhanceClass() { + return NameMatch.byName(ENHANCE_CLASS); + } + + @Override + public ConstructorInterceptPoint[] getConstructorsInterceptPoints() { + return new ConstructorInterceptPoint[] { + new ConstructorInterceptPoint() { + @Override + public ElementMatcher getConstructorMatcher() { + return ElementMatchers.any(); + } + + @Override + public String getConstructorInterceptor() { + return CONSTRUCTOR_INTERCEPTOR; + } + } + }; + } + + @Override + public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() { + return new InstanceMethodsInterceptPoint[] { + new InstanceMethodsInterceptPoint() { + @Override + public ElementMatcher getMethodsMatcher() { + return ElementMatchers + .named(METHOD_NAME) + .and(ArgumentTypeNameMatch.takesArgumentWithType(2, ARGUMENT_TYPE)) + .or(ElementMatchers.named(METHOD_NAME) + .and(ArgumentTypeNameMatch.takesArgumentWithType(3, ARGUMENT_TYPE))); + } + + @Override + public String getMethodsInterceptor() { + return EXECUTE_INTERCEPTOR; + } + + @Override + public boolean isOverrideArgs() { + return false; + } + } + }; + } +} diff --git a/apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/mongodb/v5/interceptor/MongoClusterImplConstructorInterceptor.java b/apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/mongodb/v5/interceptor/MongoClusterImplConstructorInterceptor.java new file mode 100644 index 0000000000..98cf52ac5b --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/mongodb/v5/interceptor/MongoClusterImplConstructorInterceptor.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.skywalking.apm.plugin.mongodb.v5.interceptor; + +import com.mongodb.internal.connection.Cluster; +import org.apache.skywalking.apm.agent.core.logging.api.ILog; +import org.apache.skywalking.apm.agent.core.logging.api.LogManager; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceConstructorInterceptor; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult; +import org.apache.skywalking.apm.plugin.mongodb.v4.support.MongoRemotePeerHelper; + +import java.lang.reflect.Method; + +/** + * Intercept {@code MongoClusterImpl} constructor and {@code getOperationExecutor()}. + *

+ * Constructor: extract remotePeer from Cluster (arg[1]) and store in dynamic field. + * getOperationExecutor(): pass remotePeer to the returned OperationExecutor. + */ +public class MongoClusterImplConstructorInterceptor + implements InstanceConstructorInterceptor, InstanceMethodsAroundInterceptor { + + private static final ILog LOGGER = LogManager.getLogger(MongoClusterImplConstructorInterceptor.class); + + @Override + public void onConstruct(EnhancedInstance objInst, Object[] allArguments) { + Cluster cluster = (Cluster) allArguments[1]; + String remotePeer = MongoRemotePeerHelper.getRemotePeer(cluster); + objInst.setSkyWalkingDynamicField(remotePeer); + + // The OperationExecutorImpl is created INSIDE this constructor (before onConstruct fires), + // so its constructor interceptor couldn't read the peer yet. Set it now. + // MongoClusterImpl is package-private, access getOperationExecutor via reflection. + try { + java.lang.reflect.Method getExecutor = objInst.getClass().getMethod("getOperationExecutor"); + getExecutor.setAccessible(true); + Object executor = getExecutor.invoke(objInst); + if (executor instanceof EnhancedInstance) { + ((EnhancedInstance) executor).setSkyWalkingDynamicField(remotePeer); + } + } catch (Exception e) { + LOGGER.warn("Failed to set remotePeer on OperationExecutor", e); + } + } + + @Override + public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, + Class[] argumentsTypes, MethodInterceptResult result) { + } + + @Override + public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, + Class[] argumentsTypes, Object ret) { + if (ret instanceof EnhancedInstance) { + EnhancedInstance retInstance = (EnhancedInstance) ret; + String remotePeer = (String) objInst.getSkyWalkingDynamicField(); + if (LOGGER.isDebugEnable()) { + LOGGER.debug("Mark OperationExecutor remotePeer: {}", remotePeer); + } + retInstance.setSkyWalkingDynamicField(remotePeer); + } + return ret; + } + + @Override + public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, + Class[] argumentsTypes, Throwable t) { + } +} diff --git a/apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/mongodb/v5/interceptor/OperationExecutorImplConstructorInterceptor.java b/apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/mongodb/v5/interceptor/OperationExecutorImplConstructorInterceptor.java new file mode 100644 index 0000000000..0b7c11862e --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/mongodb/v5/interceptor/OperationExecutorImplConstructorInterceptor.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.skywalking.apm.plugin.mongodb.v5.interceptor; + +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceConstructorInterceptor; + +/** + * Intercept {@code MongoClusterImpl$OperationExecutorImpl} constructor. + * As a non-static inner class, the compiled constructor has the enclosing + * {@code MongoClusterImpl} instance as a synthetic first argument (arg index 0). + * + * Note: This interceptor fires during MongoClusterImpl's constructor, before + * MongoClusterImpl.onConstruct() sets the remotePeer. So the dynamic field + * on the enclosing instance is not yet set. The primary peer propagation + * happens in MongoClusterImplConstructorInterceptor.onConstruct() which + * calls getOperationExecutor() after the constructor completes. + * + * This interceptor serves as a secondary path for OperationExecutorImpl + * instances created later (e.g., via withTimeoutSettings()). + */ +public class OperationExecutorImplConstructorInterceptor implements InstanceConstructorInterceptor { + + @Override + public void onConstruct(EnhancedInstance objInst, Object[] allArguments) { + for (Object arg : allArguments) { + if (arg instanceof EnhancedInstance) { + EnhancedInstance enclosingInstance = (EnhancedInstance) arg; + String remotePeer = (String) enclosingInstance.getSkyWalkingDynamicField(); + if (remotePeer != null && !remotePeer.isEmpty()) { + objInst.setSkyWalkingDynamicField(remotePeer); + return; + } + } + } + } +} diff --git a/apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/resources/skywalking-plugin.def b/apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/resources/skywalking-plugin.def new file mode 100644 index 0000000000..fcd2e7be8f --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/resources/skywalking-plugin.def @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +# MongoDB 5.2+ (MongoClusterImpl replaces MongoClientDelegate) +mongodb-5.x=org.apache.skywalking.apm.plugin.mongodb.v5.define.MongoClusterImplInstrumentation +mongodb-5.x=org.apache.skywalking.apm.plugin.mongodb.v5.define.MongoClusterOperationExecutorInstrumentation diff --git a/apm-sniffer/apm-sdk-plugin/pom.xml b/apm-sniffer/apm-sdk-plugin/pom.xml index fca054b6a5..775ec34e12 100644 --- a/apm-sniffer/apm-sdk-plugin/pom.xml +++ b/apm-sniffer/apm-sdk-plugin/pom.xml @@ -37,6 +37,7 @@ motan-plugin mongodb-3.x-plugin mongodb-4.x-plugin + mongodb-5.x-plugin feign-default-http-9.x-plugin okhttp-3.x-plugin okhttp-4.x-plugin diff --git a/docs/en/setup/service-agent/java-agent/Plugin-list.md b/docs/en/setup/service-agent/java-agent/Plugin-list.md index a829e1a174..2bfd58f872 100644 --- a/docs/en/setup/service-agent/java-agent/Plugin-list.md +++ b/docs/en/setup/service-agent/java-agent/Plugin-list.md @@ -70,6 +70,7 @@ - mongodb-2.x - mongodb-3.x - mongodb-4.x +- mongodb-5.x - motan-0.x - mybatis-3.x - mysql-5.x diff --git a/docs/en/setup/service-agent/java-agent/Supported-list.md b/docs/en/setup/service-agent/java-agent/Supported-list.md index a875618a02..0a74828b0e 100644 --- a/docs/en/setup/service-agent/java-agent/Supported-list.md +++ b/docs/en/setup/service-agent/java-agent/Supported-list.md @@ -91,7 +91,7 @@ metrics based on the tracing data. * [Jedis](https://github.com/xetorthio/jedis) 2.x-4.x * [Redisson](https://github.com/redisson/redisson) Easy Java Redis client 3.5.0 -> 3.30.0 * [Lettuce](https://github.com/lettuce-io/lettuce-core) 5.x -> 6.7.1 - * [MongoDB Java Driver](https://github.com/mongodb/mongo-java-driver) 2.13-2.14, 3.4.0-3.12.7, 4.0.0-4.11.5 + * [MongoDB Java Driver](https://github.com/mongodb/mongo-java-driver) 2.13-2.14, 3.4.0-3.12.7, 4.0.0-5.5.x * Memcached Client * [Spymemcached](https://github.com/couchbase/spymemcached) 2.x * [Xmemcached](https://github.com/killme2008/xmemcached) 2.x diff --git a/test/plugin/scenarios/mongodb-5.x-scenario/bin/startup.sh b/test/plugin/scenarios/mongodb-5.x-scenario/bin/startup.sh new file mode 100644 index 0000000000..8d9acc35ea --- /dev/null +++ b/test/plugin/scenarios/mongodb-5.x-scenario/bin/startup.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +home="$(cd "$(dirname $0)"; pwd)" + +java -jar ${agent_opts} -Dskywalking.plugin.mongodb.trace_param=true ${home}/../libs/mongodb-5.x-scenario.jar & \ No newline at end of file diff --git a/test/plugin/scenarios/mongodb-5.x-scenario/config/expectedData.yaml b/test/plugin/scenarios/mongodb-5.x-scenario/config/expectedData.yaml new file mode 100644 index 0000000000..58d2556342 --- /dev/null +++ b/test/plugin/scenarios/mongodb-5.x-scenario/config/expectedData.yaml @@ -0,0 +1,162 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +segmentItems: + - serviceName: mongodb-5.x-scenario + segmentSize: ge 2 + segments: + - segmentId: not null + spans: + - operationName: MongoDB/CreateCollectionOperation + parentSpanId: 0 + spanId: 1 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 42 + isError: false + spanType: Exit + peer: mongodb-server:27017 + tags: + - {key: db.type, value: MongoDB} + - {key: db.instance, value: test-database} + - {key: db.bind_vars, value: 'testCollection'} + skipAnalysis: 'false' + - operationName: MongoDB/MixedBulkWriteOperation + parentSpanId: 0 + spanId: 2 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 42 + isError: false + spanType: Exit + peer: mongodb-server:27017 + tags: + - {key: db.type, value: MongoDB} + - {key: db.instance, value: test-database} + - {key: db.collection, value: testCollection} + - {key: db.bind_vars, value: not null} + skipAnalysis: 'false' + - operationName: MongoDB/FindOperation + parentSpanId: 0 + spanId: 3 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 42 + isError: false + spanType: Exit + peer: mongodb-server:27017 + tags: + - {key: db.type, value: MongoDB} + - {key: db.instance, value: test-database} + - {key: db.collection, value: testCollection} + - {key: db.bind_vars, value: '{"name": "org"}'} + skipAnalysis: 'false' + - operationName: MongoDB/AggregateOperation + parentSpanId: 0 + spanId: 4 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 42 + isError: false + spanType: Exit + peer: mongodb-server:27017 + tags: + - {key: db.type, value: MongoDB} + - {key: db.instance, value: test-database} + - {key: db.collection, value: testCollection} + - {key: db.bind_vars, value: '{"$match": {"name": "test"}},'} + skipAnalysis: 'false' + - operationName: MongoDB/MixedBulkWriteOperation + parentSpanId: 0 + spanId: 5 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 42 + isError: false + spanType: Exit + peer: mongodb-server:27017 + tags: + - {key: db.type, value: MongoDB} + - {key: db.instance, value: test-database} + - {key: db.collection, value: testCollection} + - {key: db.bind_vars, value: '{"name": "org"},'} + skipAnalysis: 'false' + - operationName: MongoDB/FindOperation + parentSpanId: 0 + spanId: 6 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 42 + isError: false + spanType: Exit + peer: mongodb-server:27017 + tags: + - {key: db.type, value: MongoDB} + - {key: db.instance, value: test-database} + - {key: db.collection, value: testCollection} + - {key: db.bind_vars, value: '{"name": "testA"}'} + skipAnalysis: 'false' + - operationName: MongoDB/MixedBulkWriteOperation + parentSpanId: 0 + spanId: 7 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 42 + isError: false + spanType: Exit + peer: mongodb-server:27017 + tags: + - {key: db.type, value: MongoDB} + - {key: db.instance, value: test-database} + - {key: db.collection, value: testCollection} + - {key: db.bind_vars, value: '{"id": "1"},'} + skipAnalysis: 'false' + - operationName: MongoDB/DropDatabaseOperation + parentSpanId: 0 + spanId: 8 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 42 + isError: false + spanType: Exit + peer: mongodb-server:27017 + tags: + - {key: db.type, value: MongoDB} + - {key: db.instance, value: test-database} + - {key: db.bind_vars, value: null} + skipAnalysis: 'false' + - operationName: GET:/mongodb-5.x-scenario/case/mongodb-5.x-scenario + parentSpanId: -1 + spanId: 0 + spanLayer: Http + startTime: nq 0 + endTime: nq 0 + componentId: 1 + isError: false + spanType: Entry + peer: '' + tags: + - {key: url, value: 'http://localhost:8080/mongodb-5.x-scenario/case/mongodb-5.x-scenario'} + - {key: http.method, value: GET} + - {key: http.status_code, value: '200'} + skipAnalysis: 'false' diff --git a/test/plugin/scenarios/mongodb-5.x-scenario/configuration.yml b/test/plugin/scenarios/mongodb-5.x-scenario/configuration.yml new file mode 100644 index 0000000000..187a8601a7 --- /dev/null +++ b/test/plugin/scenarios/mongodb-5.x-scenario/configuration.yml @@ -0,0 +1,24 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +type: jvm +entryService: http://localhost:8080/mongodb-5.x-scenario/case/mongodb-5.x-scenario +healthCheck: http://localhost:8080/mongodb-5.x-scenario/case/healthCheck +startScript: ./bin/startup.sh +dependencies: + mongodb-server: + image: mongo:6.0 + hostname: mongodb-server diff --git a/test/plugin/scenarios/mongodb-5.x-scenario/pom.xml b/test/plugin/scenarios/mongodb-5.x-scenario/pom.xml new file mode 100644 index 0000000000..9a0985910c --- /dev/null +++ b/test/plugin/scenarios/mongodb-5.x-scenario/pom.xml @@ -0,0 +1,148 @@ + + + + + org.apache.skywalking.apm.testcase + mongodb-5.x-scenario + 1.0.0 + jar + + 4.0.0 + + + UTF-8 + 1.8 + 3.8.1 + 5.2.0 + 2.1.6.RELEASE + 1.18.20 + + + skywalking-mongodb-5.x-scenario + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + org.mongodb + mongodb-driver-core + ${test.framework.version} + + + org.mongodb + mongodb-driver-sync + ${test.framework.version} + + + org.mongodb + bson + ${test.framework.version} + + + + + + + org.mongodb + mongodb-driver-core + + + org.mongodb + mongodb-driver-sync + + + org.mongodb + bson + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-log4j2 + + + org.projectlombok + lombok + ${lombok.version} + provided + + + + + mongodb-5.x-scenario + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + + repackage + + + + + + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${compiler.version} + ${compiler.version} + ${project.build.sourceEncoding} + + + + org.apache.maven.plugins + maven-assembly-plugin + + + assemble + package + + single + + + + src/main/assembly/assembly.xml + + ./target/ + + + + + + + diff --git a/test/plugin/scenarios/mongodb-5.x-scenario/src/main/assembly/assembly.xml b/test/plugin/scenarios/mongodb-5.x-scenario/src/main/assembly/assembly.xml new file mode 100644 index 0000000000..ff39fb806e --- /dev/null +++ b/test/plugin/scenarios/mongodb-5.x-scenario/src/main/assembly/assembly.xml @@ -0,0 +1,41 @@ + + + + + zip + + + + + ./bin + 0775 + + + + + + ${project.build.directory}/mongodb-5.x-scenario.jar + ./libs + 0775 + + + diff --git a/test/plugin/scenarios/mongodb-5.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/mongodb/Application.java b/test/plugin/scenarios/mongodb-5.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/mongodb/Application.java new file mode 100644 index 0000000000..1dd56fec23 --- /dev/null +++ b/test/plugin/scenarios/mongodb-5.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/mongodb/Application.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.skywalking.apm.testcase.mongodb; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + try { + SpringApplication.run(Application.class, args); + } catch (Exception e) { + // Never do this + } + } +} diff --git a/test/plugin/scenarios/mongodb-5.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/mongodb/controller/CaseController.java b/test/plugin/scenarios/mongodb-5.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/mongodb/controller/CaseController.java new file mode 100644 index 0000000000..06a196e216 --- /dev/null +++ b/test/plugin/scenarios/mongodb-5.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/mongodb/controller/CaseController.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.skywalking.apm.testcase.mongodb.controller; + +import com.mongodb.client.AggregateIterable; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.FindIterable; +import com.mongodb.client.model.Aggregates; +import com.mongodb.client.model.Filters; +import org.bson.BsonDocument; +import org.bson.Document; +import org.bson.conversions.Bson; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Arrays; +import java.util.List; + +import static com.mongodb.client.model.Filters.eq; + +@RestController +@RequestMapping("/case") +public class CaseController { + + @Value(value = "${mongodb.uri}") + private String connectionString; + + @GetMapping("/healthCheck") + public String health() { + // check connect to mongodb server + try (MongoClient mongoClient = MongoClients.create(connectionString)) { + return "success"; + } + } + + @RequestMapping("/mongodb-5.x-scenario") + public String mongoDBCase() { + try (MongoClient mongoClient = MongoClients.create(connectionString)) { + MongoDatabase db = mongoClient.getDatabase("test-database"); + // CreateCollectionOperation + db.createCollection("testCollection"); + + MongoCollection collection = db.getCollection("testCollection"); + Document document = Document.parse("{id: 1, name: \"test\"}"); + // MixedBulkWriteOperation + collection.insertOne(document); + + // FindOperation + FindIterable findIterable = collection.find(eq("name", "org")); + findIterable.first(); + + // AggregateOperation + List pipeline = Arrays.asList( + Aggregates.match(Filters.eq("name", "test")) + ); + AggregateIterable aggregateIterable = collection.aggregate(pipeline); + aggregateIterable.first(); + + // MixedBulkWriteOperation + collection.updateOne(eq("name", "org"), BsonDocument.parse("{ $set : { \"name\": \"testA\"} }")); + + // FindOperation + findIterable = collection.find(eq("name", "testA")); + findIterable.first(); + + // MixedBulkWriteOperation + collection.deleteOne(eq("id", "1")); + + // DropDatabaseOperation + mongoClient.getDatabase("test-database").drop(); + } + return "success"; + } +} diff --git a/test/plugin/scenarios/mongodb-5.x-scenario/src/main/resources/application.yaml b/test/plugin/scenarios/mongodb-5.x-scenario/src/main/resources/application.yaml new file mode 100644 index 0000000000..533ff2e37f --- /dev/null +++ b/test/plugin/scenarios/mongodb-5.x-scenario/src/main/resources/application.yaml @@ -0,0 +1,25 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# +# +server: + port: 8080 + servlet: + context-path: /mongodb-5.x-scenario +logging: + config: classpath:log4j2.xml +mongodb: + uri: mongodb://mongodb-server/test \ No newline at end of file diff --git a/test/plugin/scenarios/mongodb-5.x-scenario/src/main/resources/log4j2.xml b/test/plugin/scenarios/mongodb-5.x-scenario/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..9849ed5a8a --- /dev/null +++ b/test/plugin/scenarios/mongodb-5.x-scenario/src/main/resources/log4j2.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plugin/scenarios/mongodb-5.x-scenario/support-version.list b/test/plugin/scenarios/mongodb-5.x-scenario/support-version.list new file mode 100644 index 0000000000..89a4430b05 --- /dev/null +++ b/test/plugin/scenarios/mongodb-5.x-scenario/support-version.list @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +# MongoDB 5.2+ (MongoClusterImpl replaces MongoClientDelegate) +5.2.0 +5.5.1 From a9e1e149c9404c7ec361d75dea4871d4a2347422 Mon Sep 17 00:00:00 2001 From: Wu Sheng Date: Thu, 9 Apr 2026 16:49:59 +0800 Subject: [PATCH 3/5] Add MongoDB 5.x plugin and extend 4.x to cover 5.0-5.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MongoDB 5.2+ replaced MongoClientDelegate with package-private MongoClusterImpl. New mongodb-5.x-plugin module with: - MongoClusterImplInstrumentation: intercept constructor (Cluster at arg[1]) and getOperationExecutor() to propagate remotePeer - MongoClusterOperationExecutorInstrumentation: intercept inner class OperationExecutorImpl constructor and execute() methods Key challenge: OperationExecutorImpl is created inside MongoClusterImpl's constructor before onConstruct fires. Solved by calling getOperationExecutor() via setAccessible reflection in onConstruct. Note: Same-package helper classes (@InternalAccessor approach) do NOT work because agent and application use different classloaders — Java treats them as different runtime packages even with identical package names. Locally verified: 5.0.1, 5.1.4 (4.x plugin), 5.2.0, 5.5.1 (5.x plugin) all passed. --- .claude/skills/new-plugin/SKILL.md | 11 +++++++++ .gitignore | 1 + apm-sniffer/apm-sdk-plugin/CLAUDE.md | 19 +++++++++++++++ ...ongoClusterImplConstructorInterceptor.java | 4 +++- .../Java-Plugin-Development-Guide.md | 23 +++++++++++++++++++ 5 files changed, 57 insertions(+), 1 deletion(-) diff --git a/.claude/skills/new-plugin/SKILL.md b/.claude/skills/new-plugin/SKILL.md index 08dc73d0f1..d960d07058 100644 --- a/.claude/skills/new-plugin/SKILL.md +++ b/.claude/skills/new-plugin/SKILL.md @@ -57,6 +57,17 @@ Pick interception points based on these principles: **Principle 1: Data accessibility without reflection.** 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. +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: +```java +try { + java.lang.reflect.Method method = objInst.getClass().getMethod("publicMethodName"); + method.setAccessible(true); // Required for package-private class + Object result = method.invoke(objInst); +} catch (Exception e) { + LOGGER.warn("Failed to access method", e); +} +``` + **Principle 2: Use `EnhancedInstance` dynamic field to propagate context inside the library.** 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: - Store server address (peer) at connection/client creation time, retrieve it at command execution time diff --git a/.gitignore b/.gitignore index e127805e50..da77f29d42 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ packages/ /test/jacoco/classes /test/jacoco/*.exec test/jacoco +.claude/settings.local.json diff --git a/apm-sniffer/apm-sdk-plugin/CLAUDE.md b/apm-sniffer/apm-sdk-plugin/CLAUDE.md index eb88c1cbe1..b839fbe73b 100644 --- a/apm-sniffer/apm-sdk-plugin/CLAUDE.md +++ b/apm-sniffer/apm-sdk-plugin/CLAUDE.md @@ -145,6 +145,25 @@ public class MyPluginConfig { ``` Config key becomes: `plugin.myplugin.some_setting` +### Accessing Package-Private Classes + +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. + +**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`). + +**Solution: use `setAccessible` reflection** to call public methods on package-private classes: +```java +try { + java.lang.reflect.Method method = objInst.getClass().getMethod("publicMethodName"); + method.setAccessible(true); // Required for package-private class + Object result = method.invoke(objInst); +} catch (Exception e) { + LOGGER.warn("Failed to access method", e); +} +``` + +**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. + ### Dependency Management **Plugin dependencies must use `provided` scope:** diff --git a/apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/mongodb/v5/interceptor/MongoClusterImplConstructorInterceptor.java b/apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/mongodb/v5/interceptor/MongoClusterImplConstructorInterceptor.java index 98cf52ac5b..242b450c45 100644 --- a/apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/mongodb/v5/interceptor/MongoClusterImplConstructorInterceptor.java +++ b/apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/mongodb/v5/interceptor/MongoClusterImplConstructorInterceptor.java @@ -48,7 +48,9 @@ public void onConstruct(EnhancedInstance objInst, Object[] allArguments) { // The OperationExecutorImpl is created INSIDE this constructor (before onConstruct fires), // so its constructor interceptor couldn't read the peer yet. Set it now. - // MongoClusterImpl is package-private, access getOperationExecutor via reflection. + // MongoClusterImpl is package-private and loaded by application classloader. + // Same-package helpers from agent classloader cannot access it (different runtime packages). + // Use setAccessible reflection to call getOperationExecutor(). try { java.lang.reflect.Method getExecutor = objInst.getClass().getMethod("getOperationExecutor"); getExecutor.setAccessible(true); diff --git a/docs/en/setup/service-agent/java-agent/Java-Plugin-Development-Guide.md b/docs/en/setup/service-agent/java-agent/Java-Plugin-Development-Guide.md index 0498f2011d..201835a1dc 100644 --- a/docs/en/setup/service-agent/java-agent/Java-Plugin-Development-Guide.md +++ b/docs/en/setup/service-agent/java-agent/Java-Plugin-Development-Guide.md @@ -621,3 +621,26 @@ Please follow these steps: 1. Send a pull request and ask for review. 1. The plugin committers will approve your plugins, plugin CI-with-IT, e2e, and the plugin tests will be passed. 1. The plugin is accepted by SkyWalking. + +### Accessing package-private target classes + +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's `org.apache.skywalking` package. + +**Important:** Same-package helper classes do NOT work because the agent and application use different classloaders. Java treats them as different runtime packages even with identical package names, so package-private access is denied with `IllegalAccessError`. + +**Solution:** Use `setAccessible` reflection to call public methods on package-private classes: + +```java +try { + java.lang.reflect.Method method = objInst.getClass().getMethod("publicMethodName"); + method.setAccessible(true); // Required: class is package-private + Object result = method.invoke(objInst); + if (result instanceof EnhancedInstance) { + ((EnhancedInstance) result).setSkyWalkingDynamicField(value); + } +} catch (Exception e) { + logger.warn("Failed to access method", e); +} +``` + +**When to use:** Only when the target class is package-private and you need to call its public methods. Prefer normal casting in interceptors when the class is public. From 856a65682385da0a3a7f809133dec25183f3b110 Mon Sep 17 00:00:00 2001 From: Wu Sheng Date: Thu, 9 Apr 2026 17:27:39 +0800 Subject: [PATCH 4/5] Extend Jedis 4.x plugin to support Jedis 5.x Jedis 5.x is API-compatible with 4.x (all intercepted classes and methods unchanged). The only blocker was the witness method: Pipeline.persist(1 arg) was no longer declared on Pipeline in 5.x (moved to PipeliningBase parent). Fix: change witness to Connection.executeCommand(1 arg) which is declared in both 4.x and 5.x but not in 3.x, correctly distinguishing the versions. Resolves https://github.com/apache/skywalking/issues/11747 Locally verified: Jedis 5.2.0 passed. --- CHANGES.md | 1 + .../jedis/v4/define/AbstractWitnessInstrumentation.java | 7 +++++-- docs/en/setup/service-agent/java-agent/Supported-list.md | 2 +- .../scenarios/jedis-4.x-scenario/support-version.list | 1 + 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index bb89a9e688..ad1e3f6c51 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,6 +20,7 @@ Release Notes. * Extend Spring Kafka plugin to support Spring Kafka 2.4 -> 2.9 and 3.0 -> 3.3. * Enhance test/plugin/run.sh to support extra Maven properties per version in support-version.list (format: version,key=value). * Add MariaDB 3.x plugin (all classes renamed in 3.x). +* Extend Jedis 4.x plugin to support Jedis 5.x (fix witness method for 5.x compatibility). All issues and pull requests are [here](https://github.com/apache/skywalking/milestone/249?closed=1) diff --git a/apm-sniffer/apm-sdk-plugin/jedis-plugins/jedis-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jedis/v4/define/AbstractWitnessInstrumentation.java b/apm-sniffer/apm-sdk-plugin/jedis-plugins/jedis-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jedis/v4/define/AbstractWitnessInstrumentation.java index 8c736751a3..03e5dc6792 100644 --- a/apm-sniffer/apm-sdk-plugin/jedis-plugins/jedis-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jedis/v4/define/AbstractWitnessInstrumentation.java +++ b/apm-sniffer/apm-sdk-plugin/jedis-plugins/jedis-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jedis/v4/define/AbstractWitnessInstrumentation.java @@ -35,8 +35,11 @@ protected String[] witnessClasses() { @Override protected List witnessMethods() { + // Connection.executeCommand(CommandObject) exists in Jedis 4.x+ and 5.x, + // but not in 3.x. Previous witness Pipeline.persist(1) broke in 5.x + // because persist moved from Pipeline to PipeliningBase parent class. return Collections.singletonList(new WitnessMethod( - "redis.clients.jedis.Pipeline", - named("persist").and(takesArguments(1)))); + "redis.clients.jedis.Connection", + named("executeCommand").and(takesArguments(1)))); } } diff --git a/docs/en/setup/service-agent/java-agent/Supported-list.md b/docs/en/setup/service-agent/java-agent/Supported-list.md index 0a74828b0e..772f169c39 100644 --- a/docs/en/setup/service-agent/java-agent/Supported-list.md +++ b/docs/en/setup/service-agent/java-agent/Supported-list.md @@ -88,7 +88,7 @@ metrics based on the tracing data. * NoSQL * [aerospike](https://github.com/aerospike/aerospike-client-java) 3.x -> 6.x * Redis - * [Jedis](https://github.com/xetorthio/jedis) 2.x-4.x + * [Jedis](https://github.com/xetorthio/jedis) 2.x-5.x * [Redisson](https://github.com/redisson/redisson) Easy Java Redis client 3.5.0 -> 3.30.0 * [Lettuce](https://github.com/lettuce-io/lettuce-core) 5.x -> 6.7.1 * [MongoDB Java Driver](https://github.com/mongodb/mongo-java-driver) 2.13-2.14, 3.4.0-3.12.7, 4.0.0-5.5.x diff --git a/test/plugin/scenarios/jedis-4.x-scenario/support-version.list b/test/plugin/scenarios/jedis-4.x-scenario/support-version.list index 31258966c3..7545c1ec00 100644 --- a/test/plugin/scenarios/jedis-4.x-scenario/support-version.list +++ b/test/plugin/scenarios/jedis-4.x-scenario/support-version.list @@ -19,3 +19,4 @@ 4.2.3 4.1.1 4.0.1 +5.2.0 From 98d39fa955e5f34ea1387f0f6ef38c56c798fb1f Mon Sep 17 00:00:00 2001 From: Wu Sheng Date: Thu, 9 Apr 2026 17:56:22 +0800 Subject: [PATCH 5/5] Fix Spring MVC plugin: check javax and jakarta servlet independently When both javax.servlet and jakarta.servlet exist on the classpath (e.g., Spring MVC 6.x app with javax.servlet as transitive dependency), the exclusive if/else in the static initializer set IS_JAVAX=true and skipped the Jakarta check. At runtime, the request was jakarta type but IS_JAKARTA was false, causing IllegalStateException("this line should not be reached"). Fix: check both servlet APIs independently. Both IS_JAVAX and IS_JAKARTA can be true. The runtime isAssignableFrom checks on the actual request object type already select the correct branch. --- .../AbstractMethodInterceptor.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/mvc-annotation-commons/src/main/java/org/apache/skywalking/apm/plugin/spring/mvc/commons/interceptor/AbstractMethodInterceptor.java b/apm-sniffer/apm-sdk-plugin/spring-plugins/mvc-annotation-commons/src/main/java/org/apache/skywalking/apm/plugin/spring/mvc/commons/interceptor/AbstractMethodInterceptor.java index ac51e60b93..67f2709c8f 100644 --- a/apm-sniffer/apm-sdk-plugin/spring-plugins/mvc-annotation-commons/src/main/java/org/apache/skywalking/apm/plugin/spring/mvc/commons/interceptor/AbstractMethodInterceptor.java +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/mvc-annotation-commons/src/main/java/org/apache/skywalking/apm/plugin/spring/mvc/commons/interceptor/AbstractMethodInterceptor.java @@ -74,19 +74,21 @@ public abstract class AbstractMethodInterceptor implements InstanceMethodsAround AbstractMethodInterceptor.class.getClassLoader(), JAKARTA_SERVLET_RESPONSE_CLASS, GET_STATUS_METHOD ); + // Check both javax and jakarta independently — both may exist on the classpath. + // For example, a Spring MVC 6.x (Jakarta) app may have javax.servlet as a + // transitive dependency. The runtime request type determines which path is used. try { Class.forName(SERVLET_RESPONSE_CLASS, true, AbstractMethodInterceptor.class.getClassLoader()); - IN_SERVLET_CONTAINER = true; IS_JAVAX = true; + IN_SERVLET_CONTAINER = true; + } catch (Exception ignore) { + } + try { + Class.forName( + JAKARTA_SERVLET_RESPONSE_CLASS, true, AbstractMethodInterceptor.class.getClassLoader()); + IS_JAKARTA = true; + IN_SERVLET_CONTAINER = true; } catch (Exception ignore) { - try { - Class.forName( - JAKARTA_SERVLET_RESPONSE_CLASS, true, AbstractMethodInterceptor.class.getClassLoader()); - IN_SERVLET_CONTAINER = true; - IS_JAKARTA = true; - } catch (Exception ignore2) { - IN_SERVLET_CONTAINER = false; - } } }