Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JAX-RS Callback #167

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions bugsnag/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ repositories {
dependencies {
compile "com.fasterxml.jackson.core:jackson-databind:2.13.3"
compile "org.slf4j:slf4j-api:1.7.25"
compileOnly "org.jboss.resteasy:resteasy-core:${resteasyVersion}"
compileOnly "org.jboss.resteasy:resteasy-core-spi:${resteasyVersion}"
compileOnly "javax.servlet:javax.servlet-api:${servletApiVersion}"
compileOnly("ch.qos.logback:logback-classic:${logbackVersion}") {
exclude group: "org.slf4j"
Expand All @@ -24,6 +26,8 @@ dependencies {
testCompile("ch.qos.logback:logback-classic:${logbackVersion}") {
exclude group: "org.slf4j"
}
testCompile "org.jboss.resteasy:resteasy-core:${resteasyVersion}"
testCompile "org.jboss.resteasy:resteasy-core-spi:${resteasyVersion}"
}

task testJar(type: Jar) {
Expand Down
18 changes: 18 additions & 0 deletions bugsnag/src/main/java/com/bugsnag/Bugsnag.java
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,15 @@ public Delivery getDelivery() {
return config.delivery;
}

/**
* Get the request callback.
*
* @return the used request callback.
*/
public String getRequestCallback() {
return config.requestCallback;
}

/**
* Get the delivery to use to send sessions.
*
Expand Down Expand Up @@ -197,6 +206,15 @@ public void setDelivery(Delivery delivery) {
config.delivery = delivery;
}

/**
* Set which request callback to use.
* Accepted values are "servlet" for Java Servlet API and "jaxrs" for Jakarta RESTful Web Services
*
* @param requestCallback either "servlet" or "jaxrs"
*/
public void setRequestCallback(String requestCallback) {
config.requestCallback = requestCallback;
}

/**
* Set the method of delivery for Bugsnag sessions. By default we'll
Expand Down
6 changes: 5 additions & 1 deletion bugsnag/src/main/java/com/bugsnag/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.bugsnag.callbacks.AppCallback;
import com.bugsnag.callbacks.Callback;
import com.bugsnag.callbacks.DeviceCallback;
import com.bugsnag.callbacks.JaxrsCallback;
import com.bugsnag.callbacks.ServletCallback;
import com.bugsnag.delivery.AsyncHttpDelivery;
import com.bugsnag.delivery.Delivery;
Expand Down Expand Up @@ -42,6 +43,7 @@ public class Configuration {
public String[] notifyReleaseStages = null;
public String[] projectPackages;
public String releaseStage;
public String requestCallback = "servlet";
public boolean sendThreads = false;

Collection<Callback> callbacks = new ConcurrentLinkedQueue<Callback>();
Expand All @@ -57,8 +59,10 @@ public class Configuration {
addCallback(new DeviceCallback());
DeviceCallback.initializeCache();

if (ServletCallback.isAvailable()) {
if ("servlet".equals(requestCallback) && ServletCallback.isAvailable()) {
addCallback(new ServletCallback());
} else if ("jaxrs".equals(requestCallback) && JaxrsCallback.isAvailable()) {
addCallback(new JaxrsCallback());
}
}

Expand Down
92 changes: 92 additions & 0 deletions bugsnag/src/main/java/com/bugsnag/callbacks/JaxrsCallback.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.bugsnag.callbacks;

import com.bugsnag.Report;
import com.bugsnag.filters.BugsnagContainerRequestFilter;

import org.jboss.resteasy.spi.HttpRequest;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class JaxrsCallback implements Callback {
private static final String HEADER_X_FORWARDED_FOR = "X-FORWARDED-FOR";

/**
* @return true if the servlet request listener is available.
*/
public static boolean isAvailable() {
try {
Class.forName("javax.ws.rs.container.ContainerRequestFilter", false,
JaxrsCallback.class.getClassLoader());
return true;
} catch (ClassNotFoundException ex) {
return false;
}
}

@Override
public void beforeNotify(Report report) {
// Check if we have any servlet request data available
HttpRequest request = BugsnagContainerRequestFilter.getRequest();

if (request == null) {
return;
}

// Add request information to metaData
report
.addToTab("request", "url", request.getUri().getRequestUri().toString())
.addToTab("request", "method", request.getHttpMethod())
.addToTab("request", "params", request.getFormParameters())
.addToTab("request", "clientIp", getClientIp(request))
.addToTab("request", "headers", getHeaderMap(request));

// Set default context
if (report.getContext() == null) {
report.setContext(request.getHttpMethod() + " " + request.getUri().getRequestUri());
}

// Clear servlet request data
BugsnagContainerRequestFilter.clearRequest();
}

private String getClientIp(HttpRequest request) {
String remoteAddr = request.getRemoteAddress();
String forwardedAddr = request.getHttpHeaders().getHeaderString(HEADER_X_FORWARDED_FOR);
if (forwardedAddr != null) {
remoteAddr = forwardedAddr;
int idx = remoteAddr.indexOf(',');
if (idx > -1) {
remoteAddr = remoteAddr.substring(0, idx);
}
}
return remoteAddr;
}

private Map<String, String> getHeaderMap(HttpRequest request) {
Map<String, String> headers = new HashMap<String, String>();
Set<Map.Entry<String, List<String>>> headerNames = request.getMutableHeaders().entrySet();
for (Map.Entry<String, List<String>> header : headerNames) {
Iterator<String> headerValues = header.getValue().iterator();
StringBuilder value = new StringBuilder();

if (headerValues.hasNext()) {
value.append(headerValues.next());

// If there are multiple values for the header, do comma-separated concat
// as per RFC 2616:
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
while (headerValues.hasNext()) {
value.append(",").append(headerValues.next());
}
}

headers.put(header.getKey(), value.toString());
}

return headers;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.bugsnag.filters;

import static javax.ws.rs.Priorities.AUTHENTICATION;

import com.bugsnag.Bugsnag;

import org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext;
import org.jboss.resteasy.spi.HttpRequest;

import javax.annotation.Priority;

import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.ext.Provider;

@Provider
@PreMatching
@Priority(AUTHENTICATION)
public class BugsnagContainerRequestFilter implements ContainerRequestFilter {

private static final ThreadLocal<HttpRequest> HTTP_REQUEST = new ThreadLocal<HttpRequest>();

public static HttpRequest getRequest() {
return HTTP_REQUEST.get();
}

public static void clearRequest() {
HTTP_REQUEST.remove();
Bugsnag.clearThreadMetaData();
}

private void trackServletSession() {
for (Bugsnag bugsnag : Bugsnag.uncaughtExceptionClients()) {
if (bugsnag.shouldAutoCaptureSessions()) {
bugsnag.startSession();
}
}
}

@Override
public void filter(ContainerRequestContext containerRequestContext) {
trackServletSession();
if (containerRequestContext instanceof PreMatchContainerRequestContext) {
HTTP_REQUEST.set(((PreMatchContainerRequestContext) containerRequestContext).getHttpRequest());
}
}
}
1 change: 1 addition & 0 deletions bugsnag/src/test/java/com/bugsnag/ConfigurationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public void setUp() {
@Test
public void testDefaults() {
assertTrue(config.shouldAutoCaptureSessions());
assertEquals("servlet", config.requestCallback);
}

@Test
Expand Down
153 changes: 153 additions & 0 deletions bugsnag/src/test/java/com/bugsnag/JaxrsCallbackTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package com.bugsnag;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.bugsnag.callbacks.JaxrsCallback;
import com.bugsnag.callbacks.ServletCallback;
import com.bugsnag.filters.BugsnagContainerRequestFilter;

import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.core.util.VersionUtil;

import org.jboss.resteasy.core.Headers;
import org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext;
import org.jboss.resteasy.specimpl.ResteasyHttpHeaders;
import org.jboss.resteasy.specimpl.ResteasyUriInfo;
import org.jboss.resteasy.spi.HttpRequest;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.ws.rs.core.MultivaluedMap;

public class JaxrsCallbackTest {

private Bugsnag bugsnag;

/**
* Only run class if java runtime version is 1.8 or later
*/
@BeforeClass
public static void checkJavaRuntimeVersion() {
Version runtimeVersion = VersionUtil.parseVersion(System.getProperty("java.runtime.version"), null, null);
Version minimalVersion = new Version(1, 8, 0, null);
assumeTrue(runtimeVersion.compareTo(minimalVersion) >= 0);
}

/**
* Generate a new request instance which will be read by the servlet
* context and callback
*/
@Before
public void setUp() throws URISyntaxException {
bugsnag = new Bugsnag("apikey", false);
bugsnag.setDelivery(null);
bugsnag.setRequestCallback("jaxrs");

HttpRequest request = mock(HttpRequest.class);

MultivaluedMap<String, String> params = new Headers<String>();
params.put("account", Collections.singletonList("Acme Co"));
params.put("name", Collections.singletonList("Bill"));
when(request.getFormParameters()).thenReturn(params);

when(request.getHttpMethod()).thenReturn("PATCH");
when(request.getUri()).thenReturn(new ResteasyUriInfo(new URI("/foo/bar")));
when(request.getRemoteAddress()).thenReturn("12.0.4.57");

MultivaluedMap<String, String> headers = new Headers<String>();
headers.put("Content-Type", Collections.singletonList("application/json"));
headers.put("Content-Length", Collections.singletonList("54"));
headers.put("X-Custom-Header", Arrays.asList("some-data-1", "some-data-2"));
headers.put("Authorization", Collections.singletonList("Basic ABC123"));
headers.put("Cookie", Collections.singletonList("name1=val1; name2=val2"));
ResteasyHttpHeaders httpHeaders = new ResteasyHttpHeaders(headers);
when(request.getHttpHeaders()).thenReturn(httpHeaders);
when(request.getMutableHeaders()).thenReturn(headers);

PreMatchContainerRequestContext context = mock(PreMatchContainerRequestContext.class);
when(context.getHttpRequest()).thenReturn(request);
BugsnagContainerRequestFilter filter = new BugsnagContainerRequestFilter();
filter.filter(context);
}

/**
* Close test Bugsnag
*/
@After
public void closeBugsnag() {
bugsnag.close();
}

@SuppressWarnings("unchecked")
@Test
public void testRequestMetadataAdded() {
Report report = generateReport(new java.lang.Exception("Spline reticulation failed"));
JaxrsCallback callback = new JaxrsCallback();
callback.beforeNotify(report);

Map<String, Object> metadata = report.getMetaData();
assertTrue(metadata.containsKey("request"));

Map<String, Object> request = (Map<String, Object>) metadata.get("request");
assertEquals("/foo/bar", request.get("url"));
assertEquals("PATCH", request.get("method"));
assertEquals("12.0.4.57", request.get("clientIp"));

assertTrue(request.containsKey("headers"));
Map<String, String> headers = (Map<String, String>) request.get("headers");
assertEquals("application/json", headers.get("Content-Type"));
assertEquals("54", headers.get("Content-Length"));
assertEquals("some-data-1,some-data-2", headers.get("X-Custom-Header"));

// Make sure that actual Authorization header value is not in the report
assertEquals("[FILTERED]", headers.get("Authorization"));

// Make sure that actual cookies are not in the report
assertEquals("[FILTERED]", headers.get("Cookie"));

assertTrue(request.containsKey("params"));
Map<String, List<String>> params = (Map<String, List<String>>) request.get("params");
assertTrue(params.containsKey("account"));
List<String> account = params.get("account");
assertEquals("Acme Co", account.get(0));

assertTrue(params.containsKey("name"));
List<String> name = params.get("name");
assertEquals("Bill", name.get(0));
}

@Test
public void testRequestContextSet() {
Report report = generateReport(new java.lang.Exception("Spline reticulation failed"));
JaxrsCallback callback = new JaxrsCallback();
callback.beforeNotify(report);

assertEquals("PATCH /foo/bar", report.getContext());
}

@Test
public void testExistingContextNotOverridden() {
Report report = generateReport(new java.lang.Exception("Spline reticulation failed"));
report.setContext("Honey nut corn flakes");
ServletCallback callback = new ServletCallback();
callback.beforeNotify(report);

assertEquals("Honey nut corn flakes", report.getContext());
}

private Report generateReport(java.lang.Exception exception) {
return bugsnag.buildReport(exception);
}
}
1 change: 1 addition & 0 deletions common.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
ext {
servletApiVersion = "3.1.0"
logbackVersion = "1.2.3"
resteasyVersion = "4.5.12.Final"
}

if (JavaVersion.current().isJava8Compatible()) {
Expand Down