Skip to content

Commit

Permalink
Merge pull request #1951 from newrelic/jsp-4-instrumentation
Browse files Browse the repository at this point in the history
JSP v4 module
  • Loading branch information
jtduffy committed Jun 20, 2024
2 parents 95da4e0 + cedec73 commit ff741c0
Show file tree
Hide file tree
Showing 11 changed files with 737 additions and 0 deletions.
99 changes: 99 additions & 0 deletions instrumentation/jsp-4/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# jsp-4 Instrumentation Module

## Injection of the Real User Monitoring Script Via JSP Tag Libraries
Prior to the additions to this instrumentation module, the only way to inject the RUM script into JSPs was during the
compilation phase of the Jasper compiler, which injects the script into the HTML `<head>` element, if present in the page
source.

Some applications use custom JSP tag libraries to create the head tag (and other HTML page elements). In this scenario, the
RUM script will not be injected because of the way the Jasper compiler instrumentation detects the head tag. This
instrumentation module weaves the `SimpleTagSupport` and `TagSupport` classes to detect the creation of head elements via the
tag execution and inject the RUM script at that time.

### Configuration
To enable tag library instrumentation, both of the following settings must be `true`:
```
browser_monitoring:
auto_instrument: true
tag_lib_instrument: true
```

By default, the `tag_lib_instrument` setting is `false`.

The instrumentation will use the regular expression pattern of `<head>` to detect the start of HTML head elements.
If a tag library emits a more complex head start element, the regular expression can be modified via the `tag_lib_head_pattern`
config setting. For example:
```
browser_monitoring:
tag_lib_head_pattern: '<head.*>'
```
The regular expression will be compiled to be case-insensitive. If the defined regular expression is invalid it will default to `<head>`.


### Requirements for Script Injection
The following are the requirements for the RUM script to be injected from instrumented tag libraries:
- The application must utilize version 4 of the JSP libraries (jakarta namespace)
- Only the first instance of a `<head>` string emitted by a tag library will be considered, regardless of context (in a comment, for example)
- The custom tag must extend either the [SimpleTagSupport](https://jakarta.ee/specifications/pages/3.0/apidocs/jakarta/servlet/jsp/tagext/simpletagsupport) or
[TagSupport](https://jakarta.ee/specifications/pages/3.0/apidocs/jakarta/servlet/jsp/tagext/tagsupport) classes
- The `<head>` element must be emitted from the tag class via the `print(String s)` or `println(String s)` methods of the JspWriter
fetched from a [JspContext.getOut()](https://jakarta.ee/specifications/pages/3.0/apidocs/jakarta/servlet/jsp/jspcontext) call
- The head start tag (`<head>`) must be totally emitted in a single `print`/`println` call. For example, these are all valid:
```java
out.println("<head>");
out.print("<head>");
out.println("<head><title>Test Site</title></head>");
out.println("<head><title>");
out.println("<he" + "ad>")
```

### Example SimpleTagSupport Use Case
##### Tag Library Class
```java
// Package/imports omitted
// Also assumes the existence of a valid, corresponding tld file
public class SimpleTagExample extends SimpleTagSupport {
public void doTag() throws JspException, IOException {
JspWriter out = getJspContext().getOut();
out.println("<head>\n" +
"<meta charset=\"ISO-8859-1\">\n" +
"<title>Test Site</title>" +
"</head>");
}
}
```

##### JSP
```html
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="simple" uri="http://com.sun.simpletagexample"%>
<!DOCTYPE html>
<html>
<simple:SimpleTag/>

<body>
<h1>Sample JSP</h1>
</body>
</html>
```

In this scenario, the `SimpleTagSupport` instrumentation will detect the output of the head tag and inject the RUM script
just like the Jasper compiler instrumentation, which will result in the following HTML:
```html

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript"><!-- RUM script here --></script>
<meta charset="ISO-8859-1">
<title>Test Site</title></head>

<body>
<h1>Sample JSP</h1>
</body>
</html>
```

### Example TagSupport Use Case
Tag libraries that extend the `TagSupport` class work in largely the same way as the `SimpleTagSupport` based tag libraries. The instrumentation
will detect head element generation via the `doStartTag` or `doEndTag` method calls and will inject the RUM script appropriately.
30 changes: 30 additions & 0 deletions instrumentation/jsp-4/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

dependencies {
implementation(project(":agent-bridge"))
implementation("jakarta.servlet.jsp:jakarta.servlet.jsp-api:4.0.0")
implementation("jakarta.servlet:jakarta.servlet-api:6.1.0")
implementation("jakarta.el:jakarta.el-api:6.0.0")
}

jar {
manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.jsp-4' }
}

verifyInstrumentation {
passesOnly('jakarta.servlet.jsp:jakarta.servlet.jsp-api:[4.0.0,)') {
implementation("jakarta.el:jakarta.el-api:6.0.0")
implementation("jakarta.servlet:jakarta.servlet-api:6.1.0")
}
}

java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}

site {
title 'JSP'
type 'Other'
versionOverride '[4.0,)'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
*
* * Copyright 2024 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package com.nr.agent.instrumentation.jsp4;

import jakarta.servlet.jsp.JspContext;
import jakarta.servlet.jsp.JspWriter;
import java.util.Enumeration;

public class JspContextWrapper extends JspContext {
private final JspContext wrappedContext;
private final JspWriter wrappedWriter;

public JspContextWrapper(JspContext originalContext, JspWriter wrappedWriter) {
this.wrappedContext = originalContext;
this.wrappedWriter = wrappedWriter;
}

@Override
public void setAttribute(String name, Object value) {
wrappedContext.setAttribute(name, value);
}

@Override
public void setAttribute(String name, Object value, int scope) {
wrappedContext.setAttribute(name, value, scope);
}

@Override
public Object getAttribute(String name) {
return wrappedContext.getAttribute(name);
}

@Override
public Object getAttribute(String name, int scope) {
return wrappedContext.getAttribute(name, scope);
}

@Override
public Object findAttribute(String name) {
return wrappedContext.findAttribute(name);
}

@Override
public void removeAttribute(String name) {
wrappedContext.removeAttribute(name);
}

@Override
public void removeAttribute(String name, int scope) {
wrappedContext.removeAttribute(name, scope);
}

@Override
public int getAttributesScope(String name) {
return wrappedContext.getAttributesScope(name);
}

@Override
public Enumeration<String> getAttributeNamesInScope(int scope) {
return wrappedContext.getAttributeNamesInScope(scope);
}

@Override
public JspWriter getOut() {
return wrappedWriter;
}

@Override
public jakarta.el.ELContext getELContext() {
return wrappedContext.getELContext();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
*
* * Copyright 2022 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package com.nr.agent.instrumentation.jsp4;

import java.util.logging.Level;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import com.newrelic.agent.bridge.AgentBridge;
import com.newrelic.agent.bridge.TracedMethod;
import com.newrelic.agent.bridge.TransactionNamePriority;
import com.newrelic.api.agent.NewRelic;

public class JspUtils {
public static final String JSP_CATEGORY = "JSP";
public static final String ORG_APACHE_JSP = "org.apache.jsp.";
public static final Pattern JSP_PATTERN = Pattern.compile("_jsp$");
public static final Pattern WEB_INF_PATTERN = Pattern.compile("WEB_002dINF");
private static final String AUTO_INSTRUMENT_ENABLED_CONFIG = "browser_monitoring.auto_instrument";
private static final String TAG_LIB_ENABLED_CONFIG = "browser_monitoring.tag_lib_instrument";
private static final String TAG_LIB_HEAD_REGEX = "browser_monitoring.tag_lib_head_pattern";
private static final String TAG_LIB_HEAD_REGEX_DEFAULT = "<head>";

public static final Pattern START_HEAD_REGEX = generateStartHeadElementRegExPattern();

public static void setTransactionName(Class<?> jspClass, TracedMethod timedMethod) {
String name = jspClass.getName();
try {
if (name.startsWith(ORG_APACHE_JSP)) {
name = name.substring(ORG_APACHE_JSP.length()).replace('.', '/');
name = WEB_INF_PATTERN.matcher(name).replaceFirst("WEB-INF");
} else {
int index = name.lastIndexOf('.');
if (index > 0) {
name = name.substring(index + 1);
}
}
name = JSP_PATTERN.matcher(name).replaceAll(".jsp");
AgentBridge.getAgent().getTransaction().setTransactionName(TransactionNamePriority.JSP, false,
JSP_CATEGORY, name);

timedMethod.setMetricName("Jsp", name);
} catch (Exception e) {
NewRelic.getAgent().getLogger().log(Level.FINER, "An error occurred formatting a jsp name : {0}", e);
}
}

public static boolean isTagLibInstrumentationEnabled() {
return NewRelic.getAgent().getConfig().getValue(AUTO_INSTRUMENT_ENABLED_CONFIG, Boolean.FALSE) &&
NewRelic.getAgent().getConfig().getValue(TAG_LIB_ENABLED_CONFIG, Boolean.FALSE);
}

private static Pattern generateStartHeadElementRegExPattern() {
String regexString = NewRelic.getAgent().getConfig().getValue(TAG_LIB_HEAD_REGEX, TAG_LIB_HEAD_REGEX_DEFAULT);
Pattern pattern;

try {
pattern = Pattern.compile(regexString, Pattern.CASE_INSENSITIVE + Pattern.MULTILINE);
} catch (PatternSyntaxException e) {
NewRelic.getAgent().getLogger().log(Level.WARNING, "Invalid pattern defined for tag lib start head regex: {0} Defaulting to: {1}",
regexString, TAG_LIB_HEAD_REGEX_DEFAULT);
pattern = Pattern.compile(TAG_LIB_HEAD_REGEX_DEFAULT, Pattern.CASE_INSENSITIVE + Pattern.MULTILINE);
}

if (NewRelic.getAgent().getLogger().isLoggable(Level.FINEST)) {
NewRelic.getAgent().getLogger().log(Level.FINEST, "Tag lib start head regex to be used for RUM script injection: {0}", pattern.pattern());
}

return pattern;
}
}
Loading

0 comments on commit ff741c0

Please sign in to comment.