diff --git a/docs/README.md b/docs/README.md index 9419513ba586bdb3ee6c8bd876350908f040fdb6..4e192ca655b4e168eaff70e0f8c80c4ce4782dc7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -76,6 +76,11 @@ If you have been familiar with SkyWalking, you could use this catalog to find th * [Dynamic Configuration](en/setup/backend/dynamic-config.md). Make configuration of OAP changed dynamic, from remote service or 3rd party configuration management system. * [Uninstrumented Gateways](en/setup/backend/uninstrumented-gateways.md). Configure gateways/proxies that are not supported by SkyWalking agent plugins, to reflect the delegation in topology graph. * [UI setup document](en/setup/backend/ui-setup.md). +* [Contributing Guides](en/guides/README.md). Guides are for PMC member, committer or new contributor. At here, you can know how to contribute from beginning. + * [Contact us](en/guides/README.md#contact-us). Guide users about how to contact the official committer team or communicate with the project community. + * [Process to become official Apache SkyWalking Committer](en/guides/asf/committer.md). How to become an official committer or PMC member. + * [Compiling Guide](en/guides/How-to-build.md). Follow this to compile the whole project from the source code. + * [Agent plugin development guide](en/guides/Java-Plugin-Development-Guide.md). Guide the developer to write their plugin, and follow [plugin test requirement](en/guides/Plugin-test.md) when you push the plugin to the upstream. * [Protocols](en/protocols/README.md). Protocols show the communication ways between agents/probes and backend. Anyone, interested in uplink telemetry data, definitely should read this. * [FAQs](en/FAQ/README.md). Include a manifest, including already known setup problems, secondary developments experiments. When you are facing a problem, check here first. diff --git a/docs/en/guides/Java-Plugin-Development-Guide.md b/docs/en/guides/Java-Plugin-Development-Guide.md index 03c54c3438793a61633948ebdc532798be0bb6a2..3094627bd495b3eee409576f2541833f6cdf862f 100644 --- a/docs/en/guides/Java-Plugin-Development-Guide.md +++ b/docs/en/guides/Java-Plugin-Development-Guide.md @@ -365,7 +365,7 @@ Please follow there steps: 1. Create sub modules under `apm-sniffer/apm-sdk-plugin` or `apm-sniffer/optional-plugins`, and the name should include supported library name and versions 1. Follow this guide to develop. Make sure comments and test cases are provided. 1. Develop and test. -1. Provide the automatic test cases. Read existing cases in `test/plugin/scenarios`, and add your new one. +1. Provide the automatic test cases. Learn `how to write the plugin test case` from this [doc](Plugin-test.md) 1. Send the pull request and ask for review. 1. The plugin committers approve your plugins, plugin CI-with-IT, e2e and plugin tests passed. 1. The plugin accepted by SkyWalking. diff --git a/docs/en/guides/Plugin-test.md b/docs/en/guides/Plugin-test.md new file mode 100644 index 0000000000000000000000000000000000000000..0093516f5b9e49cae87761f758049cd6ce912ac5 --- /dev/null +++ b/docs/en/guides/Plugin-test.md @@ -0,0 +1,596 @@ +# Plugin automatic test framework + +Plugin test frameworks is designed for verifying the plugin function and compatible status. As there are dozens of plugins and +hundreds versions need to be verified, it is impossible to do manually. +The test framework uses container based tech stack, requires a set of real services with agent installed, then the test mock +OAP backend is running to check the segment and register data sent from agents. + +Every plugin maintained in the main repo is required having its test cases, also matching the versions in the supported list doc. + +## Environment Requirements + +1. MacOS/Linux +2. jdk 8+ +3. Docker +4. Docker Compose + +## Case Base Image Introduction + +The test framework provides `JVM-container` and `Tomcat-container` base images. You could choose the suitable one for your test case, if either is suitable, **recommend choose `JVM-container`**. + +### JVM-container Image Introduction + +[JVM-container](../../../test/plugin/containers/jvm-container) uses `openjdk:8` as the basic image. +The test case project is required to be packaged as `project-name.zip`, including `startup.sh` and uber jar, by using `mvn package`. + +Take the following test projects as good examples +* [sofarpc-scenario](../../../test/plugin/scenarios/sofarpc-scenario) as a single project case. +* [webflux-scenario](../../../test/plugin/scenarios/webflux-scenario) as a case including multiple projects. + +### Tomcat-container Image Introduction + +[Tomcat-container](../../../test/plugin/containers/tomcat-container) uses `tomcat:8.5.42-jdk8-openjdk` as the basic image. +The test case project is required to ba packaged as `project-name.war` by using `mvn package`. + +Take the following test project as a good example +* [spring-4.3.x-scenario](https://github.com/apache/skywalking/tree/master/test/plugin/scenarios/spring-4.3.x-scenario) + + +## Test project hierarchical structure +The test case is an independent maven project, and it is required to be packaged as a war tar ball or zip file, depends +on the chosen basic image. Also, two external accessible endpoints, mostly two URLs, are required. + +All test case codes should be in `org.apache.skywalking.apm.testcase.*` package, unless there are some codes expected being instrumented, +then the classes could be in `test.org.apache.skywalking.apm.testcase.*` package. + +**JVM-container test project hierarchical structure** + +``` +[plugin-scenario] + |- [bin] + |- startup.sh + |- [config] + |- expectedData.yaml + |- [src] + |- [main] + |- ... + |- [resource] + |- log4j2.xml + |- pom.xml + |- configuration.yaml + |- support-version.list + +[] = directory +``` + +**Tomcat-container test project hierarchical structure** + +``` +[plugin-scenario] + |- [config] + |- expectedData.yaml + |- [src] + |- [main] + |- ... + |- [resource] + |- log4j2.xml + |- [webapp] + |- [WEB-INF] + |- web.xml + |- pom.xml + |- configuration.yaml + |- support-version.list + +[] = directory +``` + +## Test case configuration files +The following files are required in every test case. + +File Name | Descriptions +---|--- +`configuration.yml` | Declare the basic case inform, including, case name, entrance endpoints, mode, dependencies. +`expectedData.yaml` | Describe the expected Segment(s), including two major parts, (1) Register metadata (2) Segments +`support-version.list` | List the target versions for this case +`startup.sh` |`JVM-container` only, don't need this when use`Tomcat-container` + +`*` support-version.list format requires every line for a singe version. Could use `#` to comment out this version. + +### configuration.yml + +| Field | description +| --- | --- +| type | Image type, options, `jvm` or `tomcat`. Required. +| entryService | The entrance endpoint(URL) for test case access. Required. +| healthCheck | The health check endpoint(URL) for test case access. Required. +| startScript | Path of start up script. Required in `type: jvm` only. +| framework | Case name. +| runningMode | Running mode whether with the optional plugin, options, `default`(default), `with_optional`, `with_bootstrap` +| withPlugins | Plugin selector rule. eg:`apm-spring-annotation-plugin-*.jar`. Required when `runningMode=with_optional` or `runningMode=with_bootstrap`. +| environment | Same as `docker-compose#environment`. +| depends_on | Same as `docker-compose#depends_on`. +| dependencies | Same as `docker-compose#services`, `image、links、hostname、environment、depends_on` are supported. + +**Notice:, `docker-compose` active only when `dependencies` is only blank.** + +**runningMode** option description. + +| Option | description +| --- | --- +| default | Active all plugins in `plugin` folder like the official distribution agent. +| with_optional | Active `default` and plugins in `optional-plugin` by the give selector. +| with_bootstrap | Active `default` and plugins in `bootstrap-plugin` by the give selector. + +with_optional/with_bootstrap supports multiple selectors, separated by `;`. + +**File Format** + +``` +type: +entryService: +healthCheck: +startScript: +framework: +runningMode: +withPlugins: +environment: + ... +depends_on: + ... +dependencies: + service1: + image: + hostname: + expose: + ... + environment: + ... + depends_on: + ... + links: + ... + entrypoint: + ... + healthcheck: + ... +``` + +* dependencies supports docker compose `healthcheck`. But the format is a little difference. We need `-` as the start of every config item, +and describe it as a string line. + +Such as in official doc, the health check is +```yaml +healthcheck: + test: ["CMD", "curl", "-f", "http://localhost"] + interval: 1m30s + timeout: 10s + retries: 3 + start_period: 40s +``` + +The here, you should write as +```yaml +healthcheck: + - 'test: ["CMD", "curl", "-f", "http://localhost"]' + - "interval: 1m30s" + - "timeout: 10s" + - "retries: 3" + - "start_period: 40s" +``` + +In some cases, the dependency service, mostly 3rd party server like SolrJ server, is required to keep the same version +as client lib version, which defined as `${test.framework.version}` in pom. Could use `${CASE_SERVER_IMAGE_VERSION}` +as the version number, it will be changed in the test for every version. + +> Don't support resource related configurations, such as volumes, ports and ulimits. Because in test scenarios, +> don't need mapping any port to the host VM, or mount any folder. + +**Take following test cases as examples** +* [dubbo-2.7.x with JVM-container](../../../test/plugin/scenarios/dubbo-2.7.x-scenario/configuration.yml) +* [jetty with Tomcat-container](../../../test/plugin/scenarios/jetty-scenario/configuration.yml) +* [gateway with runningMode](../../../test/plugin/scenarios/gateway-scenario/configuration.yml) +* [canal with docker-compose](../../../test/plugin/scenarios/canal-scenario/configuration.yml) + +### expectedData.yaml + +**Operator for number** + +| Operator | Description | +| :--- | :--- | +| `nq` | Not equal | +| `eq` | Equal(default) | +| `ge` | Greater than or equal | +| `gt` | Greater than | + +**Operator for String** + +| Operator | Description | +| :--- | :--- | +| `not null` | Not null | +| `null` | Null or empty String | +| `eq` | Equal(default) | + + +**Register verify description format** +```yml +registryItems: + applications: + - APPLICATION_CODE: APPLICATION_ID(int) + ... + instances: + - APPLICATION_CODE: INSTANCE_COUNT(int) + ... + operationNames: + - APPLICATION_CODE: [ SPAN_OPERATION(string), ... ] + ... +``` + + +| Field | Description +| --- | --- +| applications | The registered service codes. Normally, not 0 should be enough. +| instances | The number of service instances exists in this test case. +| operationNames | All endpoint registered in this test case. Also means, the operation names of all entry and exit spans. + + +**Segment verify description format** +```yml +segments: +- + applicationCode: APPLICATION_CODE(string) + segmentSize: SEGMENT_SIZE(int) + segments: + - segmentId: SEGMENT_ID(string) + spans: + ... +``` + + +| Field | Description +| --- | --- +| applicationCode | Service code. +| segmentSize | The number of segments is expected. +| segmentId | trace ID. +| spans | segment span list. Follow the next section to see how to describe every span. + +**Span verify description format** + +**Notice**: The order of span list should follow the order of the span finish time. + +```yml + operationName: OPERATION_NAME(string) + operationId: SPAN_ID(int) + parentSpanId: PARENT_SPAN_ID(int) + spanId: SPAN_ID(int) + startTime: START_TIME(int) + endTime: END_TIME(int) + isError: IS_ERROR(string: true, false) + spanLayer: SPAN_LAYER(string: DB, RPC_FRAMEWORK, HTTP, MQ, CACHE) + spanType: SPAN_TYPE(string: Exit, Entry, Local ) + componentName: COMPONENT_NAME(string) + componentId: COMPONENT_ID(int) + tags: + - {key: TAG_KEY(string), value: TAG_VALUE(string)} + ... + logs: + - {key: LOG_KEY(string), value: LOG_VALUE(string)} + ... + peer: PEER(string) + peerId: PEER_ID(int) + refs: + - { + parentSpanId: PARENT_SPAN_ID(int), + parentTraceSegmentId: PARENT_TRACE_SEGMENT_ID(string), + entryServiceName: ENTRY_SERVICE_NAME(string), + networkAddress: NETWORK_ADDRESS(string), + parentServiceName: PARENT_SERVICE_NAME(string), + entryApplicationInstanceId: ENTRY_APPLICATION_INSTANCE_ID(int) + } + ... +``` + +| Field | Description +|--- |--- +| operationName | Span Operation Name +| operationId | Should be 0 for now +| parentSpanId | Parent span id. **Notice**: The parent span id of the first span should be -1. +| spanId | Span Id. **Notice**, start from 0. +| startTime | Span start time. It is impossible to get the accurate time, not 0 should be enough. +| endTime | Span finish time. It is impossible to get the accurate time, not 0 should be enough. +| isError | Span status, true or false. +| componentName | Component name, should be null in most cases, use component id instead. +| componentId | Component id for your plugin. +| tags | Span tag list. **Notice**, Keep in the same order as the plugin coded. +| logs | Span log list. **Notice**, Keep in the same order as the plugin coded. +| SpanLayer | Options, DB, RPC_FRAMEWORK, HTTP, MQ, CACHE +| SpanType | Span type, options, Exit, Entry or Local +| peer | Remote network address, IP + port mostly. For exit span, this should be required. +| peerId | Not 0 for now. + + +The verify description for SegmentRef, + +| Field | Description +|---- |---- +| parentSpanId | Parent SpanID, pointing to the span id in the parent segment. +| parentTraceSegmentId | Parent SegmentID. Format is `${APPLICATION_CODE[SEGMENT_INDEX]}`, pointing to the index of parent service segment list. +| entryServiceName | Entrance service code of the whole distributed call chain. Such HttpClient entryServiceName is `/httpclient-case/case/httpclient` +| networkAddress | The peer value of parent exit span. +| parentServiceName | Parent service code. +| entryApplicationInstanceId | Not 0 should be enough. + + +### startup.sh + +This script provide a start point to JVM based service, most of them starts by a `java -jar`, with some variables. +The following system environment variables are available in the shell. + +| Variable | Description | +|:---- |:---- | +| agent_opts | Agent plugin opts, check the detail in plugin doc or the same opt added in this PR. | +| SCENARIO_NAME | Service name. Default same as the case folder name | +| SCENARIO_VERSION | Version | +| SCENARIO_ENTRY_SERVICE | Entrance URL to access this service | +| SCENARIO_HEALTH_CHECK_URL | Health check URL | + + +> `${agent_opts}` is required to add into your `java -jar` command, which including the parameter injected by test framework, and +> make agent installed. All other parameters should be added after `${agent_opts}`. + +The test framework will set the service name as the test case folder name by default, but in some cases, there are more +than one test projects are required to run in different service codes, could set it explicitly like the following example. + +Example +```bash +home="$(cd "$(dirname $0)"; pwd)" + +java -jar ${agent_opts} "-Dskywalking.agent.service_name=jettyserver-scenario" ${home}/../libs/jettyserver-scenario.jar & +sleep 1 + +java -jar ${agent_opts} "-Dskywalking.agent.service_name=jettyclient-scenario" ${home}/../libs/jettyclient-scenario.jar & + +``` + +> Only set this or use other skywalking options when it is really necessary. + +**Take the following test cases as examples** +* [undertow](../../../test/plugin/scenarios/undertow-scenario/bin/startup.sh) +* [webflux](../../../test/plugin/scenarios/webflux-scenario/webflux-dist/bin/startup.sh) + + +## Best Practices +### Recommendations for pom + +```xml + + + + + 4.3 + + + + org.apache.httpcomponents + httpclient + ${test.framework.version} + + ... + + + + + httpclient-4.3.x-scenario + .... + +``` + +### How To Implement Heartbeat Service + +Heartbeat service is designed for checking the service available status. This service is a simple HTTP service, returning 200 means the +target service is ready. Then the traffic generator will access the entry service and verify the expected data. +User should consider to use this service to detect such as whether the dependent services are ready, especially when +dependent services are database or cluster. + +Notice, because heartbeat service could be traced fully or partially, so, segmentSize in `expectedData.yaml` should use `ge` as the operator, +and don't include the segments of heartbeat service in the expected segment data. + +### The example Process of Writing Expected Data + +Expected data file, `expectedData.yaml`, includes `RegistryItems` and `SegmentIntems`. + +We are using the HttpClient plugin to show how to write the expected data. + +There are two key points of testing +1. Whether is HttpClient span created. +1. Whether the ContextCarrier created correctly, and propagates across processes. + +``` ++-------------+ +------------------+ +-------------------------+ +| Browser | | Case Servlet | | ContextPropagateServlet | +| | | | | | ++-----|-------+ +---------|--------+ +------------|------------+ + | | | + | | | + | WebHttp +-+ | + +------------------------> |-| HttpClient +-+ + | |--------------------------------> |-| + | |-| |-| + | |-| |-| + | |-| <--------------------------------| + | |-| +-+ + | <--------------------------| | + | +-+ | + | | | + | | | + | | | + | | | + + + + +``` + +#### RegistryItems + +HttpClient test case is running in Tomcat container, only one instance exists, so +1. instance number is 1 +1. applicationId is not 0 +1. Because we have two servlet mapping paths, so two operation names. No health check operation name here. + +```yml +registryItems: + applications: + - {httpclient-case: nq 0} + instances: + - {httpclient-case: 1} + operationNames: + - httpclient-case: [/httpclient-case/case/httpclient,/httpclient-case/case/context-propagate] +``` + +#### segmentItems + +By following the flow of HttpClient case, there should be two segments created. +1. Segment represents the CaseServlet access. Let's name it as `SegmentA`. +1. Segment represents the ContextPropagateServlet access. Let's name it as `SegmentB`. + +```yml +segments: + - applicationCode: httpclient-case + segmentSize: ge 2 # Could have more than one health check segments, because, the dependency is not standby. +``` + +Because Tomcat plugin is a default plugin of SkyWalking, so, in SegmentA, there are two spans +1. Tomcat entry span +1. HttpClient exit span + +SegmentA span list should like following +```yml + - segmentId: not null + spans: + - operationName: /httpclient-case/case/context-propagate + operationId: eq 0 + parentSpanId: 0 + spanId: 1 + startTime: nq 0 + endTime: nq 0 + isError: false + spanLayer: Http + spanType: Exit + componentName: null + componentId: eq 2 + tags: + - {key: url, value: 'http://127.0.0.1:8080/httpclient-case/case/context-propagate'} + - {key: http.method, value: GET} + logs: [] + peer: null + peerId: eq 0 + - operationName: /httpclient-case/case/httpclient + operationId: eq 0 + parentSpanId: -1 + spanId: 0 + startTime: nq 0 + endTime: nq 0 + spanLayer: Http + isError: false + spanType: Entry + componentName: null + componentId: 1 + tags: + - {key: url, value: 'http://localhost:{SERVER_OUTPUT_PORT}/httpclient-case/case/httpclient'} + - {key: http.method, value: GET} + logs: [] + peer: null + peerId: eq 0 +``` + +SegmentB should only have one Tomcat entry span, but includes the Ref pointing to SegmentA. + +SegmentB span list should like following +```yml +- segmentId: not null + spans: + - + operationName: /httpclient-case/case/context-propagate + operationId: eq 0 + parentSpanId: -1 + spanId: 0 + tags: + - {key: url, value: 'http://127.0.0.1:8080/httpclient-case/case/context-propagate'} + - {key: http.method, value: GET} + logs: [] + startTime: nq 0 + endTime: nq 0 + spanLayer: Http + isError: false + spanType: Entry + componentName: null + componentId: 1 + peer: null + peerId: eq 0 + refs: + - {parentSpanId: 1, parentTraceSegmentId: "${httpclient-case[0]}", entryServiceName: "/httpclient-case/case/httpclient", networkAddress: "127.0.0.1:8080",parentServiceName: "/httpclient-case/case/httpclient",entryApplicationInstanceId: nq 0 } +``` + +## Local Test and Pull Request To The Upstream + +First of all, the test case project could be compiled successfully, with right project structure and be able to deploy. +The developer should test the start script could run in Linux/MacOS, and entryService/health services are able to provide +the response. + +You could run test by using following commands + +```bash +cd ${SKYWALKING_HOME} +bash ./test/pugin/run.sh -f ${scenario_name} +``` + +**Notice**,if codes in `./apm-sniffer` have been changed, no matter because your change or git update, +please recompile the `skywalking-agent`. Because the test framework will use the existing `skywalking-agent` folder, +rather than recompiling it every time. + +Use `${SKYWALKING_HOME}/test/plugin/run.sh -h` to know more command options. + +If the local test passed, then you could add it to Jenkins file, which will drive the tests running on the official SkyWalking INFRA. +We have 3 JenkinsFile to control the test jobs, `jenkinsfile-agent-test`, `jenkinsfile-agent-test-2` and `jenkinsfile-agent-test-3`(maybe will have 4 later) +each file declares two parallel groups. Please check the prev agent related PRs, and add your case to the fastest group, +in order to make the whole test finished as soon as possible. + +Every test case is a Jenkins stage. Please use the scenario name, version range and version number to combine the name, +take the existing one as a reference. And update the total case number in `Test Cases Report` stage. + +Example + +``` +stage('Test Cases Report (15)') { # 15=12+3 The total number of test cases + steps { + echo "Test Cases Report" + } +} + +stage('Run Agent Plugin Tests') { + when { + expression { + return sh(returnStatus: true, script: 'bash tools/ci/agent-build-condition.sh') + } + } + parallel { + stage('Group1') { + stages { + stage('spring-cloud-gateway 2.1.x (3)') { # For Spring clound gateway 2.1.x, having 3 versions to be tested. + steps { + sh 'bash test/plugin/run.sh gateway-scenario' + } + } + } + ... + } + stage('Group2') { + stages { + stage('solrj 7.x (12)') { # For Solrj 7.x, having 12 versions to be tested. + steps { + sh 'bash test/plugin/run.sh solrj-7.x-scenario' + } + } + } + ... + } + } +} +```