This document demonstrates using the New Relic Java agent API to instrument a simple client and server application. The instrumentation has these goals:
- To record external HTTP and datastore transactions.
- To link external transactions between two applications running New Relic agents (known as distributed tracing or DT).
See the Java agent API Javadoc for full descriptions of the available API classes and methods.
중요
For best results when using the API, ensure that you have the latest Java agent release. Several APIs used in the examples require Java agent 3.36.0 or higher.
Client-side example
Here is an example of the client-side code for a simple client-server application:
package com.newrelic.example;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
// New Relic API imports
import com.newrelic.api.agent.ExternalParameters;
import com.newrelic.api.agent.HeaderType;
import com.newrelic.api.agent.Headers;
import com.newrelic.api.agent.HttpParameters;
import com.newrelic.api.agent.NewRelic;
import com.newrelic.api.agent.Trace;
import com.newrelic.api.agent.TracedMethod;
import com.newrelic.api.agent.Transaction;
import fi.iki.elonen.NanoHTTPD;
import org.apache.http.HttpMessage;
import org.apache.http.NameValuePair;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
public class NewRelicApiExample extends NanoHTTPD {
public NewRelicApiExample() throws IOException, URISyntaxException {
super(8080);
// Set Dispatcher name and version
NewRelic.setServerInfo("NanoHttp", "2.3.1");
// Set Appserver port for JVM identification
NewRelic.setAppServerPort(8080);
// Set JVM instance name
NewRelic.setInstanceName("Client");
start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
System.out.println("Running at: http://localhost:8080/");
}
public static void main(String[] args) throws URISyntaxException {
try {
new NewRelicApiExample();
} catch (IOException ioe) {
System.err.println("Unable to start the server:\n" + ioe);
}
}
@Trace(dispatcher = true)
@Override
public Response serve(IHTTPSession session) {
URI uri = null;
int status = 0;
try {
createDB();
Thread.sleep(1000);
uri = new URI("http://localhost:8081");
status = makeExternalCall(uri);
} catch (URISyntaxException | InterruptedException | IOException e) {
e.printStackTrace();
}
if (status == 200) {
return newFixedLengthResponse("<html><body><h1>Successful Response</h1>\n</body></html>\n");
} else {
return newFixedLengthResponse("<html><body><h1>Error\n" + status + "</h1>\n</body></html>\n");
}
}
@Trace
public int makeExternalCall(URI uri) throws IOException {
HttpUriRequest request = RequestBuilder.get().setUri(uri).build();
// Wrap the outbound Request object
Headers outboundHeaders = new HeadersWrapper(request);
// Obtain a reference to the current transaction
Transaction transaction = NewRelic.getAgent().getTransaction();
// Add headers for outbound external request
transaction.insertDistributedTraceHeaders(outboundHeaders);
CloseableHttpClient connection = HttpClientBuilder.create().build();
CloseableHttpResponse response = connection.execute(request);
// Wrap the incoming Response object
Headers inboundHeaders = new HeadersWrapper(response);
// Create an input parameter object for a call to an external HTTP service
ExternalParameters params = HttpParameters
.library("HttpClient")
.uri(uri)
.procedure("execute")
.inboundHeaders(inboundHeaders)
.build();
// Obtain a reference to the method currently being traced
TracedMethod tracedMethod = NewRelic.getAgent().getTracedMethod();
// Report a call to an external HTTP service
tracedMethod.reportAsExternal(params);
return response.getStatusLine().getStatusCode();
}
// Implement Headers interface to create a wrapper for the outgoing Request/incoming Response headers
static class HeadersWrapper implements Headers {
private final HttpMessage delegate;
public HeadersWrapper(HttpMessage request) {
this.delegate = request;
}
@Override
public void setHeader(String name, String value) {
delegate.setHeader(name, value);
}
@Override
public HeaderType getHeaderType() {
return HeaderType.HTTP;
}
@Override
public String getHeader(String name) {
return delegate.getFirstHeader(name).getValue();
}
@Override
public Collection<String> getHeaders(String name) {
return Arrays.stream(delegate.getHeaders(name))
.map(NameValuePair::getValue)
.collect(Collectors.toList());
}
@Override
public void addHeader(String name, String value) {
delegate.addHeader(name, value);
}
@Override
public Collection<String> getHeaderNames() {
return Arrays.stream(delegate.getAllHeaders())
.map(NameValuePair::getName)
.collect(Collectors.toSet());
}
@Override
public boolean containsHeader(String name) {
return Arrays.stream(delegate.getAllHeaders())
.map(NameValuePair::getName)
.anyMatch(headerName -> headerName.equals(name));
}
}
@Trace
public void createDB() {
Connection c = null;
Statement stmt = null;
try {
Class.forName("org.sqlite.JDBC");
c = DriverManager.getConnection("jdbc:sqlite:/tmp/test.db");
System.out.println("Opened database successfully");
stmt = c.createStatement();
String dropSql = "DROP TABLE IF EXISTS COMPANY;";
stmt.executeUpdate(dropSql);
String sql = "CREATE TABLE COMPANY " +
"(ID INT PRIMARY KEY NOT NULL," +
" NAME TEXT NOT NULL, " +
" AGE INT NOT NULL, " +
" ADDRESS CHAR(50), " +
" SALARY REAL)";
stmt.executeUpdate(sql);
sql = "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) " +
"VALUES (1, 'Paul', 32, 'California', 20000.00 );";
stmt.executeUpdate(sql);
sql = "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) " +
"VALUES (2, 'Allen', 25, 'Texas', 15000.00 );";
stmt.executeUpdate(sql);
sql = "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) " +
"VALUES (3, 'Teddy', 23, 'Norway', 20000.00 );";
stmt.executeUpdate(sql);
sql = "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) " +
"VALUES (4, 'Mark', 25, 'Rich-Mond ', 65000.00 );";
stmt.executeUpdate(sql);
stmt.close();
c.close();
} catch (Exception e) {
System.err.println(e.getClass().getName() + ": " + e.getMessage());
System.exit(0);
}
}
}Here is the same client app code divided into sections that describe how the API is used:
This section calls the Java agent API imports used to add distributed tracing to the client application later in the example code.
import java.io.IOException;import java.net.URI;import java.net.URISyntaxException;import java.sql.Connection;import java.sql.DriverManager;import java.sql.Statement;import java.util.Arrays;import java.util.Collection;import java.util.stream.Collectors;
// New Relic API importsimport com.newrelic.api.agent.ExternalParameters;import com.newrelic.api.agent.HeaderType;import com.newrelic.api.agent.Headers;import com.newrelic.api.agent.HttpParameters;import com.newrelic.api.agent.NewRelic;import com.newrelic.api.agent.Trace;import com.newrelic.api.agent.TracedMethod;import com.newrelic.api.agent.Transaction;
import fi.iki.elonen.NanoHTTPD;import org.apache.http.HttpMessage;import org.apache.http.NameValuePair;import org.apache.http.client.methods.CloseableHttpResponse;import org.apache.http.client.methods.HttpUriRequest;import org.apache.http.client.methods.RequestBuilder;import org.apache.http.impl.client.CloseableHttpClient;import org.apache.http.impl.client.HttpClientBuilder;This section starts up the client server on port 8080 and uses the NewRelic class from the API to call the methods setServerInfo, setAppServerPort, and setInstanceName. These API calls affect what is shown in the New Relic UI.
public NewRelicApiExample() throws IOException, URISyntaxException { super(8080);
// Set Dispatcher name and version NewRelic.setServerInfo("NanoHttp", "2.3.1"); // Set Appserver port for jvm identification NewRelic.setAppServerPort(8080); // Set JVM instance name NewRelic.setInstanceName("Client");
start(NanoHTTPD.SOCKET_READ_TIMEOUT, false); System.out.println("Running at: http://localhost:8080/");}
public static void main(String[] args) throws URISyntaxException { try { new NewRelicApiExample(); } catch (IOException ioe) { System.err.println("Unable to start the server:\n" + ioe); }}This method creates a sample database, sleeps the thread, and makes an external call to the server app listening on port 8081. The @Trace(dispatcher = true) annotation tells the agent to start a new transaction when the serve method is called, if it is not called as part of an existing transaction (and in this case, it is not). If it were called as part of an existing transaction, it would simply be included as part of that transaction rather than start a new one.
@Trace(dispatcher = true)@Overridepublic Response serve(IHTTPSession session) { URI uri = null; int status = 0;
try { createDB(); Thread.sleep(1000); uri = new URI("http://localhost:8081"); status = makeExternalCall(uri); } catch (URISyntaxException | InterruptedException | IOException e) { e.printStackTrace(); }
if (status == 200) { return newFixedLengthResponse("<html><body><h1>Successful Response</h1>\n</body></html>\n"); } else { return newFixedLengthResponse("<html><body><h1>Error\n" + status + "</h1>\n</body></html>\n"); }}This section contains the code that initiates distributed tracing to the application making the request. The @Trace annotation tells the agent to track this method as part of an existing transaction as started by the serve method.
The request object is wrapped by a class that implements the Java agent API's Headers interface, which ensures that the proper HeaderType is set (in this case HTTP). A call to insertDistributedTraceHeaders adds the headers to the request and the request is sent to the server.
When the response returns it is wrapped by a class implementing the Java agent API's InboundHeaders interface. Headers is a sub interface of InboundHeaders and, in this case, the wrapper class could be reused.
The inboundHeaders, along with the "library", URI, and "procedure" arguments, are used to build an HttpParameters object. The params object is then passed as an argument to the reportAsExternal method, which reports the TracedMethod as an external HTTP call.
@Tracepublic int makeExternalCall(URI uri) throws IOException { HttpUriRequest request = RequestBuilder.get().setUri(uri).build();
// Wrap the outbound Request object Headers outboundHeaders = new HeadersWrapper(request);
// Obtain a reference to the current transaction Transaction transaction = NewRelic.getAgent().getTransaction(); // Add headers for outbound external request transaction.insertDistributedTraceHeaders(outboundHeaders);
CloseableHttpClient connection = HttpClientBuilder.create().build(); CloseableHttpResponse response = connection.execute(request);
// Wrap the incoming Response object Headers inboundHeaders = new HeadersWrapper(response);
// Create an input parameter object for a call to an external HTTP service ExternalParameters params = HttpParameters .library("HttpClient") .uri(uri) .procedure("execute") .inboundHeaders(inboundHeaders) .build();
// Obtain a reference to the method currently being traced TracedMethod tracedMethod = NewRelic.getAgent().getTracedMethod(); // Report a call to an external HTTP service tracedMethod.reportAsExternal(params);
return response.getStatusLine().getStatusCode();}An implementation of the Java agent API's Headers Interface is used to wrap the request object of the client server, which in this example is of type HttpUriRequest. The request is passed into the constructor of the HeadersWrapper class and implementations of the necessary methods are provided.
The getHeaderType method returns a HeaderType enum that can either be HeaderType.HTTP or HeaderType.MESSAGE, as defined by the Java agent API. In this example the external call protocol is HTTP, so HeaderType.HTTP is returned.
// Implement Headers interface to create a wrapper for the outgoing Request/incoming Response headersstatic class HeadersWrapper implements Headers { private final HttpMessage delegate;
public HeadersWrapper(HttpMessage request) { this.delegate = request; }
@Override public void setHeader(String name, String value) { delegate.setHeader(name, value); }
@Override public HeaderType getHeaderType() { return HeaderType.HTTP; }
@Override public String getHeader(String name) { return delegate.getFirstHeader(name).getValue(); }
@Override public Collection<String> getHeaders(String name) { return Arrays.stream(delegate.getHeaders(name)) .map(NameValuePair::getValue) .collect(Collectors.toList()); }
@Override public void addHeader(String name, String value) { delegate.addHeader(name, value); }
@Override public Collection<String> getHeaderNames() { return Arrays.stream(delegate.getAllHeaders()) .map(NameValuePair::getName) .collect(Collectors.toSet()); }
@Override public boolean containsHeader(String name) { return Arrays.stream(delegate.getAllHeaders()) .map(NameValuePair::getName) .anyMatch(headerName -> headerName.equals(name)); }}This method simply creates an example SQLite database. The @Trace annotation tells the agent to track this method as part of an existing transaction as started by the serve method.
@Tracepublic void createDB() { Connection c = null; Statement stmt = null;
try { Class.forName("org.sqlite.JDBC"); c = DriverManager.getConnection("jdbc:sqlite:/tmp/test.db"); System.out.println("Opened database successfully");
stmt = c.createStatement();
String dropSql = "DROP TABLE IF EXISTS COMPANY;"; stmt.executeUpdate(dropSql);
String sql = "CREATE TABLE COMPANY " + "(ID INT PRIMARY KEY NOT NULL," + " NAME TEXT NOT NULL, " + " AGE INT NOT NULL, " + " ADDRESS CHAR(50), " + " SALARY REAL)"; stmt.executeUpdate(sql);
sql = "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) " + "VALUES (1, 'Paul', 32, 'California', 20000.00 );"; stmt.executeUpdate(sql);
sql = "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) " + "VALUES (2, 'Allen', 25, 'Texas', 15000.00 );"; stmt.executeUpdate(sql);
sql = "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) " + "VALUES (3, 'Teddy', 23, 'Norway', 20000.00 );"; stmt.executeUpdate(sql);
sql = "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) " + "VALUES (4, 'Mark', 25, 'Rich-Mond ', 65000.00 );"; stmt.executeUpdate(sql);
stmt.close(); c.close(); } catch (Exception e) { System.err.println(e.getClass().getName() + ": " + e.getMessage()); System.exit(0); }}Server-side example
Here is the server-side code for this example application:
package com.newrelic.example;
import java.io.IOException;
import java.net.URISyntaxException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Collection;
import java.util.Collections;
// New Relic API imports
import com.newrelic.api.agent.DatastoreParameters;
import com.newrelic.api.agent.HeaderType;
import com.newrelic.api.agent.Headers;
import com.newrelic.api.agent.NewRelic;
import com.newrelic.api.agent.Trace;
import com.newrelic.api.agent.TracedMethod;
import com.newrelic.api.agent.Transaction;
import com.newrelic.api.agent.TransportType;
import fi.iki.elonen.NanoHTTPD;
public class NewRelicApiServer extends NanoHTTPD {
public NewRelicApiServer() throws IOException, URISyntaxException {
super(8081);
// Set Dispatcher name and version
NewRelic.setServerInfo("NanoHttp", "2.3.1");
// Set Appserver port for jvm identification
NewRelic.setAppServerPort(8081);
// Set JVM instance name
NewRelic.setInstanceName("Server");
start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
System.out.println("\nRunning on http://localhost:8081/ \n");
}
public static void main(String[] args) throws URISyntaxException {
try {
new NewRelicApiServer();
} catch (IOException ioe) {
System.err.println("Unable to start the server:\n" + ioe);
}
}
@Trace(dispatcher = true)
@Override
public Response serve(IHTTPSession session) {
// Obtain a reference to the current Transaction
Transaction tx = NewRelic.getAgent().getTransaction();
// Set the name of the current transaction
NewRelic.setTransactionName("Custom", "ExternalHTTPServer");
// Wrap the Request object
Headers req = new HeadersWrapper(session);
// Set the request for the current transaction and convert it into a web transaction
tx.acceptDistributedTraceHeaders(TransportType.HTTP, req);
queryDB();
return newFixedLengthResponse("<html><body><h1>SuccessfulResponse</h1>\n</body></html>\n");
}
// Implement Headers interface to create a wrapper for the outgoing Request/incoming Response headers
static class HeadersWrapper implements Headers {
private final IHTTPSession delegate;
public HeadersWrapper(IHTTPSession request) {
this.delegate = request;
}
@Override
public HeaderType getHeaderType() {
return HeaderType.HTTP;
}
@Override
public String getHeader(String name) {
return delegate.getHeaders().get(name);
}
@Override
public Collection<String> getHeaders(String name) {
return Collections.singletonList(getHeader(name));
}
@Override
public void setHeader(String name, String value) {
delegate.getHeaders().put(name, value);
}
@Override
public void addHeader(String name, String value) {
delegate.getHeaders().put(name, value);
}
@Override
public Collection<String> getHeaderNames() {
return delegate.getHeaders().keySet();
}
@Override
public boolean containsHeader(String name) {
return delegate.getHeaders().containsKey(name);
}
}
@Trace
public void queryDB() {
Connection c = null;
Statement stmt = null;
try {
Class.forName("org.sqlite.JDBC");
c = DriverManager.getConnection("jdbc:sqlite:/tmp/test.db");
c.setAutoCommit(false);
System.out.println("Opened database successfully");
stmt = c.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM COMPANY;");
while (rs.next()) {
int id = rs.getInt("id");
String name = rs.getString("name");
int age = rs.getInt("age");
String address = rs.getString("address");
float salary = rs.getFloat("salary");
System.out.println("ID = " + id);
System.out.println("NAME = " + name);
System.out.println("AGE = " + age);
System.out.println("ADDRESS = " + address);
System.out.println("SALARY = " + salary);
System.out.println();
}
rs.close();
stmt.close();
c.close();
} catch (Exception e) {
System.err.println(e.getClass().getName() + ": " + e.getMessage());
System.exit(0);
}
// Obtain a reference to the method currently being traced
TracedMethod method = NewRelic.getAgent().getTracedMethod();
// Create a DatastoreParameters object and report a call to an external datastore service
method.reportAsExternal(
DatastoreParameters
.product("sqlite")
.collection("test.db")
.operation("select")
.instance("localhost", 8080)
.databaseName("test.db")
.build());
}
}Here is the same example server code broken into sections that describe how the API is used:
This section shows the relevant Java agent API imports needed to add distributed tracing and reporting of external datastore calls to the server application.
import java.io.IOException;import java.net.URISyntaxException;import java.sql.Connection;import java.sql.DriverManager;import java.sql.ResultSet;import java.sql.Statement;import java.util.Collection;import java.util.Collections;
// New Relic API importsimport com.newrelic.api.agent.DatastoreParameters;import com.newrelic.api.agent.HeaderType;import com.newrelic.api.agent.Headers;import com.newrelic.api.agent.NewRelic;import com.newrelic.api.agent.Trace;import com.newrelic.api.agent.TracedMethod;import com.newrelic.api.agent.Transaction;import com.newrelic.api.agent.TransportType;
import fi.iki.elonen.NanoHTTPD;This section starts up the server on port 8081 and uses the NewRelic class from the API to call the methods setServerInfo, setAppServerPort, and setInstanceName. These API calls affect what is shown in the APM UI.
public NewRelicApiServer() throws IOException, URISyntaxException { super(8081);
// Set Dispatcher name and version NewRelic.setServerInfo("NanoHttp", "2.3.1"); // Set Appserver port for jvm identification NewRelic.setAppServerPort(8081); // Set JVM instance name NewRelic.setInstanceName("Server");
start(NanoHTTPD.SOCKET_READ_TIMEOUT, false); System.out.println("\nRunning on http://localhost:8081/ \n");}
public static void main(String[] args) throws URISyntaxException { try { new NewRelicApiServer(); } catch (IOException ioe) { System.err.println("Unable to start the server:\n" + ioe); }}The @Trace(dispatcher = true) annotation tells the agent to start a new transaction when the serve method is called if it is not called as part of an existing transaction (which in this case, it is not). If it were called as part of an existing transaction, it would simply be included as part of that transaction rather than start a new one.
A reference to the current Transaction is obtained via a call to getTransaction and the name of the transaction is set via a call to the setTransactionName method.
The request object, which in this example is of type IHTTPSession, is then wrapped using a class extending the Java agent API's Headers class. The current Transaction is then linked to the parent trace via a call to setWebRequest which takes the wrapped ExtendedRequest as an argument.
A call to the database is then made and the response object is returned.
@Trace(dispatcher = true)@Overridepublic Response serve(IHTTPSession session) { // Obtain a reference to the current Transaction Transaction tx = NewRelic.getAgent().getTransaction(); // Set the name of the current transaction NewRelic.setTransactionName("Custom", "ExternalHTTPServer");
// Wrap the Request object Headers req = new HeadersWrapper(session);
// Set the request for the current transaction and convert it into a web transaction tx.acceptDistributedTraceHeaders(TransportType.HTTP, req);
queryDB();
return newFixedLengthResponse("<html><body><h1>SuccessfulResponse</h1>\n</body></html>\n");}Another implementation of the Java agent API's Headers class is used to wrap the request object of the server, which in this example is of type IHTTPSession. The request is passed into the constructor of the HeadersWrapper class, which provides implementations of the getRequestURI, getHeader, getRemoteUser, getParameterNames, getParameterValues, getAttribute, getCookieValue, getHeaderType, and getMethod methods.
The getHeaderType method returns a HeaderType enum that can either be HeaderType.HTTP or HeaderType.MESSAGE, as defined by the Java agent API. In this example the external call protocol is HTTP so HeaderType.HTTP is returned.
// Implement Headers interface to create a wrapper for the outgoing Request/incoming Response headersstatic class HeadersWrapper implements Headers { private final IHTTPSession delegate;
public HeadersWrapper(IHTTPSession request) { this.delegate = request; }
@Override public HeaderType getHeaderType() { return HeaderType.HTTP; }
@Override public String getHeader(String name) { return delegate.getHeaders().get(name); }
@Override public Collection<String> getHeaders(String name) { return Collections.singletonList(getHeader(name)); }
@Override public void setHeader(String name, String value) { delegate.getHeaders().put(name, value); }
@Override public void addHeader(String name, String value) { delegate.getHeaders().put(name, value); }
@Override public Collection<String> getHeaderNames() { return delegate.getHeaders().keySet(); }
@Override public boolean containsHeader(String name) { return delegate.getHeaders().containsKey(name); }}This method makes an external call to the SQLite database that is created by the client. The @Trace annotation tells the agent to track this method as part of an existing transaction as started by the serve method.
A reference to the current TracedMethod is obtained via a call to getTracedMethod. A DatastoreParameters object is then created using the builder pattern. The ExternalParameters object is then passed as an argument to the reportAsExternal method, which has the effect of reporting the TracedMethod as an external datastore call.
@Tracepublic void queryDB() { Connection c = null; Statement stmt = null; try { Class.forName("org.sqlite.JDBC"); c = DriverManager.getConnection("jdbc:sqlite:/tmp/test.db"); c.setAutoCommit(false); System.out.println("Opened database successfully");
stmt = c.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM COMPANY;"); while (rs.next()) { int id = rs.getInt("id"); String name = rs.getString("name"); int age = rs.getInt("age"); String address = rs.getString("address"); float salary = rs.getFloat("salary"); System.out.println("ID = " + id); System.out.println("NAME = " + name); System.out.println("AGE = " + age); System.out.println("ADDRESS = " + address); System.out.println("SALARY = " + salary); System.out.println(); } rs.close(); stmt.close(); c.close(); } catch (Exception e) { System.err.println(e.getClass().getName() + ": " + e.getMessage()); System.exit(0); } // Obtain a reference to the method currently being traced TracedMethod method = NewRelic.getAgent().getTracedMethod();
// Create a DatastoreParameters object and report a call to an external datastore service method.reportAsExternal( DatastoreParameters .product("sqlite") .collection("test.db") .operation("select") .instance("localhost", 8080) .databaseName("test.db") .build());}