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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/plugins-jdk17-test.0.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/plugins-jdk17-test.1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions apm-sniffer/apm-sdk-plugin/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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+
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
~
-->

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>jetty-plugins</artifactId>
<groupId>org.apache.skywalking</groupId>
<version>9.7.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>apm-jetty-server-12.x-plugin</artifactId>
<packaging>jar</packaging>

<name>jetty-server-12.x-plugin</name>
<url>http://maven.apache.org</url>

<!--
Jetty 12 removed the concrete org.eclipse.jetty.server.HttpChannel class that the merged
jetty-server plugin enhances, and redesigned request handling around the namespace-neutral
core API org.eclipse.jetty.server.Server#handle(Request, Response, Callback), which is async
(the response status is only known when the Callback completes). Hence a separate plugin that
does not use the servlet-commons wrapper. Covers Jetty 12.0.x and 12.1.x (same signature).
-->
<properties>
<jetty-server.version>12.0.36</jetty-server.version>
</properties>

<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${jetty-server.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -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<SwCallback> HOLDER = new ThreadLocal<SwCallback>();

@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();
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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<WitnessMethod> 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<MethodDescription> 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);
}
}
Loading
Loading