diff --git a/.github/workflows/plugins-jdk17-test.0.yaml b/.github/workflows/plugins-jdk17-test.0.yaml index 49b3675c01..b62166b155 100644 --- a/.github/workflows/plugins-jdk17-test.0.yaml +++ b/.github/workflows/plugins-jdk17-test.0.yaml @@ -79,6 +79,7 @@ jobs: - grizzly-2.3.x-4.x-workthreadpool-scenario - jetty-11.x-scenario - jetty-10.x-scenario + - jetty-12.x-scenario - spring-ai-1.x-scenario - spring-rabbitmq-scenario - graphql-20plus-scenario diff --git a/.github/workflows/plugins-jdk17-test.1.yaml b/.github/workflows/plugins-jdk17-test.1.yaml index 6be997dc0e..e4f7144dfd 100644 --- a/.github/workflows/plugins-jdk17-test.1.yaml +++ b/.github/workflows/plugins-jdk17-test.1.yaml @@ -72,6 +72,7 @@ jobs: case: - spring-6.x-scenario - spring-boot-3.x-scenario + - struts2.7-scenario - resteasy-6.x-scenario - gateway-4.x-scenario - gateway-4.1.2.x-scenario diff --git a/.gitignore b/.gitignore index da77f29d42..1efb481266 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,8 @@ packages/ /test/jacoco/*.exec test/jacoco .claude/settings.local.json + +# Generated by tools/plugin/check-javaagent-plugin-list.sh (not source) +/javaagent-position.txt +/tools/plugin/genernate-javaagent-plugin-list.txt +/tools/plugin/md-javaagent-plugin-list.txt diff --git a/CHANGES.md b/CHANGES.md index 9f994787ff..48310efdab 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,8 @@ Release Notes. * Fix `plugin.http.include_http_headers` not working on Spring Boot 3.x / Jakarta EE (apache/skywalking#13938). * Unify the Tomcat plugins into a single `tomcat` plugin (Tomcat 7 - 10) and the Jetty server plugins into a single `jetty-server` plugin (Jetty 9 - 11), each supporting both `javax.servlet` and `jakarta.servlet`. Breaking change: plugin names `tomcat-7.x/8.x` and `tomcat-10.x` are replaced by `tomcat`, and `jetty-server-9.x` and `jetty-server-11.x` by `jetty-server`; update `plugin.exclude_plugins` if you reference the old names. +* Add a Jetty 12 server plugin (`jetty-server-12.x`). Jetty 12 removed the `HttpChannel` handle target and moved request handling to the async `Server#handle(Request, Response, Callback)` core API, so it needs a separate plugin from the merged `jetty-server`. +* Add a Struts 7 plugin (`struts2-7.x`) for Jakarta Struts, whose `DefaultActionInvocation` moved to `org.apache.struts2`. * Added support for Lettuce reactive Redis commands. * Add Spring AI 1.x plugin and GenAI layer. * Fix httpclient-5.x plugin injecting sw8 propagation headers into ClickHouse HTTP requests (port 8123), causing HTTP 400. Add `PROPAGATION_EXCLUDE_PORTS` config to skip tracing (including header injection) for specified ports in the classic client interceptor. diff --git a/apm-sniffer/apm-sdk-plugin/CLAUDE.md b/apm-sniffer/apm-sdk-plugin/CLAUDE.md index b839fbe73b..81de9bc670 100644 --- a/apm-sniffer/apm-sdk-plugin/CLAUDE.md +++ b/apm-sniffer/apm-sdk-plugin/CLAUDE.md @@ -251,6 +251,8 @@ histogram.addValue(3); The plugin test framework verifies plugin functionality using Docker containers with real services and a mock OAP backend. +> See `test/plugin/CLAUDE.md` for the testing philosophy: a plugin's scenario (with one-version-per-minor coverage) is the **key** test — it proves the interceptor's cast/fetch-data logic and version compatibility by running the real framework. Do **not** over-engineer with many mock-based unit tests for a plugin. + ### Environment Requirements - MacOS/Linux - JDK 8+ diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/pom.xml b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/pom.xml new file mode 100644 index 0000000000..dfc04b5933 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/pom.xml @@ -0,0 +1,52 @@ + + + + + jetty-plugins + org.apache.skywalking + 9.7.0-SNAPSHOT + + 4.0.0 + + apm-jetty-server-12.x-plugin + jar + + jetty-server-12.x-plugin + http://maven.apache.org + + + + 12.0.36 + + + + + org.eclipse.jetty + jetty-server + ${jetty-server.version} + provided + + + diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/HandleInterceptor.java b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/HandleInterceptor.java new file mode 100644 index 0000000000..74e08614dd --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/HandleInterceptor.java @@ -0,0 +1,100 @@ +/* + * 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.jetty.v12.server; + +import java.lang.reflect.Method; +import org.apache.skywalking.apm.agent.core.context.CarrierItem; +import org.apache.skywalking.apm.agent.core.context.ContextCarrier; +import org.apache.skywalking.apm.agent.core.context.ContextManager; +import org.apache.skywalking.apm.agent.core.context.tag.Tags; +import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan; +import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; +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.network.trace.component.ComponentsDefine; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +/** + * Enhances the core, namespace-neutral {@code org.eclipse.jetty.server.Server#handle(Request, Response, + * Callback)} — the single top-level entry invoked once per request in Jetty 12. Handling is async, so + * the entry span is prepared for async in beforeMethod, its synchronous segment ends in afterMethod, and + * it is finished (with the response status) when the wrapped {@link SwCallback} completes. + */ +public class HandleInterceptor implements InstanceMethodsAroundInterceptor { + + private static final ThreadLocal HOLDER = new ThreadLocal(); + + @Override + public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, + MethodInterceptResult result) throws Throwable { + Request request = (Request) allArguments[0]; + Response response = (Response) allArguments[1]; + Callback callback = (Callback) allArguments[2]; + + ContextCarrier contextCarrier = new ContextCarrier(); + HttpFields headers = request.getHeaders(); + CarrierItem next = contextCarrier.items(); + while (next.hasNext()) { + next = next.next(); + next.setHeadValue(headers.get(next.getHeadKey())); + } + + HttpURI uri = request.getHttpURI(); + AbstractSpan span = ContextManager.createEntrySpan(uri.getPath(), contextCarrier); + Tags.URL.set(span, uri.asString()); + Tags.HTTP.METHOD.set(span, request.getMethod()); + span.setComponent(ComponentsDefine.JETTY_SERVER); + SpanLayer.asHttp(span); + span.prepareForAsync(); + + SwCallback swCallback = new SwCallback(callback, response, span); + allArguments[2] = swCallback; + HOLDER.set(swCallback); + } + + @Override + public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, + Object ret) throws Throwable { + SwCallback swCallback = HOLDER.get(); + HOLDER.remove(); + ContextManager.stopSpan(); + // ret == true: handled; the wrapped callback fires on response completion and finishes the span. + // ret == false: not handled — Jetty writes 404 on the original callback, bypassing ours. + // ret == null: handle() threw (see handleMethodException) — Jetty writes 500. + // For the latter two the wrapped callback never fires and the status is written only after + // handle() returns, so finish here with the status Jetty's contract guarantees. + if (swCallback != null && !Boolean.TRUE.equals(ret)) { + swCallback.finishWithStatus(ret == null ? 500 : 404); + } + return ret; + } + + @Override + public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, + Class[] argumentsTypes, Throwable t) { + AbstractSpan span = ContextManager.activeSpan(); + span.log(t); + span.errorOccurred(); + } +} diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/SwCallback.java b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/SwCallback.java new file mode 100644 index 0000000000..f5193d7c05 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/SwCallback.java @@ -0,0 +1,98 @@ +/* + * 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.jetty.v12.server; + +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.skywalking.apm.agent.core.context.tag.Tags; +import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.thread.Invocable; + +/** + * Wraps the {@link Callback} passed to {@code Server#handle}. Jetty 12 request handling is async — the + * response status is only known when the callback completes on the (possibly different) response thread. + * On completion this reads the status, records it on the entry span and finishes the async span. It + * delegates every method to the original callback so Jetty's threading model is preserved. + */ +public class SwCallback implements Callback { + + private final Callback delegate; + private final Response response; + private final AbstractSpan span; + private final AtomicBoolean finished = new AtomicBoolean(); + + public SwCallback(Callback delegate, Response response, AbstractSpan span) { + this.delegate = delegate; + this.response = response; + this.span = span; + } + + @Override + public void succeeded() { + finishOnce(); + delegate.succeeded(); + } + + @Override + public void failed(Throwable x) { + if (finished.compareAndSet(false, true)) { + span.log(x); + span.errorOccurred(); + Tags.HTTP_RESPONSE_STATUS_CODE.set(span, response.getStatus()); + span.asyncFinish(); + } + delegate.failed(x); + } + + @Override + public Invocable.InvocationType getInvocationType() { + return delegate.getInvocationType(); + } + + /** + * Records the response status and finishes the async span exactly once. Safe to call from either the + * callback completion or the interceptor's afterMethod fallback (not-handled / exception paths). + */ + void finishOnce() { + if (finished.compareAndSet(false, true)) { + int statusCode = response.getStatus(); + Tags.HTTP_RESPONSE_STATUS_CODE.set(span, statusCode); + if (statusCode >= 400) { + span.errorOccurred(); + } + span.asyncFinish(); + } + } + + /** + * Finishes the async span exactly once with an explicit status. Used for the not-handled / exception + * fallback paths where Jetty writes the error status on the original callback after {@code handle()} + * returns — so the wrapped callback never fires and {@link Response#getStatus()} is still 0 (uncommitted). + */ + void finishWithStatus(int statusCode) { + if (finished.compareAndSet(false, true)) { + Tags.HTTP_RESPONSE_STATUS_CODE.set(span, statusCode); + if (statusCode >= 400) { + span.errorOccurred(); + } + span.asyncFinish(); + } + } +} diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/define/Jetty12ServerInstrumentation.java b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/define/Jetty12ServerInstrumentation.java new file mode 100644 index 0000000000..8b4d347af9 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/define/Jetty12ServerInstrumentation.java @@ -0,0 +1,89 @@ +/* + * 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.jetty.v12.server.define; + +import java.util.Collections; +import java.util.List; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.skywalking.apm.agent.core.plugin.WitnessMethod; +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 net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; +import static org.apache.skywalking.apm.agent.core.plugin.match.NameMatch.byName; + +/** + * Enhances {@code org.eclipse.jetty.server.Server#handle(Request, Response, Callback)} by + * {@code HandleInterceptor}. Only Jetty 12+ declares this three-argument, {@code Callback}-terminated + * signature, so the witness method both activates the plugin exclusively on Jetty 12 and keeps it from + * overlapping the merged jetty-server plugin (which enhances the Jetty 9/10/11 {@code HttpChannel}, + * removed in Jetty 12). + */ +public class Jetty12ServerInstrumentation extends ClassInstanceMethodsEnhancePluginDefine { + + private static final String ENHANCE_CLASS = "org.eclipse.jetty.server.Server"; + private static final String ENHANCE_METHOD = "handle"; + private static final String CALLBACK_CLASS = "org.eclipse.jetty.util.Callback"; + private static final String INTERCEPT_CLASS = "org.apache.skywalking.apm.plugin.jetty.v12.server.HandleInterceptor"; + + @Override + protected List witnessMethods() { + return Collections.singletonList(new WitnessMethod( + ENHANCE_CLASS, + named(ENHANCE_METHOD).and(takesArgument(2, named(CALLBACK_CLASS))) + )); + } + + @Override + public ConstructorInterceptPoint[] getConstructorsInterceptPoints() { + return new ConstructorInterceptPoint[0]; + } + + @Override + public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() { + return new InstanceMethodsInterceptPoint[] { + new InstanceMethodsInterceptPoint() { + @Override + public ElementMatcher getMethodsMatcher() { + return named(ENHANCE_METHOD).and(takesArguments(3)).and(takesArgument(2, named(CALLBACK_CLASS))); + } + + @Override + public String getMethodsInterceptor() { + return INTERCEPT_CLASS; + } + + @Override + public boolean isOverrideArgs() { + return true; + } + } + }; + } + + @Override + protected ClassMatch enhanceClass() { + return byName(ENHANCE_CLASS); + } +} diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/resources/skywalking-plugin.def b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/resources/skywalking-plugin.def new file mode 100644 index 0000000000..2c466d5f66 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/resources/skywalking-plugin.def @@ -0,0 +1,18 @@ +# +# 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. + +jetty-server-12.x=org.apache.skywalking.apm.plugin.jetty.v12.server.define.Jetty12ServerInstrumentation diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml b/apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml index 1ac21fb14d..671cc92f91 100644 --- a/apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml +++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml @@ -31,6 +31,7 @@ jetty-client-9.x-plugin jetty-client-9.0-plugin jetty-server-plugin + jetty-server-12.x-plugin jetty-client-11.x-plugin pom diff --git a/apm-sniffer/apm-sdk-plugin/pom.xml b/apm-sniffer/apm-sdk-plugin/pom.xml index 829d63dd84..8bc2e1faaa 100644 --- a/apm-sniffer/apm-sdk-plugin/pom.xml +++ b/apm-sniffer/apm-sdk-plugin/pom.xml @@ -44,6 +44,7 @@ okhttp-common spring-plugins struts2-2.x-plugin + struts2-7.x-plugin nutz-plugins jetty-plugin spymemcached-2.x-plugin diff --git a/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/pom.xml b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/pom.xml new file mode 100644 index 0000000000..f314ff5fba --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/pom.xml @@ -0,0 +1,56 @@ + + + + + org.apache.skywalking + apm-sdk-plugin + 9.7.0-SNAPSHOT + + 4.0.0 + + apm-struts2-7.x-plugin + jar + + struts2-7.x-plugin + + + + 7.2.1 + 6.0.0 + + + + + org.apache.struts + struts2-core + ${struts.version} + provided + + + jakarta.servlet + jakarta.servlet-api + ${jakarta-servlet-api.version} + provided + + + diff --git a/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/struts2/v7/Struts2Interceptor.java b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/struts2/v7/Struts2Interceptor.java new file mode 100644 index 0000000000..80a42b2eaa --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/struts2/v7/Struts2Interceptor.java @@ -0,0 +1,82 @@ +/* + * 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.struts2.v7; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; +import org.apache.skywalking.apm.agent.core.context.CarrierItem; +import org.apache.skywalking.apm.agent.core.context.ContextCarrier; +import org.apache.skywalking.apm.agent.core.context.ContextManager; +import org.apache.skywalking.apm.agent.core.context.tag.Tags; +import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan; +import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; +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.network.trace.component.ComponentsDefine; +import org.apache.struts2.ServletActionContext; + +/** + * Enhances {@code org.apache.struts2.DefaultActionInvocation#invokeAction} in Struts 7.x. Struts 7 is + * Jakarta-only (Jakarta Servlet 6.0), so the servlet request/response are referenced through the + * jakarta.servlet namespace directly. + */ +public class Struts2Interceptor implements InstanceMethodsAroundInterceptor { + + @Override + public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, + MethodInterceptResult result) throws Throwable { + HttpServletRequest request = ServletActionContext.getRequest(); + ContextCarrier contextCarrier = new ContextCarrier(); + + CarrierItem next = contextCarrier.items(); + while (next.hasNext()) { + next = next.next(); + next.setHeadValue(request.getHeader(next.getHeadKey())); + } + + AbstractSpan span = ContextManager.createEntrySpan(request.getRequestURI(), contextCarrier); + Tags.URL.set(span, request.getRequestURL().toString()); + Tags.HTTP.METHOD.set(span, request.getMethod()); + span.setComponent(ComponentsDefine.STRUTS2); + SpanLayer.asHttp(span); + } + + @Override + public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, + Object ret) throws Throwable { + HttpServletResponse response = ServletActionContext.getResponse(); + + AbstractSpan span = ContextManager.activeSpan(); + int statusCode = response.getStatus(); + Tags.HTTP_RESPONSE_STATUS_CODE.set(span, statusCode); + if (statusCode >= 400) { + span.errorOccurred(); + } + ContextManager.stopSpan(); + return ret; + } + + @Override + public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, + Class[] argumentsTypes, Throwable t) { + ContextManager.activeSpan().log(t); + } +} diff --git a/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/struts2/v7/define/Struts2Instrumentation.java b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/struts2/v7/define/Struts2Instrumentation.java new file mode 100644 index 0000000000..68302e8c5a --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/struts2/v7/define/Struts2Instrumentation.java @@ -0,0 +1,75 @@ +/* + * 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.struts2.v7.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.match.NameMatch.byName; + +/** + * {@link Struts2Instrumentation} enhances the invokeAction method of Struts 7's + * org.apache.struts2.DefaultActionInvocation (relocated from + * com.opensymphony.xwork2.DefaultActionInvocation in Struts 2.x). The class name is + * unique to Struts 7, so it self-witnesses — no witness class is needed, and there is no overlap + * with the struts2-2.x plugin, which enhances the old XWork class. + */ +public class Struts2Instrumentation extends ClassInstanceMethodsEnhancePluginDefine { + + private static final String ENHANCE_CLASS = "org.apache.struts2.DefaultActionInvocation"; + private static final String ENHANCE_METHOD = "invokeAction"; + private static final String INTERCEPT_CLASS = "org.apache.skywalking.apm.plugin.struts2.v7.Struts2Interceptor"; + + @Override + public ConstructorInterceptPoint[] getConstructorsInterceptPoints() { + return new ConstructorInterceptPoint[0]; + } + + @Override + public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() { + return new InstanceMethodsInterceptPoint[] { + new InstanceMethodsInterceptPoint() { + @Override + public ElementMatcher getMethodsMatcher() { + return named(ENHANCE_METHOD); + } + + @Override + public String getMethodsInterceptor() { + return INTERCEPT_CLASS; + } + + @Override + public boolean isOverrideArgs() { + return false; + } + } + }; + } + + @Override + protected ClassMatch enhanceClass() { + return byName(ENHANCE_CLASS); + } +} diff --git a/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/resources/skywalking-plugin.def b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/resources/skywalking-plugin.def new file mode 100644 index 0000000000..a9a45c666b --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/resources/skywalking-plugin.def @@ -0,0 +1,18 @@ +# +# 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. + +struts2-7.x=org.apache.skywalking.apm.plugin.struts2.v7.define.Struts2Instrumentation 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 2e5ad78336..ac9ea19eeb 100644 --- a/docs/en/setup/service-agent/java-agent/Plugin-list.md +++ b/docs/en/setup/service-agent/java-agent/Plugin-list.md @@ -56,6 +56,7 @@ - jetty-client-9.0 - jetty-client-9.x - jetty-server +- jetty-server-12.x - kafka-0.11.x/1.x/2.x - kafka-3.7.x - kafka-3.9.x @@ -132,6 +133,7 @@ - spring-webflux-5.x-webclient - spymemcached-2.x - struts2-2.x +- struts2-7.x - thrift - tomcat - toolkit-counter diff --git a/docs/en/setup/service-agent/java-agent/Plugin-test.md b/docs/en/setup/service-agent/java-agent/Plugin-test.md index a40eb82e35..ca1d6ef2c2 100644 --- a/docs/en/setup/service-agent/java-agent/Plugin-test.md +++ b/docs/en/setup/service-agent/java-agent/Plugin-test.md @@ -16,11 +16,11 @@ Every plugin maintained in the main repo requires corresponding test cases as we ## Case Base Image Introduction -The test framework provides `JVM-container` and `Tomcat-container` base images including JDK8 and JDK17. You can choose the best one for your test case. If both are suitable for your case, **`JVM-container` is preferred**. +The test framework provides `JVM-container` and `Tomcat-container` base images. You can choose the best one for your test case. If both are suitable for your case, **`JVM-container` is preferred**. ### JVM-container Image Introduction -[JVM-container](../../../../../test/plugin/containers/jvm-container) uses `eclipse-temurin:8-jdk` as the base image. `JVM-container` supports JDK8 and JDK17 as well in CI, which inherits `eclipse-temurin:8-jdk` and `eclipse-temurin:17-jdk`. +[JVM-container](../../../../../test/plugin/containers/jvm-container) uses `eclipse-temurin:8-jdk` as the default base image. In CI it is exercised on **JDK 8, 11, 17, 21 and 25** (via the `eclipse-temurin:{8,11,17,21,25}-jdk` images). It is supported to custom the base Java docker image by specify `base_image_java`. The test case project must be packaged as `project-name.zip`, including `startup.sh` and uber jar, by using `mvn clean package`. @@ -31,8 +31,8 @@ Take the following test projects as examples: ### Tomcat-container Image Introduction -[Tomcat-container](../../../../../test/plugin/containers/tomcat-container) uses `tomcat:8.5-jdk8-openjdk`, `tomcat:8.5-jdk17-openjdk` as the base image. -It is supported to custom the base Tomcat docker image by specify `base_image_tomcat`. +[Tomcat-container](../../../../../test/plugin/containers/tomcat-container) uses `tomcat:8.5-jdk8-openjdk` as the default base image. In CI it is exercised on **Tomcat 8.5, 9.0, 10.0 and 10.1** (e.g. `tomcat:8.5-jdk8-openjdk`, `tomcat:9.0.71-jdk8`, `tomcat:10.0.22-jdk8`, `tomcat:8.5-jdk17-openjdk`, `tomcat:10.1-jdk17-temurin`, `tomcat:10.1-jdk21-temurin`). Tomcat 8.5/9.0 are `javax.servlet`; Tomcat 10.0/10.1 are `jakarta.servlet`. +It is supported to custom the base Tomcat docker image by specify `base_image_tomcat`. The base image must provide `curl` (used by the container's health check); if an official image lacks it, build a small custom image that adds `curl` first — as the JDK 25 lane does with `tomcat-curl:10.1.50-jdk25-temurin`. The test case project must be packaged as `project-name.war` by using `mvn package`. Take the following test project as an example 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 ca6f481f06..1416ec3471 100644 --- a/docs/en/setup/service-agent/java-agent/Supported-list.md +++ b/docs/en/setup/service-agent/java-agent/Supported-list.md @@ -11,10 +11,10 @@ metrics based on the tracing data. * Spring MVC 3.x, 4.x 5.x with servlet 3.x * Spring MVC 6.x (Optional²) * [Nutz Web Framework](https://github.com/nutzam/nutz) 1.x - * [Struts2 MVC](http://struts.apache.org/) 2.3.x -> 2.5.x + * [Struts2 MVC](http://struts.apache.org/) 2.3.x -> 2.5.x, 7.x (Jakarta, JDK17) * Resin 3 (Optional¹), See [SkySPM Plugin Repository](https://github.com/SkyAPM/java-plugin-extensions) * Resin 4 (Optional¹), See [SkySPM Plugin Repository](https://github.com/SkyAPM/java-plugin-extensions) - * [Jetty Server](http://www.eclipse.org/jetty/) 9.x -> 11.x + * [Jetty Server](http://www.eclipse.org/jetty/) 9.x -> 12.x * [Spring WebFlux](https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html) 5.x (Optional²) -> 6.x (Optional²) * [Undertow](http://undertow.io/) 1.3.0.Final -> 2.3.18.Final * [RESTEasy](https://resteasy.dev/) 3.1.0.Final -> 6.2.4.Final @@ -172,7 +172,7 @@ The meter plugin provides the advanced metrics collections, which are not a part * [Undertow](https://github.com/undertow-io/undertow) 2.1.x -> 2.6.x * [Tomcat](https://github.com/apache/tomcat) 7.0.x -> 10.0.x * [Dubbo](https://github.com/apache/dubbo) 2.5.x -> 2.7.x - * [Jetty](https://github.com/eclipse/jetty.project) 9.1.x -> 11.x + * [Jetty](https://github.com/eclipse/jetty.project) 9.1.x -> 12.x * [Grizzly](https://github.com/eclipse-ee4j/grizzly) 2.3.x -> 4.x * Connection Pool * Supported JDBC drviers diff --git a/test/plugin/CLAUDE.md b/test/plugin/CLAUDE.md new file mode 100644 index 0000000000..6b73f93540 --- /dev/null +++ b/test/plugin/CLAUDE.md @@ -0,0 +1,134 @@ +# CLAUDE.md - Plugin Test Guide + +This file guides AI assistants writing and changing **plugin tests** under `test/plugin/scenarios/`. +For the full test-framework mechanics (scenario layout, `configuration.yml`, `expectedData.yaml`, +containers, running locally) see the **"Plugin Test Framework"** section of +`apm-sniffer/apm-sdk-plugin/CLAUDE.md`, and `docs/.../Plugin-test.md`. + +## Testing philosophy: plugin scenarios are the key test — not unit tests + +For an agent plugin, the **plugin test scenario is the primary and sufficient test**. Do **not** +over-engineer by adding many unit tests for a plugin. + +A plugin's real risk is two things, and a scenario proves both at once by running the **real +framework**: + +1. **Cast / fetch-data logic** — the interceptor casting the intercepted arguments to the framework + types and reading request/response fields off them (headers, method, URI, status, params). Mocks + can't validate that the real framework class actually exposes those methods the way you assumed. +2. **Version compatibility** — the same interceptor must work across the supported version range. + +So: **when adding or changing a plugin, add or extend its scenario**, not a pile of mock-based unit +tests. Reserve unit tests for genuinely logic/namespace-critical **shared** code where a deterministic +in-JVM assertion earns its keep (e.g. `servlet-commons` `wrap()` resolving javax vs jakarta with both +APIs on the test classpath) — never for per-plugin interceptor mocking. + +## Validation path: assert the full span shape your plugin produces + +A scenario proves a plugin by asserting the **exact spans** the real framework makes the agent +produce. Pick the shape by what the framework actually does — and don't settle for a single entry +span when the plugin also does propagation, or you silently skip the inject/extract path. + +- **RPC / web frameworks that have both a server and a client side** (Struts, Spring MVC, gRPC, + Dubbo, HTTP servers …): drive the full round-trip — + `curl → server → its own client call → another service's endpoint`. The trace then contains + **2 entry spans + 1 exit span**: + 1. the **server entry** span for the request `curl` hit (proves the server side: extract + entry), + 2. the **client exit** span for the outbound call (proves inject + exit), + 3. a **second entry** span on the other endpoint carrying a **cross-process ref** back to the + first (proves propagation across the wire — the receiving/extract side). + + **The ref itself must be asserted** — for RPC the cross-process `refs:` block is the whole point, + so `expectedData.yaml` must include it on that second entry span with `refType: CrossProcess`, + `parentEndpoint`, `parentSpanId` (pointing at the exit span), `parentTraceSegmentId: not null`, + `parentService`, and `traceId: not null`. A scenario that asserts spans but not the ref does not + prove propagation. + + The struts2.7 scenario is exactly this: `case.action` (entry) → HttpClient (exit) → `case1.action` + (entry + ref); jetty-12 uses the same shape via a JDK-HttpURLConnection self-call. Keep that nested + self-call — it is the only thing that proves inject / extract / propagation; a lone entry span does not. + +- **Client-only plugins** (JDBC/PostgreSQL, Redis, MongoDB, an HTTP client, a message producer): the + peer is a database/broker, not another instrumented SkyWalking service, so there is **no second + entry span**. Assert just the **client exit** span (component, `peer`, and the `db.*` / `http.*` + tags). You do NOT need to stand up another service endpoint. + +## Version coverage: one version per minor (latest patch) + +`support-version.list` must cover the framework's supported range, but keep **one version per minor +version — the latest patch**, not every patch. E.g. Jetty `12.0.36` + `12.1.10`; Struts `7.0.3`, +`7.1.1`, `7.2.1`; Spring `6.0.4`, `6.1.1`, `6.2.19`. Verify each version resolves on Maven Central. +This is the compatibility proof; CI runs the scenario against each listed version. + +## How the test containers work (and where the JDK/Tomcat version really comes from) + +A scenario runs inside one of two prebuilt container images, selected by `type:` in +`configuration.yml`: + +- **`type: jvm`** → the **`agent-test-jvm`** container. Your app is a fat-jar started by + `bin/startup.sh` with `${agent_opts}` (the `-javaagent:` line — you MUST include it in + `startup.sh`). Built `FROM ${base_image_java}`. +- **`type: tomcat`** → the **`agent-test-tomcat`** container. Your app is a `*.war` dropped into + `/usr/local/tomcat/webapps/`; the agent is wired in via the container's patched `catalina.sh`. + Built `FROM ${base_image_tomcat}`. + +**The version pin is in the CI lane, not `configuration.yml`.** `configuration.yml` only says +`type: tomcat` — it does **not** choose a Tomcat version. The concrete JDK and Tomcat versions come +from the `base_image_java` / `base_image_tomcat` inputs passed to `./.github/actions/build` in the +lane's `Build` job, and **every scenario in that lane shares that one image**. So: + +- `type: tomcat` on the `plugins-jdk8-*` lane → `tomcat:8.5-jdk8` (javax); + on `plugins-jdk11-*` → `tomcat:9.0-jdk11` (javax); + on `plugins-jdk17-*` → `tomcat:10.1-jdk17` (**jakarta**). +- That is why a jakarta/Servlet-6 framework (Struts 7, Jetty 12, Spring 6) MUST be registered on a + JDK-17 lane — it needs the Tomcat-10.1 base — and a javax one on an older lane. Putting a jakarta + WAR on a Tomcat-8.5 lane just won't deploy. + +To run locally you pass the same two knobs explicitly (the defaults are the javax/JDK-8 pair): +`bash test/plugin/run.sh --base_image_java eclipse-temurin:17-jdk --base_image_tomcat tomcat:10.1-jdk17-temurin `. +Some newer bases lack `curl` (the container health check needs it) — the JDK-25 lane bakes it in +via `.github/workflows/Dockerfile-tomcat-jdk25-withCurl`. + +## How the GitHub Actions lanes work + +Scenarios are load-balanced across `.github/workflows/plugins--test..yaml` files. Each +file is one lane: its `Build` job pins `base_image_java` + `base_image_tomcat`, and its `test` job's +`matrix.case:` list names the scenarios that run on that image. Lanes exist per JDK +(`jdk8`/`jdk11`/`jdk17`/`jdk21`/`jdk25`); the trailing `.0`/`.1`/… is just a bucket to spread load. +Register a new scenario by adding its directory name to the `matrix.case:` list of the lane whose +base image matches the framework's needs — use `python3 tools/select-group.py` to pick the +least-loaded bucket in that JDK lane. + +## Cold-start and the 3s entry-service timeout (a real gotcha) + +The container's `run.sh` runs under `set -e` and hits the entry service with +`curl -s --max-time 3 ${SCENARIO_ENTRY_SERVICE}` — **no warm-up first**. So the very first (cold) +entry request must finish within **3 seconds** or `curl` exits 28 and the whole scenario fails. That +first request pays a lot of one-time cost at once: the agent's ByteBuddy enhancement of every class +it touches on that path, JIT, and — for WARs — a cold Jasper **JSP compile**, all under the +container's tight `-Xmx256m` heap and whatever CPU the runner gives, with **jacoco** simultaneously +instrumenting the `org.apache.skywalking.*` classes for coverage. + +Keep the entry path's cold cost low: + +- Don't render a **JSP** from the entry action if a lightweight result works (e.g. Struts + `httpheader`, a plain-text/stream result). The view is irrelevant to what a plugin scenario + verifies (entry/exit spans, tags, propagation), and a cold Jasper compile is expensive. +- Be extra careful when the entry action makes a **nested internal call** (for cross-process + propagation coverage): if that nested request also does cold work (a JSP compile) *while* the + outer request is still cold-enhancing Struts/HttpClient, the two cold-start storms contend for the + limited CPU and heap and blow up **super-linearly** (observed ~0.5s each in isolation but ~3–20s + nested under throttle). This is a slow first request, not a plugin bug — verify with a + CPU-throttled container (`docker run --cpus=0.5 …`) before assuming a hang. + +## Practical notes + +- One scenario per plugin (or per javax/jakarta era). Mirror an existing sibling scenario as a + template and change only what differs (framework version, namespace, deployment model). +- Match `type:` to the deployment: `jvm` (fat-jar started by `bin/startup.sh`, `${agent_opts}` + required) or `tomcat` (WAR on a container). Jakarta/newer frameworks go in a JDK-17 CI group. +- Assert the entry span shape that exercises the plugin's data logic: `componentId`, `spanType: Entry`, + `spanLayer: Http`, and the `url` / `http.method` / `http.status_code` (and `http.params` / + `http.headers` where the plugin collects them) tags, plus cross-process refs. +- Register the scenario in the right `.github/workflows/plugins-*.yaml` matrix (use + `python3 tools/select-group.py` to pick the least-loaded group for its JDK lane). diff --git a/test/plugin/scenarios/jetty-12.x-scenario/bin/startup.sh b/test/plugin/scenarios/jetty-12.x-scenario/bin/startup.sh new file mode 100644 index 0000000000..ea5bec3c39 --- /dev/null +++ b/test/plugin/scenarios/jetty-12.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} ${home}/../libs/jetty-12.x-scenario.jar & diff --git a/test/plugin/scenarios/jetty-12.x-scenario/config/expectedData.yaml b/test/plugin/scenarios/jetty-12.x-scenario/config/expectedData.yaml new file mode 100644 index 0000000000..b3e44c9df3 --- /dev/null +++ b/test/plugin/scenarios/jetty-12.x-scenario/config/expectedData.yaml @@ -0,0 +1,86 @@ +# 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: jetty-12.x-scenario + segmentSize: gt 2 + segments: + - segmentId: not null + spans: + - operationName: /jetty-12.x-scenario/case/case1 + parentSpanId: '-1' + spanId: '0' + spanLayer: Http + startTime: gt 0 + endTime: gt 0 + componentId: '19' + isError: 'false' + spanType: Entry + peer: '' + skipAnalysis: 'false' + tags: + - key: url + value: http://localhost:18080/jetty-12.x-scenario/case/case1 + - key: http.method + value: GET + - key: http.status_code + value: '200' + refs: + - parentEndpoint: /jetty-12.x-scenario/case/jetty-12.x-scenario + networkAddress: localhost:18080 + refType: CrossProcess + parentSpanId: '1' + parentTraceSegmentId: not null + parentServiceInstance: not null + parentService: jetty-12.x-scenario + traceId: not null + - segmentId: not null + spans: + - operationName: /jetty-12.x-scenario/case/case1 + parentSpanId: '0' + spanId: '1' + spanLayer: Http + startTime: gt 0 + endTime: gt 0 + componentId: '2' + isError: 'false' + spanType: Exit + peer: localhost:18080 + skipAnalysis: 'false' + tags: + - key: url + value: http://localhost:18080/jetty-12.x-scenario/case/case1 + - key: http.method + value: GET + - key: http.status_code + value: '200' + - operationName: /jetty-12.x-scenario/case/jetty-12.x-scenario + parentSpanId: '-1' + spanId: '0' + spanLayer: Http + startTime: gt 0 + endTime: gt 0 + componentId: '19' + isError: 'false' + spanType: Entry + peer: '' + skipAnalysis: 'false' + tags: + - key: url + value: http://localhost:18080/jetty-12.x-scenario/case/jetty-12.x-scenario + - key: http.method + value: GET + - key: http.status_code + value: '200' diff --git a/test/plugin/scenarios/jetty-12.x-scenario/configuration.yml b/test/plugin/scenarios/jetty-12.x-scenario/configuration.yml new file mode 100644 index 0000000000..2db90d0c99 --- /dev/null +++ b/test/plugin/scenarios/jetty-12.x-scenario/configuration.yml @@ -0,0 +1,22 @@ +# 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:18080/jetty-12.x-scenario/case/jetty-12.x-scenario +healthCheck: http://localhost:18080/jetty-12.x-scenario/case/healthCheck +startScript: ./bin/startup.sh +environment: +dependencies: diff --git a/test/plugin/scenarios/jetty-12.x-scenario/pom.xml b/test/plugin/scenarios/jetty-12.x-scenario/pom.xml new file mode 100644 index 0000000000..6a28e254bf --- /dev/null +++ b/test/plugin/scenarios/jetty-12.x-scenario/pom.xml @@ -0,0 +1,128 @@ + + + + + org.apache.skywalking.apm.testcase + jetty-12.x-scenario + 1.0.0 + jar + + 4.0.0 + + + UTF-8 + 17 + 3.8.1 + + 12.0.36 + + 2.6.2 + + + skywalking-jetty-12.x-scenario + + + + org.eclipse.jetty + jetty-server + ${test.framework.version} + + + + org.apache.httpcomponents + httpclient + 4.3 + + + + + + jetty-12.x-scenario + + + org.apache.maven.plugins + maven-shade-plugin + 3.1.0 + + + package + + shade + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + org.apache.skywalking.apm.testcase.jetty12xserver.Application + + + + + + + + + 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/jetty-12.x-scenario/src/main/assembly/assembly.xml b/test/plugin/scenarios/jetty-12.x-scenario/src/main/assembly/assembly.xml new file mode 100644 index 0000000000..4be83c4cb7 --- /dev/null +++ b/test/plugin/scenarios/jetty-12.x-scenario/src/main/assembly/assembly.xml @@ -0,0 +1,41 @@ + + + + + zip + + + + + ./bin + 0775 + + + + + + ${project.build.directory}/jetty-12.x-scenario.jar + ./libs + 0775 + + + diff --git a/test/plugin/scenarios/jetty-12.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/jetty12xserver/Application.java b/test/plugin/scenarios/jetty-12.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/jetty12xserver/Application.java new file mode 100644 index 0000000000..aecb666125 --- /dev/null +++ b/test/plugin/scenarios/jetty-12.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/jetty12xserver/Application.java @@ -0,0 +1,37 @@ +/* + * 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.jetty12xserver; + +import org.apache.skywalking.apm.testcase.jetty12xserver.handler.CaseHandler; +import org.eclipse.jetty.server.Server; + +/** + * Embedded Jetty 12 server. Jetty 12 handling routes every request through + * {@code org.eclipse.jetty.server.Server#handle(Request, Response, Callback)} — the method the + * jetty-server-12.x plugin enhances — before delegating to the top-level {@link CaseHandler}. + */ +public class Application { + + public static void main(String[] args) throws Exception { + Server server = new Server(18080); + server.setHandler(new CaseHandler()); + server.start(); + server.join(); + } +} diff --git a/test/plugin/scenarios/jetty-12.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/jetty12xserver/handler/CaseHandler.java b/test/plugin/scenarios/jetty-12.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/jetty12xserver/handler/CaseHandler.java new file mode 100644 index 0000000000..201ab06792 --- /dev/null +++ b/test/plugin/scenarios/jetty-12.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/jetty12xserver/handler/CaseHandler.java @@ -0,0 +1,67 @@ +/* + * 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.jetty12xserver.handler; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import org.apache.http.HttpEntity; +import org.apache.http.client.ResponseHandler; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +/** + * Top-level Jetty 12 core handler. + * + * The entry service {@code /jetty-12.x-scenario/case/jetty-12.x-scenario} makes a nested internal + * call to {@code /jetty-12.x-scenario/case/case1} on the same server. This exercises both sides of + * the jetty-server-12.x plugin in one trace: the entry span for the inbound request, the HttpClient + * exit span for the outbound call (inject), and a second entry span on {@code case1} that extracts + * the propagated context into a cross-process ref (the receiving/extract side). The health check + * {@code /case/healthCheck} and the nested {@code /case/case1} endpoint simply return 200. + */ +public class CaseHandler extends Handler.Abstract { + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + String path = request.getHttpURI().getPath(); + if (path != null && path.endsWith("/case/jetty-12.x-scenario")) { + visit("http://localhost:18080/jetty-12.x-scenario/case/case1"); + } + response.setStatus(200); + response.write(true, ByteBuffer.wrap("hello".getBytes(StandardCharsets.UTF_8)), callback); + return true; + } + + private static void visit(String url) throws Exception { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpGet httpget = new HttpGet(url); + ResponseHandler responseHandler = response -> { + HttpEntity entity = response.getEntity(); + return entity != null ? EntityUtils.toString(entity) : null; + }; + httpClient.execute(httpget, responseHandler); + } + } +} diff --git a/test/plugin/scenarios/jetty-12.x-scenario/src/main/resources/log4j2.xml b/test/plugin/scenarios/jetty-12.x-scenario/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..b5cda5ae8a --- /dev/null +++ b/test/plugin/scenarios/jetty-12.x-scenario/src/main/resources/log4j2.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + diff --git a/test/plugin/scenarios/jetty-12.x-scenario/support-version.list b/test/plugin/scenarios/jetty-12.x-scenario/support-version.list new file mode 100644 index 0000000000..6c62f269a7 --- /dev/null +++ b/test/plugin/scenarios/jetty-12.x-scenario/support-version.list @@ -0,0 +1,18 @@ +# 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. + +12.0.36 +12.1.10 diff --git a/test/plugin/scenarios/struts2.7-scenario/config/expectedData.yaml b/test/plugin/scenarios/struts2.7-scenario/config/expectedData.yaml new file mode 100644 index 0000000000..02d7206ab2 --- /dev/null +++ b/test/plugin/scenarios/struts2.7-scenario/config/expectedData.yaml @@ -0,0 +1,72 @@ +# 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: struts2.7-scenario + segmentSize: gt 2 + segments: + - segmentId: not null + spans: + - operationName: /struts2.7-scenario/case/case1.action + parentSpanId: -1 + spanId: 0 + spanLayer: Http + startTime: gt 0 + endTime: gt 0 + componentId: 15 + isError: false + spanType: Entry + peer: '' + tags: + - {key: url, value: 'http://localhost:8080/struts2.7-scenario/case/case1.action'} + - {key: http.method, value: GET} + - {key: http.status_code, value: '200'} + refs: + - {parentEndpoint: /struts2.7-scenario/case/case.action, networkAddress: 'localhost:8080', + refType: CrossProcess, parentSpanId: 1, parentTraceSegmentId: not null, parentServiceInstance: not + null, parentService: struts2.7-scenario, traceId: not null} + skipAnalysis: 'false' + - segmentId: not null + spans: + - operationName: /struts2.7-scenario/case/case1.action + parentSpanId: 0 + spanId: 1 + spanLayer: Http + startTime: gt 0 + endTime: gt 0 + componentId: 2 + isError: false + spanType: Exit + peer: localhost:8080 + tags: + - {key: url, value: 'http://localhost:8080/struts2.7-scenario/case/case1.action'} + - {key: http.method, value: GET} + - {key: http.status_code, value: '200'} + skipAnalysis: 'false' + - operationName: /struts2.7-scenario/case/case.action + parentSpanId: -1 + spanId: 0 + spanLayer: Http + startTime: gt 0 + endTime: gt 0 + componentId: 15 + isError: false + spanType: Entry + peer: '' + tags: + - {key: url, value: 'http://localhost:8080/struts2.7-scenario/case/case.action'} + - {key: http.method, value: GET} + - {key: http.status_code, value: '200'} + skipAnalysis: 'false' diff --git a/test/plugin/scenarios/struts2.7-scenario/configuration.yml b/test/plugin/scenarios/struts2.7-scenario/configuration.yml new file mode 100644 index 0000000000..b16e56768e --- /dev/null +++ b/test/plugin/scenarios/struts2.7-scenario/configuration.yml @@ -0,0 +1,21 @@ +# 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: tomcat +entryService: http://localhost:8080/struts2.7-scenario/case/case.action +healthCheck: http://localhost:8080/struts2.7-scenario/healthCheck +environment: +dependencies: diff --git a/test/plugin/scenarios/struts2.7-scenario/pom.xml b/test/plugin/scenarios/struts2.7-scenario/pom.xml new file mode 100644 index 0000000000..beede09526 --- /dev/null +++ b/test/plugin/scenarios/struts2.7-scenario/pom.xml @@ -0,0 +1,96 @@ + + + + + org.apache.skywalking.apm.testcase + struts2.7-scenario + 1.0.0 + war + + 4.0.0 + + skywalking-struts2.7-scenario + + + UTF-8 + + 17 + 3.8.1 + 7.2.1 + 6.0.0 + 1.18.42 + + + + + jakarta.servlet + jakarta.servlet-api + ${jakarta-servlet-api.version} + provided + + + org.apache.logging.log4j + log4j-api + 2.8.1 + + + org.apache.logging.log4j + log4j-core + 2.8.1 + + + org.projectlombok + lombok + ${lombok.version} + provided + + + org.apache.struts + struts2-core + ${test.framework.version} + + + org.apache.httpcomponents + httpclient + 4.3 + + + + + struts2.7-scenario + + + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${compiler.version} + ${compiler.version} + ${project.build.sourceEncoding} + + + + org.apache.maven.plugins + maven-war-plugin + 3.3.1 + + + + diff --git a/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/Case1Action.java b/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/Case1Action.java new file mode 100644 index 0000000000..9f40d9c99f --- /dev/null +++ b/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/Case1Action.java @@ -0,0 +1,28 @@ +/* + * 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.struts; + +import java.io.IOException; + +public class Case1Action { + + public String execute() throws IOException { + return "SUCCESS"; + } +} diff --git a/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/CaseAction.java b/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/CaseAction.java new file mode 100644 index 0000000000..89a4c70e36 --- /dev/null +++ b/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/CaseAction.java @@ -0,0 +1,47 @@ +/* + * 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.struts; + +import org.apache.http.HttpEntity; +import org.apache.http.client.ResponseHandler; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; + +import java.io.IOException; + +public class CaseAction { + + public String execute() throws IOException { + visit(); + return "SUCCESS"; + } + + private static void visit() throws IOException { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpGet httpget = new HttpGet("http://localhost:8080/struts2.7-scenario/case/case1.action"); + ResponseHandler responseHandler = response -> { + HttpEntity entity = response.getEntity(); + return entity != null ? EntityUtils.toString(entity) : null; + }; + httpClient.execute(httpget, responseHandler); + } + } +} diff --git a/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/HealthCheckServlet.java b/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/HealthCheckServlet.java new file mode 100644 index 0000000000..31458cea43 --- /dev/null +++ b/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/HealthCheckServlet.java @@ -0,0 +1,41 @@ +/* + * 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.struts; + +import java.io.IOException; +import java.io.PrintWriter; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class HealthCheckServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // your codes + PrintWriter writer = resp.getWriter(); + writer.write("Success"); + writer.flush(); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + doGet(req, resp); + } +} diff --git a/test/plugin/scenarios/struts2.7-scenario/src/main/resources/log4j2.xml b/test/plugin/scenarios/struts2.7-scenario/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..b5cda5ae8a --- /dev/null +++ b/test/plugin/scenarios/struts2.7-scenario/src/main/resources/log4j2.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + diff --git a/test/plugin/scenarios/struts2.7-scenario/src/main/resources/struts.xml b/test/plugin/scenarios/struts2.7-scenario/src/main/resources/struts.xml new file mode 100644 index 0000000000..62faf006d4 --- /dev/null +++ b/test/plugin/scenarios/struts2.7-scenario/src/main/resources/struts.xml @@ -0,0 +1,46 @@ + + + + + + + + + + 200 + + + + + 200 + + + + diff --git a/test/plugin/scenarios/struts2.7-scenario/src/main/webapp/WEB-INF/web.xml b/test/plugin/scenarios/struts2.7-scenario/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..a43b33cc55 --- /dev/null +++ b/test/plugin/scenarios/struts2.7-scenario/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,44 @@ + + + skywalking-struts2.7-scenario + + + struts2 + org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter + + + + struts2 + /case/* + + + + healthCheck + org.apache.skywalking.apm.testcase.struts.HealthCheckServlet + + + + healthCheck + /healthCheck + + diff --git a/test/plugin/scenarios/struts2.7-scenario/support-version.list b/test/plugin/scenarios/struts2.7-scenario/support-version.list new file mode 100644 index 0000000000..5fcb5f103f --- /dev/null +++ b/test/plugin/scenarios/struts2.7-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. + +7.0.3 +7.1.1 +7.2.1