Skip to content
Merged
68 changes: 68 additions & 0 deletions instrumentation/netty-reactor-http-1.0.0/README.md
Original file line number Diff line number Diff line change
@@ -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
34 changes: 34 additions & 0 deletions instrumentation/netty-reactor-http-1.0.0/build.gradle
Original file line number Diff line number Diff line change
@@ -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'
}
Original file line number Diff line number Diff line change
@@ -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<String> headers = response.responseHeaders().getAll(name);
if (headers != null && !headers.isEmpty()) {
return headers.get(0);
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<Connection, SegmentData> 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;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
37 changes: 37 additions & 0 deletions instrumentation/spring-resttemplate-6.0.0/README.md
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading