Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .claude/skills/new-plugin/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/plugins-test.1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ packages/
/test/jacoco/classes
/test/jacoco/*.exec
test/jacoco
.claude/settings.local.json
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
19 changes: 19 additions & 0 deletions apm-sniffer/apm-sdk-plugin/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ protected String[] witnessClasses() {

@Override
protected List<WitnessMethod> 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))));
}
}
50 changes: 50 additions & 0 deletions apm-sniffer/apm-sdk-plugin/mongodb-5.x-plugin/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
~
-->

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<artifactId>apm-sdk-plugin</artifactId>
<groupId>org.apache.skywalking</groupId>
<version>9.7.0-SNAPSHOT</version>
</parent>

<artifactId>apm-mongodb-5.x-plugin</artifactId>
<packaging>jar</packaging>

<name>mongodb-5.x-plugin</name>

<dependencies>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId>
<version>5.2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-mongodb-4.x-plugin</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -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<MethodDescription> 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<MethodDescription> getMethodsMatcher() {
return named("getOperationExecutor");
}

@Override
public String getMethodsInterceptor() {
return CONSTRUCTOR_INTERCEPTOR;
}

@Override
public boolean isOverrideArgs() {
return false;
}
}
};
}
}
Original file line number Diff line number Diff line change
@@ -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+.
* <p>
* Constructor interception: propagate remotePeer from enclosing MongoClusterImpl
* (synthetic arg[0] for non-static inner class).
* <p>
* 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<MethodDescription> getConstructorMatcher() {
return ElementMatchers.any();
}

@Override
public String getConstructorInterceptor() {
return CONSTRUCTOR_INTERCEPTOR;
}
}
};
}

@Override
public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
return new InstanceMethodsInterceptPoint[] {
new InstanceMethodsInterceptPoint() {
@Override
public ElementMatcher<MethodDescription> getMethodsMatcher() {
return ElementMatchers
.named(METHOD_NAME)
.and(ArgumentTypeNameMatch.takesArgumentWithType(2, ARGUMENT_TYPE))
.or(ElementMatchers.<MethodDescription>named(METHOD_NAME)
.and(ArgumentTypeNameMatch.takesArgumentWithType(3, ARGUMENT_TYPE)));
}

@Override
public String getMethodsInterceptor() {
return EXECUTE_INTERCEPTOR;
}

@Override
public boolean isOverrideArgs() {
return false;
}
}
};
}
}
Loading
Loading