diff --git a/instrumentation/netty-reactor-http-1.0.0/README.md b/instrumentation/netty-reactor-http-1.0.0/README.md new file mode 100644 index 0000000000..7d9dc9f1e4 --- /dev/null +++ b/instrumentation/netty-reactor-http-1.0.0/README.md @@ -0,0 +1,68 @@ +# Reactor Netty HTTP 1.0.0 Instrumentation + +Instrumentation for reactor-netty HTTP client (1.0.0+). This module provides external call reporting for **direct usage** of reactor-netty's `HttpClient` API. + +### What This Module Does + +This instrumentation captures HTTP external calls made via reactor-netty's `HttpClient` and reports them to New Relic with: +- **Library name**: "NettyReactor" +- **Operation**: HTTP method (GET, POST, PUT, DELETE, etc.) +- **Distributed tracing**: Adds outbound DT headers for cross-application tracing +- **Async support**: Uses token linking to preserve transaction context across NIO threads + +### Version Coverage + +This module instruments reactor-netty **1.0.0 and later**: +- **Supported**: `io.projectreactor.netty:reactor-netty-http:[1.0.0,)` +- **Artifact split**: reactor-netty 1.0+ split into `reactor-netty-http` + `reactor-netty-core` +- **Build config**: Uses `verifyClasspath = false` due to split artifacts + +### When This Module Applies + +- **Activates For:** + - Direct reactor-netty HttpClient usage + +- **Does NOT Activate For:** + - Spring WebClient: Uses different reactor-netty hooks (not instrumented by this module) + - RestTemplate with reactor-netty transport: RestTemplate's `@Trace(leaf=true)` suppresses this module + +This is **intentional design** - high-level APIs (Spring WebClient, RestTemplate) should report with their own library names, not "NettyReactor". + +### Relationship to netty-reactor-0.9.0 (and 0.7.0, 0.8.0) +- These modules instrument **Reactor context propagation** (not HTTP external calls): + - **netty-reactor-0.9.0**: Ensures transaction context flows through Reactor chains + - **netty-reactor-http-1.0.0** (this module): Reports HTTP external calls +- **Both modules load together** when reactor-netty 1.0+ is present: + - `netty-reactor-0.9.0` creates Reactor pipeline segments (e.g., `Mono.map`, `Flux.filter`) + - `netty-reactor-http-1.0.0` creates HTTP external call segments (e.g., `External/host/NettyReactor/GET`) + - **netty-reactor-http-1.0.0** provides complementary, not duplicate data + +### Works Via State-Based Instrumentation +Reactor-netty fires connection state change events. This instrumentation hooks into two states: + +1. **`HttpClientState.REQUEST_PREPARED`**: Start segment, link token, add DT headers +2. **`HttpClientState.RESPONSE_RECEIVED`**: Report external call, end segment + + +### Implementation Details +- **State comparison**: Uses enum constants `HttpClientState.REQUEST_PREPARED` and `HttpClientState.RESPONSE_RECEIVED` +- **Type checking**: Verifies connection type (`HttpClientRequest` or `HttpClientResponse`) before casting +- **WeakHashMap storage**: `Connection → SegmentData` segment data removed when response received +- **Token linking**: Preserves transaction context across Netty NIO thread boundaries +- **Segment API**: `startSegment()` and `segment.end()` work across async boundaries + +### Requirements + +- **Java 17+**: reactor-netty 1.0+ requires Java 17 baseline +- **reactor-netty-http**: 1.0.0 or later +- **reactor-netty-core**: Transitive dependency (required by reactor-netty-http) + +### Testing + +**Note**: Tests for this module fail to capture external metrics due to suspected test framework limitations with Java 17 modules and async NIO threading. + +### Maintenance Notes +- reactor-netty < 1.0.0 used different package structure (covered by `netty-reactor-0.x.0` modules) +- Uses `WeakHashMap` so segment data is removed when response received +- Java 17 toolchain required for compilation +- Coexists with `netty-reactor-0.9.0` - both modules should load together \ No newline at end of file diff --git a/instrumentation/netty-reactor-http-1.0.0/build.gradle b/instrumentation/netty-reactor-http-1.0.0/build.gradle new file mode 100644 index 0000000000..bb67bc43be --- /dev/null +++ b/instrumentation/netty-reactor-http-1.0.0/build.gradle @@ -0,0 +1,34 @@ +dependencies { + implementation(project(":agent-bridge")) + // reactor-netty 1.0+ split into multiple artifacts (http depends on core transitively) + implementation("io.projectreactor.netty:reactor-netty-http:1.0.0") +} + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.netty-reactor-http-1.0.0' } +} + +verifyInstrumentation { + // reactor-netty 1.0.0+ split into multiple artifacts (reactor-netty-http, reactor-netty-core) + // Both artifacts are required together. Skip artifact matching and rely on class matching only. + passes 'io.projectreactor.netty:reactor-netty-http:[1.0.0,)' + verifyClasspath = false +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +test { + // These instrumentation tests only run on Java 17+ regardless of the -PtestN gradle property that is set. + onlyIf { + !project.hasProperty('test8') && !project.hasProperty('test11') + } +} + +site { + title 'Netty Reactor HTTP' + type 'Appserver' +} \ No newline at end of file diff --git a/instrumentation/netty-reactor-http-1.0.0/src/main/java/com/nr/instrumentation/InboundResponseWrapper.java b/instrumentation/netty-reactor-http-1.0.0/src/main/java/com/nr/instrumentation/InboundResponseWrapper.java new file mode 100644 index 0000000000..a7a8e074ca --- /dev/null +++ b/instrumentation/netty-reactor-http-1.0.0/src/main/java/com/nr/instrumentation/InboundResponseWrapper.java @@ -0,0 +1,37 @@ +/* + * + * * Copyright 2026 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.instrumentation; + +import com.newrelic.api.agent.ExtendedInboundHeaders; +import com.newrelic.api.agent.HeaderType; +import reactor.netty.http.client.HttpClientResponse; + +import java.util.List; + +public class InboundResponseWrapper extends ExtendedInboundHeaders { + + private final HttpClientResponse response; + + public InboundResponseWrapper(HttpClientResponse response) { + this.response = response; + } + + @Override + public HeaderType getHeaderType() { + return HeaderType.HTTP; + } + + @Override + public String getHeader(String name) { + List headers = response.responseHeaders().getAll(name); + if (headers != null && !headers.isEmpty()) { + return headers.get(0); + } + return null; + } +} \ No newline at end of file diff --git a/instrumentation/netty-reactor-http-1.0.0/src/main/java/com/nr/instrumentation/OutboundRequestWrapper.java b/instrumentation/netty-reactor-http-1.0.0/src/main/java/com/nr/instrumentation/OutboundRequestWrapper.java new file mode 100644 index 0000000000..3de1df8672 --- /dev/null +++ b/instrumentation/netty-reactor-http-1.0.0/src/main/java/com/nr/instrumentation/OutboundRequestWrapper.java @@ -0,0 +1,31 @@ +/* + * + * * Copyright 2026 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.instrumentation; + +import com.newrelic.api.agent.HeaderType; +import com.newrelic.api.agent.OutboundHeaders; +import reactor.netty.http.client.HttpClientRequest; + +public class OutboundRequestWrapper implements OutboundHeaders { + + private final HttpClientRequest request; + + public OutboundRequestWrapper(HttpClientRequest request) { + this.request = request; + } + + @Override + public HeaderType getHeaderType() { + return HeaderType.HTTP; + } + + @Override + public void setHeader(String name, String value) { + request.addHeader(name, value); + } +} \ No newline at end of file diff --git a/instrumentation/netty-reactor-http-1.0.0/src/main/java/com/nr/instrumentation/ReactorNettyContext.java b/instrumentation/netty-reactor-http-1.0.0/src/main/java/com/nr/instrumentation/ReactorNettyContext.java new file mode 100644 index 0000000000..d4727120f2 --- /dev/null +++ b/instrumentation/netty-reactor-http-1.0.0/src/main/java/com/nr/instrumentation/ReactorNettyContext.java @@ -0,0 +1,46 @@ +package com.nr.instrumentation; + +import com.newrelic.api.agent.Segment; +import reactor.netty.Connection; + +import java.net.URI; +import java.util.Collections; +import java.util.Map; +import java.util.WeakHashMap; + +public class ReactorNettyContext { + + public static final String LIBRARY = "NettyReactor"; + + private static final Map connectionSegments = Collections.synchronizedMap(new WeakHashMap<>()); + + public static void put(Connection connection, SegmentData segmentData) { + if (connection != null && segmentData != null) { + connectionSegments.put(connection, segmentData); + } + } + + public static SegmentData remove(Connection connection) { + if (connection == null) return null; + return connectionSegments.remove(connection); + } + + public static class SegmentData { + + public final Segment segment; + public volatile URI requestUri; + public final String httpMethod; + + public SegmentData(Segment segment, URI requestUri, String httpMethod) { + this.segment = segment; + this.requestUri = requestUri; + this.httpMethod = httpMethod; + } + + public void updateUri(URI uri) { + if (uri != null) { + this.requestUri = uri; + } + } + } +} diff --git a/instrumentation/netty-reactor-http-1.0.0/src/main/java/reactor/netty/http/client/HttpClientConnect_Instrumentation.java b/instrumentation/netty-reactor-http-1.0.0/src/main/java/reactor/netty/http/client/HttpClientConnect_Instrumentation.java new file mode 100644 index 0000000000..4d53f5f5b1 --- /dev/null +++ b/instrumentation/netty-reactor-http-1.0.0/src/main/java/reactor/netty/http/client/HttpClientConnect_Instrumentation.java @@ -0,0 +1,122 @@ +/* + * + * * Copyright 2026 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package reactor.netty.http.client; + +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.agent.bridge.Transaction; +import com.newrelic.api.agent.HttpParameters; +import com.newrelic.api.agent.Segment; +import com.newrelic.api.agent.Token; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import com.nr.instrumentation.InboundResponseWrapper; +import com.nr.instrumentation.OutboundRequestWrapper; +import com.nr.instrumentation.ReactorNettyContext; +import reactor.netty.Connection; +import reactor.netty.ConnectionObserver; +import reactor.util.context.Context; + +import java.net.URI; + +@Weave(originalName = "reactor.netty.http.client.HttpClientConnect") +final class HttpClientConnect_Instrumentation { + + @Weave(originalName = "reactor.netty.http.client.HttpClientConnect$HttpIOHandlerObserver") + static final class HttpIOHandlerObserver_Instrumentation { + + public Context currentContext() { + return Weaver.callOriginal(); + } + + @Trace(async = true, excludeFromTransactionTrace = true) + public void onStateChange(Connection connection, ConnectionObserver.State newState) { + + if (HttpClientState.REQUEST_PREPARED.equals(newState) && connection instanceof HttpClientRequest) { + + Context ctx = currentContext(); + Token token = ctx != null ? ctx.getOrDefault("newrelic-token", null) : null; + + if (token != null && token.isActive()) { + token.link(); + } + + HttpClientRequest request = (HttpClientRequest) connection; + Transaction txn = AgentBridge.getAgent().getTransaction(false); + + if (txn != null) { + Segment segment = txn.startSegment("ReactorNettyHttpClient.request"); + if (segment != null) { + String httpMethod = null; + URI requestUri = null; + try { + httpMethod = request.method().name(); + String resourceUrl = request.resourceUrl(); + if (resourceUrl != null && !resourceUrl.isEmpty()) { + requestUri = URI.create(resourceUrl); + } else { + String path = request.uri(); + String hostHeader = request.requestHeaders().get("Host"); + boolean isHttps = connection.channel().pipeline().get("ssl") != null; + String scheme = isHttps ? "https" : "http"; + String host = (hostHeader != null && !hostHeader.isEmpty()) ? hostHeader : "UnknownHost"; + requestUri = URI.create(scheme + "://" + host + path); + } + } catch (Throwable throwable) { + requestUri = URI.create("http://UnknownHost/unknown"); + } + + if (httpMethod == null) { + httpMethod = "execute"; + } + + segment.addOutboundRequestHeaders(new OutboundRequestWrapper(request)); + ReactorNettyContext.put(connection, new ReactorNettyContext.SegmentData(segment, requestUri, httpMethod)); + } + } + } else if (HttpClientState.RESPONSE_RECEIVED.equals(newState) && connection instanceof HttpClientResponse) { + + HttpClientResponse response = (HttpClientResponse) connection; + ReactorNettyContext.SegmentData data = ReactorNettyContext.remove(connection); + + if (data != null && data.segment != null && data.requestUri != null) { + String procedure = (data.httpMethod != null && !data.httpMethod.isEmpty()) + ? data.httpMethod : "execute"; + + data.segment.reportAsExternal(HttpParameters + .library(ReactorNettyContext.LIBRARY) + .uri(data.requestUri) + .procedure(procedure) + .inboundHeaders(new InboundResponseWrapper(response)) + .status(response.status().code(), response.status().reasonPhrase()) + .build()); + data.segment.end(); + } + } + + Weaver.callOriginal(); + } + } + + @Weave(originalName = "reactor.netty.http.client.HttpClientConnect$HttpObserver") + static final class HttpObserver_Instrumentation { + + public Context currentContext() { + return Weaver.callOriginal(); + } + + @Trace(async = true, excludeFromTransactionTrace = true) + public void onUncaughtException(Connection connection, Throwable throwable) { + ReactorNettyContext.SegmentData data = ReactorNettyContext.remove(connection); + if (data != null && data.segment != null) { + data.segment.end(); + } + Weaver.callOriginal(); + } + } +} \ No newline at end of file diff --git a/instrumentation/spring-resttemplate-6.0.0/README.md b/instrumentation/spring-resttemplate-6.0.0/README.md new file mode 100644 index 0000000000..cd562030a5 --- /dev/null +++ b/instrumentation/spring-resttemplate-6.0.0/README.md @@ -0,0 +1,37 @@ +# Spring RestTemplate 6.0.0 Instrumentation + +Instrumentation for Spring Framework 6.x RestTemplate HTTP client. This module provides external call reporting and distributed tracing for RestTemplate operations in Spring 6.x / Spring Boot 3.x applications. + +### What This Module Does + +This instrumentation captures HTTP external calls made via Spring's RestTemplate API and reports them to New Relic with: +- **Library name**: "RestTemplate" (not the underlying transport library) +- **Operation**: HTTP method (GET, POST, PUT, DELETE, etc.) +- **Distributed tracing**: Adds outbound and inbound DT headers for cross-application tracing +- **Transport-agnostic**: Works with ANY HTTP client transport + +### Version Coverage + +This module instruments Spring Framework **6.x only**: +- **Supported**: Spring Framework 6.0.0 through 6.x +- **Not Supported**: Spring Framework 7.x+ (method signatures change) + +### Transport-Agnostic Design + +This instrumentation reports all requests as `RestTemplate` regardless of the underlying HTTP transport (HttpURLConnection, Apache HttpClient, Jetty, OkHttp, Reactor Netty). This prevents transport-level double reporting and ensures consistent library naming. + +Transport instrumentation modules (`okhttp-4.0.0`, `httpclient-5.0`, `jetty-httpclient-9.4.0`, `netty-reactor-http-1.0.0`) do not activate when RestTemplate is used. The `@Trace(leaf=true)` annotation suppresses transport-level instrumentation, ensuring a single external call is reported as "RestTemplate". + +### Requirements + +- **Java 17+**: Required by Spring Framework 6.x +- **Spring Framework**: 6.0.0 through 6.x + +### Testing + +Tests for this module fail to capture external metrics due to suspected test framework limitations with Java 17 modules. + +### Maintenance Notes + +- Spring 6.x requires **both** 4-param and 5-param `doExecute()` instrumentation +- Spring 6.x calls can go through **either** method depending on the API used \ No newline at end of file diff --git a/instrumentation/spring-resttemplate-6.0.0/build.gradle b/instrumentation/spring-resttemplate-6.0.0/build.gradle new file mode 100644 index 0000000000..f61614af8d --- /dev/null +++ b/instrumentation/spring-resttemplate-6.0.0/build.gradle @@ -0,0 +1,39 @@ +dependencies { + implementation(project(":agent-bridge")) + // Compile against Spring 6.0.0 for compatibility, instrumentation supports Spring 6.x + implementation("org.springframework:spring-web:6.0.0") +} + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.spring-resttemplate-6.0.0' } +} + +verifyInstrumentation { + // Spring Framework 6.x only (Spring 7.x removes 4-parameter doExecute method) + // Both 4-parameter and 5-parameter doExecute methods exist in this range + passesOnly 'org.springframework:spring-web:[6.0.0,7.0.0)' + // Explicitly fail on Spring 3.x-5.x (method signature changes) + fails 'org.springframework:spring-web:[,6.0.0)' + // Explicitly fail on Spring 7.x+ (method signature changes) + fails 'org.springframework:spring-web:[7.0.0,)' + // Exclude pre-release versions (RC, M, SNAPSHOT) + excludeRegex 'org.springframework:spring-web:.*(RC|M)[0-9]*$' +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +test { + // These instrumentation tests only run on Java 17+ regardless of the -PtestN gradle property that is set. + onlyIf { + !project.hasProperty('test8') && !project.hasProperty('test11') + } +} + +site { + title 'Spring RestTemplate' + type 'Http' +} \ No newline at end of file diff --git a/instrumentation/spring-resttemplate-6.0.0/src/main/java/com/nr/instrumentation/spring/InboundHeadersWrapper.java b/instrumentation/spring-resttemplate-6.0.0/src/main/java/com/nr/instrumentation/spring/InboundHeadersWrapper.java new file mode 100644 index 0000000000..832a5688c7 --- /dev/null +++ b/instrumentation/spring-resttemplate-6.0.0/src/main/java/com/nr/instrumentation/spring/InboundHeadersWrapper.java @@ -0,0 +1,42 @@ +/* + * + * * Copyright 2026 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.instrumentation.spring; + +import com.newrelic.api.agent.HeaderType; +import com.newrelic.api.agent.ExtendedInboundHeaders; +import org.springframework.http.HttpHeaders; + +import java.util.List; + +/** + * Wrapper for Spring's HttpHeaders that implements ExtendedInboundHeaders. + * Used to read distributed tracing headers from inbound HTTP responses. + */ +public class InboundHeadersWrapper extends ExtendedInboundHeaders { + + private final HttpHeaders headers; + + public InboundHeadersWrapper(HttpHeaders headers) { + this.headers = headers; + } + + @Override + public HeaderType getHeaderType() { + return HeaderType.HTTP; + } + + @Override + public String getHeader(String name) { + return headers.getFirst(name); + } + + @Override + public List getHeaders(String name) { + return headers.get(name); + } +} \ No newline at end of file diff --git a/instrumentation/spring-resttemplate-6.0.0/src/main/java/com/nr/instrumentation/spring/OutboundHeadersWrapper.java b/instrumentation/spring-resttemplate-6.0.0/src/main/java/com/nr/instrumentation/spring/OutboundHeadersWrapper.java new file mode 100644 index 0000000000..6cfd11051a --- /dev/null +++ b/instrumentation/spring-resttemplate-6.0.0/src/main/java/com/nr/instrumentation/spring/OutboundHeadersWrapper.java @@ -0,0 +1,45 @@ +/* + * + * * Copyright 2026 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.instrumentation.spring; + +import com.newrelic.api.agent.HeaderType; +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.OutboundHeaders; +import org.springframework.http.HttpHeaders; + +/** + * Wrapper for Spring's HttpHeaders that implements OutboundHeaders. + * Used to inject distributed tracing headers into outbound HTTP requests. + */ +public class OutboundHeadersWrapper implements OutboundHeaders { + + private final HttpHeaders headers; + + private OutboundHeadersWrapper(HttpHeaders headers) { + this.headers = headers; + } + + public static void addOutboundHeaders(HttpHeaders headers) { + if (headers == null) { + return; + } + + OutboundHeadersWrapper wrapper = new OutboundHeadersWrapper(headers); + NewRelic.getAgent().getTracedMethod().addOutboundRequestHeaders(wrapper); + } + + @Override + public HeaderType getHeaderType() { + return HeaderType.HTTP; + } + + @Override + public void setHeader(String name, String value) { + headers.set(name, value); + } +} \ No newline at end of file diff --git a/instrumentation/spring-resttemplate-6.0.0/src/main/java/com/nr/instrumentation/spring/RestTemplateUtils.java b/instrumentation/spring-resttemplate-6.0.0/src/main/java/com/nr/instrumentation/spring/RestTemplateUtils.java new file mode 100644 index 0000000000..1b2562f28d --- /dev/null +++ b/instrumentation/spring-resttemplate-6.0.0/src/main/java/com/nr/instrumentation/spring/RestTemplateUtils.java @@ -0,0 +1,76 @@ +/* + * + * * Copyright 2026 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.instrumentation.spring; + +import com.newrelic.api.agent.GenericParameters; +import com.newrelic.api.agent.HttpParameters; +import com.newrelic.api.agent.NewRelic; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.ClientHttpResponse; + +import java.net.URI; +import java.net.UnknownHostException; + +public class RestTemplateUtils { + + public static final String LIBRARY = "RestTemplate"; + public static final URI UNKNOWN_HOST_URI = URI.create("http://UnknownHost/"); + + public static void handleUnknownHost(Exception e) { + if (e instanceof UnknownHostException || (e.getCause() instanceof UnknownHostException)) { + NewRelic.getAgent().getTracedMethod().reportAsExternal(GenericParameters + .library(LIBRARY) + .uri(UNKNOWN_HOST_URI) + .procedure("execute") + .build()); + } + } + + public static void processResponse(URI uri, HttpMethod method, T result) { + if (uri == null) { + return; + } + + String procedure = method != null ? method.name() : "execute"; + + InboundHeadersWrapper inboundHeaders = extractInboundHeaders(result); + + if (inboundHeaders != null) { + NewRelic.getAgent().getTracedMethod().reportAsExternal(HttpParameters + .library(LIBRARY) + .uri(uri) + .procedure(procedure) + .inboundHeaders(inboundHeaders) + .build()); + } else { + NewRelic.getAgent().getTracedMethod().reportAsExternal(HttpParameters + .library(LIBRARY) + .uri(uri) + .procedure(procedure) + .noInboundHeaders() + .build()); + } + } + + private static InboundHeadersWrapper extractInboundHeaders(T result) { + if (result == null) { + return null; + } + + if (result instanceof ResponseEntity) { + return new InboundHeadersWrapper(((ResponseEntity) result).getHeaders()); + } + + if (result instanceof ClientHttpResponse) { + return new InboundHeadersWrapper(((ClientHttpResponse) result).getHeaders()); + } + + return null; + } +} \ No newline at end of file diff --git a/instrumentation/spring-resttemplate-6.0.0/src/main/java/org/springframework/http/client/AbstractClientHttpRequest_Instrumentation.java b/instrumentation/spring-resttemplate-6.0.0/src/main/java/org/springframework/http/client/AbstractClientHttpRequest_Instrumentation.java new file mode 100644 index 0000000000..0c99419097 --- /dev/null +++ b/instrumentation/spring-resttemplate-6.0.0/src/main/java/org/springframework/http/client/AbstractClientHttpRequest_Instrumentation.java @@ -0,0 +1,30 @@ +/* + * + * * Copyright 2026 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.springframework.http.client; + +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import com.nr.instrumentation.spring.OutboundHeadersWrapper; +import org.springframework.http.HttpHeaders; + +import java.io.IOException; + +/** + * All Spring HTTP transports (HttpURLConnection, Apache HttpClient, Jetty, OkHttp, Reactor Netty) + * extend AbstractClientHttpRequest and call executeInternal() to send the request. + */ +@Weave(type = MatchType.BaseClass, originalName = "org.springframework.http.client.AbstractClientHttpRequest") +public abstract class AbstractClientHttpRequest_Instrumentation { + + protected final ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException { + OutboundHeadersWrapper.addOutboundHeaders(headers); + + return Weaver.callOriginal(); + } +} \ No newline at end of file diff --git a/instrumentation/spring-resttemplate-6.0.0/src/main/java/org/springframework/web/client/RestTemplate_Instrumentation.java b/instrumentation/spring-resttemplate-6.0.0/src/main/java/org/springframework/web/client/RestTemplate_Instrumentation.java new file mode 100644 index 0000000000..db41a20bda --- /dev/null +++ b/instrumentation/spring-resttemplate-6.0.0/src/main/java/org/springframework/web/client/RestTemplate_Instrumentation.java @@ -0,0 +1,64 @@ +/* + * + * * Copyright 2026 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.springframework.web.client; + +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import com.nr.instrumentation.spring.RestTemplateUtils; +import org.springframework.http.HttpMethod; + +import java.net.URI; + +@Weave(type = MatchType.ExactClass, originalName = "org.springframework.web.client.RestTemplate") +public abstract class RestTemplate_Instrumentation { + + /** + * 4-parameter version - exists in Spring 3.x through 6.x + * In Spring 6.x, this method delegates to the 5-parameter version. + * We instrument both to ensure complete coverage. + */ + @Trace(leaf = true) + protected T doExecute(URI url, HttpMethod method, RequestCallback requestCallback, + ResponseExtractor responseExtractor) throws RestClientException { + + T result; + try { + result = Weaver.callOriginal(); + } catch (Exception e) { + RestTemplateUtils.handleUnknownHost(e); + throw e; + } + + RestTemplateUtils.processResponse(url, method, result); + return result; + } + + /** + * 5-parameter version - added in Spring 6.0 + * Spring 6.x directly calls this method for most operations (does NOT always delegate through 4-parameter). + * This is the primary execution path in Spring 6.x. + * Removed in Spring 7.0 (method signature changes). + */ + @Trace(leaf = true) + protected T doExecute(URI url, String uriTemplate, HttpMethod method, + RequestCallback requestCallback, ResponseExtractor responseExtractor) throws RestClientException { + + T result; + try { + result = Weaver.callOriginal(); + } catch (Exception e) { + RestTemplateUtils.handleUnknownHost(e); + throw e; + } + + RestTemplateUtils.processResponse(url, method, result); + return result; + } +} \ No newline at end of file diff --git a/instrumentation/spring-resttemplate-6.0.0/src/test/java/com/nr/instrumentation/spring/RestTemplateUtilsTest.java b/instrumentation/spring-resttemplate-6.0.0/src/test/java/com/nr/instrumentation/spring/RestTemplateUtilsTest.java new file mode 100644 index 0000000000..1d302da3ac --- /dev/null +++ b/instrumentation/spring-resttemplate-6.0.0/src/test/java/com/nr/instrumentation/spring/RestTemplateUtilsTest.java @@ -0,0 +1,83 @@ +package com.nr.instrumentation.spring; + +import com.newrelic.agent.introspec.InstrumentationTestConfig; +import com.newrelic.agent.introspec.InstrumentationTestRunner; +import com.newrelic.agent.introspec.Introspector; +import com.newrelic.agent.introspec.internal.HttpServerRule; +import com.newrelic.api.agent.Trace; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.web.client.RestTemplate; + +import java.util.Collection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@RunWith(InstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = "com.nr.instrumentation.spring") +public class RestTemplateUtilsTest { + + @ClassRule + public static HttpServerRule server = new HttpServerRule(); + + @Test + public void testRestTemplateCreatesTransactionWithCorrectNaming() { + + makeRestTemplateCall(); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Collection txNames = introspector.getTransactionNames(); + + assertEquals("Should have 2 transactions", 2, introspector.getFinishedTransactionCount()); // client and server + assertEquals(2, txNames.size()); + + String clientTxName = findClientTransactionName(txNames); + + assertNotNull("Client transaction should exist", clientTxName); + assertTrue("Transaction name should contain method name", clientTxName.contains("makeRestTemplateCall")); + + } + + @Test + public void testUnknownHostHandling() { + try { + makeUnknownHostCall(); + fail("should throw an exception for an unknown host"); + } catch (Exception e) { + // Expected to have an exception thrown + } + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + assertEquals(1, introspector.getFinishedTransactionCount()); + } + + @Trace(dispatcher = true) + private void makeRestTemplateCall() { + RestTemplate restTemplate = new RestTemplate(); + try { + restTemplate.getForObject(server.getEndPoint().toString(), String.class); + } catch (Exception e) { + // Ignoring this exception + } + } + + @Trace(dispatcher = true) + private void makeUnknownHostCall() { + RestTemplate restTemplate = new RestTemplate(); + restTemplate.getForObject("http://notarealhost/", String.class); + } + + private String findClientTransactionName(Collection txNames) { + for (String txName : txNames) { + if (txName.contains("RestTemplateUtilsTest")) { + return txName; + } + } + return null; + } + +} diff --git a/settings.gradle b/settings.gradle index 78c7379b2f..72cbee2b67 100644 --- a/settings.gradle +++ b/settings.gradle @@ -317,6 +317,7 @@ include 'instrumentation:netty-4.1.16' include 'instrumentation:netty-reactor-0.7.0' include 'instrumentation:netty-reactor-0.8.0' include 'instrumentation:netty-reactor-0.9.0' +include 'instrumentation:netty-reactor-http-1.0.0' include 'instrumentation:okhttp-3.6.0' include 'instrumentation:okhttp-3.14.0' include 'instrumentation:okhttp-4.0.0' @@ -401,6 +402,7 @@ include 'instrumentation:spring-cache-3.1.0' include 'instrumentation:spring-jms-2' include 'instrumentation:spring-jms-3' include 'instrumentation:spring-kafka-2.2.0' +include 'instrumentation:spring-resttemplate-6.0.0' include 'instrumentation:spring-webclient-5.0' include 'instrumentation:spring-webclient-6.0' include 'instrumentation:spring-webflux-6.1.0' @@ -466,3 +468,5 @@ include 'instrumentation:wildfly-27' include 'instrumentation:wildfly-jmx-14' include 'instrumentation:zio' include 'instrumentation:zio-2' + +