This file provides guidance for AI assistants working with the Apache SkyWalking Java Agent codebase.
Apache SkyWalking Java Agent is a Java-based APM (Application Performance Monitoring) agent designed for microservices, cloud-native, and container-based architectures. It provides automatic instrumentation for distributed tracing, performance metrics collection, and context propagation across service boundaries using bytecode manipulation via ByteBuddy.
skywalking-java/ ├── apm-commons/ # Shared utilities and libraries │ ├── apm-datacarrier/ # Data buffering and transport │ └── apm-util/ # Common utilities ├── apm-protocol/ # Protocol definitions │ └── apm-network/ # gRPC protocol (submodule: skywalking-data-collect-protocol) ├── apm-sniffer/ # Core agent and plugins (MAIN MODULE) │ ├── apm-agent/ # Main agent bootstrap and premain entry │ ├── apm-agent-core/ # Core agent logic, instrumentation engine │ ├── apm-sdk-plugin/ # Standard SDK plugins (70+ plugins) │ ├── bootstrap-plugins/ # Bootstrap-level plugins (JDK-level) │ ├── optional-plugins/ # Optional framework plugins │ ├── optional-reporter-plugins/ # Reporter plugins (Kafka, etc.) │ ├── apm-toolkit-activation/ # Toolkit activations │ ├── apm-test-tools/ # Testing utilities │ ├── bytebuddy-patch/ # ByteBuddy patches │ └── config/ # Default agent configurations ├── apm-application-toolkit/ # Public API for applications │ ├── apm-toolkit-trace/ # Tracing API │ ├── apm-toolkit-log4j-1.x/ # Log4j 1.x integration │ ├── apm-toolkit-log4j-2.x/ # Log4j 2.x integration │ ├── apm-toolkit-logback-1.x/ # Logback integration │ ├── apm-toolkit-meter/ # Meter API │ └── apm-toolkit-opentracing/ # OpenTracing API ├── apm-checkstyle/ # Code style configuration │ ├── checkStyle.xml # Checkstyle rules │ └── importControl.xml # Import control rules ├── test/ # Testing infrastructure │ ├── plugin/ # Plugin E2E tests │ │ ├── scenarios/ # Test scenarios (100+ scenarios) │ │ ├── agent-test-tools/ # Mock collector, test utilities │ │ ├── runner-helper/ # Test runner │ │ └── containers/ # Docker test containers │ └── e2e/ # End-to-end tests ├── docs/ # Documentation ├── tools/ # Build and utility tools ├── skywalking-agent/ # Built agent distribution output ├── changes/ # Changelog └── dist-material/ # Distribution materials
# Clone with submodules git clone --recurse-submodules https://github.com/apache/skywalking-java.git # Or initialize submodules after clone git submodule init && git submodule update # Full build with tests ./mvnw clean install # Build without tests (recommended for development) ./mvnw clean package -Dmaven.test.skip=true # CI build with javadoc verification ./mvnw clean verify install javadoc:javadoc # Run checkstyle only ./mvnw checkstyle:check # Build with submodule update ./mvnw clean package -Pall # Docker build make build make docker
all: Includes git submodule update for protocol definitionsThe agent uses ByteBuddy for bytecode manipulation at runtime:
apm-agent/ contains the agent bootstrap via Java's -javaagent mechanismapm-agent-core/ handles class transformation and plugin loading1. SDK Plugins (apm-sniffer/apm-sdk-plugin/)
2. Bootstrap Plugins (apm-sniffer/bootstrap-plugins/)
3. Optional Plugins (apm-sniffer/optional-plugins/)
4. Optional Reporter Plugins (apm-sniffer/optional-reporter-plugins/)
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 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):
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<MethodDescription> 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):
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 methodsClassInstanceMethodsEnhancePluginDefineV2 - For instance methods onlyClassStaticMethodsEnhancePluginDefineV2 - For static methods onlyInstanceMethodsAroundInterceptorV2 - Interceptor interface with MethodInvocationContextStaticMethodsAroundInterceptorV2 - Static method interceptor with contextV1 uses MethodInterceptResult only in beforeMethod and has no shared context between phases. Only use for maintaining existing legacy plugins.
Key V1 classes (legacy):
ClassEnhancePluginDefineClassInstanceMethodsEnhancePluginDefineClassStaticMethodsEnhancePluginDefineInstanceMethodsAroundInterceptorStaticMethodsAroundInterceptorCRITICAL: Never use .class references in instrumentation definitions:
// 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) - preferredbyClassAnnotationMatch: Match classes with specific annotations (does NOT support inherited annotations)byMethodAnnotationMatch: Match classes with methods having specific annotationsbyHierarchyMatch: Match by parent class/interface - avoid unless necessary (performance impact)Use witness classes/methods to activate plugins only for specific library versions:
@Override protected String[] witnessClasses() { return new String[] { "com.example.VersionSpecificClass" }; } @Override protected List<WitnessMethod> witnessMethods() { return Collections.singletonList( new WitnessMethod("com.example.SomeClass", ElementMatchers.named("specificMethod")) ); }
For JDK core classes (rt.jar), override isBootstrapInstrumentation():
@Override public boolean isBootstrapInstrumentation() { return true; }
WARNING: Use bootstrap instrumentation only where absolutely necessary.
Use @PluginConfig annotation for custom plugin settings:
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
Plugin dependencies must use provided scope:
<dependency> <groupId>com.example</groupId> <artifactId>target-library</artifactId> <version>${version}</version> <scope>provided</scope> </dependency>
Agent core dependency policy:
DB: Database accessRPC_FRAMEWORK: RPC calls (not ordinary HTTP)HTTP: HTTP callsMQ: Message queueUNKNOWN: DefaultFor EntrySpan and ExitSpan, always set:
span.setComponent(ComponentsDefine.YOUR_COMPONENT); span.setLayer(SpanLayer.HTTP); // or DB, MQ, RPC_FRAMEWORK
| 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 |
For collecting numeric metrics (alternative to tracing):
// 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);
-javaagent flagapm-checkstyle/checkStyle.xml)Prohibited patterns:
System.out.println - use proper logging@author tags - ASF projects don't use author annotationsimport xxx.*)Required patterns:
@Override annotation required for overridden methodsequals() and hashCode() must be overridden togetherNaming conventions:
UPPER_CASE_WITH_UNDERSCORESorg.apache.skywalking.apm.* or test.apache.skywalking.apm.*PascalCasecamelCase{framework}-{version}-plugin*Instrumentation.java*Interceptor.javaFile limits:
Use Lombok annotations for boilerplate code:
@Getter, @Setter, @Data@Builder@Slf4j for loggingUnit Tests (in each module's src/test/java)
*Test.javaPlugin E2E Tests (test/plugin/scenarios/)
{framework}-{version}-scenarioEnd-to-End Tests (test/e2e/)
# Unit tests ./mvnw test # Full verification including checkstyle ./mvnw clean verify # Skip tests during build ./mvnw package -Dmaven.test.skip=true
The plugin test framework verifies plugin functionality using Docker containers with real services and a mock OAP backend.
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
configuration.yml:
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:
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:
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):
#!/bin/bash home="$(cd "$(dirname $0)"; pwd)" # ${agent_opts} is REQUIRED - contains agent parameters java -jar ${agent_opts} ${home}/../libs/your-scenario.jar &
# 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
Add scenario to the appropriate .github/workflows/ file:
python3 tools/select-group.py to find the file with fewest casesplugins-test.<n>.yamlplugins-jdk17-test.<n>.yamlplugins-jdk21-test.<n>.yamlplugins-jdk25-test.<n>.yamlmatrix: case: - your-scenario-scenario
org.apache.skywalking.apm.testcase.*test.org.apache.skywalking.apm.testcase.*The project uses submodules for protocol definitions:
apm-protocol/apm-network/src/main/proto - skywalking-data-collect-protocolAlways use --recurse-submodules when cloning or update submodules manually:
git submodule init && git submodule update
./mvnw compile -Dmaven.test.skip=true to generate protobuf sources*/target/generated-sources/protobuf/java*/target/generated-sources/protobuf/grpc-javaapm-sniffer/apm-agent/ - Agent entry point (premain)apm-sniffer/apm-agent-core/src/main/java/.../enhance/ - Instrumentation engineapm-sniffer/apm-agent-core/src/main/java/.../plugin/ - Plugin loading systemapm-sniffer/apm-sdk-plugin/ - All standard plugins (reference implementations)apm-sniffer/config/agent.config - Default agent configurationapm-sniffer/apm-sdk-plugin/{framework}-{version}-plugin/ClassInstanceMethodsEnhancePluginDefineV2)InstanceMethodsAroundInterceptorV2)skywalking-plugin.def filetest/plugin/scenarios/apm-sniffer/optional-plugins/docs/en/setup/service-agent/java-agent/Optional-plugins.mdapm-sniffer/config/agent.configdocs/en/setup/service-agent/java-agent/ - Main agent documentationdocs/en/setup/service-agent/java-agent/Plugin-list.md - Complete plugin listdocs/en/setup/service-agent/java-agent/Optional-plugins.md - Optional plugins guideCHANGES.md - Changelog (update when making changes)Follow .github/PULL_REQUEST_TEMPLATE based on change type:
CHANGES.md for user-facing changesGitHub Actions workflows:
ClassEnhancePluginDefineV2, InstanceMethodsAroundInterceptorV2) for new plugins; V1 is legacy.class references: In instrumentation definitions, always use string literals for class names (e.g., byName("com.example.MyClass") not byName(MyClass.class.getName()))setComponent() and setLayer()byName for class matching: Avoid byHierarchyMatch unless necessary (causes performance issues)witnessClasses() or witnessMethods() to activate plugins only for specific library versionsmvnw compile before analyzing generated code{framework}-{version}-plugin conventionskywalking-plugin.def file