diff --git a/CLAUDE.md b/CLAUDE.md index 90e3bf0f6c..02c69c17e3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -110,11 +110,12 @@ The agent uses ByteBuddy for bytecode manipulation at runtime: **1. SDK Plugins** (`apm-sniffer/apm-sdk-plugin/`) - Framework-specific instrumentations (70+ plugins) - Examples: grpc-1.x, spring, dubbo, mybatis, mongodb, redis, etc. -- Pattern: One directory per library/framework version +- See `apm-sniffer/apm-sdk-plugin/CLAUDE.md` for plugin development guide **2. Bootstrap Plugins** (`apm-sniffer/bootstrap-plugins/`) - Load at JVM bootstrap phase for JDK-level instrumentation - Examples: jdk-threading, jdk-http, jdk-httpclient, jdk-virtual-thread-executor +- See `apm-sniffer/bootstrap-plugins/CLAUDE.md` for bootstrap plugin guide **3. Optional Plugins** (`apm-sniffer/optional-plugins/`) - Not included by default, user must copy to plugins directory @@ -122,237 +123,6 @@ The agent uses ByteBuddy for bytecode manipulation at runtime: **4. Optional Reporter Plugins** (`apm-sniffer/optional-reporter-plugins/`) - Alternative data collection backends (e.g., Kafka) -### Plugin Instrumentation APIs (v1 vs v2) - -The agent provides two instrumentation APIs. **V2 is recommended** for all new plugins; v1 is legacy and should only be used for maintaining existing plugins. - -#### V2 API (Recommended) - -V2 provides a `MethodInvocationContext` that is shared across all interception phases (`beforeMethod`, `afterMethod`, `handleMethodException`), allowing you to pass data (e.g., spans) between phases. - -**Instrumentation class (extends `ClassEnhancePluginDefineV2`):** -```java -public class XxxInstrumentation extends ClassInstanceMethodsEnhancePluginDefineV2 { - @Override - protected ClassMatch enhanceClass() { - return NameMatch.byName("target.class.Name"); - } - - @Override - public ConstructorInterceptPoint[] getConstructorsInterceptPoints() { - return new ConstructorInterceptPoint[] { ... }; - } - - @Override - public InstanceMethodsInterceptV2Point[] getInstanceMethodsInterceptV2Points() { - return new InstanceMethodsInterceptV2Point[] { - new InstanceMethodsInterceptV2Point() { - @Override - public ElementMatcher getMethodsMatcher() { - return named("targetMethod"); - } - - @Override - public String getMethodsInterceptorV2() { - return "org.apache.skywalking.apm.plugin.xxx.XxxInterceptor"; - } - - @Override - public boolean isOverrideArgs() { - return false; - } - } - }; - } -} -``` - -**Interceptor class (implements `InstanceMethodsAroundInterceptorV2`):** -```java -public class XxxInterceptor implements InstanceMethodsAroundInterceptorV2 { - @Override - public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, - Class[] argumentsTypes, MethodInvocationContext context) { - // Create span and store in context for later use - AbstractSpan span = ContextManager.createLocalSpan("operationName"); - context.setContext(span); // Pass to afterMethod/handleMethodException - } - - @Override - public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, - Class[] argumentsTypes, Object ret, MethodInvocationContext context) { - // Retrieve span from context - AbstractSpan span = (AbstractSpan) context.getContext(); - span.asyncFinish(); - return ret; - } - - @Override - public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, - Class[] argumentsTypes, Throwable t, MethodInvocationContext context) { - AbstractSpan span = (AbstractSpan) context.getContext(); - span.log(t); - } -} -``` - -**Key V2 classes:** -- `ClassEnhancePluginDefineV2` - Base class for plugins with both instance and static methods -- `ClassInstanceMethodsEnhancePluginDefineV2` - For instance methods only -- `ClassStaticMethodsEnhancePluginDefineV2` - For static methods only -- `InstanceMethodsAroundInterceptorV2` - Interceptor interface with `MethodInvocationContext` -- `StaticMethodsAroundInterceptorV2` - Static method interceptor with context - -#### V1 API (Legacy) - -V1 uses `MethodInterceptResult` only in `beforeMethod` and has no shared context between phases. **Only use for maintaining existing legacy plugins.** - -**Key V1 classes (legacy):** -- `ClassEnhancePluginDefine` -- `ClassInstanceMethodsEnhancePluginDefine` -- `ClassStaticMethodsEnhancePluginDefine` -- `InstanceMethodsAroundInterceptor` -- `StaticMethodsAroundInterceptor` - -### Plugin Development Rules - -#### Class Matching Restrictions - -**CRITICAL: Never use `.class` references in instrumentation definitions:** -```java -// WRONG - will break the agent if ThirdPartyClass doesn't exist -takesArguments(ThirdPartyClass.class) -byName(ThirdPartyClass.class.getName()) - -// CORRECT - use string literals -takesArguments("com.example.ThirdPartyClass") -byName("com.example.ThirdPartyClass") -``` - -**ClassMatch options:** -- `byName(String)`: Match by full class name (package + class name) - **preferred** -- `byClassAnnotationMatch`: Match classes with specific annotations (does NOT support inherited annotations) -- `byMethodAnnotationMatch`: Match classes with methods having specific annotations -- `byHierarchyMatch`: Match by parent class/interface - **avoid unless necessary** (performance impact) - -#### Witness Classes/Methods - -Use witness classes/methods to activate plugins only for specific library versions: -```java -@Override -protected String[] witnessClasses() { - return new String[] { "com.example.VersionSpecificClass" }; -} - -@Override -protected List witnessMethods() { - return Collections.singletonList( - new WitnessMethod("com.example.SomeClass", ElementMatchers.named("specificMethod")) - ); -} -``` - -#### Bootstrap Instrumentation - -For JDK core classes (rt.jar), override `isBootstrapInstrumentation()`: -```java -@Override -public boolean isBootstrapInstrumentation() { - return true; -} -``` -**WARNING**: Use bootstrap instrumentation only where absolutely necessary. - -#### Plugin Configuration - -Use `@PluginConfig` annotation for custom plugin settings: -```java -public class MyPluginConfig { - public static class Plugin { - @PluginConfig(root = MyPluginConfig.class) - public static class MyPlugin { - public static boolean SOME_SETTING = false; - } - } -} -``` -Config key becomes: `plugin.myplugin.some_setting` - -#### Dependency Management - -**Plugin dependencies must use `provided` scope:** -```xml - - com.example - target-library - ${version} - provided - -``` - -**Agent core dependency policy:** -- New dependencies in agent core are treated with extreme caution -- Prefer using existing imported libraries already in the project -- Prefer JDK standard libraries over third-party libraries -- Plugins should rely on the target application's libraries (provided scope), not bundle them - -### Tracing Concepts - -#### Span Types -- **EntrySpan**: Service provider/endpoint (HTTP server, MQ consumer) -- **LocalSpan**: Internal method (no remote calls) -- **ExitSpan**: Client call (HTTP client, DB access, MQ producer) - -#### SpanLayer (required for EntrySpan/ExitSpan) -- `DB`: Database access -- `RPC_FRAMEWORK`: RPC calls (not ordinary HTTP) -- `HTTP`: HTTP calls -- `MQ`: Message queue -- `UNKNOWN`: Default - -#### Context Propagation -- **ContextCarrier**: Cross-process propagation (serialize to headers/attachments) -- **ContextSnapshot**: Cross-thread propagation (in-memory, no serialization) - -#### Required Span Attributes -For EntrySpan and ExitSpan, always set: -```java -span.setComponent(ComponentsDefine.YOUR_COMPONENT); -span.setLayer(SpanLayer.HTTP); // or DB, MQ, RPC_FRAMEWORK -``` - -#### Special Tags for OAP Analysis -| Tag | Purpose | -|-----|---------| -| `http.status_code` | HTTP response code (integer) | -| `db.type` | Database type (e.g., "sql", "redis") | -| `db.statement` | SQL/query statement (enables slow query analysis) | -| `cache.type`, `cache.op`, `cache.cmd`, `cache.key` | Cache metrics | -| `mq.queue`, `mq.topic` | MQ metrics | - -### Meter Plugin APIs - -For collecting numeric metrics (alternative to tracing): -```java -// Counter -Counter counter = MeterFactory.counter("metric_name") - .tag("key", "value") - .mode(Counter.Mode.INCREMENT) - .build(); -counter.increment(1d); - -// Gauge -Gauge gauge = MeterFactory.gauge("metric_name", () -> getValue()) - .tag("key", "value") - .build(); - -// Histogram -Histogram histogram = MeterFactory.histogram("metric_name") - .steps(Arrays.asList(1, 5, 10)) - .build(); -histogram.addValue(3); -``` - ### Data Flow 1. Agent attaches to JVM via `-javaagent` flag 2. ByteBuddy transforms target classes at load time @@ -411,6 +181,7 @@ Use Lombok annotations for boilerplate code: - 100+ test scenarios for plugin validation - Docker-based testing with actual frameworks - Pattern: `{framework}-{version}-scenario` +- See `apm-sniffer/apm-sdk-plugin/CLAUDE.md` for full test framework documentation **End-to-End Tests** (`test/e2e/`) - Full system integration testing @@ -427,165 +198,6 @@ Use Lombok annotations for boilerplate code: ./mvnw package -Dmaven.test.skip=true ``` -### Plugin Test Framework - -The plugin test framework verifies plugin functionality using Docker containers with real services and a mock OAP backend. - -#### Environment Requirements -- MacOS/Linux -- JDK 8+ -- Docker & Docker Compose - -#### Test Case Structure - -**JVM-container (preferred):** -``` -{scenario}-scenario/ -├── bin/ -│ └── startup.sh # JVM startup script (required) -├── config/ -│ └── expectedData.yaml # Expected trace/meter/log data -├── src/main/java/... # Test application code -├── pom.xml -├── configuration.yml # Test case configuration -└── support-version.list # Supported versions (one per line) -``` - -**Tomcat-container:** -``` -{scenario}-scenario/ -├── config/ -│ └── expectedData.yaml -├── src/main/ -│ ├── java/... -│ └── webapp/WEB-INF/web.xml -├── pom.xml -├── configuration.yml -└── support-version.list -``` - -#### Key Configuration Files - -**configuration.yml:** -```yaml -type: jvm # or tomcat -entryService: http://localhost:8080/case # Entry endpoint (GET) -healthCheck: http://localhost:8080/health # Health check endpoint (HEAD) -startScript: ./bin/startup.sh # JVM-container only -runningMode: default # default|with_optional|with_bootstrap -withPlugins: apm-spring-annotation-plugin-*.jar # For optional/bootstrap modes -environment: - - KEY=value -dependencies: # External services (docker-compose style) - mysql: - image: mysql:8.0 - hostname: mysql - environment: - - MYSQL_ROOT_PASSWORD=root -``` - -**support-version.list:** -``` -# One version per line, use # for comments -# Only include ONE version per minor version (not all patch versions) -4.3.6 -4.4.1 -4.5.0 -``` - -**expectedData.yaml:** - -Trace and meter expectations are typically in separate scenarios. - -*For tracing plugins:* -```yaml -segmentItems: - - serviceName: your-scenario - segmentSize: ge 1 # Operators: eq, ge, gt, nq - segments: - - segmentId: not null - spans: - - operationName: /your/endpoint - parentSpanId: -1 # -1 for root span - spanId: 0 - spanLayer: Http # Http, DB, RPC_FRAMEWORK, MQ, CACHE, Unknown - spanType: Entry # Entry, Exit, Local - startTime: nq 0 - endTime: nq 0 - componentId: 1 - isError: false - peer: '' # Empty string for Entry/Local, required for Exit - skipAnalysis: false - tags: - - {key: url, value: not null} - - {key: http.method, value: GET} - - {key: http.status_code, value: '200'} - logs: [] - refs: [] # SegmentRefs for cross-process/cross-thread -``` - -*For meter plugins:* -```yaml -meterItems: - - serviceName: your-scenario - meterSize: ge 1 - meters: - - meterId: - name: test_counter - tags: - - {name: key1, value: value1} # Note: uses 'name' not 'key' - singleValue: gt 0 # For counter/gauge - - meterId: - name: test_histogram - tags: - - {name: key1, value: value1} - histogramBuckets: # For histogram - - 0.0 - - 1.0 - - 5.0 - - 10.0 -``` - -**startup.sh (JVM-container):** -```bash -#!/bin/bash -home="$(cd "$(dirname $0)"; pwd)" -# ${agent_opts} is REQUIRED - contains agent parameters -java -jar ${agent_opts} ${home}/../libs/your-scenario.jar & -``` - -#### Running Plugin Tests Locally - -```bash -# Run a specific scenario -bash ./test/plugin/run.sh -f {scenario_name} - -# IMPORTANT: Rebuild agent if apm-sniffer code changed -./mvnw clean package -DskipTests -pl apm-sniffer - -# Use generator to create new test case -bash ./test/plugin/generator.sh -``` - -#### Adding Tests to CI - -Add scenario to the appropriate `.github/workflows/` file: -- Use `python3 tools/select-group.py` to find the file with fewest cases -- **JDK 8 tests**: `plugins-test..yaml` -- **JDK 17 tests**: `plugins-jdk17-test..yaml` -- **JDK 21 tests**: `plugins-jdk21-test..yaml` -- **JDK 25 tests**: `plugins-jdk25-test..yaml` - -```yaml -matrix: - case: - - your-scenario-scenario -``` - -#### Test Code Package Naming -- Test code: `org.apache.skywalking.apm.testcase.*` -- Code to be instrumented: `test.org.apache.skywalking.apm.testcase.*` - ## Git Submodules The project uses submodules for protocol definitions: @@ -616,11 +228,7 @@ git submodule init && git submodule update ## Common Development Tasks ### Adding a New Plugin -1. Create directory in `apm-sniffer/apm-sdk-plugin/{framework}-{version}-plugin/` -2. Implement instrumentation class using **V2 API** (e.g., extend `ClassInstanceMethodsEnhancePluginDefineV2`) -3. Implement interceptor class using **V2 API** (e.g., implement `InstanceMethodsAroundInterceptorV2`) -4. Register plugin in `skywalking-plugin.def` file -5. Add test scenario in `test/plugin/scenarios/` +See `apm-sniffer/apm-sdk-plugin/CLAUDE.md` for detailed guide. ### Adding an Optional Plugin 1. Create in `apm-sniffer/optional-plugins/` @@ -679,19 +287,10 @@ GitHub Actions workflows: ## Tips for AI Assistants -1. **Use V2 instrumentation API**: Always use V2 classes (`ClassEnhancePluginDefineV2`, `InstanceMethodsAroundInterceptorV2`) for new plugins; V1 is legacy -2. **NEVER use `.class` references**: In instrumentation definitions, always use string literals for class names (e.g., `byName("com.example.MyClass")` not `byName(MyClass.class.getName())`) -3. **Always set component and layer**: For EntrySpan and ExitSpan, always call `setComponent()` and `setLayer()` -4. **Prefer `byName` for class matching**: Avoid `byHierarchyMatch` unless necessary (causes performance issues) -5. **Use witness classes for version-specific plugins**: Implement `witnessClasses()` or `witnessMethods()` to activate plugins only for specific library versions -6. **Always check submodules**: Protocol changes may require submodule updates -7. **Generate sources first**: Run `mvnw compile` before analyzing generated code -8. **Respect checkstyle**: No System.out, no @author, no Chinese characters -9. **Follow plugin patterns**: Use existing V2 plugins as templates -10. **Use Lombok**: Prefer annotations over boilerplate code -11. **Test both unit and E2E**: Different test patterns for different scopes -12. **Plugin naming**: Follow `{framework}-{version}-plugin` convention -13. **Shaded dependencies**: Core dependencies are shaded to avoid classpath conflicts -14. **Java version compatibility**: Agent core must maintain Java 8 compatibility, but individual plugins may target higher JDK versions (e.g., jdk-httpclient-plugin for JDK 11+, virtual-thread plugins for JDK 21+) -15. **Bootstrap instrumentation**: Only use for JDK core classes, and only when absolutely necessary -16. **Register plugins**: Always add plugin definition to `skywalking-plugin.def` file +1. **Always check submodules**: Protocol changes may require submodule updates +2. **Generate sources first**: Run `mvnw compile` before analyzing generated code +3. **Respect checkstyle**: No System.out, no @author, no Chinese characters +4. **Use Lombok**: Prefer annotations over boilerplate code +5. **Test both unit and E2E**: Different test patterns for different scopes +6. **Java version compatibility**: Agent core must maintain Java 8 compatibility, but individual plugins may target higher JDK versions (e.g., jdk-httpclient-plugin for JDK 11+, virtual-thread plugins for JDK 21+) +7. **For plugin development**: See `apm-sniffer/apm-sdk-plugin/CLAUDE.md` and `apm-sniffer/bootstrap-plugins/CLAUDE.md` diff --git a/apm-sniffer/apm-sdk-plugin/CLAUDE.md b/apm-sniffer/apm-sdk-plugin/CLAUDE.md new file mode 100644 index 0000000000..a0c73a2a04 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/CLAUDE.md @@ -0,0 +1,401 @@ +# CLAUDE.md - SDK Plugin Development Guide + +This guide covers developing standard SDK plugins in `apm-sniffer/apm-sdk-plugin/`. + +## Plugin Instrumentation APIs (v1 vs v2) + +The agent provides two instrumentation APIs. **V2 is recommended** for all new plugins; v1 is legacy and should only be used for maintaining existing plugins. + +### V2 API (Recommended) + +V2 provides a `MethodInvocationContext` that is shared across all interception phases (`beforeMethod`, `afterMethod`, `handleMethodException`), allowing you to pass data (e.g., spans) between phases. + +**Instrumentation class (extends `ClassEnhancePluginDefineV2`):** +```java +public class XxxInstrumentation extends ClassInstanceMethodsEnhancePluginDefineV2 { + @Override + protected ClassMatch enhanceClass() { + return NameMatch.byName("target.class.Name"); + } + + @Override + public ConstructorInterceptPoint[] getConstructorsInterceptPoints() { + return new ConstructorInterceptPoint[] { ... }; + } + + @Override + public InstanceMethodsInterceptV2Point[] getInstanceMethodsInterceptV2Points() { + return new InstanceMethodsInterceptV2Point[] { + new InstanceMethodsInterceptV2Point() { + @Override + public ElementMatcher getMethodsMatcher() { + return named("targetMethod"); + } + + @Override + public String getMethodsInterceptorV2() { + return "org.apache.skywalking.apm.plugin.xxx.XxxInterceptor"; + } + + @Override + public boolean isOverrideArgs() { + return false; + } + } + }; + } +} +``` + +**Interceptor class (implements `InstanceMethodsAroundInterceptorV2`):** +```java +public class XxxInterceptor implements InstanceMethodsAroundInterceptorV2 { + @Override + public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, + Class[] argumentsTypes, MethodInvocationContext context) { + AbstractSpan span = ContextManager.createLocalSpan("operationName"); + context.setContext(span); // Pass to afterMethod/handleMethodException + } + + @Override + public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, + Class[] argumentsTypes, Object ret, MethodInvocationContext context) { + AbstractSpan span = (AbstractSpan) context.getContext(); + span.asyncFinish(); + return ret; + } + + @Override + public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, + Class[] argumentsTypes, Throwable t, MethodInvocationContext context) { + AbstractSpan span = (AbstractSpan) context.getContext(); + span.log(t); + } +} +``` + +**Key V2 classes:** +- `ClassEnhancePluginDefineV2` - Base class for plugins with both instance and static methods +- `ClassInstanceMethodsEnhancePluginDefineV2` - For instance methods only +- `ClassStaticMethodsEnhancePluginDefineV2` - For static methods only +- `InstanceMethodsAroundInterceptorV2` - Interceptor interface with `MethodInvocationContext` +- `StaticMethodsAroundInterceptorV2` - Static method interceptor with context + +### V1 API (Legacy) + +V1 uses `MethodInterceptResult` only in `beforeMethod` and has no shared context between phases. **Only use for maintaining existing legacy plugins.** + +**Key V1 classes (legacy):** +- `ClassEnhancePluginDefine` +- `ClassInstanceMethodsEnhancePluginDefine` +- `ClassStaticMethodsEnhancePluginDefine` +- `InstanceMethodsAroundInterceptor` +- `StaticMethodsAroundInterceptor` + +## Plugin Development Rules + +### Class Matching Restrictions + +**CRITICAL: Never use `.class` references in instrumentation definitions:** +```java +// WRONG - will break the agent if ThirdPartyClass doesn't exist +takesArguments(ThirdPartyClass.class) +byName(ThirdPartyClass.class.getName()) + +// CORRECT - use string literals +takesArguments("com.example.ThirdPartyClass") +byName("com.example.ThirdPartyClass") +``` + +**ClassMatch options:** +- `byName(String)`: Match by full class name (package + class name) - **preferred** +- `byClassAnnotationMatch`: Match classes with specific annotations (does NOT support inherited annotations) +- `byMethodAnnotationMatch`: Match classes with methods having specific annotations +- `byHierarchyMatch`: Match by parent class/interface - **avoid unless necessary** (performance impact) + +### Witness Classes/Methods + +Use witness classes/methods to activate plugins only for specific library versions: +```java +@Override +protected String[] witnessClasses() { + return new String[] { "com.example.VersionSpecificClass" }; +} + +@Override +protected List witnessMethods() { + return Collections.singletonList( + new WitnessMethod("com.example.SomeClass", ElementMatchers.named("specificMethod")) + ); +} +``` + +### Plugin Configuration + +Use `@PluginConfig` annotation for custom plugin settings: +```java +public class MyPluginConfig { + public static class Plugin { + @PluginConfig(root = MyPluginConfig.class) + public static class MyPlugin { + public static boolean SOME_SETTING = false; + } + } +} +``` +Config key becomes: `plugin.myplugin.some_setting` + +### Dependency Management + +**Plugin dependencies must use `provided` scope:** +```xml + + com.example + target-library + ${version} + provided + +``` + +**Agent core dependency policy:** +- New dependencies in agent core are treated with extreme caution +- Prefer using existing imported libraries already in the project +- Prefer JDK standard libraries over third-party libraries +- Plugins should rely on the target application's libraries (provided scope), not bundle them + +## Tracing Concepts + +### Span Types +- **EntrySpan**: Service provider/endpoint (HTTP server, MQ consumer) +- **LocalSpan**: Internal method (no remote calls) +- **ExitSpan**: Client call (HTTP client, DB access, MQ producer) + +### SpanLayer (required for EntrySpan/ExitSpan) +- `DB`: Database access +- `RPC_FRAMEWORK`: RPC calls (not ordinary HTTP) +- `HTTP`: HTTP calls +- `MQ`: Message queue +- `UNKNOWN`: Default + +### Context Propagation +- **ContextCarrier**: Cross-process propagation (serialize to headers/attachments) +- **ContextSnapshot**: Cross-thread propagation (in-memory, no serialization) + +### Required Span Attributes +For EntrySpan and ExitSpan, always set: +```java +span.setComponent(ComponentsDefine.YOUR_COMPONENT); +span.setLayer(SpanLayer.HTTP); // or DB, MQ, RPC_FRAMEWORK +``` + +### Special Tags for OAP Analysis +| Tag | Purpose | +|-----|---------| +| `http.status_code` | HTTP response code (integer) | +| `db.type` | Database type (e.g., "sql", "redis") | +| `db.statement` | SQL/query statement (enables slow query analysis) | +| `cache.type`, `cache.op`, `cache.cmd`, `cache.key` | Cache metrics | +| `mq.queue`, `mq.topic` | MQ metrics | + +## Meter Plugin APIs + +For collecting numeric metrics (alternative to tracing): +```java +// Counter +Counter counter = MeterFactory.counter("metric_name") + .tag("key", "value") + .mode(Counter.Mode.INCREMENT) + .build(); +counter.increment(1d); + +// Gauge +Gauge gauge = MeterFactory.gauge("metric_name", () -> getValue()) + .tag("key", "value") + .build(); + +// Histogram +Histogram histogram = MeterFactory.histogram("metric_name") + .steps(Arrays.asList(1, 5, 10)) + .build(); +histogram.addValue(3); +``` + +## Adding a New SDK Plugin + +1. Create directory: `apm-sniffer/apm-sdk-plugin/{framework}-{version}-plugin/` +2. Implement instrumentation class using **V2 API** (extend `ClassInstanceMethodsEnhancePluginDefineV2`) +3. Implement interceptor class using **V2 API** (implement `InstanceMethodsAroundInterceptorV2`) +4. Register plugin in `skywalking-plugin.def` file +5. Add test scenario in `test/plugin/scenarios/` + +## Plugin Test Framework + +The plugin test framework verifies plugin functionality using Docker containers with real services and a mock OAP backend. + +### Environment Requirements +- MacOS/Linux +- JDK 8+ +- Docker & Docker Compose + +### Test Case Structure + +**JVM-container (preferred):** +``` +{scenario}-scenario/ +├── bin/ +│ └── startup.sh # JVM startup script (required) +├── config/ +│ └── expectedData.yaml # Expected trace/meter/log data +├── src/main/java/... # Test application code +├── pom.xml +├── configuration.yml # Test case configuration +└── support-version.list # Supported versions (one per line) +``` + +**Tomcat-container:** +``` +{scenario}-scenario/ +├── config/ +│ └── expectedData.yaml +├── src/main/ +│ ├── java/... +│ └── webapp/WEB-INF/web.xml +├── pom.xml +├── configuration.yml +└── support-version.list +``` + +### Key Configuration Files + +**configuration.yml:** +```yaml +type: jvm # or tomcat +entryService: http://localhost:8080/case # Entry endpoint (GET) +healthCheck: http://localhost:8080/health # Health check endpoint (HEAD) +startScript: ./bin/startup.sh # JVM-container only +runningMode: default # default|with_optional|with_bootstrap +withPlugins: apm-spring-annotation-plugin-*.jar # For optional/bootstrap modes +environment: + - KEY=value +dependencies: # External services (docker-compose style) + mysql: + image: mysql:8.0 + hostname: mysql + environment: + - MYSQL_ROOT_PASSWORD=root +``` + +**support-version.list:** +``` +# One version per line, use # for comments +# Only include ONE version per minor version (not all patch versions) +4.3.6 +4.4.1 +4.5.0 +``` + +**expectedData.yaml:** + +Trace and meter expectations are typically in separate scenarios. + +*For tracing plugins:* +```yaml +segmentItems: + - serviceName: your-scenario + segmentSize: ge 1 # Operators: eq, ge, gt, nq + segments: + - segmentId: not null + spans: + - operationName: /your/endpoint + parentSpanId: -1 # -1 for root span + spanId: 0 + spanLayer: Http # Http, DB, RPC_FRAMEWORK, MQ, CACHE, Unknown + spanType: Entry # Entry, Exit, Local + startTime: nq 0 + endTime: nq 0 + componentId: 1 + isError: false + peer: '' # Empty string for Entry/Local, required for Exit + skipAnalysis: false + tags: + - {key: url, value: not null} + - {key: http.method, value: GET} + - {key: http.status_code, value: '200'} + logs: [] + refs: [] # SegmentRefs for cross-process/cross-thread +``` + +*For meter plugins:* +```yaml +meterItems: + - serviceName: your-scenario + meterSize: ge 1 + meters: + - meterId: + name: test_counter + tags: + - {name: key1, value: value1} # Note: uses 'name' not 'key' + singleValue: gt 0 # For counter/gauge + - meterId: + name: test_histogram + tags: + - {name: key1, value: value1} + histogramBuckets: # For histogram + - 0.0 + - 1.0 + - 5.0 + - 10.0 +``` + +**startup.sh (JVM-container):** +```bash +#!/bin/bash +home="$(cd "$(dirname $0)"; pwd)" +# ${agent_opts} is REQUIRED - contains agent parameters +java -jar ${agent_opts} ${home}/../libs/your-scenario.jar & +``` + +### Running Plugin Tests Locally + +```bash +# Run a specific scenario +bash ./test/plugin/run.sh -f {scenario_name} + +# IMPORTANT: Rebuild agent if apm-sniffer code changed +./mvnw clean package -DskipTests -pl apm-sniffer + +# Use generator to create new test case +bash ./test/plugin/generator.sh +``` + +### Adding Tests to CI + +Add scenario to the appropriate `.github/workflows/` file: +- Use `python3 tools/select-group.py` to find the file with fewest cases +- **JDK 8 tests**: `plugins-test..yaml` +- **JDK 17 tests**: `plugins-jdk17-test..yaml` +- **JDK 21 tests**: `plugins-jdk21-test..yaml` +- **JDK 25 tests**: `plugins-jdk25-test..yaml` + +```yaml +matrix: + case: + - your-scenario-scenario +``` + +### Test Code Package Naming +- Test code: `org.apache.skywalking.apm.testcase.*` +- Code to be instrumented: `test.org.apache.skywalking.apm.testcase.*` + +## Tips for AI Assistants + +1. **Use V2 instrumentation API**: Always use V2 classes for new plugins; V1 is legacy +2. **NEVER use `.class` references**: Always use string literals for class names +3. **Always set component and layer**: For EntrySpan and ExitSpan, always call `setComponent()` and `setLayer()` +4. **Prefer `byName` for class matching**: Avoid `byHierarchyMatch` unless necessary (performance impact) +5. **Use witness classes for version-specific plugins**: Implement `witnessClasses()` or `witnessMethods()` +6. **Follow plugin patterns**: Use existing V2 plugins as templates +7. **Plugin naming**: Follow `{framework}-{version}-plugin` convention +8. **Register plugins**: Always add plugin definition to `skywalking-plugin.def` file +9. **Java version compatibility**: Agent core must maintain Java 8 compatibility, but individual plugins may target higher JDK versions +10. **Shaded dependencies**: Core dependencies are shaded to avoid classpath conflicts diff --git a/apm-sniffer/bootstrap-plugins/CLAUDE.md b/apm-sniffer/bootstrap-plugins/CLAUDE.md new file mode 100644 index 0000000000..51b4db99e7 --- /dev/null +++ b/apm-sniffer/bootstrap-plugins/CLAUDE.md @@ -0,0 +1,53 @@ +# CLAUDE.md - Bootstrap Plugin Development Guide + +This guide covers developing bootstrap-level plugins in `apm-sniffer/bootstrap-plugins/`. + +For general plugin development concepts (V2 API, tracing, class matching, testing), see `apm-sniffer/apm-sdk-plugin/CLAUDE.md`. + +## What Are Bootstrap Plugins? + +Bootstrap plugins instrument JDK core classes (rt.jar / java.base module) at the JVM bootstrap phase. They are loaded before application classes and can intercept fundamental JDK APIs. + +**Examples:** +- `jdk-threading-plugin` - Thread pool context propagation +- `jdk-http-plugin` - `HttpURLConnection` instrumentation +- `jdk-httpclient-plugin` - JDK 11+ `HttpClient` instrumentation +- `jdk-virtual-thread-executor-plugin` - JDK 21+ virtual thread support +- `jdk-forkjoinpool-plugin` - `ForkJoinPool` instrumentation + +## Key Difference from SDK Plugins + +Bootstrap plugins **must** override `isBootstrapInstrumentation()`: +```java +@Override +public boolean isBootstrapInstrumentation() { + return true; +} +``` + +**WARNING**: Use bootstrap instrumentation only where absolutely necessary. It affects JDK core classes and has broader impact than SDK plugins. + +## Development Rules + +All general plugin development rules apply (see `apm-sdk-plugin/CLAUDE.md`), plus: + +1. **Use V2 API** for new bootstrap plugins, same as SDK plugins +2. **Minimal scope**: Only intercept what is strictly necessary in JDK classes +3. **Performance critical**: Bootstrap plugins run on core JDK paths - keep interceptor logic lightweight +4. **Class loading awareness**: JDK core classes are loaded by the bootstrap classloader; be careful with class references that might not be visible at bootstrap level + +## Testing Bootstrap Plugins + +Bootstrap plugin test scenarios use `runningMode: with_bootstrap` in `configuration.yml`: +```yaml +type: jvm +entryService: http://localhost:8080/case +healthCheck: http://localhost:8080/health +startScript: ./bin/startup.sh +runningMode: with_bootstrap +withPlugins: jdk-threading-plugin-*.jar +``` + +This tells the test framework to load the plugin at the bootstrap level instead of the normal plugin directory. + +See `apm-sdk-plugin/CLAUDE.md` for full test framework documentation.