Skip to content

Commit dd77102

Browse files
authored
Merge pull request #2269 from newrelic/undertow-2-module
Undertow stand alone module
2 parents 667a598 + 2121c10 commit dd77102

15 files changed

+789
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Undertow Server Standalone Instrumentation
2+
3+
__Note:__ This module is disabled by default in order to not conflict with any Wildfly instrumentation module.
4+
If you wish to enable the undertow-server module, add the following to your agent configuration file:
5+
```yaml
6+
class_transformer:
7+
com.newrelic.instrumentation.undertow-server-1.1.0:
8+
enabled: true
9+
```
10+
11+
This module is intended for applications using Undertow in stand alone mode, using any of the following
12+
route handlers:
13+
- io.undertow.server.RoutingHandler
14+
- io.undertow.server.handlers.PathTemplateHandler
15+
- io.undertow.predicate.PathTemplatePredicate
16+
17+
If any other handler is used, the transaction will be named `{connectors_placeholder_name}/`. This is
18+
to prevent an explosion of unique transaction names when parameterized request paths are used.
19+
20+
This instrumentation module is not compatible with any Wildfly instrumentation modules.
21+
22+
Example code of supported handlers are below.
23+
24+
## Example Code
25+
26+
### RoutingHandler
27+
28+
```
29+
RoutingHandler routingHandler = new RoutingHandler();
30+
31+
routingHandler.get("/greet/{name}", exchange -> {
32+
String name = exchange.getQueryParameters().get("name").getFirst();
33+
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");
34+
exchange.getResponseSender().send("Hello, " + name + "!");
35+
}).get("/static/content", exchange -> {
36+
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");
37+
exchange.getResponseSender().send("Static content");
38+
});
39+
```
40+
41+
### PathTemplateHandler
42+
```
43+
PathTemplateHandler pathTemplateHandler = new PathTemplateHandler();
44+
45+
pathTemplateHandler.add("/greet/{name}/{count}", exchange -> {
46+
PathTemplateMatch pathMatch = exchange.getAttachment(PathTemplateMatch.ATTACHMENT_KEY);
47+
String name = pathMatch.getParameters().get("name");
48+
int count = Integer.parseInt(pathMatch.getParameters().get("count"));
49+
50+
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");
51+
exchange.getResponseSender().send("Hello " + name + String.valueOf("!").repeat(count));
52+
});
53+
```
54+
55+
### PathTemplatePredicate (wrapped in a PredicateHandler Instance)
56+
```
57+
HttpHandler matchedPathHandler = exchange -> {
58+
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");
59+
exchange.getResponseSender().send("Path matched!");
60+
};
61+
HttpHandler unmatchedPathHandler = exchange -> {
62+
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");
63+
exchange.getResponseSender().send("Path not matched.");
64+
};
65+
PredicateHandler predicateHandler = new PredicateHandler(
66+
new PathTemplatePredicate("/greet4/{name}", ExchangeAttributes.relativePath()),
67+
matchedPathHandler,
68+
unmatchedPathHandler
69+
);
70+
```
71+
72+
### Full Example using RoutingHandler
73+
```java
74+
public class UndertowHttpServer {
75+
76+
public static void main(String[] args) throws IOException {
77+
RoutingHandler routingHandler = new RoutingHandler();
78+
routingHandler.get("/greet/{name}", exchange -> {
79+
String name = exchange.getQueryParameters().get("name").getFirst();
80+
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");
81+
exchange.setStatusCode(200);
82+
exchange.getResponseSender().send("Hello, " + name + "!");
83+
}).get("/static", exchange -> {
84+
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");
85+
exchange.setStatusCode(200);
86+
exchange.getResponseSender().send("Static static static");
87+
});
88+
89+
System.out.println("Building Undertow server");
90+
Undertow.Builder builder = Undertow.builder();
91+
Undertow undertow = builder
92+
.addHttpListener(8080, "localhost")
93+
.setHandler(routingHandler)
94+
.build();
95+
96+
System.out.println("Undertow started");
97+
undertow.start();
98+
}
99+
}
100+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
2+
dependencies {
3+
implementation(project(":agent-bridge"))
4+
implementation("io.undertow:undertow-core:2.0.0.Final")
5+
}
6+
7+
jar {
8+
manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.undertow-server-1.1.0', 'Enabled': 'false',
9+
'Implementation-Title-Alias': 'undertow-server'}
10+
}
11+
12+
verifyInstrumentation {
13+
passesOnly 'io.undertow:undertow-core:[1.1.0.Final,)'
14+
excludeRegex 'io.undertow:undertow-core:.*\\.(Alpha|Beta|CR)[0-9]+'
15+
}
16+
17+
site {
18+
title 'Undertow'
19+
type 'Framework'
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
*
3+
* * Copyright 2025 New Relic Corporation. All rights reserved.
4+
* * SPDX-License-Identifier: Apache-2.0
5+
*
6+
*/
7+
package com.nr.agent.instrumentation.undertow;
8+
9+
import com.newrelic.api.agent.ExtendedRequest;
10+
import com.newrelic.api.agent.HeaderType;
11+
import io.undertow.server.HttpServerExchange;
12+
import io.undertow.server.handlers.Cookie;
13+
import io.undertow.util.HeaderMap;
14+
import io.undertow.util.HttpString;
15+
16+
import java.util.Collections;
17+
import java.util.Deque;
18+
import java.util.Enumeration;
19+
import java.util.Map;
20+
21+
public class RequestWrapper extends ExtendedRequest {
22+
private final Map<String, Cookie> cookies;
23+
private final Map<String, Deque<String>> params;
24+
private final String requestUri;
25+
private final HeaderMap headers;
26+
private final HttpString method;
27+
28+
public RequestWrapper(HttpServerExchange e) {
29+
cookies = e.getRequestCookies();
30+
params = e.getPathParameters();
31+
requestUri = e.getRequestURI();
32+
headers = e.getRequestHeaders();
33+
method = e.getRequestMethod();
34+
}
35+
36+
@Override
37+
public Object getAttribute(String name) {
38+
return null;
39+
}
40+
41+
@Override
42+
public String getCookieValue(String name) {
43+
if (cookies != null) {
44+
Cookie cookie = cookies.get(name);
45+
if(cookie != null)
46+
return cookie.getValue();
47+
}
48+
49+
return null;
50+
}
51+
52+
@SuppressWarnings("rawtypes")
53+
@Override
54+
public Enumeration getParameterNames() {
55+
if (params != null) {
56+
return Collections.enumeration(params.keySet());
57+
}
58+
59+
return null;
60+
}
61+
62+
@Override
63+
public String[] getParameterValues(String name) {
64+
if (params != null) {
65+
Deque<String> values = params.get(name);
66+
if (values != null) {
67+
String[] retValue = new String[values.size()];
68+
values.toArray(retValue);
69+
return retValue;
70+
}
71+
}
72+
73+
return null;
74+
}
75+
76+
@Override
77+
public String getRemoteUser() {
78+
return null;
79+
}
80+
81+
@Override
82+
public String getRequestURI() {
83+
return requestUri;
84+
}
85+
86+
@Override
87+
public String getHeader(String name) {
88+
if (headers != null) {
89+
return headers.getFirst(name);
90+
}
91+
92+
return null;
93+
}
94+
95+
@Override
96+
public HeaderType getHeaderType() {
97+
return HeaderType.HTTP;
98+
}
99+
100+
@Override
101+
public String getMethod() {
102+
return method.toString();
103+
}
104+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
*
3+
* * Copyright 2025 New Relic Corporation. All rights reserved.
4+
* * SPDX-License-Identifier: Apache-2.0
5+
*
6+
*/
7+
package com.nr.agent.instrumentation.undertow;
8+
9+
import com.newrelic.api.agent.ExtendedResponse;
10+
import com.newrelic.api.agent.HeaderType;
11+
import io.undertow.server.HttpServerExchange;
12+
import io.undertow.util.HeaderMap;
13+
import io.undertow.util.Headers;
14+
import io.undertow.util.HttpString;
15+
16+
public class ResponseWrapper extends ExtendedResponse {
17+
private final long contentLength;
18+
private final HeaderMap headers;
19+
private final int statusCode;
20+
21+
public ResponseWrapper(HttpServerExchange exchange) {
22+
contentLength = exchange.getResponseContentLength();
23+
headers = exchange.getResponseHeaders();
24+
statusCode = exchange.getStatusCode();
25+
}
26+
27+
@Override
28+
public long getContentLength() {
29+
return contentLength;
30+
}
31+
32+
@Override
33+
public HeaderType getHeaderType() {
34+
return HeaderType.HTTP;
35+
}
36+
37+
@Override
38+
public void setHeader(String name, String value) {
39+
if (name != null && value != null) {
40+
headers.add(new HttpString(name), value);
41+
}
42+
}
43+
44+
@Override
45+
public int getStatus() {
46+
return statusCode;
47+
}
48+
49+
@Override
50+
public String getStatusMessage() {
51+
// Not available in the Undertow exchange object
52+
return null;
53+
}
54+
55+
@Override
56+
public String getContentType() {
57+
if (headers.contains(Headers.CONTENT_TYPE)) {
58+
return headers.get(Headers.CONTENT_TYPE).get(0);
59+
}
60+
61+
return null;
62+
}
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
*
3+
* * Copyright 2025 New Relic Corporation. All rights reserved.
4+
* * SPDX-License-Identifier: Apache-2.0
5+
*
6+
*/
7+
package com.nr.agent.instrumentation.undertow;
8+
9+
import com.newrelic.api.agent.NewRelic;
10+
import com.newrelic.api.agent.Token;
11+
12+
import java.util.logging.Level;
13+
14+
public class RunnableWrapper implements Runnable {
15+
private final Runnable delegate;
16+
private Token token;
17+
18+
public RunnableWrapper(Runnable delegate, Token token) {
19+
this.delegate = delegate;
20+
this.token = token;
21+
}
22+
23+
public void run() {
24+
if (token != null) {
25+
token.link();
26+
token = null;
27+
}
28+
if (delegate != null) {
29+
delegate.run();
30+
}
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
*
3+
* * Copyright 2025 New Relic Corporation. All rights reserved.
4+
* * SPDX-License-Identifier: Apache-2.0
5+
*
6+
*/
7+
package com.nr.agent.instrumentation.undertow;
8+
9+
import com.newrelic.api.agent.NewRelic;
10+
import com.newrelic.api.agent.Transaction;
11+
import io.undertow.server.HttpServerExchange;
12+
13+
public class Util {
14+
public enum NamedBySource {
15+
ConnectorInstrumentation,
16+
RoutingHandler,
17+
PathTemplateHandler,
18+
PathTemplatePredicate,
19+
}
20+
21+
public static void addTransactionNamedByParameter(NamedBySource source) {
22+
NewRelic.addCustomParameter("Transaction-Named-By", source.toString());
23+
}
24+
25+
public static String createTransactionName(String requestPath, String method) {
26+
return (requestPath != null ? requestPath : "(Unknown)") + " (" + (method != null ? method : "Unknown") + ")";
27+
}
28+
29+
public static void setWebRequestAndResponse(HttpServerExchange exchange) {
30+
Transaction transaction = NewRelic.getAgent().getTransaction();
31+
transaction.setWebRequest(new RequestWrapper(exchange));
32+
transaction.setWebResponse(new ResponseWrapper(exchange));
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
*
3+
* * Copyright 2025 New Relic Corporation. All rights reserved.
4+
* * SPDX-License-Identifier: Apache-2.0
5+
*
6+
*/
7+
package io.undertow.predicate;
8+
9+
import com.newrelic.api.agent.NewRelic;
10+
import com.newrelic.api.agent.TransactionNamePriority;
11+
import com.newrelic.api.agent.weaver.MatchType;
12+
import com.newrelic.api.agent.weaver.Weave;
13+
import com.newrelic.api.agent.weaver.Weaver;
14+
import com.nr.agent.instrumentation.undertow.Util;
15+
import io.undertow.attribute.ExchangeAttribute;
16+
import io.undertow.server.HttpServerExchange;
17+
import io.undertow.util.PathTemplate;
18+
19+
import java.util.logging.Level;
20+
21+
@Weave(type = MatchType.ExactClass, originalName = "io.undertow.predicate.PathTemplatePredicate")
22+
public abstract class PathTemplatePredicate_Instrumentation {
23+
private final ExchangeAttribute attribute = Weaver.callOriginal();
24+
private final PathTemplate value = Weaver.callOriginal();
25+
26+
public boolean resolve(final HttpServerExchange exchange) {
27+
boolean result = Weaver.callOriginal();
28+
29+
if (result) {
30+
Util.setWebRequestAndResponse(exchange);
31+
Util.addTransactionNamedByParameter(Util.NamedBySource.PathTemplatePredicate);
32+
NewRelic.getAgent().getTransaction().setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "Undertow",
33+
Util.createTransactionName(value.getTemplateString(), exchange.getRequestMethod().toString()));
34+
}
35+
36+
return result;
37+
}
38+
}

0 commit comments

Comments
 (0)