diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 91cdb616..7dc9a22c 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -11,7 +11,7 @@ steps: plugins: - docker-compose#v3.7.0: run: java-common - command: './gradlew :bugsnag:assemble :bugsnag-spring:assemble' + command: './gradlew :bugsnag:assemble :bugsnag-spring:javax:assemble :bugsnag-spring:assemble' - label: ':docker: Run JVM tests' key: 'java-jvm-tests' @@ -21,19 +21,36 @@ steps: run: java-common command: './gradlew check test' - - label: ':docker: Mazerunner tests batch 1' + - label: ':docker: Mazerunner java8 tests batch 1' key: 'java-mazerunner-tests-1' timeout_in_minutes: 30 plugins: - docker-compose#v3.7.0: - run: java-mazerunner + run: java8-mazerunner command: 'bundle exec maze-runner --exclude=features/[^a-m].*.feature' - - label: ':docker: Mazerunner tests batch 2' + - label: ':docker: Mazerunner java8 tests batch 2' key: 'java-mazerunner-tests-2' timeout_in_minutes: 30 plugins: - docker-compose#v3.7.0: - pull: java-mazerunner - run: java-mazerunner + pull: java8-mazerunner + run: java8-mazerunner + command: 'bundle exec maze-runner --exclude=features/[^n-z].*.feature' + + - label: ':docker: Mazerunner java17 tests batch 1' + key: 'java-mazerunner-tests-3' + timeout_in_minutes: 30 + plugins: + - docker-compose#v3.7.0: + run: java17-mazerunner + command: 'bundle exec maze-runner --exclude=features/[^a-m].*.feature' + + - label: ':docker: Mazerunner java17 tests batch 2' + key: 'java-mazerunner-tests-4' + timeout_in_minutes: 30 + plugins: + - docker-compose#v3.7.0: + pull: java17-mazerunner + run: java17-mazerunner command: 'bundle exec maze-runner --exclude=features/[^n-z].*.feature' diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c775499..766627fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## TBD +* Support Spring 6 / Spring Boot 3 + [#191](https://github.com/bugsnag/bugsnag-java/pull/191) + * Bump Jackson from 2.13.3 for critical vulnerability fixes [#184](https://github.com/bugsnag/bugsnag-java/pull/184) diff --git a/Gemfile b/Gemfile index 6f2460eb..208e640d 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,4 @@ source 'https://rubygems.org' -gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', tag: 'v7.10.2' +gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', tag: 'v7.24.0' gem 'os' diff --git a/bugsnag-spring/build.gradle b/bugsnag-spring/build.gradle index 786ac088..56374aa2 100644 --- a/bugsnag-spring/build.gradle +++ b/bugsnag-spring/build.gradle @@ -1,6 +1,6 @@ ext { - springVersion = '4.3.18.RELEASE' - springBootVersion = '1.5.15.RELEASE' + springVersion = '6.0.0' + springBootVersion = '3.0.0' } apply plugin: 'java-library' @@ -12,16 +12,32 @@ repositories { } dependencies { - compile project(':bugsnag') - testCompile project(path: ':bugsnag', configuration: 'testRuntime') + compileOnly project(':bugsnag-spring:javax') + api project(':bugsnag') - compileOnly "javax.servlet:javax.servlet-api:${servletApiVersion}" + compileOnly "jakarta.servlet:jakarta.servlet-api:${jakartaServletApiVersion}" compileOnly "org.springframework:spring-webmvc:${springVersion}" compileOnly "org.springframework.boot:spring-boot:${springBootVersion}" compileOnly "ch.qos.logback:logback-core:${logbackVersion}" + compileOnly "org.slf4j:slf4j-api:${slf4jApiVersion}" - testCompile "junit:junit:4.13.2" - testCompile "javax.servlet:javax.servlet-api:${servletApiVersion}" - testCompile "org.springframework.boot:spring-boot-starter-test:${springBootVersion}" - testCompile "org.springframework.boot:spring-boot-starter-web:${springBootVersion}" + testImplementation project(':bugsnag').sourceSets.test.output + testImplementation project(':bugsnag-spring:javax') + testImplementation project(':bugsnag') + testImplementation "junit:junit:${junitVersion}" + testImplementation "jakarta.servlet:jakarta.servlet-api:${jakartaServletApiVersion}" + testImplementation "org.springframework.boot:spring-boot-starter-test:${springBootVersion}" + testImplementation "org.springframework.boot:spring-boot-starter-web:${springBootVersion}" + testImplementation "org.mockito:mockito-core:${mockitoVersion}" +} + +evaluationDependsOnChildren() + +tasks['jar'].dependsOn(project(':bugsnag-spring:javax').tasks['jar']) + +jar { + from project.sourceSets.main.allSource + from project(':bugsnag-spring:javax').configurations.archives.artifacts.files.collect { file -> + zipTree(file) + } } diff --git a/bugsnag-spring/javax/build.gradle b/bugsnag-spring/javax/build.gradle new file mode 100644 index 00000000..3beef567 --- /dev/null +++ b/bugsnag-spring/javax/build.gradle @@ -0,0 +1,35 @@ +ext { + springVersion = '5.3.20' + springBootVersion = '2.5.14' +} + +apply plugin: 'java' +apply plugin: 'java-library' + +apply from: '../../common.gradle' + +compileJava { + sourceCompatibility = '1.8' + targetCompatibility = '1.8' +} + +repositories { + mavenCentral() +} + +dependencies { + api project(':bugsnag') + testImplementation project(':bugsnag').sourceSets.test.output + + compileOnly "javax.servlet:javax.servlet-api:${javaxServletApiVersion}" + compileOnly "org.springframework:spring-webmvc:${springVersion}" + compileOnly "org.springframework.boot:spring-boot:${springBootVersion}" + compileOnly "ch.qos.logback:logback-core:${logbackVersion}" + compileOnly "org.slf4j:slf4j-api:${slf4jApiVersion}" + + testImplementation "junit:junit:${junitVersion}" + testImplementation "javax.servlet:javax.servlet-api:${javaxServletApiVersion}" + testImplementation "org.springframework.boot:spring-boot-starter-test:${springBootVersion}" + testImplementation "org.springframework.boot:spring-boot-starter-web:${springBootVersion}" + testImplementation "org.mockito:mockito-core:${mockitoVersion}" +} \ No newline at end of file diff --git a/bugsnag-spring/src/main/java/com/bugsnag/BugsnagAsyncExceptionHandler.java b/bugsnag-spring/javax/src/main/java/com/bugsnag/BugsnagAsyncExceptionHandler.java similarity index 100% rename from bugsnag-spring/src/main/java/com/bugsnag/BugsnagAsyncExceptionHandler.java rename to bugsnag-spring/javax/src/main/java/com/bugsnag/BugsnagAsyncExceptionHandler.java diff --git a/bugsnag-spring/javax/src/main/java/com/bugsnag/BugsnagImportSelector.java b/bugsnag-spring/javax/src/main/java/com/bugsnag/BugsnagImportSelector.java new file mode 100644 index 00000000..6d6e91da --- /dev/null +++ b/bugsnag-spring/javax/src/main/java/com/bugsnag/BugsnagImportSelector.java @@ -0,0 +1,54 @@ +package com.bugsnag; + +import org.springframework.context.annotation.ImportSelector; +import org.springframework.core.SpringVersion; +import org.springframework.core.type.AnnotationMetadata; + +public class BugsnagImportSelector implements ImportSelector { + + private static final String[] SPRING_JAKARTA_CLASSES = { + "com.bugsnag.SpringBootJakartaConfiguration", + "com.bugsnag.JakartaMvcConfiguration", + "com.bugsnag.ScheduledTaskConfiguration" + }; + + private static final String[] SPRING_JAVAX_CLASSES = { + "com.bugsnag.SpringBootJavaxConfiguration", + "com.bugsnag.JavaxMvcConfiguration", + "com.bugsnag.ScheduledTaskConfiguration" + }; + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + if (isSpringJakartaCompatible() && isJava17Compatible()) { + return SPRING_JAKARTA_CLASSES; + } + + return SPRING_JAVAX_CLASSES; + } + + private static boolean isSpringJakartaCompatible() { + return getMajorVersion(SpringVersion.getVersion()) >= 6; + } + + private static boolean isJava17Compatible() { + return getMajorVersion(System.getProperty("java.version")) >= 17; + } + + private static int getMajorVersion(String version) { + if (version == null) { + return 0; + } + int firstDot = version.indexOf("."); + if (firstDot == -1) { + return 0; + } + + String majorVersion = version.substring(0, firstDot); + try { + return Integer.parseInt(majorVersion); + } catch (NumberFormatException nfe) { + return 0; + } + } +} diff --git a/bugsnag-spring/src/main/java/com/bugsnag/BugsnagMvcExceptionHandler.java b/bugsnag-spring/javax/src/main/java/com/bugsnag/BugsnagJavaxMvcExceptionHandler.java similarity index 92% rename from bugsnag-spring/src/main/java/com/bugsnag/BugsnagMvcExceptionHandler.java rename to bugsnag-spring/javax/src/main/java/com/bugsnag/BugsnagJavaxMvcExceptionHandler.java index 36d87856..62cbf9ea 100644 --- a/bugsnag-spring/src/main/java/com/bugsnag/BugsnagMvcExceptionHandler.java +++ b/bugsnag-spring/javax/src/main/java/com/bugsnag/BugsnagJavaxMvcExceptionHandler.java @@ -8,7 +8,6 @@ import org.springframework.web.servlet.ModelAndView; import java.util.Collections; - import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -20,11 +19,11 @@ * resolvers. */ @Order(Ordered.HIGHEST_PRECEDENCE) -class BugsnagMvcExceptionHandler implements HandlerExceptionResolver { +class BugsnagJavaxMvcExceptionHandler implements HandlerExceptionResolver { private final Bugsnag bugsnag; - BugsnagMvcExceptionHandler(final Bugsnag bugsnag) { + BugsnagJavaxMvcExceptionHandler(final Bugsnag bugsnag) { this.bugsnag = bugsnag; } diff --git a/bugsnag-spring/src/main/java/com/bugsnag/BugsnagScheduledTaskExceptionHandler.java b/bugsnag-spring/javax/src/main/java/com/bugsnag/BugsnagScheduledTaskExceptionHandler.java similarity index 100% rename from bugsnag-spring/src/main/java/com/bugsnag/BugsnagScheduledTaskExceptionHandler.java rename to bugsnag-spring/javax/src/main/java/com/bugsnag/BugsnagScheduledTaskExceptionHandler.java diff --git a/bugsnag-spring/src/main/java/com/bugsnag/BugsnagSpringConfiguration.java b/bugsnag-spring/javax/src/main/java/com/bugsnag/BugsnagSpringConfiguration.java similarity index 90% rename from bugsnag-spring/src/main/java/com/bugsnag/BugsnagSpringConfiguration.java rename to bugsnag-spring/javax/src/main/java/com/bugsnag/BugsnagSpringConfiguration.java index 36fd5ea6..86d503ef 100644 --- a/bugsnag-spring/src/main/java/com/bugsnag/BugsnagSpringConfiguration.java +++ b/bugsnag-spring/javax/src/main/java/com/bugsnag/BugsnagSpringConfiguration.java @@ -2,6 +2,7 @@ import com.bugsnag.callbacks.Callback; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -9,17 +10,13 @@ import org.springframework.core.SpringVersion; import java.util.Map; -import javax.annotation.PostConstruct; /** * Configuration to integrate Bugsnag with Spring. */ @Configuration -@Import({ - SpringBootConfiguration.class, - MvcConfiguration.class, - ScheduledTaskConfiguration.class}) -public class BugsnagSpringConfiguration { +@Import(BugsnagImportSelector.class) +public class BugsnagSpringConfiguration implements InitializingBean { @Autowired private Bugsnag bugsnag; @@ -64,9 +61,8 @@ ScheduledTaskBeanLocator scheduledTaskBeanLocator() { * If using Logback, stop any configured appender from creating Bugsnag reports for Spring log * messages as they effectively duplicate error reports for unhandled exceptions. */ - @PostConstruct - @SuppressWarnings("checkstyle:emptycatchblock") - void excludeLoggers() { + @Override + public void afterPropertiesSet() { try { // Exclude Tomcat logger when processing HTTP requests via a servlet. // Regex specified to match the servlet variable parts of the logger name, e.g. @@ -86,5 +82,4 @@ void excludeLoggers() { // logback was not in classpath, ignore throwable to allow further initialisation } } - } diff --git a/bugsnag-spring/src/main/java/com/bugsnag/ExceptionClassCallback.java b/bugsnag-spring/javax/src/main/java/com/bugsnag/ExceptionClassCallback.java similarity index 100% rename from bugsnag-spring/src/main/java/com/bugsnag/ExceptionClassCallback.java rename to bugsnag-spring/javax/src/main/java/com/bugsnag/ExceptionClassCallback.java diff --git a/bugsnag-spring/src/main/java/com/bugsnag/MvcConfiguration.java b/bugsnag-spring/javax/src/main/java/com/bugsnag/JavaxMvcConfiguration.java similarity index 72% rename from bugsnag-spring/src/main/java/com/bugsnag/MvcConfiguration.java rename to bugsnag-spring/javax/src/main/java/com/bugsnag/JavaxMvcConfiguration.java index f0f27c2f..8ba8e5e8 100644 --- a/bugsnag-spring/src/main/java/com/bugsnag/MvcConfiguration.java +++ b/bugsnag-spring/javax/src/main/java/com/bugsnag/JavaxMvcConfiguration.java @@ -1,18 +1,17 @@ package com.bugsnag; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; -import javax.annotation.PostConstruct; - /** * If spring-webmvc is loaded, add configuration for reporting unhandled exceptions. */ @Configuration @Conditional(SpringWebMvcLoadedCondition.class) -class MvcConfiguration { +class JavaxMvcConfiguration implements InitializingBean { @Autowired private Bugsnag bugsnag; @@ -22,15 +21,15 @@ class MvcConfiguration { * for uncaught exceptions thrown from request handlers. */ @Bean - BugsnagMvcExceptionHandler bugsnagHandlerExceptionResolver() { - return new BugsnagMvcExceptionHandler(bugsnag); + BugsnagJavaxMvcExceptionHandler bugsnagHandlerExceptionResolver() { + return new BugsnagJavaxMvcExceptionHandler(bugsnag); } /** * Add a callback to assign specified severities for some Spring exceptions. */ - @PostConstruct - void addExceptionClassCallback() { + @Override + public void afterPropertiesSet() { bugsnag.addCallback(new ExceptionClassCallback()); } } diff --git a/bugsnag-spring/src/main/java/com/bugsnag/ScheduledTaskBeanLocator.java b/bugsnag-spring/javax/src/main/java/com/bugsnag/ScheduledTaskBeanLocator.java similarity index 100% rename from bugsnag-spring/src/main/java/com/bugsnag/ScheduledTaskBeanLocator.java rename to bugsnag-spring/javax/src/main/java/com/bugsnag/ScheduledTaskBeanLocator.java diff --git a/bugsnag-spring/src/main/java/com/bugsnag/ScheduledTaskConfiguration.java b/bugsnag-spring/javax/src/main/java/com/bugsnag/ScheduledTaskConfiguration.java similarity index 100% rename from bugsnag-spring/src/main/java/com/bugsnag/ScheduledTaskConfiguration.java rename to bugsnag-spring/javax/src/main/java/com/bugsnag/ScheduledTaskConfiguration.java diff --git a/bugsnag-spring/src/main/java/com/bugsnag/SpringBootConfiguration.java b/bugsnag-spring/javax/src/main/java/com/bugsnag/SpringBootConfiguration.java similarity index 52% rename from bugsnag-spring/src/main/java/com/bugsnag/SpringBootConfiguration.java rename to bugsnag-spring/javax/src/main/java/com/bugsnag/SpringBootConfiguration.java index e1a68105..f8f71ab0 100644 --- a/bugsnag-spring/src/main/java/com/bugsnag/SpringBootConfiguration.java +++ b/bugsnag-spring/javax/src/main/java/com/bugsnag/SpringBootConfiguration.java @@ -1,25 +1,14 @@ package com.bugsnag; import com.bugsnag.callbacks.Callback; -import com.bugsnag.servlet.BugsnagServletRequestListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringBootVersion; -import org.springframework.boot.web.servlet.ServletListenerRegistrationBean; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Conditional; -import org.springframework.context.annotation.Configuration; import java.util.Map; -import javax.servlet.ServletRequestListener; - -/** - * If spring-boot is loaded, add configuration specific to Spring Boot - */ -@Configuration -@Conditional(SpringBootLoadedCondition.class) -class SpringBootConfiguration { +public class SpringBootConfiguration { @Autowired private Bugsnag bugsnag; @@ -53,19 +42,4 @@ public void beforeSendSession(SessionPayload payload) { private void addSpringRuntimeVersion(Map device) { Diagnostics.addDeviceRuntimeVersion(device, "springBoot", SpringBootVersion.getVersion()); } - - /** - * The {@link com.bugsnag.servlet.BugsnagServletContainerInitializer} does not work for Spring Boot, need to - * register the {@link BugsnagServletRequestListener} using a Spring Boot - * {@link ServletListenerRegistrationBean} instead. This adds session tracking and - * automatic servlet request metadata collection. - */ - @Bean - @Conditional(SpringWebMvcLoadedCondition.class) - ServletListenerRegistrationBean listenerRegistrationBean() { - ServletListenerRegistrationBean srb = - new ServletListenerRegistrationBean(); - srb.setListener(new BugsnagServletRequestListener()); - return srb; - } } diff --git a/bugsnag-spring/javax/src/main/java/com/bugsnag/SpringBootJavaxConfiguration.java b/bugsnag-spring/javax/src/main/java/com/bugsnag/SpringBootJavaxConfiguration.java new file mode 100644 index 00000000..b7ae8183 --- /dev/null +++ b/bugsnag-spring/javax/src/main/java/com/bugsnag/SpringBootJavaxConfiguration.java @@ -0,0 +1,33 @@ +package com.bugsnag; + +import com.bugsnag.servlet.javax.BugsnagServletRequestListener; + +import org.springframework.boot.web.servlet.ServletListenerRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; + +import javax.servlet.ServletRequestListener; + +/** + * If spring-boot is loaded, add configuration specific to Spring Boot + */ +@Configuration +@Conditional(SpringBootLoadedCondition.class) +class SpringBootJavaxConfiguration extends SpringBootConfiguration { + + /** + * The {@link com.bugsnag.servlet.javax.BugsnagServletContainerInitializer} does not work for Spring Boot, need to + * register the {@link BugsnagServletRequestListener} using a Spring Boot + * {@link ServletListenerRegistrationBean} instead. This adds session tracking and + * automatic servlet request metadata collection. + */ + @Bean + @Conditional(SpringWebMvcLoadedCondition.class) + ServletListenerRegistrationBean listenerRegistrationBean() { + ServletListenerRegistrationBean srb = + new ServletListenerRegistrationBean(); + srb.setListener(new BugsnagServletRequestListener()); + return srb; + } +} diff --git a/bugsnag-spring/src/main/java/com/bugsnag/SpringBootLoadedCondition.java b/bugsnag-spring/javax/src/main/java/com/bugsnag/SpringBootLoadedCondition.java similarity index 100% rename from bugsnag-spring/src/main/java/com/bugsnag/SpringBootLoadedCondition.java rename to bugsnag-spring/javax/src/main/java/com/bugsnag/SpringBootLoadedCondition.java diff --git a/bugsnag-spring/src/main/java/com/bugsnag/SpringWebMvcLoadedCondition.java b/bugsnag-spring/javax/src/main/java/com/bugsnag/SpringWebMvcLoadedCondition.java similarity index 100% rename from bugsnag-spring/src/main/java/com/bugsnag/SpringWebMvcLoadedCondition.java rename to bugsnag-spring/javax/src/main/java/com/bugsnag/SpringWebMvcLoadedCondition.java diff --git a/bugsnag-spring/javax/src/test/java/com/bugsnag/ScheduledTaskBeanLocatorTest.java b/bugsnag-spring/javax/src/test/java/com/bugsnag/ScheduledTaskBeanLocatorTest.java new file mode 100644 index 00000000..74eb856d --- /dev/null +++ b/bugsnag-spring/javax/src/test/java/com/bugsnag/ScheduledTaskBeanLocatorTest.java @@ -0,0 +1,81 @@ +package com.bugsnag; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.when; + +import com.bugsnag.testapp.springboot.TestSpringBootApplication; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.ApplicationContext; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = TestSpringBootApplication.class) +public class ScheduledTaskBeanLocatorTest { + + @Autowired + private ScheduledTaskBeanLocator beanLocator; + + @MockBean + private ApplicationContext context; + + @Before + public void setUp() { + beanLocator.setApplicationContext(context); + } + + @Test + public void findSchedulerByType() { + ThreadPoolTaskScheduler expected = new ThreadPoolTaskScheduler(); + when(context.getBean(TaskScheduler.class)).thenReturn(expected); + assertEquals(expected, beanLocator.resolveTaskScheduler()); + } + + @Test + public void findSchedulerByName() { + ThreadPoolTaskScheduler expected = new ThreadPoolTaskScheduler(); + Throwable exc = new NoUniqueBeanDefinitionException(TaskScheduler.class); + when(context.getBean(TaskScheduler.class)).thenThrow(exc); + when(context.getBean("taskScheduler", TaskScheduler.class)).thenReturn(expected); + assertEquals(expected, beanLocator.resolveTaskScheduler()); + } + + @Test + public void noTaskSchedulerAvailable() { + assertNull(beanLocator.resolveTaskScheduler()); + } + + @Test + public void findExecutorByType() { + ScheduledExecutorService expected = Executors.newScheduledThreadPool(1); + when(context.getBean(ScheduledExecutorService.class)).thenReturn(expected); + assertEquals(expected, beanLocator.resolveScheduledExecutorService()); + } + + @Test + public void findExecutorByName() { + ScheduledExecutorService expected = Executors.newScheduledThreadPool(4); + Throwable exc = new NoUniqueBeanDefinitionException(ScheduledExecutorService.class); + when(context.getBean(ScheduledExecutorService.class)).thenThrow(exc); + when(context.getBean("taskScheduler", ScheduledExecutorService.class)) + .thenReturn(expected); + assertEquals(expected, beanLocator.resolveScheduledExecutorService()); + } + + @Test + public void noScheduledExecutorAvailable() { + assertNull(beanLocator.resolveScheduledExecutorService()); + } +} diff --git a/bugsnag-spring/javax/src/test/java/com/bugsnag/ScheduledTaskConfigurationTest.java b/bugsnag-spring/javax/src/test/java/com/bugsnag/ScheduledTaskConfigurationTest.java new file mode 100644 index 00000000..5de97b42 --- /dev/null +++ b/bugsnag-spring/javax/src/test/java/com/bugsnag/ScheduledTaskConfigurationTest.java @@ -0,0 +1,120 @@ +package com.bugsnag; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +import com.bugsnag.testapp.springboot.TestSpringBootApplication; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.ApplicationContext; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.test.context.junit4.SpringRunner; + +import java.lang.reflect.Field; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = TestSpringBootApplication.class) +public class ScheduledTaskConfigurationTest { + + @Autowired + private ScheduledTaskConfiguration configuration; + + @Mock + private ScheduledTaskRegistrar registrar; + + @Autowired + private ScheduledTaskBeanLocator beanLocator; + + @MockBean + private ApplicationContext context; + + @Before + public void setUp() { + registrar = new ScheduledTaskRegistrar(); + beanLocator.setApplicationContext(context); + } + + @Test + public void existingSchedulerUsed() { + ThreadPoolTaskScheduler expected = new ThreadPoolTaskScheduler(); + registrar.setScheduler(expected); + configuration.configureTasks(registrar); + assertEquals(expected, registrar.getScheduler()); + } + + @Test + public void noSchedulersAvailable() { + configuration.configureTasks(registrar); + assertTrue(registrar.getScheduler() instanceof ThreadPoolTaskScheduler); + } + + @Test + public void findSchedulerByType() throws NoSuchFieldException, IllegalAccessException { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + when(context.getBean(TaskScheduler.class)).thenReturn(scheduler); + + configuration.configureTasks(registrar); + assertNull(registrar.getScheduler()); + Object errorHandler = accessField(scheduler, "errorHandler"); + assertTrue(errorHandler instanceof BugsnagScheduledTaskExceptionHandler); + } + + @Test + public void findSchedulerByName() throws NoSuchFieldException, IllegalAccessException { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + Throwable exc = new NoUniqueBeanDefinitionException(TaskScheduler.class); + when(context.getBean(TaskScheduler.class)).thenThrow(exc); + when(context.getBean("taskScheduler", TaskScheduler.class)).thenReturn(scheduler); + + configuration.configureTasks(registrar); + assertNull(registrar.getScheduler()); + Object errorHandler = accessField(scheduler, "errorHandler"); + assertTrue(errorHandler instanceof BugsnagScheduledTaskExceptionHandler); + } + + @Test + public void findExecutorByType() throws NoSuchFieldException, IllegalAccessException { + ScheduledExecutorService expected = Executors.newScheduledThreadPool(1); + when(context.getBean(ScheduledExecutorService.class)).thenReturn(expected); + + configuration.configureTasks(registrar); + TaskScheduler scheduler = registrar.getScheduler(); + assertTrue(scheduler instanceof ConcurrentTaskScheduler); + assertEquals(expected, accessField(scheduler, "scheduledExecutor")); + } + + @Test + public void findExecutorByName() throws NoSuchFieldException, IllegalAccessException { + ScheduledExecutorService expected = Executors.newScheduledThreadPool(4); + Throwable exc = new NoUniqueBeanDefinitionException(ScheduledExecutorService.class); + when(context.getBean(ScheduledExecutorService.class)).thenThrow(exc); + when(context.getBean("taskScheduler", ScheduledExecutorService.class)) + .thenReturn(expected); + + configuration.configureTasks(registrar); + TaskScheduler scheduler = registrar.getScheduler(); + assertTrue(scheduler instanceof ConcurrentTaskScheduler); + assertEquals(expected, accessField(scheduler, "scheduledExecutor")); + } + + private Object accessField(Object object, String fieldName) + throws NoSuchFieldException, IllegalAccessException { + Field field = object.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(object); + } +} diff --git a/bugsnag-spring/javax/src/test/java/com/bugsnag/SpringAsyncTest.java b/bugsnag-spring/javax/src/test/java/com/bugsnag/SpringAsyncTest.java new file mode 100644 index 00000000..42a91df2 --- /dev/null +++ b/bugsnag-spring/javax/src/test/java/com/bugsnag/SpringAsyncTest.java @@ -0,0 +1,90 @@ +package com.bugsnag; + +import static com.bugsnag.TestUtils.verifyAndGetReport; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +import com.bugsnag.HandledState.SeverityReasonType; +import com.bugsnag.delivery.Delivery; +import com.bugsnag.testapp.springboot.AsyncService; +import com.bugsnag.testapp.springboot.TestSpringBootApplication; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.Collections; + +/** + * Test that a Spring Boot application configured with the + * {@link BugsnagSpringConfiguration} performs as expected. + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = TestSpringBootApplication.class) +public class SpringAsyncTest { + + @Autowired + private Bugsnag bugsnag; + + @Autowired + private AsyncService asyncService; + + private Delivery delivery; + + /** + * Initialize test state + */ + @Before + public void setUp() { + delivery = mock(Delivery.class); + bugsnag.setDelivery(delivery); + } + + @Test + public void bugsnagNotifyWhenAsyncVoidReturnTypeException() { + asyncService.throwExceptionVoid(); + + Report report = verifyAndGetReport(delivery); + + // Assert that the exception was detected correctly + assertEquals("Async void test", report.getExceptionMessage()); + assertEquals("java.lang.RuntimeException", report.getExceptionName()); + + // Assert that the severity, severity reason and unhandled values are correct + assertEquals(Severity.ERROR.getValue(), report.getSeverity()); + assertEquals( + SeverityReasonType.REASON_UNHANDLED_EXCEPTION_MIDDLEWARE.toString(), + report.getSeverityReason().getType()); + assertThat( + report.getSeverityReason().getAttributes(), + is(Collections.singletonMap("framework", "Spring"))); + assertTrue(report.getUnhandled()); + } + + @Test + public void bugsnagNotifyWhenAsyncFutureReturnTypeException() { + asyncService.throwExceptionFuture(); + + Report report = verifyAndGetReport(delivery); + + // Assert that the exception was detected correctly + assertEquals("Async future test", report.getExceptionMessage()); + assertEquals("java.lang.RuntimeException", report.getExceptionName()); + + // Assert that the severity, severity reason and unhandled values are correct + assertEquals(Severity.ERROR.getValue(), report.getSeverity()); + assertEquals( + SeverityReasonType.REASON_UNHANDLED_EXCEPTION_MIDDLEWARE.toString(), + report.getSeverityReason().getType()); + assertThat( + report.getSeverityReason().getAttributes(), + is(Collections.singletonMap("framework", "Spring"))); + assertTrue(report.getUnhandled()); + } +} diff --git a/bugsnag-spring/javax/src/test/java/com/bugsnag/SpringMvcTest.java b/bugsnag-spring/javax/src/test/java/com/bugsnag/SpringMvcTest.java new file mode 100644 index 00000000..3b7e6361 --- /dev/null +++ b/bugsnag-spring/javax/src/test/java/com/bugsnag/SpringMvcTest.java @@ -0,0 +1,281 @@ +package com.bugsnag; + +import static com.bugsnag.TestUtils.anyMapOf; +import static com.bugsnag.TestUtils.verifyAndGetReport; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.bugsnag.HandledState.SeverityReasonType; +import com.bugsnag.callbacks.Callback; +import com.bugsnag.delivery.Delivery; +import com.bugsnag.serialization.Serializer; +import com.bugsnag.testapp.springboot.TestSpringBootApplication; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootVersion; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.core.SpringVersion; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.test.context.junit4.SpringRunner; + +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +/** + * Test that a Spring Boot application configured with the + * {@link BugsnagSpringConfiguration} performs as expected. + */ +@RunWith(SpringRunner.class) +@SpringBootTest( + classes = TestSpringBootApplication.class, + webEnvironment = WebEnvironment.RANDOM_PORT) +public class SpringMvcTest { + + @LocalServerPort + private int randomServerPort; + + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + private Bugsnag bugsnag; + + private Delivery delivery; + + private long sessionsStartedBeforeTest; + + /** + * Initialize test state + */ + @Before + public void setUp() { + delivery = mock(Delivery.class); + + bugsnag.setDelivery(delivery); + bugsnag.getConfig().setSendUncaughtExceptions(true); + bugsnag.getConfig().setAutoCaptureSessions(true); + + // Cannot reset the session count on the bugsnag bean for each test, so note + // the current session count before the test starts instead. + sessionsStartedBeforeTest = getSessionCount(); + } + + @Test + public void bugsnagNotifyWhenUncaughtControllerException() { + callRuntimeExceptionEndpoint(); + + Report report = verifyAndGetReport(delivery); + + // Assert that the exception was detected correctly + assertEquals("Test", report.getExceptionMessage()); + assertEquals("java.lang.RuntimeException", report.getExceptionName()); + + // Assert that the severity, severity reason and unhandled values are correct + assertEquals(Severity.ERROR.getValue(), report.getSeverity()); + assertEquals( + SeverityReasonType.REASON_UNHANDLED_EXCEPTION_MIDDLEWARE.toString(), + report.getSeverityReason().getType()); + assertThat( + report.getSeverityReason().getAttributes(), + is(Collections.singletonMap("framework", "Spring"))); + assertTrue(report.getUnhandled()); + } + + @Test + public void noBugsnagNotifyWhenSendUncaughtExceptionsFalse() { + bugsnag.getConfig().setSendUncaughtExceptions(false); + + callRuntimeExceptionEndpoint(); + + verifyNoReport(); + } + + @Test + public void bugsnagSessionStartedWhenAutoCaptureSessions() { + callRuntimeExceptionEndpoint(); + + assertSessionsStarted(1); + } + + @Test + public void noBugsnagSessionStartedWhenAutoCaptureSessionsFalse() { + bugsnag.getConfig().setAutoCaptureSessions(false); + + callRuntimeExceptionEndpoint(); + + assertSessionsStarted(0); + } + + @Test + public void requestMetadataSetCorrectly() { + callRuntimeExceptionEndpoint(); + + Report report = verifyAndGetReport(delivery); + + // Check that the context is set to the HTTP method and URI of the endpoint + assertEquals("GET /throw-runtime-exception", report.getContext()); + + // Check that the request metadata is set as expected + @SuppressWarnings(value = "unchecked") Map requestMetadata = + (Map) report.getMetaData().get("request"); + assertEquals("http://localhost:" + randomServerPort + "/throw-runtime-exception", + requestMetadata.get("url")); + assertEquals("GET", requestMetadata.get("method")); + assertEquals("127.0.0.1", requestMetadata.get("clientIp")); + + // Assert that the request params are as expected + @SuppressWarnings(value = "unchecked") Map params = + (Map) requestMetadata.get("params"); + assertEquals("paramVal1", params.get("param1")[0]); + assertEquals("paramVal2", params.get("param2")[0]); + + // Assert that the request headers are as expected, including headers with + // multiple values represented as a comma-separated string. + @SuppressWarnings(value = "unchecked") Map headers = + (Map) requestMetadata.get("headers"); + assertEquals("header1Val1,header1Val2", headers.get("header1")); + assertEquals("header2Val1", headers.get("header2")); + } + + @Test + @SuppressWarnings("unchecked") + public void springVersionSetCorrectly() { + callRuntimeExceptionEndpoint(); + + Report report = verifyAndGetReport(delivery); + + // Check that the Spring version is set as expected + Map deviceMetadata = report.getDevice(); + Map runtimeVersions = + (Map) deviceMetadata.get("runtimeVersions"); + assertEquals(SpringVersion.getVersion(), runtimeVersions.get("springFramework")); + assertEquals(SpringBootVersion.getVersion(), runtimeVersions.get("springBoot")); + } + + @Test + public void unhandledTypeMismatchExceptionSeverityInfo() { + callUnhandledTypeMismatchExceptionEndpoint(); + + Report report = verifyAndGetReport(delivery); + + assertTrue(report.getUnhandled()); + assertEquals("info", report.getSeverity()); + assertEquals("exceptionClass", report.getSeverityReason().getType()); + assertThat(report.getSeverityReason().getAttributes(), + is(Collections.singletonMap("exceptionClass", "TypeMismatchException"))); + } + + @Test + public void unhandledTypeMismatchExceptionCallbackSeverity() + throws IllegalAccessException, NoSuchFieldException { + Report report; + Callback callback = new Callback() { + @Override + public void beforeNotify(Report report) { + report.setSeverity(Severity.WARNING); + } + }; + + try { + bugsnag.addCallback(callback); + + callUnhandledTypeMismatchExceptionEndpoint(); + + report = verifyAndGetReport(delivery); + } finally { + // Remove the callback via reflection so that subsequent tests do not use it + Field callbacksField = Configuration.class.getDeclaredField("callbacks"); + @SuppressWarnings(value = "unchecked") Collection callbacks = + (Collection) callbacksField.get(bugsnag.getConfig()); + callbacks.remove(callback); + } + + assertTrue(report.getUnhandled()); + assertEquals("warning", report.getSeverity()); + assertEquals("userCallbackSetSeverity", report.getSeverityReason().getType()); + } + + @Test + public void handledTypeMismatchExceptionUserSeverity() { + callHandledTypeMismatchExceptionUserSeverityEndpoint(); + + Report report = verifyAndGetReport(delivery); + + assertFalse(report.getUnhandled()); + assertEquals("warning", report.getSeverity()); + assertEquals("userSpecifiedSeverity", report.getSeverityReason().getType()); + assertThat(report.getSeverityReason().getAttributes(), is(Collections.EMPTY_MAP)); + } + + @Test + public void handledTypeMismatchExceptionCallbackSeverity() { + callHandledTypeMismatchExceptionCallbackSeverityEndpoint(); + + Report report = verifyAndGetReport(delivery); + + assertFalse(report.getUnhandled()); + assertEquals("warning", report.getSeverity()); + assertEquals("userCallbackSetSeverity", report.getSeverityReason().getType()); + } + + private void callUnhandledTypeMismatchExceptionEndpoint() { + this.restTemplate.getForEntity( + "/throw-type-mismatch-exception", String.class); + } + + private void callHandledTypeMismatchExceptionUserSeverityEndpoint() { + this.restTemplate.getForEntity( + "/handled-type-mismatch-exception-user-severity", String.class); + } + + private void callHandledTypeMismatchExceptionCallbackSeverityEndpoint() { + this.restTemplate.getForEntity( + "/handled-type-mismatch-exception-callback-severity", String.class); + } + + private void callRuntimeExceptionEndpoint() { + HttpHeaders headers = new HttpHeaders(); + headers.add("header1", "header1Val1"); + headers.add("header1", "header1Val2"); + headers.add("header2", "header2Val1"); + HttpEntity entity = new HttpEntity("parameters", headers); + this.restTemplate.exchange( + "/throw-runtime-exception?param1=paramVal1¶m2=paramVal2", + HttpMethod.GET, + entity, + String.class); + } + + private void verifyNoReport() { + verify(delivery, times(0)).deliver( + any(Serializer.class), + any(), + anyMapOf(String.class, String.class)); + } + + private void assertSessionsStarted(int sessionsStarted) { + assertEquals(sessionsStartedBeforeTest + sessionsStarted, getSessionCount()); + } + + private long getSessionCount() { + return bugsnag.getSessionTracker().getBatchCount() != null + ? bugsnag.getSessionTracker().getBatchCount().getSessionsStarted() : 0; + } +} diff --git a/bugsnag-spring/javax/src/test/java/com/bugsnag/SpringScheduledTaskTest.java b/bugsnag-spring/javax/src/test/java/com/bugsnag/SpringScheduledTaskTest.java new file mode 100644 index 00000000..944b95e9 --- /dev/null +++ b/bugsnag-spring/javax/src/test/java/com/bugsnag/SpringScheduledTaskTest.java @@ -0,0 +1,95 @@ +package com.bugsnag; + +import static com.bugsnag.TestUtils.verifyAndGetReport; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.bugsnag.HandledState.SeverityReasonType; +import com.bugsnag.delivery.Delivery; +import com.bugsnag.testapp.springboot.TestSpringBootApplication; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.ErrorHandler; + +import java.util.Collections; +import java.util.concurrent.ExecutionException; + +/** + * Test that a Spring Boot application configured with the + * {@link BugsnagSpringConfiguration} performs as expected. + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = TestSpringBootApplication.class) +public class SpringScheduledTaskTest { + + @Autowired + private Bugsnag bugsnag; + + @Autowired + private ThreadPoolTaskScheduler scheduler; + + @MockBean + private ErrorHandler mockErrorHandler; + + private Delivery delivery; + + /** + * Initialize test state + */ + @Before + public void setUp() { + delivery = mock(Delivery.class); + bugsnag.setDelivery(delivery); + } + + @Test + public void bugsnagNotifyWhenScheduledTaskException() + throws ExecutionException, InterruptedException { + + // The task to schedule + Runnable exampleRunnable = new Runnable() { + @Override + public void run() { + throw new RuntimeException("Scheduled test"); + } + }; + + // Run the task now and wait for it to finish + scheduler.submit(exampleRunnable).get(); + + Report report = verifyAndGetReport(delivery); + + // Assert that the exception was detected correctly + assertEquals("Scheduled test", report.getExceptionMessage()); + assertEquals("java.lang.RuntimeException", report.getExceptionName()); + + // Assert that the severity, severity reason and unhandled values are correct + assertEquals(Severity.ERROR.getValue(), report.getSeverity()); + assertEquals( + SeverityReasonType.REASON_UNHANDLED_EXCEPTION_MIDDLEWARE.toString(), + report.getSeverityReason().getType()); + assertThat( + report.getSeverityReason().getAttributes(), + is(Collections.singletonMap("framework", "Spring"))); + assertTrue(report.getUnhandled()); + + // Assert that the exception is passed to an existing exception handler + ArgumentCaptor exceptionCaptor = + ArgumentCaptor.forClass(RuntimeException.class); + verify(mockErrorHandler, times(1)).handleError(exceptionCaptor.capture()); + assertEquals("Scheduled test", exceptionCaptor.getValue().getMessage()); + } +} diff --git a/bugsnag-spring/javax/src/test/java/com/bugsnag/TestUtils.java b/bugsnag-spring/javax/src/test/java/com/bugsnag/TestUtils.java new file mode 100644 index 00000000..4bc70115 --- /dev/null +++ b/bugsnag-spring/javax/src/test/java/com/bugsnag/TestUtils.java @@ -0,0 +1,33 @@ +package com.bugsnag; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +import com.bugsnag.delivery.Delivery; +import com.bugsnag.serialization.Serializer; + +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; + +import java.util.Map; + +class TestUtils { + + /** + * Verify that a report was received, then capture and return that report + */ + static Report verifyAndGetReport(Delivery delivery) { + ArgumentCaptor notificationCaptor = + ArgumentCaptor.forClass(Notification.class); + verify(delivery, timeout(100).times(1)).deliver( + any(Serializer.class), + notificationCaptor.capture(), + anyMapOf(String.class, String.class)); + return notificationCaptor.getValue().getEvents().get(0); + } + + static Map anyMapOf(Class keyClazz, Class valueClazz) { + return ArgumentMatchers.anyMap(); + } +} diff --git a/bugsnag-spring/javax/src/test/java/com/bugsnag/testapp/springboot/AsyncService.java b/bugsnag-spring/javax/src/test/java/com/bugsnag/testapp/springboot/AsyncService.java new file mode 100644 index 00000000..54376bac --- /dev/null +++ b/bugsnag-spring/javax/src/test/java/com/bugsnag/testapp/springboot/AsyncService.java @@ -0,0 +1,19 @@ +package com.bugsnag.testapp.springboot; + +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.util.concurrent.Future; + +@Service +public class AsyncService { + @Async + public void throwExceptionVoid() { + throw new RuntimeException("Async void test"); + } + + @Async + public Future throwExceptionFuture() { + throw new RuntimeException("Async future test"); + } +} diff --git a/bugsnag-spring/javax/src/test/java/com/bugsnag/testapp/springboot/TestConfiguration.java b/bugsnag-spring/javax/src/test/java/com/bugsnag/testapp/springboot/TestConfiguration.java new file mode 100644 index 00000000..c1691243 --- /dev/null +++ b/bugsnag-spring/javax/src/test/java/com/bugsnag/testapp/springboot/TestConfiguration.java @@ -0,0 +1,50 @@ +package com.bugsnag.testapp.springboot; + +import com.bugsnag.Bugsnag; +import com.bugsnag.BugsnagAsyncExceptionHandler; +import com.bugsnag.BugsnagSpringConfiguration; + +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.scheduling.annotation.AsyncConfigurerSupport; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.util.ErrorHandler; + +/** + * This test configuration loads the BugsnagSpringConfiguration + * that will be used for real Spring bugsnag integration. + */ +@Configuration +@Import(BugsnagSpringConfiguration.class) +public class TestConfiguration extends AsyncConfigurerSupport implements SchedulingConfigurer { + + @Autowired(required = false) + private ErrorHandler scheduledTaskErrorHandler; + + @Bean + public Bugsnag bugsnag() { + return new Bugsnag("apiKey"); + } + + @Bean + ThreadPoolTaskScheduler scheduler() { + ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); + taskScheduler.setErrorHandler(scheduledTaskErrorHandler); + return taskScheduler; + } + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + taskRegistrar.setScheduler(scheduler()); + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return new BugsnagAsyncExceptionHandler(bugsnag()); + } +} diff --git a/bugsnag-spring/javax/src/test/java/com/bugsnag/testapp/springboot/TestController.java b/bugsnag-spring/javax/src/test/java/com/bugsnag/testapp/springboot/TestController.java new file mode 100644 index 00000000..9d0091b5 --- /dev/null +++ b/bugsnag-spring/javax/src/test/java/com/bugsnag/testapp/springboot/TestController.java @@ -0,0 +1,75 @@ +package com.bugsnag.testapp.springboot; + +import com.bugsnag.Bugsnag; +import com.bugsnag.Report; +import com.bugsnag.Severity; +import com.bugsnag.callbacks.Callback; + +import org.springframework.beans.TypeMismatchException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TestController { + + @Autowired + private Bugsnag bugsnag; + + /** + * Throw a runtime exception + */ + @RequestMapping("/throw-runtime-exception") + public void throwRuntimeException() { + throw new RuntimeException("Test"); + } + + /** + * Throw an exception where the severity reason is exceptionClass + */ + @RequestMapping("/throw-type-mismatch-exception") + public void throwTypeMismatchException() { + throw new TypeMismatchException("Test", String.class); + } + + /** + * Report a handled exception where the severity reason is exceptionClass + */ + @RequestMapping("/handled-type-mismatch-exception") + public void handledTypeMismatchException() { + try { + throw new TypeMismatchException("Test", String.class); + } catch (TypeMismatchException ex) { + bugsnag.notify(ex); + } + } + + /** + * Report a handled exception where the severity is set in the notify call + */ + @RequestMapping("/handled-type-mismatch-exception-user-severity") + public void handledTypeMismatchExceptionUserSeverity() { + try { + throw new TypeMismatchException("Test", String.class); + } catch (TypeMismatchException ex) { + bugsnag.notify(ex, Severity.WARNING); + } + } + + /** + * Report a handled exception where the severity reason is set in a callback + */ + @RequestMapping("/handled-type-mismatch-exception-callback-severity") + public void handledTypeMismatchExceptionCallbackSeverity() { + try { + throw new TypeMismatchException("Test", String.class); + } catch (TypeMismatchException ex) { + bugsnag.notify(ex, new Callback() { + @Override + public void beforeNotify(Report report) { + report.setSeverity(Severity.WARNING); + } + }); + } + } +} diff --git a/bugsnag-spring/javax/src/test/java/com/bugsnag/testapp/springboot/TestSpringBootApplication.java b/bugsnag-spring/javax/src/test/java/com/bugsnag/testapp/springboot/TestSpringBootApplication.java new file mode 100644 index 00000000..f02cde84 --- /dev/null +++ b/bugsnag-spring/javax/src/test/java/com/bugsnag/testapp/springboot/TestSpringBootApplication.java @@ -0,0 +1,15 @@ +package com.bugsnag.testapp.springboot; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + +@SpringBootApplication +@EnableScheduling +@EnableAsync +public class TestSpringBootApplication { + public static void main(String[] args) { + SpringApplication.run(TestSpringBootApplication.class, args); + } +} diff --git a/bugsnag-spring/javax/src/test/resources/logback.xml b/bugsnag-spring/javax/src/test/resources/logback.xml new file mode 100644 index 00000000..073f1525 --- /dev/null +++ b/bugsnag-spring/javax/src/test/resources/logback.xml @@ -0,0 +1,13 @@ + + + + + + apiKey + + + + + + + \ No newline at end of file diff --git a/bugsnag-spring/src/main/java/com/bugsnag/BugsnagJakartaMvcExceptionHandler.java b/bugsnag-spring/src/main/java/com/bugsnag/BugsnagJakartaMvcExceptionHandler.java new file mode 100644 index 00000000..ac41ca01 --- /dev/null +++ b/bugsnag-spring/src/main/java/com/bugsnag/BugsnagJakartaMvcExceptionHandler.java @@ -0,0 +1,49 @@ +package com.bugsnag; + +import com.bugsnag.HandledState.SeverityReasonType; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.ModelAndView; + +import java.util.Collections; + +/** + * Reports uncaught exceptions thrown from handler mapping or execution to Bugsnag + * and then passes the exception to the next handler in the chain. + * + * Set to highest precedence so that it should be called before other exception + * resolvers. + */ +@Order(Ordered.HIGHEST_PRECEDENCE) +class BugsnagJakartaMvcExceptionHandler implements HandlerExceptionResolver { + + private final Bugsnag bugsnag; + + BugsnagJakartaMvcExceptionHandler(final Bugsnag bugsnag) { + this.bugsnag = bugsnag; + } + + @Override + public ModelAndView resolveException(HttpServletRequest request, + HttpServletResponse response, + Object handler, + java.lang.Exception ex) { + + if (bugsnag.getConfig().shouldSendUncaughtExceptions()) { + HandledState handledState = HandledState.newInstance( + SeverityReasonType.REASON_UNHANDLED_EXCEPTION_MIDDLEWARE, + Collections.singletonMap("framework", "Spring"), + Severity.ERROR, + true); + + bugsnag.notify(ex, handledState, Thread.currentThread()); + } + + // Returning null passes the exception onto the next resolver in the chain. + return null; + } +} diff --git a/bugsnag-spring/src/main/java/com/bugsnag/JakartaMvcConfiguration.java b/bugsnag-spring/src/main/java/com/bugsnag/JakartaMvcConfiguration.java new file mode 100644 index 00000000..2f5b7832 --- /dev/null +++ b/bugsnag-spring/src/main/java/com/bugsnag/JakartaMvcConfiguration.java @@ -0,0 +1,35 @@ +package com.bugsnag; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; + +/** + * If spring-webmvc is loaded, add configuration for reporting unhandled exceptions. + */ +@Configuration +@Conditional(SpringWebMvcLoadedCondition.class) +class JakartaMvcConfiguration implements InitializingBean { + + @Autowired + private Bugsnag bugsnag; + + /** + * Register an exception resolver to send unhandled reports to Bugsnag + * for uncaught exceptions thrown from request handlers. + */ + @Bean + BugsnagJakartaMvcExceptionHandler bugsnagHandlerExceptionResolver() { + return new BugsnagJakartaMvcExceptionHandler(bugsnag); + } + + /** + * Add a callback to assign specified severities for some Spring exceptions. + */ + @Override + public void afterPropertiesSet() { + bugsnag.addCallback(new ExceptionClassCallback()); + } +} diff --git a/bugsnag-spring/src/main/java/com/bugsnag/SpringBootJakartaConfiguration.java b/bugsnag-spring/src/main/java/com/bugsnag/SpringBootJakartaConfiguration.java new file mode 100644 index 00000000..c9f0d413 --- /dev/null +++ b/bugsnag-spring/src/main/java/com/bugsnag/SpringBootJakartaConfiguration.java @@ -0,0 +1,32 @@ +package com.bugsnag; + +import com.bugsnag.servlet.jakarta.BugsnagServletRequestListener; + +import jakarta.servlet.ServletRequestListener; +import org.springframework.boot.web.servlet.ServletListenerRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; + +/** + * If spring-boot is loaded, add configuration specific to Spring Boot + */ +@Configuration +@Conditional(SpringBootLoadedCondition.class) +class SpringBootJakartaConfiguration extends SpringBootConfiguration { + + /** + * The {@link com.bugsnag.servlet.javax.BugsnagServletContainerInitializer} does not work for Spring Boot, need to + * register the {@link BugsnagServletRequestListener} using a Spring Boot + * {@link ServletListenerRegistrationBean} instead. This adds session tracking and + * automatic servlet request metadata collection. + */ + @Bean + @Conditional(SpringWebMvcLoadedCondition.class) + ServletListenerRegistrationBean listenerRegistrationBean() { + ServletListenerRegistrationBean srb = + new ServletListenerRegistrationBean(); + srb.setListener(new BugsnagServletRequestListener()); + return srb; + } +} diff --git a/bugsnag-spring/src/test/java/com/bugsnag/SpringMvcTest.java b/bugsnag-spring/src/test/java/com/bugsnag/SpringMvcTest.java index 694bb98b..ed03f19f 100644 --- a/bugsnag-spring/src/test/java/com/bugsnag/SpringMvcTest.java +++ b/bugsnag-spring/src/test/java/com/bugsnag/SpringMvcTest.java @@ -25,10 +25,10 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringBootVersion; -import org.springframework.boot.context.embedded.LocalServerPort; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.core.SpringVersion; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -42,7 +42,7 @@ /** * Test that a Spring Boot application configured with the - * {@link com.bugsnag.BugsnagSpringConfiguration} performs as expected. + * {@link BugsnagSpringConfiguration} performs as expected. */ @RunWith(SpringRunner.class) @SpringBootTest( diff --git a/bugsnag-spring/src/test/java/com/bugsnag/TestUtils.java b/bugsnag-spring/src/test/java/com/bugsnag/TestUtils.java index d0f5506f..4bc70115 100644 --- a/bugsnag-spring/src/test/java/com/bugsnag/TestUtils.java +++ b/bugsnag-spring/src/test/java/com/bugsnag/TestUtils.java @@ -27,11 +27,7 @@ static Report verifyAndGetReport(Delivery delivery) { return notificationCaptor.getValue().getEvents().get(0); } - /** - * {@link ArgumentMatchers#anyMapOf} is deprecated but we still need it for JDK 7 builds - */ - @SuppressWarnings("deprecation") static Map anyMapOf(Class keyClazz, Class valueClazz) { - return ArgumentMatchers.anyMapOf(keyClazz, valueClazz); + return ArgumentMatchers.anyMap(); } } diff --git a/bugsnag/build.gradle b/bugsnag/build.gradle index 3c81c924..3c791160 100644 --- a/bugsnag/build.gradle +++ b/bugsnag/build.gradle @@ -1,40 +1,36 @@ plugins { - id "com.github.hierynomus.license" version "0.15.0" + id "com.github.hierynomus.license" version "0.16.1" } apply plugin: 'java-library' apply from: '../common.gradle' +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + repositories { mavenCentral() } dependencies { - compile "com.fasterxml.jackson.core:jackson-databind:2.14.1" - compile "org.slf4j:slf4j-api:1.7.25" - compileOnly "javax.servlet:javax.servlet-api:${servletApiVersion}" + api "com.fasterxml.jackson.core:jackson-databind:2.14.1" + api "org.slf4j:slf4j-api:${slf4jApiVersion}" + compileOnly "javax.servlet:javax.servlet-api:${javaxServletApiVersion}" + compileOnly "jakarta.servlet:jakarta.servlet-api:${jakartaServletApiVersion}" compileOnly("ch.qos.logback:logback-classic:${logbackVersion}") { exclude group: "org.slf4j" } - testCompile "junit:junit:4.13.2" - testCompile "org.slf4j:log4j-over-slf4j:1.7.25" - testCompile "javax.servlet:javax.servlet-api:${servletApiVersion}" - testCompile "org.mockito:mockito-core:2.10.0" - testCompile("ch.qos.logback:logback-classic:${logbackVersion}") { + testImplementation "junit:junit:${junitVersion}" + testImplementation "org.slf4j:log4j-over-slf4j:${slf4jApiVersion}" + testImplementation "javax.servlet:javax.servlet-api:${javaxServletApiVersion}" + testImplementation "jakarta.servlet:jakarta.servlet-api:${jakartaServletApiVersion}" + testImplementation "org.mockito:mockito-core:${mockitoVersion}" + testImplementation("ch.qos.logback:logback-classic:${logbackVersion}") { exclude group: "org.slf4j" } } -task testJar(type: Jar) { - classifier = 'test' - from sourceSets.test.output -} - -artifacts { - testRuntime testJar -} - // license checking license { header rootProject.file('LICENSE') diff --git a/bugsnag/src/main/java/com/bugsnag/Configuration.java b/bugsnag/src/main/java/com/bugsnag/Configuration.java index b9010e8f..2ebfbe52 100644 --- a/bugsnag/src/main/java/com/bugsnag/Configuration.java +++ b/bugsnag/src/main/java/com/bugsnag/Configuration.java @@ -3,7 +3,8 @@ import com.bugsnag.callbacks.AppCallback; import com.bugsnag.callbacks.Callback; import com.bugsnag.callbacks.DeviceCallback; -import com.bugsnag.callbacks.ServletCallback; +import com.bugsnag.callbacks.JakartaServletCallback; +import com.bugsnag.callbacks.JavaxServletCallback; import com.bugsnag.delivery.AsyncHttpDelivery; import com.bugsnag.delivery.Delivery; import com.bugsnag.delivery.HttpDelivery; @@ -57,8 +58,12 @@ public class Configuration { addCallback(new DeviceCallback()); DeviceCallback.initializeCache(); - if (ServletCallback.isAvailable()) { - addCallback(new ServletCallback()); + if (JavaxServletCallback.isAvailable()) { + addCallback(new JavaxServletCallback()); + } + + if (JakartaServletCallback.isAvailable()) { + addCallback(new JakartaServletCallback()); } } diff --git a/bugsnag/src/main/java/com/bugsnag/callbacks/JakartaServletCallback.java b/bugsnag/src/main/java/com/bugsnag/callbacks/JakartaServletCallback.java new file mode 100644 index 00000000..3945f48e --- /dev/null +++ b/bugsnag/src/main/java/com/bugsnag/callbacks/JakartaServletCallback.java @@ -0,0 +1,89 @@ +package com.bugsnag.callbacks; + +import com.bugsnag.Report; +import com.bugsnag.servlet.jakarta.BugsnagServletRequestListener; + +import jakarta.servlet.http.HttpServletRequest; + +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +public class JakartaServletCallback 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("jakarta.servlet.ServletRequestListener", false, + JakartaServletCallback.class.getClassLoader()); + return true; + } catch (ClassNotFoundException ex) { + return false; + } + } + + @Override + public void beforeNotify(Report report) { + // Check if we have any servlet request data available + HttpServletRequest request = BugsnagServletRequestListener.getServletRequest(); + if (request == null) { + return; + } + + // Add request information to metaData + report + .addToTab("request", "url", request.getRequestURL().toString()) + .addToTab("request", "method", request.getMethod()) + .addToTab("request", "params", + new HashMap(request.getParameterMap())) + .addToTab("request", "clientIp", getClientIp(request)) + .addToTab("request", "headers", getHeaderMap(request)); + + // Set default context + if (report.getContext() == null) { + report.setContext(request.getMethod() + " " + request.getRequestURI()); + } + } + + private String getClientIp(HttpServletRequest request) { + String remoteAddr = request.getRemoteAddr(); + String forwardedAddr = request.getHeader(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 getHeaderMap(HttpServletRequest request) { + Map headers = new HashMap(); + Enumeration headerNames = request.getHeaderNames(); + + while (headerNames != null && headerNames.hasMoreElements()) { + String key = headerNames.nextElement(); + Enumeration headerValues = request.getHeaders(key); + StringBuilder value = new StringBuilder(); + + if (headerValues != null && headerValues.hasMoreElements()) { + value.append(headerValues.nextElement()); + + // 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.hasMoreElements()) { + value.append(",").append(headerValues.nextElement()); + } + } + + headers.put(key, value.toString()); + } + + return headers; + } +} diff --git a/bugsnag/src/main/java/com/bugsnag/callbacks/ServletCallback.java b/bugsnag/src/main/java/com/bugsnag/callbacks/JavaxServletCallback.java similarity index 94% rename from bugsnag/src/main/java/com/bugsnag/callbacks/ServletCallback.java rename to bugsnag/src/main/java/com/bugsnag/callbacks/JavaxServletCallback.java index 7d54e77c..84912fbb 100644 --- a/bugsnag/src/main/java/com/bugsnag/callbacks/ServletCallback.java +++ b/bugsnag/src/main/java/com/bugsnag/callbacks/JavaxServletCallback.java @@ -1,14 +1,14 @@ package com.bugsnag.callbacks; import com.bugsnag.Report; -import com.bugsnag.servlet.BugsnagServletRequestListener; +import com.bugsnag.servlet.javax.BugsnagServletRequestListener; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import javax.servlet.http.HttpServletRequest; -public class ServletCallback implements Callback { +public class JavaxServletCallback implements Callback { private static final String HEADER_X_FORWARDED_FOR = "X-FORWARDED-FOR"; /** @@ -17,7 +17,7 @@ public class ServletCallback implements Callback { public static boolean isAvailable() { try { Class.forName("javax.servlet.ServletRequestListener", false, - ServletCallback.class.getClassLoader()); + JavaxServletCallback.class.getClassLoader()); return true; } catch (ClassNotFoundException ex) { return false; diff --git a/bugsnag/src/main/java/com/bugsnag/servlet/jakarta/BugsnagServletContainerInitializer.java b/bugsnag/src/main/java/com/bugsnag/servlet/jakarta/BugsnagServletContainerInitializer.java new file mode 100644 index 00000000..65ad972b --- /dev/null +++ b/bugsnag/src/main/java/com/bugsnag/servlet/jakarta/BugsnagServletContainerInitializer.java @@ -0,0 +1,14 @@ +package com.bugsnag.servlet.jakarta; + +import jakarta.servlet.ServletContainerInitializer; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; + +import java.util.Set; + +public class BugsnagServletContainerInitializer implements ServletContainerInitializer { + @Override + public void onStartup(Set> cls, ServletContext context) throws ServletException { + context.addListener(BugsnagServletRequestListener.class); + } +} diff --git a/bugsnag/src/main/java/com/bugsnag/servlet/jakarta/BugsnagServletRequestListener.java b/bugsnag/src/main/java/com/bugsnag/servlet/jakarta/BugsnagServletRequestListener.java new file mode 100644 index 00000000..1867078f --- /dev/null +++ b/bugsnag/src/main/java/com/bugsnag/servlet/jakarta/BugsnagServletRequestListener.java @@ -0,0 +1,42 @@ +package com.bugsnag.servlet.jakarta; + +import com.bugsnag.Bugsnag; + +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletRequestEvent; +import jakarta.servlet.ServletRequestListener; +import jakarta.servlet.http.HttpServletRequest; + +public class BugsnagServletRequestListener implements ServletRequestListener { + + private static final ThreadLocal SERVLET_REQUEST = + new ThreadLocal(); + + public static HttpServletRequest getServletRequest() { + return SERVLET_REQUEST.get(); + } + + @Override + public void requestInitialized(ServletRequestEvent servletRequestEvent) { + trackServletSession(); + ServletRequest servletRequest = servletRequestEvent.getServletRequest(); + + if (servletRequest instanceof HttpServletRequest) { + SERVLET_REQUEST.set((HttpServletRequest) servletRequest); + } + } + + @Override + public void requestDestroyed(ServletRequestEvent servletRequestEvent) { + SERVLET_REQUEST.remove(); + Bugsnag.clearThreadMetaData(); + } + + private void trackServletSession() { + for (Bugsnag bugsnag : Bugsnag.uncaughtExceptionClients()) { + if (bugsnag.shouldAutoCaptureSessions()) { + bugsnag.startSession(); + } + } + } +} diff --git a/bugsnag/src/main/java/com/bugsnag/servlet/BugsnagServletContainerInitializer.java b/bugsnag/src/main/java/com/bugsnag/servlet/javax/BugsnagServletContainerInitializer.java similarity index 92% rename from bugsnag/src/main/java/com/bugsnag/servlet/BugsnagServletContainerInitializer.java rename to bugsnag/src/main/java/com/bugsnag/servlet/javax/BugsnagServletContainerInitializer.java index 187573b8..af177c35 100644 --- a/bugsnag/src/main/java/com/bugsnag/servlet/BugsnagServletContainerInitializer.java +++ b/bugsnag/src/main/java/com/bugsnag/servlet/javax/BugsnagServletContainerInitializer.java @@ -1,4 +1,4 @@ -package com.bugsnag.servlet; +package com.bugsnag.servlet.javax; import java.util.Set; import javax.servlet.ServletContainerInitializer; diff --git a/bugsnag/src/main/java/com/bugsnag/servlet/BugsnagServletRequestListener.java b/bugsnag/src/main/java/com/bugsnag/servlet/javax/BugsnagServletRequestListener.java similarity index 97% rename from bugsnag/src/main/java/com/bugsnag/servlet/BugsnagServletRequestListener.java rename to bugsnag/src/main/java/com/bugsnag/servlet/javax/BugsnagServletRequestListener.java index b95b2e86..a3e9e47a 100644 --- a/bugsnag/src/main/java/com/bugsnag/servlet/BugsnagServletRequestListener.java +++ b/bugsnag/src/main/java/com/bugsnag/servlet/javax/BugsnagServletRequestListener.java @@ -1,4 +1,4 @@ -package com.bugsnag.servlet; +package com.bugsnag.servlet.javax; import com.bugsnag.Bugsnag; diff --git a/bugsnag/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer b/bugsnag/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer new file mode 100644 index 00000000..9ce59b4c --- /dev/null +++ b/bugsnag/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer @@ -0,0 +1 @@ +com.bugsnag.servlet.jakarta.BugsnagServletContainerInitializer diff --git a/bugsnag/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer b/bugsnag/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer index 585bea80..a59bf666 100644 --- a/bugsnag/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer +++ b/bugsnag/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer @@ -1 +1 @@ -com.bugsnag.servlet.BugsnagServletContainerInitializer +com.bugsnag.servlet.javax.BugsnagServletContainerInitializer diff --git a/bugsnag/src/test/java/com/bugsnag/JakartaServletCallbackTest.java b/bugsnag/src/test/java/com/bugsnag/JakartaServletCallbackTest.java new file mode 100644 index 00000000..5bd78ec4 --- /dev/null +++ b/bugsnag/src/test/java/com/bugsnag/JakartaServletCallbackTest.java @@ -0,0 +1,145 @@ +package com.bugsnag; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.bugsnag.callbacks.JakartaServletCallback; +import com.bugsnag.servlet.jakarta.BugsnagServletRequestListener; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletRequestEvent; +import jakarta.servlet.http.HttpServletRequest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +public class JakartaServletCallbackTest { + + private Bugsnag bugsnag; + + /** + * Generate a new request instance which will be read by the servlet + * context and callback + */ + @Before + public void setUp() { + bugsnag = new Bugsnag("apikey", false); + bugsnag.setDelivery(null); + + HttpServletRequest request = mock(HttpServletRequest.class); + + Map params = new HashMap(); + params.put("account", new String[]{"Acme Co"}); + params.put("name", new String[]{"Bill"}); + when(request.getParameterMap()).thenReturn(params); + + when(request.getMethod()).thenReturn("PATCH"); + when(request.getRequestURL()).thenReturn(new StringBuffer("/foo/bar")); + when(request.getRequestURI()).thenReturn("/foo/bar"); + when(request.getRemoteAddr()).thenReturn("12.0.4.57"); + + when(request.getHeaderNames()).thenReturn( + stringsToEnumeration( + "Content-Type", + "Content-Length", + "X-Custom-Header", + "Authorization", + "Cookie")); + when(request.getHeaders("Content-Type")).thenReturn( + stringsToEnumeration("application/json")); + when(request.getHeaders("Content-Length")).thenReturn( + stringsToEnumeration("54")); + when(request.getHeaders("X-Custom-Header")).thenReturn( + stringsToEnumeration("some-data-1", "some-data-2")); + when(request.getHeaders("Authorization")).thenReturn( + stringsToEnumeration("Basic ABC123")); + when(request.getHeaders("Cookie")).thenReturn( + stringsToEnumeration("name1=val1; name2=val2")); + + ServletContext context = mock(ServletContext.class); + BugsnagServletRequestListener listener = new BugsnagServletRequestListener(); + listener.requestInitialized(new ServletRequestEvent(context, request)); + } + + /** + * 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")); + JakartaServletCallback callback = new JakartaServletCallback(); + callback.beforeNotify(report); + + Map metadata = report.getMetaData(); + assertTrue(metadata.containsKey("request")); + + Map request = (Map) 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 headers = (Map) 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 params = (Map) request.get("params"); + assertTrue(params.containsKey("account")); + String[] account = params.get("account"); + assertEquals("Acme Co", account[0]); + + assertTrue(params.containsKey("name")); + String[] name = params.get("name"); + assertEquals("Bill", name[0]); + } + + @Test + public void testRequestContextSet() { + Report report = generateReport(new java.lang.Exception("Spline reticulation failed")); + JakartaServletCallback callback = new JakartaServletCallback(); + 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"); + JakartaServletCallback callback = new JakartaServletCallback(); + callback.beforeNotify(report); + + assertEquals("Honey nut corn flakes", report.getContext()); + } + + private Report generateReport(java.lang.Exception exception) { + return bugsnag.buildReport(exception); + } + + private Enumeration stringsToEnumeration(String... strings) { + return Collections.enumeration(Arrays.asList(strings)); + } +} diff --git a/bugsnag/src/test/java/com/bugsnag/ServletCallbackTest.java b/bugsnag/src/test/java/com/bugsnag/JavaxServletCallbackTest.java similarity index 93% rename from bugsnag/src/test/java/com/bugsnag/ServletCallbackTest.java rename to bugsnag/src/test/java/com/bugsnag/JavaxServletCallbackTest.java index 6b1fabb0..a4ffa9f7 100644 --- a/bugsnag/src/test/java/com/bugsnag/ServletCallbackTest.java +++ b/bugsnag/src/test/java/com/bugsnag/JavaxServletCallbackTest.java @@ -5,9 +5,9 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import com.bugsnag.callbacks.ServletCallback; +import com.bugsnag.callbacks.JavaxServletCallback; -import com.bugsnag.servlet.BugsnagServletRequestListener; +import com.bugsnag.servlet.javax.BugsnagServletRequestListener; import org.junit.After; import org.junit.Before; @@ -22,7 +22,7 @@ import javax.servlet.ServletRequestEvent; import javax.servlet.http.HttpServletRequest; -public class ServletCallbackTest { +public class JavaxServletCallbackTest { private Bugsnag bugsnag; @@ -82,7 +82,7 @@ public void closeBugsnag() { @Test public void testRequestMetadataAdded() { Report report = generateReport(new java.lang.Exception("Spline reticulation failed")); - ServletCallback callback = new ServletCallback(); + JavaxServletCallback callback = new JavaxServletCallback(); callback.beforeNotify(report); Map metadata = report.getMetaData(); @@ -119,7 +119,7 @@ public void testRequestMetadataAdded() { @Test public void testRequestContextSet() { Report report = generateReport(new java.lang.Exception("Spline reticulation failed")); - ServletCallback callback = new ServletCallback(); + JavaxServletCallback callback = new JavaxServletCallback(); callback.beforeNotify(report); assertEquals("PATCH /foo/bar", report.getContext()); @@ -129,7 +129,7 @@ public void testRequestContextSet() { public void testExistingContextNotOverridden() { Report report = generateReport(new java.lang.Exception("Spline reticulation failed")); report.setContext("Honey nut corn flakes"); - ServletCallback callback = new ServletCallback(); + JavaxServletCallback callback = new JavaxServletCallback(); callback.beforeNotify(report); assertEquals("Honey nut corn flakes", report.getContext()); diff --git a/build.gradle b/build.gradle index b2d60564..3e9e1ea3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,14 @@ buildscript { repositories { mavenCentral() + maven { + url = uri("https://plugins.gradle.org/m2/") + } } dependencies { if (project.hasProperty('releasing')) { - classpath 'com.bmuschko:gradle-nexus-plugin:2.3.1' - classpath 'io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.11.0' - classpath 'net.researchgate:gradle-release:2.4.0' + classpath 'io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.30.0' + classpath 'net.researchgate:gradle-release:3.0.2' } } } diff --git a/common.gradle b/common.gradle index fb779f1b..d69f4398 100644 --- a/common.gradle +++ b/common.gradle @@ -1,19 +1,20 @@ ext { - servletApiVersion = "3.1.0" + javaxServletApiVersion = "3.1.0" + jakartaServletApiVersion = "5.0.0" logbackVersion = "1.2.3" + slf4jApiVersion = "1.7.25" + junitVersion = "4.13.2" + mockitoVersion = "5.0.0" } if (JavaVersion.current().isJava8Compatible()) { apply plugin: 'checkstyle' } -if (project.hasProperty('releasing')) { +if (project.hasProperty('releasing') && project.depth <= 1) { apply from: "../release.gradle" } -sourceCompatibility = 1.6 -targetCompatibility = 1.6 - test { testLogging { events "passed", "skipped", "failed", "standardOut", "standardError" diff --git a/docker-compose.yml b/docker-compose.yml index d6a188bf..80a6cc42 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,11 @@ services: build: context: . dockerfile: dockerfiles/Dockerfile.java-common - java-mazerunner: + java8-mazerunner: build: context: . - dockerfile: dockerfiles/Dockerfile.java-mazerunner + dockerfile: dockerfiles/Dockerfile.java8-mazerunner + java17-mazerunner: + build: + context: . + dockerfile: dockerfiles/Dockerfile.java17-mazerunner \ No newline at end of file diff --git a/dockerfiles/Dockerfile.java-common b/dockerfiles/Dockerfile.java-common index 9ed9fa29..31b27d5e 100644 --- a/dockerfiles/Dockerfile.java-common +++ b/dockerfiles/Dockerfile.java-common @@ -1,4 +1,4 @@ -FROM openjdk:8 +FROM openjdk:17-jdk-slim WORKDIR /app # Force download of gradle zip early to avoid repeating diff --git a/dockerfiles/Dockerfile.java-mazerunner b/dockerfiles/Dockerfile.java17-mazerunner similarity index 93% rename from dockerfiles/Dockerfile.java-mazerunner rename to dockerfiles/Dockerfile.java17-mazerunner index 2ca0aad8..c578ca05 100644 --- a/dockerfiles/Dockerfile.java-mazerunner +++ b/dockerfiles/Dockerfile.java17-mazerunner @@ -1,4 +1,4 @@ -FROM tomcat:9.0.56-jdk8 +FROM tomcat:10-jdk17-openjdk-slim WORKDIR /app RUN apt-get update && DEBIAN_FRONTEND=noninteractive \ diff --git a/dockerfiles/Dockerfile.java8-mazerunner b/dockerfiles/Dockerfile.java8-mazerunner new file mode 100644 index 00000000..7bee52f2 --- /dev/null +++ b/dockerfiles/Dockerfile.java8-mazerunner @@ -0,0 +1,39 @@ +FROM openjdk:17-jdk-slim as builder + +WORKDIR /app + +# Force download of gradle zip early to avoid repeating +# if Docker cache is invalidated by branch changes. +COPY gradlew gradle.properties /app/ +COPY gradle/ /app/gradle/ +ENV GRADLE_OPTS="-Dorg.gradle.daemon=false" +COPY settings.gradle /app/ +RUN ./gradlew + +# Copy repo into docker +COPY . /app + +RUN features/scripts/assemble-fixtures.sh + +FROM tomcat:9.0.56-jdk8 + +RUN apt-get update && DEBIAN_FRONTEND=noninteractive \ + apt-get install -y -q docker-compose bundler libcurl4-openssl-dev + +WORKDIR /app + +# Force download of gradle zip early to avoid repeating +# if Docker cache is invalidated by branch changes. +COPY gradlew gradle.properties /app/ +COPY gradle/ /app/gradle/ +ENV GRADLE_OPTS="-Dorg.gradle.daemon=false" +COPY settings.gradle /app/ +RUN ./gradlew + +COPY . /app + +COPY --from=builder /app/features/fixtures/libs/* features/fixtures/libs/ + +# Setup mazerunner +RUN gem install bundler:1.16.5 +RUN bundle install diff --git a/dockerfiles/Dockerfile.license-audit b/dockerfiles/Dockerfile.license-audit index ab640982..c94c939f 100644 --- a/dockerfiles/Dockerfile.license-audit +++ b/dockerfiles/Dockerfile.license-audit @@ -1,4 +1,4 @@ -FROM openjdk:8-jdk-slim +FROM openjdk:17-jdk-slim RUN apt-get update RUN apt-get install -y ruby-full curl diff --git a/examples/logback/build.gradle b/examples/logback/build.gradle index 6cc4cb94..cbc22ae0 100644 --- a/examples/logback/build.gradle +++ b/examples/logback/build.gradle @@ -11,8 +11,8 @@ repositories { } dependencies { - compile("ch.qos.logback:logback-classic:1.2.3") - compile project(':bugsnag') + implementation "ch.qos.logback:logback-classic:1.2.3" + implementation project(':bugsnag') } mainClassName = "com.bugsnag.example.logback.cli.Application" diff --git a/examples/servlet-jakarta/README.md b/examples/servlet-jakarta/README.md new file mode 100644 index 00000000..e8bc33be --- /dev/null +++ b/examples/servlet-jakarta/README.md @@ -0,0 +1,21 @@ +# Bugsnag Jakarta Servlet Example + +Demonstrates how to use Bugsnag in a Servlet-based Java application. + +1. Open `ExampleServlet` and alter the value of `bugsnag = new Bugsnag("YOUR-API-KEY");` to match your API key + +2. Build the app + + ```shell + gradle clean assemble + ``` + +3. Start the web server + + ```shell + gradle appRun + ``` + +4. Cause a crash by visiting [http://localhost:8080/servlet](http://localhost:8080/servlet) + +5. View the captured errors in [your dashboard](https://app.bugsnag.com) \ No newline at end of file diff --git a/examples/servlet-jakarta/build.gradle b/examples/servlet-jakarta/build.gradle new file mode 100644 index 00000000..0ea1f37f --- /dev/null +++ b/examples/servlet-jakarta/build.gradle @@ -0,0 +1,29 @@ +apply plugin: 'java' +apply plugin: 'war' +apply plugin: 'org.gretty' + +buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath 'org.gretty:gretty:4.0.3' + } +} + +repositories { + mavenCentral() +} + +dependencies { + runtimeOnly 'org.slf4j:slf4j-simple:1.7.25' + implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" + implementation project(':bugsnag') +} + +gretty { + httpPort = 8083 + contextPath = '/' + jvmArgs = ['-Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG'] +} \ No newline at end of file diff --git a/examples/servlet-jakarta/src/main/java/com/bugsnag/example/servlet/ErrorHandler.java b/examples/servlet-jakarta/src/main/java/com/bugsnag/example/servlet/ErrorHandler.java new file mode 100644 index 00000000..3d93adf5 --- /dev/null +++ b/examples/servlet-jakarta/src/main/java/com/bugsnag/example/servlet/ErrorHandler.java @@ -0,0 +1,31 @@ +package com.bugsnag.example.servlet; + +import com.bugsnag.Bugsnag; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class ErrorHandler extends HttpServlet { + + private static final long serialVersionUID = 4926619146717832212L; + + private Bugsnag bugsnag; + + /** + * Error handler to report the error to Bugsnag + */ + public ErrorHandler() { + bugsnag = new Bugsnag("YOUR-API-KEY"); + bugsnag.setProjectPackages("com.bugsnag.example"); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // Notify Bugsnag of the exception + Throwable throwable = (Throwable) req.getAttribute("jakarta.servlet.error.exception"); + bugsnag.notify(throwable); + } +} diff --git a/examples/servlet-jakarta/src/main/java/com/bugsnag/example/servlet/ExampleServlet.java b/examples/servlet-jakarta/src/main/java/com/bugsnag/example/servlet/ExampleServlet.java new file mode 100644 index 00000000..12a0ee6f --- /dev/null +++ b/examples/servlet-jakarta/src/main/java/com/bugsnag/example/servlet/ExampleServlet.java @@ -0,0 +1,44 @@ +package com.bugsnag.example.servlet; + +import com.bugsnag.Bugsnag; +import com.bugsnag.Severity; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class ExampleServlet extends HttpServlet { + + private static final long serialVersionUID = 1432171052111530587L; + + private Bugsnag bugsnag; + + /** + * Simple servlet example + */ + public ExampleServlet() { + bugsnag = new Bugsnag("YOUR-API-KEY"); + bugsnag.setProjectPackages("com.bugsnag.example"); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException { + // Send a handled exception to Bugsnag + try { + throw new RuntimeException("Handled exception - default severity"); + } catch (RuntimeException e) { + bugsnag.notify(e); + } + + // Send a handled exception to Bugsnag with info severity + try { + throw new RuntimeException("Handled exception - INFO severity"); + } catch (RuntimeException ex) { + bugsnag.notify(ex, Severity.INFO); + } + + // Throw an exception - not automatically reported so must be handled by the error handler + throw new ServletException("Servlet exception"); + } +} diff --git a/examples/servlet-jakarta/src/main/webapp/WEB-INF/web.xml b/examples/servlet-jakarta/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..ffbc2a38 --- /dev/null +++ b/examples/servlet-jakarta/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,30 @@ + + + + ExampleServlet + ExampleServlet + com.bugsnag.example.servlet.ExampleServlet + + + ErrorHandler + com.bugsnag.example.servlet.ErrorHandler + + + + ExampleServlet + / + + + ErrorHandler + /ErrorHandler + + + + + jakarta.servlet.ServletException + /ErrorHandler + + diff --git a/examples/servlet/README.md b/examples/servlet-javax/README.md similarity index 93% rename from examples/servlet/README.md rename to examples/servlet-javax/README.md index 0bb199b7..b81c6e81 100644 --- a/examples/servlet/README.md +++ b/examples/servlet-javax/README.md @@ -1,4 +1,4 @@ -# Bugsnag Servlet Example +# Bugsnag Javax Servlet Example Demonstrates how to use Bugsnag in a Servlet-based Java application. diff --git a/examples/servlet/build.gradle b/examples/servlet-javax/build.gradle similarity index 56% rename from examples/servlet/build.gradle rename to examples/servlet-javax/build.gradle index fe158c0e..1e4412f3 100644 --- a/examples/servlet/build.gradle +++ b/examples/servlet-javax/build.gradle @@ -1,6 +1,6 @@ apply plugin: 'java' apply plugin: 'war' -apply plugin: 'org.akhikhl.gretty' +apply plugin: 'org.gretty' buildscript { repositories { @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'org.akhikhl.gretty:gretty:+' + classpath 'org.gretty:gretty:3.1.1' } } @@ -17,8 +17,9 @@ repositories { } dependencies { - runtime 'org.slf4j:slf4j-simple:1.7.25' - compile project(':bugsnag') + runtimeOnly 'org.slf4j:slf4j-simple:1.7.25' + implementation "javax.servlet:javax.servlet-api:3.1.0" + implementation project(':bugsnag') } gretty { diff --git a/examples/servlet/src/main/java/com/bugsnag/example/servlet/ErrorHandler.java b/examples/servlet-javax/src/main/java/com/bugsnag/example/servlet/ErrorHandler.java similarity index 100% rename from examples/servlet/src/main/java/com/bugsnag/example/servlet/ErrorHandler.java rename to examples/servlet-javax/src/main/java/com/bugsnag/example/servlet/ErrorHandler.java diff --git a/examples/servlet/src/main/java/com/bugsnag/example/servlet/ExampleServlet.java b/examples/servlet-javax/src/main/java/com/bugsnag/example/servlet/ExampleServlet.java similarity index 100% rename from examples/servlet/src/main/java/com/bugsnag/example/servlet/ExampleServlet.java rename to examples/servlet-javax/src/main/java/com/bugsnag/example/servlet/ExampleServlet.java diff --git a/examples/servlet/src/main/webapp/WEB-INF/web.xml b/examples/servlet-javax/src/main/webapp/WEB-INF/web.xml similarity index 100% rename from examples/servlet/src/main/webapp/WEB-INF/web.xml rename to examples/servlet-javax/src/main/webapp/WEB-INF/web.xml diff --git a/examples/simple/build.gradle b/examples/simple/build.gradle index f4d0b027..e9f1ebe7 100644 --- a/examples/simple/build.gradle +++ b/examples/simple/build.gradle @@ -5,8 +5,8 @@ repositories { } dependencies { - runtime 'org.slf4j:slf4j-simple:1.+' - compile project(':bugsnag') + runtimeOnly 'org.slf4j:slf4j-simple:1.+' + implementation project(':bugsnag') } mainClassName = "com.bugsnag.example.simple.ExampleApp" diff --git a/examples/spring-web/build.gradle b/examples/spring-web/build.gradle index 360beca9..00413b4f 100644 --- a/examples/spring-web/build.gradle +++ b/examples/spring-web/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - springBootVersion = '2.1.1.RELEASE' + springBootVersion = '2.5.14' } repositories { mavenCentral() @@ -19,8 +19,10 @@ repositories { } dependencies { - compile("org.springframework.boot:spring-boot-starter-web") + implementation "org.springframework.boot:spring-boot-starter-web" - compile project(":bugsnag-spring") + implementation project(":bugsnag") + implementation project(":bugsnag-spring") + implementation project(":bugsnag-spring:javax") } diff --git a/examples/spring/build.gradle b/examples/spring/build.gradle index aa986c0f..1dbacd45 100644 --- a/examples/spring/build.gradle +++ b/examples/spring/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - springBootVersion = '2.1.1.RELEASE' + springBootVersion = '2.5.14' } repositories { mavenCentral() @@ -19,7 +19,9 @@ repositories { } dependencies { - compile("org.springframework.boot:spring-boot-starter") + implementation "org.springframework.boot:spring-boot-starter" - compile project(":bugsnag-spring") + implementation project(":bugsnag") + implementation project(":bugsnag-spring") + implementation project(":bugsnag-spring:javax") } diff --git a/features/fixtures/mazerunner/build.gradle b/features/fixtures/mazerunner/build.gradle index 235886a6..f922c942 100644 --- a/features/fixtures/mazerunner/build.gradle +++ b/features/fixtures/mazerunner/build.gradle @@ -17,7 +17,7 @@ apply plugin: 'io.spring.dependency-management' group 'com.bugsnag.mazerunner' version '1.0-SNAPSHOT' -sourceCompatibility = 1.7 +sourceCompatibility = 1.8 repositories { mavenCentral() @@ -27,14 +27,14 @@ repositories { } dependencies { - compile("org.springframework.boot:spring-boot-starter") - compile("ch.qos.logback:logback-classic:1.2.3") + implementation("org.springframework.boot:spring-boot-starter") + implementation("ch.qos.logback:logback-classic:1.2.3") - compile("com.fasterxml.jackson.core:jackson-annotations:2.9.1") - compile("com.fasterxml.jackson.core:jackson-databind:2.9.1") - compile("com.bugsnag:bugsnag:9.9.9-test") - compile project(":scenarios") + implementation("com.fasterxml.jackson.core:jackson-annotations:2.9.1") + implementation("com.fasterxml.jackson.core:jackson-databind:2.9.1") + implementation("com.bugsnag:bugsnag:9.9.9-test") + implementation project(":scenarios") - testCompile group: 'junit', name: 'junit', version: '4.13.2' + testImplementation group: 'junit', name: 'junit', version: '4.13.2' } diff --git a/features/fixtures/mazerunner/src/main/resources/logback.xml b/features/fixtures/mazerunner/src/main/resources/logback.xml new file mode 100644 index 00000000..60ade3a1 --- /dev/null +++ b/features/fixtures/mazerunner/src/main/resources/logback.xml @@ -0,0 +1,18 @@ + + + + + + a35a2a72bd230ac0aa0f52715bbdc6aa + production + 1.0.0 + + testAppType + + http://localhost:9339/notify + + + + + + diff --git a/features/fixtures/mazerunnerplainspring/build.gradle b/features/fixtures/mazerunnerplainspring/build.gradle index 1fcc2fe5..ddce65c6 100644 --- a/features/fixtures/mazerunnerplainspring/build.gradle +++ b/features/fixtures/mazerunnerplainspring/build.gradle @@ -14,15 +14,15 @@ repositories { } dependencies { - compile("org.springframework:spring-webmvc:4.2.0.RELEASE") - compile("javax.servlet:javax.servlet-api:3.1.0") - compile("ch.qos.logback:logback-classic:1.2.3") + implementation("org.springframework:spring-webmvc:4.2.0.RELEASE") + implementation("javax.servlet:javax.servlet-api:3.1.0") + implementation("ch.qos.logback:logback-classic:1.2.3") - compile("com.fasterxml.jackson.core:jackson-annotations:2.9.1") - compile("com.fasterxml.jackson.core:jackson-databind:2.9.1") - compile("com.bugsnag:bugsnag:9.9.9-test") - compile("com.bugsnag:bugsnag-spring:9.9.9-test") - compile project(":scenarios") + implementation("com.fasterxml.jackson.core:jackson-annotations:2.9.1") + implementation("com.fasterxml.jackson.core:jackson-databind:2.9.1") + implementation("com.bugsnag:bugsnag:9.9.9-test") + implementation("com.bugsnag:bugsnag-spring:9.9.9-test") + implementation project(":scenarios") } war { diff --git a/features/fixtures/mazerunnerplainspring6/build.gradle b/features/fixtures/mazerunnerplainspring6/build.gradle new file mode 100644 index 00000000..556be222 --- /dev/null +++ b/features/fixtures/mazerunnerplainspring6/build.gradle @@ -0,0 +1,28 @@ +plugins { + id "war" +} + +group 'com.bugsnag.mazerunnerplainspring' + +repositories { + mavenCentral() + flatDir { + dirs '../libs' + } +} + +dependencies { + implementation("org.springframework:spring-webmvc:6.0.0") + implementation("jakarta.servlet:jakarta.servlet-api:5.0.0") + implementation("ch.qos.logback:logback-classic:1.2.3") + + implementation("com.fasterxml.jackson.core:jackson-annotations:2.9.1") + implementation("com.fasterxml.jackson.core:jackson-databind:2.9.1") + implementation("com.bugsnag:bugsnag:9.9.9-test") + implementation("com.bugsnag:bugsnag-spring:9.9.9-test") + implementation project(":scenarios") +} + +war { + archiveName = 'mazerunnerplainspring.war' +} \ No newline at end of file diff --git a/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/AppConfig.java b/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/AppConfig.java new file mode 100644 index 00000000..d6389828 --- /dev/null +++ b/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/AppConfig.java @@ -0,0 +1,22 @@ +package com.bugsnag.mazerunnerplainspring; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +@Configuration +@EnableWebMvc +@EnableScheduling +@EnableAsync +@ComponentScan(basePackages = "com.bugsnag.mazerunnerplainspring") +public class AppConfig { + @Bean + public static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() { + return new PropertySourcesPlaceholderConfigurer(); + } +} \ No newline at end of file diff --git a/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/AppInitializer.java b/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/AppInitializer.java new file mode 100644 index 00000000..2fd1cf0f --- /dev/null +++ b/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/AppInitializer.java @@ -0,0 +1,22 @@ +package com.bugsnag.mazerunnerplainspring; + +import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; + +public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { + + @Override + protected Class[] getRootConfigClasses() { + return new Class[] { AppConfig.class }; + } + + @Override + protected Class[] getServletConfigClasses() { + return null; + } + + @Override + protected String[] getServletMappings() { + return new String[] { "/" }; + } + +} \ No newline at end of file diff --git a/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/AsyncMethodService.java b/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/AsyncMethodService.java new file mode 100644 index 00000000..f1d52269 --- /dev/null +++ b/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/AsyncMethodService.java @@ -0,0 +1,29 @@ +package com.bugsnag.mazerunnerplainspring; + +import com.bugsnag.Bugsnag; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.concurrent.Future; + +@Service +public class AsyncMethodService { + + @Async + public void doSomethingAsync() { + + // Add some thread meta data + Bugsnag.addThreadMetaData("thread", "key1", "should be cleared from meta data"); + Bugsnag.clearThreadMetaData(); + Bugsnag.addThreadMetaData("thread", "key2", "should be included in meta data"); + + try { + Thread.sleep(100); + } catch (InterruptedException e) { + // ignore + } + + throw new RuntimeException("Unhandled exception from Async method"); + } +} diff --git a/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/BugsnagAsyncConfig.java b/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/BugsnagAsyncConfig.java new file mode 100644 index 00000000..d1b0e59e --- /dev/null +++ b/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/BugsnagAsyncConfig.java @@ -0,0 +1,19 @@ +package com.bugsnag.mazerunnerplainspring; + +import com.bugsnag.Bugsnag; +import com.bugsnag.BugsnagAsyncExceptionHandler; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.AsyncConfigurerSupport; + +@Configuration +public class BugsnagAsyncConfig extends AsyncConfigurerSupport { + @Autowired + private Bugsnag bugsnag; + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return new BugsnagAsyncExceptionHandler(bugsnag); + } +} diff --git a/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/BugsnagConfig.java b/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/BugsnagConfig.java new file mode 100644 index 00000000..cf5d52ca --- /dev/null +++ b/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/BugsnagConfig.java @@ -0,0 +1,34 @@ +package com.bugsnag.mazerunnerplainspring; + +import com.bugsnag.Bugsnag; +import com.bugsnag.BugsnagSpringConfiguration; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.util.StringUtils; + +@Configuration +@Import(BugsnagSpringConfiguration.class) +public class BugsnagConfig { + + @Value("${BUGSNAG_API_KEY}") + private String bugsnagApiKey; + + @Value("${MAZERUNNER_BASE_URL}") + private String bugsnagEndpoint; + + @Value("${AUTO_CAPTURE_SESSIONS:false}") + private boolean autoCaptureSessions; + + @Bean + public Bugsnag bugsnag() { + Bugsnag bugsnag = new Bugsnag(bugsnagApiKey); + bugsnag.setEndpoints(bugsnagEndpoint + "notify", bugsnagEndpoint + "sessions"); + bugsnag.setAutoCaptureSessions(autoCaptureSessions); + bugsnag.setReleaseStage("production"); + bugsnag.setAppVersion("1.0.0"); + return bugsnag; + } +} diff --git a/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/ScheduledTaskService.java b/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/ScheduledTaskService.java new file mode 100644 index 00000000..3574ff08 --- /dev/null +++ b/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/ScheduledTaskService.java @@ -0,0 +1,29 @@ +package com.bugsnag.mazerunnerplainspring; + +import com.bugsnag.Bugsnag; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +@Service +public class ScheduledTaskService { + + @Value("${RUN_SCHEDULED_TASK:false}") + private boolean throwException; + + private volatile boolean exceptionSent = false; + + @Scheduled(fixedDelay = 3000) + public void doSomething() { + if (throwException && !exceptionSent) { + + // Add some thread meta data + Bugsnag.addThreadMetaData("thread", "key1", "should be cleared from meta data"); + Bugsnag.clearThreadMetaData(); + Bugsnag.addThreadMetaData("thread", "key2", "should be included in meta data"); + + exceptionSent = true; + throw new RuntimeException("Unhandled exception from ScheduledTaskService"); + } + } +} diff --git a/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/TestRestController.java b/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/TestRestController.java new file mode 100644 index 00000000..59496a2f --- /dev/null +++ b/features/fixtures/mazerunnerplainspring6/src/main/java/com.bugsnag.mazerunnerplainspring/TestRestController.java @@ -0,0 +1,99 @@ +package com.bugsnag.mazerunnerplainspring; + +import com.bugsnag.Bugsnag; +import com.bugsnag.mazerunner.scenarios.Scenario; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; + +@RestController +public class TestRestController { + + private static final Logger LOGGER = LoggerFactory.getLogger(TestRestController.class); + + @Autowired + private Bugsnag bugsnag; + + @Autowired + private AsyncMethodService asyncMethodService; + + @RequestMapping("/") + public String ping() { + return "Plain Spring Fixture app ready for connections"; + } + + @RequestMapping("/send-unhandled-exception") + public void sendUnhandledException() { + throw new RuntimeException("Unhandled exception from TestRestController"); + } + + @RequestMapping("/add-session") + public void addSession() { + // A session should be automatically recorded by Bugsnag if automatic sessions are enabled + LOGGER.info("Starting a new session"); + + // Flush sessions now, otherwise need to wait for sessions to be automatically flushed + flushAllSessions(); + LOGGER.info("Flushed all sessions"); + } + + @RequestMapping("/run-async-task") + public void runAsyncTask() { + try { + asyncMethodService.doSomethingAsync(); + } catch (Exception ex) { + // This should not happen + LOGGER.info("Saw exception from async call"); + } + } + + @RequestMapping("/run-scenario/{scenario}") + public void runScenario(@PathVariable String scenario) { + try { + Class clz = Class.forName("com.bugsnag.mazerunner.scenarios." + scenario); + Constructor constructor = clz.getConstructors()[0]; + ((Scenario) constructor.newInstance(bugsnag)).run(); + } catch (Exception ex) { + LOGGER.error("Error getting scenario", ex); + } + } + + /** + * Flushes sessions from the Bugsnag object + */ + private void flushAllSessions() { + try { + Field field = bugsnag.getClass().getDeclaredField("sessionTracker"); + field.setAccessible(true); + Object sessionTracker = field.get(bugsnag); + + field = sessionTracker.getClass().getDeclaredField("enqueuedSessionCounts"); + field.setAccessible(true); + Collection sessionCounts = (Collection) field.get(sessionTracker); + + // Flush the sessions + Method method = sessionTracker.getClass().getDeclaredMethod("flushSessions", Date.class); + method.setAccessible(true); + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.MINUTE, 2); + method.invoke(sessionTracker, calendar.getTime()); + + // Wait until sessions are flushed + while (sessionCounts.size() > 0) { + Thread.sleep(1000); + } + } catch (Exception ex) { + LOGGER.error("failed to flush sessions", ex); + } + } +} diff --git a/features/fixtures/mazerunnerspringboot/build.gradle b/features/fixtures/mazerunnerspringboot/build.gradle index 43bbd822..2a348654 100644 --- a/features/fixtures/mazerunnerspringboot/build.gradle +++ b/features/fixtures/mazerunnerspringboot/build.gradle @@ -27,18 +27,18 @@ repositories { } dependencies { - compile 'org.springframework.boot:spring-boot-starter' - compile 'org.springframework.boot:spring-boot-starter-web' - compile 'ch.qos.logback:logback-classic:1.2.3' + implementation 'org.springframework.boot:spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'ch.qos.logback:logback-classic:1.2.3' - compile 'com.fasterxml.jackson.core:jackson-annotations:2.9.1' - compile 'com.fasterxml.jackson.core:jackson-databind:2.9.1' - compile 'com.bugsnag:bugsnag:9.9.9-test' - compile 'com.bugsnag:bugsnag-spring:9.9.9-test' - compile project(":scenarios") + implementation 'com.fasterxml.jackson.core:jackson-annotations:2.9.1' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.1' + implementation 'com.bugsnag:bugsnag:9.9.9-test' + implementation 'com.bugsnag:bugsnag-spring:9.9.9-test' + implementation project(":scenarios") // required for JDK 9 and above - compile('javax.xml.bind:jaxb-api:2.3.0') + implementation('javax.xml.bind:jaxb-api:2.3.0') - testCompile group: 'junit', name: 'junit', version: '4.13.2' + testImplementation group: 'junit', name: 'junit', version: '4.13.2' } \ No newline at end of file diff --git a/features/fixtures/mazerunnerspringboot3/build.gradle b/features/fixtures/mazerunnerspringboot3/build.gradle new file mode 100644 index 00000000..a5bf660c --- /dev/null +++ b/features/fixtures/mazerunnerspringboot3/build.gradle @@ -0,0 +1,39 @@ +buildscript { + ext { + springBootVersion = '3.0.0' + } + repositories { + mavenCentral() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") + } +} + +apply plugin: 'java' +apply plugin: 'org.springframework.boot' +apply plugin: 'io.spring.dependency-management' + +group 'com.bugsnag.mazerunnerspringboot3' +version '1.0-SNAPSHOT' + +repositories { + mavenCentral() + flatDir { + dirs '../libs' + } +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-web' + + implementation 'com.bugsnag:bugsnag:9.9.9-test' + implementation 'com.bugsnag:bugsnag-spring:9.9.9-test' + implementation project(":scenarios") + + // required for JDK 9 and above + implementation('javax.xml.bind:jaxb-api:2.3.0') + + testImplementation group: 'junit', name: 'junit', version: '4.13.2' +} \ No newline at end of file diff --git a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/AsyncMethodScenario.java b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/AsyncMethodScenario.java new file mode 100644 index 00000000..a7425c16 --- /dev/null +++ b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/AsyncMethodScenario.java @@ -0,0 +1,34 @@ +package com.bugsnag.mazerunner.scenarios; + +import com.bugsnag.Bugsnag; +import org.springframework.web.client.RestTemplate; + +/** + * Causes an unhandled exception in an async method + */ +public class AsyncMethodScenario extends Scenario { + + public AsyncMethodScenario(Bugsnag bugsnag) { + super(bugsnag); + } + + @Override + public void run() { + + // Don't report any sessions during this test + disableSessionDelivery(); + + // The rest endpoint will run an async task to throw the exception + final String uri = "http://localhost:1234/run-async-task"; + + try { + RestTemplate restTemplate = new RestTemplate(); + restTemplate.getForObject(uri, String.class); + + // Wait for the async task to complete + Thread.sleep(2000); + } catch (Exception ex) { + // ignore + } + } +} diff --git a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/AsyncNotifyScenario.java b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/AsyncNotifyScenario.java new file mode 100644 index 00000000..2e1d3c1f --- /dev/null +++ b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/AsyncNotifyScenario.java @@ -0,0 +1,34 @@ +package com.bugsnag.mazerunner.scenarios; + +import com.bugsnag.Bugsnag; +import org.springframework.web.client.RestTemplate; + +/** + * Notifies from an async method + */ +public class AsyncNotifyScenario extends Scenario { + + public AsyncNotifyScenario(Bugsnag bugsnag) { + super(bugsnag); + } + + @Override + public void run() { + + // Don't report any sessions during this test + disableSessionDelivery(); + + // The rest endpoint will run an async task to throw the exception + final String uri = "http://localhost:1234/notify-async-task"; + + try { + RestTemplate restTemplate = new RestTemplate(); + restTemplate.getForObject(uri, String.class); + + // Wait for the async task to complete + Thread.sleep(2000); + } catch (Exception ex) { + // ignore + } + } +} diff --git a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/AutoSessionScenario.java b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/AutoSessionScenario.java new file mode 100644 index 00000000..8058edd9 --- /dev/null +++ b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/AutoSessionScenario.java @@ -0,0 +1,38 @@ +package com.bugsnag.mazerunner.scenarios; + +import com.bugsnag.Bugsnag; +import org.springframework.web.client.RestTemplate; + +/** + * Causes an unhandled exception in the rest controller + */ +public class AutoSessionScenario extends Scenario { + + public AutoSessionScenario(Bugsnag bugsnag) { + super(bugsnag); + } + + @Override + public void run() { + bugsnag.setAutoCaptureSessions(true); + + final String uri = "http://localhost:1234/add-session"; + + try { + RestTemplate restTemplate = new RestTemplate(); + String result = restTemplate.getForObject(uri, String.class); + LOGGER.info("Completed auto session request: " + result); + Thread.sleep(2000); + } catch (Exception ex) { + LOGGER.error("Failed to complete request", ex); + } + + flushAllSessions(); + + try { + Thread.sleep(2000); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + } +} diff --git a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/RestControllerScenario.java b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/RestControllerScenario.java new file mode 100644 index 00000000..c9ce5da6 --- /dev/null +++ b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/RestControllerScenario.java @@ -0,0 +1,29 @@ +package com.bugsnag.mazerunner.scenarios; + +import com.bugsnag.Bugsnag; +import org.springframework.web.client.RestTemplate; + +/** + * Causes an unhandled exception in the rest controller + */ +public class RestControllerScenario extends Scenario { + + public RestControllerScenario(Bugsnag bugsnag) { + super(bugsnag); + } + + @Override + public void run() { + // Don't report any sessions during this test + disableSessionDelivery(); + + final String uri = "http://localhost:1234/send-unhandled-exception"; + + try { + RestTemplate restTemplate = new RestTemplate(); + String result = restTemplate.getForObject(uri, String.class); + } catch (Exception ex) { + // ignore + } + } +} diff --git a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/ScheduledTaskExecutorScenario.java b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/ScheduledTaskExecutorScenario.java new file mode 100644 index 00000000..5dc877b2 --- /dev/null +++ b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/ScheduledTaskExecutorScenario.java @@ -0,0 +1,36 @@ +package com.bugsnag.mazerunner.scenarios; + +import com.bugsnag.Bugsnag; +import com.bugsnag.Report; +import com.bugsnag.callbacks.Callback; +import com.bugsnag.mazerunnerspringboot.ScheduledTaskExecutorService; +import java.util.Collection; + +public class ScheduledTaskExecutorScenario extends Scenario { + + public ScheduledTaskExecutorScenario(Bugsnag bugsnag) { + super(bugsnag); + } + + @Override + public void run() { + // Enable throwing an exception in the scheduled task + ScheduledTaskExecutorService.setSendException(); + + // Wait for the exception + try { + Thread.sleep(5000); + } catch (InterruptedException ex) { + // ignore + } + + final Collection threadnames = ScheduledTaskExecutorService.getThreadNames(); + bugsnag.notify(new RuntimeException("Whoops"), new Callback() { + @Override + public void beforeNotify(Report report) { + report.addToTab("executor", "multiThreaded", threadnames.size() > 1); + report.addToTab("executor", "names", threadnames); + } + }); + } +} diff --git a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/ScheduledTaskScenario.java b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/ScheduledTaskScenario.java new file mode 100644 index 00000000..acf29628 --- /dev/null +++ b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunner/scenarios/ScheduledTaskScenario.java @@ -0,0 +1,27 @@ +package com.bugsnag.mazerunner.scenarios; + +import com.bugsnag.Bugsnag; +import com.bugsnag.mazerunnerspringboot.ScheduledTaskService; + +/** + * Causes an unhandled exception in a scheduled task + */ +public class ScheduledTaskScenario extends Scenario { + + public ScheduledTaskScenario(Bugsnag bugsnag) { + super(bugsnag); + } + + @Override + public void run() { + // Enable throwing an exception in the scheduled task + ScheduledTaskService.setThrowException(); + + // Wait for the exception + try { + Thread.sleep(5000); + } catch (InterruptedException ex) { + // ignore + } + } +} diff --git a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/Application.java b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/Application.java new file mode 100644 index 00000000..aa57b959 --- /dev/null +++ b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/Application.java @@ -0,0 +1,18 @@ +package com.bugsnag.mazerunnerspringboot; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * Kicks off the Spring Boot application. + */ +@SpringBootApplication +@EnableScheduling +@EnableAsync +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/AsyncMethodService.java b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/AsyncMethodService.java new file mode 100644 index 00000000..5e0593a0 --- /dev/null +++ b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/AsyncMethodService.java @@ -0,0 +1,39 @@ +package com.bugsnag.mazerunnerspringboot; + +import com.bugsnag.Bugsnag; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +@Service +public class AsyncMethodService { + + @Autowired + Bugsnag bugsnag; + + @Async + public void doSomethingAsync() { + + // Add some thread meta data + Bugsnag.addThreadMetaData("thread", "key1", "should be cleared from meta data"); + Bugsnag.clearThreadMetaData(); + Bugsnag.addThreadMetaData("thread", "key2", "should be included in meta data"); + + try { + Thread.sleep(100); + } catch (InterruptedException ex) { + // ignore + } + + throw new RuntimeException("Unhandled exception from Async method"); + } + + @Async + public void notifyAsync() { + // Add some thread meta data + Bugsnag.addThreadMetaData("thread", "inAsyncMethod", "meta data from async method"); + + bugsnag.notify(new RuntimeException("test from async")); + } +} diff --git a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/BugsnagAsyncConfig.java b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/BugsnagAsyncConfig.java new file mode 100644 index 00000000..3c05e2a6 --- /dev/null +++ b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/BugsnagAsyncConfig.java @@ -0,0 +1,21 @@ +package com.bugsnag.mazerunnerspringboot; + +import com.bugsnag.Bugsnag; +import com.bugsnag.BugsnagAsyncExceptionHandler; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.AsyncConfigurerSupport; +import org.springframework.scheduling.annotation.EnableScheduling; + +@Configuration +@EnableScheduling +public class BugsnagAsyncConfig extends AsyncConfigurerSupport { + @Autowired + private Bugsnag bugsnag; + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return new BugsnagAsyncExceptionHandler(bugsnag); + } +} diff --git a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/Config.java b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/Config.java new file mode 100644 index 00000000..35b7d45d --- /dev/null +++ b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/Config.java @@ -0,0 +1,28 @@ +package com.bugsnag.mazerunnerspringboot; + +import com.bugsnag.Bugsnag; +import com.bugsnag.BugsnagSpringConfiguration; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import(BugsnagSpringConfiguration.class) +public class Config { + + @Value("${BUGSNAG_API_KEY}") + private String bugsnagApiKey; + + @Value("${MAZERUNNER_BASE_URL}") + private String bugsnagEndpoint; + + @Bean + public Bugsnag bugsnag() { + Bugsnag bugsnag = new Bugsnag(bugsnagApiKey); + bugsnag.setEndpoints(bugsnagEndpoint + "notify", bugsnagEndpoint + "sessions"); + bugsnag.setReleaseStage("production"); + bugsnag.setAppVersion("1.0.0"); + return bugsnag; + } +} diff --git a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskConfig.java b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskConfig.java new file mode 100644 index 00000000..532bbc77 --- /dev/null +++ b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskConfig.java @@ -0,0 +1,44 @@ +package com.bugsnag.mazerunnerspringboot; + +import com.bugsnag.Bugsnag; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.scheduling.annotation.EnableScheduling; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +@Configuration +@EnableScheduling +public class ScheduledTaskConfig { + + @ConditionalOnProperty(name = "scheduled_executor_service_bean", havingValue = "true") + @Bean + public Executor taskScheduler() { + return Executors.newScheduledThreadPool(4); + } + + @ConditionalOnProperty(name = "other_scheduled_executor_service_bean", havingValue = "true") + @Bean + public Executor otherTaskScheduler() { + return Executors.newScheduledThreadPool(2); + } + + @ConditionalOnProperty(name = "custom_task_scheduler_bean", havingValue = "true") + @Bean + public TaskScheduler customTaskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setPoolSize(4); + return scheduler; + } + + @ConditionalOnProperty(name = "second_task_scheduler_bean", havingValue = "true") + @Bean(name = "taskScheduler") + public TaskScheduler secondTaskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setPoolSize(2); + return scheduler; + } +} diff --git a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskExecutorService.java b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskExecutorService.java new file mode 100644 index 00000000..99c3a988 --- /dev/null +++ b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskExecutorService.java @@ -0,0 +1,41 @@ +package com.bugsnag.mazerunnerspringboot; + +import com.bugsnag.Bugsnag; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.HashSet; +import java.util.Set; +import java.util.Collection; + +@Service +public class ScheduledTaskExecutorService { + + @Autowired + private Bugsnag bugsnag; + + private final Set threadNames = new HashSet(); + + private volatile boolean sendException = false; + + private static ScheduledTaskExecutorService instance; + + public ScheduledTaskExecutorService() { + instance = this; + } + + public static void setSendException() { + instance.sendException = true; + } + + public static Collection getThreadNames() { + return new HashSet<>(instance.threadNames); + } + + @Scheduled(fixedRate = 100) + public void doSomething() { + if (sendException) { + threadNames.add(Thread.currentThread().getName()); + } + } +} diff --git a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskService.java b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskService.java new file mode 100644 index 00000000..93cb9d6c --- /dev/null +++ b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/ScheduledTaskService.java @@ -0,0 +1,33 @@ +package com.bugsnag.mazerunnerspringboot; + +import com.bugsnag.Bugsnag; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +@Service +public class ScheduledTaskService { + + private volatile boolean throwException = false; + + private static ScheduledTaskService instance; + + public ScheduledTaskService() { + instance = this; + } + + public static void setThrowException() { + instance.throwException = true; + } + + @Scheduled(fixedDelay = 3000) + public void doSomething() { + if (throwException) { + // Add some thread meta data + Bugsnag.addThreadMetaData("thread", "key1", "should be cleared from meta data"); + Bugsnag.clearThreadMetaData(); + Bugsnag.addThreadMetaData("thread", "key2", "should be included in meta data"); + + throw new RuntimeException("Unhandled exception from ScheduledTaskService"); + } + } +} diff --git a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/TestCaseRunner.java b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/TestCaseRunner.java new file mode 100644 index 00000000..82839269 --- /dev/null +++ b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/TestCaseRunner.java @@ -0,0 +1,66 @@ +package com.bugsnag.mazerunnerspringboot; + +import com.bugsnag.Bugsnag; +import com.bugsnag.mazerunner.scenarios.Scenario; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.ExitCodeGenerator; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Constructor; + +@Component +public class TestCaseRunner implements CommandLineRunner, ApplicationContextAware { + + private static final Logger LOGGER = LoggerFactory.getLogger(TestCaseRunner.class); + + private ApplicationContext ctx; + + @Autowired + private Bugsnag bugsnag; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + ctx = applicationContext; + } + + @Override + public void run(String... args) { + // Create and run the test case + LOGGER.info("Creating test case"); + String type = System.getenv("EVENT_TYPE"); + Scenario scenario = testCaseForName(type); + if (scenario != null) { + LOGGER.info("running test case " + type); + scenario.run(); + } else { + LOGGER.error("No test case found for " + type); + } + + // Exit the application + LOGGER.info("Exiting spring"); + System.exit(SpringApplication.exit(ctx, (ExitCodeGenerator) new ExitCodeGenerator() { + @Override + public int getExitCode() { + return 0; + } + })); + } + + private Scenario testCaseForName(String eventType) { + try { + Class clz = Class.forName("com.bugsnag.mazerunner.scenarios." + eventType); + Constructor constructor = clz.getConstructors()[0]; + return (Scenario) constructor.newInstance(bugsnag); + } catch (Exception ex) { + LOGGER.error("Error getting scenario", ex); + return null; + } + } +} diff --git a/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/TestRestController.java b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/TestRestController.java new file mode 100644 index 00000000..e35c0524 --- /dev/null +++ b/features/fixtures/mazerunnerspringboot3/src/main/java/com/bugsnag/mazerunnerspringboot/TestRestController.java @@ -0,0 +1,71 @@ +package com.bugsnag.mazerunnerspringboot; + +import com.bugsnag.Bugsnag; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TestRestController { + + private static final Logger LOGGER = LoggerFactory.getLogger(TestRestController.class); + + @Autowired + Bugsnag bugsnag; + + @Autowired + private AsyncMethodService asyncMethodService; + + private static TestRestController instance; + + public TestRestController() { + instance = this; + } + + public static Bugsnag getBugsnag() { + return instance.bugsnag; + } + + @RequestMapping("/send-unhandled-exception") + public String sendUnhandledException() { + throw new RuntimeException("Unhandled exception from TestRestController"); + } + + @RequestMapping("/add-session") + public String addSession() { + // A session should be automatically recorded by Bugsnag if automatic sessions are enabled + LOGGER.info("Starting a new session"); + return ""; + } + + @RequestMapping("/run-async-task") + public String runAsyncTask() { + try { + asyncMethodService.doSomethingAsync(); + } catch (Exception ex) { + // This should not happen + LOGGER.info("Saw exception from async call"); + } + + return ""; + } + + @RequestMapping("/notify-async-task") + public String notifyAsyncTask() { + + // Add some thread meta data + Bugsnag.addThreadMetaData("thread", "controllerMethod", "meta data from controller method"); + + // Notify before calling the async method + bugsnag.notify(new RuntimeException("test from before async")); + + // Call the async method (also notifies) + asyncMethodService.notifyAsync(); + + return ""; + } + +} diff --git a/features/fixtures/mazerunnerspringboot3/src/main/resources/application.properties b/features/fixtures/mazerunnerspringboot3/src/main/resources/application.properties new file mode 100644 index 00000000..7e96c7f7 --- /dev/null +++ b/features/fixtures/mazerunnerspringboot3/src/main/resources/application.properties @@ -0,0 +1 @@ +server.port=1234 diff --git a/features/fixtures/scenarios/build.gradle b/features/fixtures/scenarios/build.gradle index 35fbc0cc..aee70512 100644 --- a/features/fixtures/scenarios/build.gradle +++ b/features/fixtures/scenarios/build.gradle @@ -12,7 +12,7 @@ repositories { } dependencies { - compile("ch.qos.logback:logback-classic:1.2.3") - compile("com.bugsnag:bugsnag:9.9.9-test") + implementation("ch.qos.logback:logback-classic:1.2.3") + implementation("com.bugsnag:bugsnag:9.9.9-test") } diff --git a/features/fixtures/settings.gradle b/features/fixtures/settings.gradle index 664f523f..1771d139 100644 --- a/features/fixtures/settings.gradle +++ b/features/fixtures/settings.gradle @@ -2,3 +2,8 @@ include ':mazerunner', ':mazerunnerplainspring', ':mazerunnerspringboot', ':scenarios' + +if (JavaVersion.current() >= JavaVersion.VERSION_17) { + include ':mazerunnerspringboot3', + ':mazerunnerplainspring6' +} \ No newline at end of file diff --git a/features/scripts/assemble-fixtures.sh b/features/scripts/assemble-fixtures.sh index 9dafd461..47a54759 100755 --- a/features/scripts/assemble-fixtures.sh +++ b/features/scripts/assemble-fixtures.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash -mkdir -p features/fixtures/libs -./gradlew bugsnag:assemble bugsnag-spring:assemble -Pversion=9.9.9-test -cp bugsnag/build/libs/bugsnag-9.9.9-test.jar features/fixtures/libs/bugsnag-9.9.9-test.jar -cp bugsnag-spring/build/libs/bugsnag-spring-9.9.9-test.jar features/fixtures/libs/bugsnag-spring-9.9.9-test.jar +if [ ! -d "features/fixtures/libs" ]; then + mkdir -p features/fixtures/libs + ./gradlew bugsnag:assemble bugsnag-spring:javax:assemble bugsnag-spring:assemble -Pversion=9.9.9-test + cp bugsnag/build/libs/bugsnag-9.9.9-test.jar features/fixtures/libs/bugsnag-9.9.9-test.jar + cp bugsnag-spring/build/libs/bugsnag-spring-9.9.9-test.jar features/fixtures/libs/bugsnag-spring-9.9.9-test.jar +fi \ No newline at end of file diff --git a/features/scripts/build-plain-spring-app.sh b/features/scripts/build-plain-spring-app.sh index d2a8d4c3..12c65cd6 100755 --- a/features/scripts/build-plain-spring-app.sh +++ b/features/scripts/build-plain-spring-app.sh @@ -2,5 +2,10 @@ # Start Tomcat then copy a WAR file across to serve it catalina.sh start -./gradlew -p features/fixtures/mazerunnerplainspring war -cp features/fixtures/mazerunnerplainspring/build/libs/mazerunnerplainspring.war $CATALINA_HOME/webapps/ROOT.war +if [[ "${JAVA_VERSION}" == "8"* ]]; then + ./gradlew -p features/fixtures/mazerunnerplainspring war + cp features/fixtures/mazerunnerplainspring/build/libs/mazerunnerplainspring.war $CATALINA_HOME/webapps/ROOT.war +else + ./gradlew -p features/fixtures/mazerunnerplainspring6 war + cp features/fixtures/mazerunnerplainspring6/build/libs/mazerunnerplainspring.war $CATALINA_HOME/webapps/ROOT.war +fi \ No newline at end of file diff --git a/features/scripts/run-java-spring-boot-app.sh b/features/scripts/run-java-spring-boot-app.sh index f806fb9b..3014bdff 100755 --- a/features/scripts/run-java-spring-boot-app.sh +++ b/features/scripts/run-java-spring-boot-app.sh @@ -1,4 +1,8 @@ #!/usr/bin/env bash catalina.sh stop -./gradlew -p features/fixtures/mazerunnerspringboot bootRun \ No newline at end of file +if [[ "${JAVA_VERSION}" == "8"* ]]; then + ./gradlew -p features/fixtures/mazerunnerspringboot bootRun +else + ./gradlew -p features/fixtures/mazerunnerspringboot3 bootRun +fi \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1ddc1cb0..05376a7d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip diff --git a/release.gradle b/release.gradle index 7a238ad5..3bd92f3e 100644 --- a/release.gradle +++ b/release.gradle @@ -1,4 +1,3 @@ -apply plugin: 'com.bmuschko.nexus' apply plugin: 'net.researchgate.release' apply plugin: 'maven-publish' @@ -13,41 +12,6 @@ nexusStaging { packageGroup = "com.bugsnag" } -modifyPom { - project { - name = project.findProperty('projectName') - description = project.findProperty('projectDescription') - url 'https://github.com/bugsnag/bugsnag-java' - - scm { - url 'https://github.com/bugsnag/bugsnag-java' - connection 'scm:git:git://github.com/bugsnag/bugsnag-java.git' - developerConnection 'scm:git:ssh://git@github.com/bugsnag/bugsnag-java.git' - } - - licenses { - license { - name 'MIT' - url 'http://opensource.org/licenses/MIT' - distribution 'repo' - } - } - - organization { - name 'Bugsnag' - url 'https://bugsnag.com' - } - - developers { - developer { - id 'loopj' - name 'James Smith' - email 'james@bugsnag.com' - } - } - } -} - task sourceJar(type: Jar) { from sourceSets.main.allJava } @@ -62,6 +26,38 @@ publishing { artifact sourceJar { classifier "sources" } + pom { + name = project.findProperty('projectName') + description = project.findProperty('projectDescription') + url = 'https://github.com/bugsnag/bugsnag-java' + + scm { + url = 'https://github.com/bugsnag/bugsnag-java' + connection = 'scm:git:git://github.com/bugsnag/bugsnag-java.git' + developerConnection = 'scm:git:ssh://git@github.com/bugsnag/bugsnag-java.git' + } + + licenses { + license { + name = 'MIT' + url = 'http://opensource.org/licenses/MIT' + distribution = 'repo' + } + } + + organization { + name = 'Bugsnag' + url = 'https://bugsnag.com' + } + + developers { + developer { + id = 'loopj' + name = 'James Smith' + email = 'james@bugsnag.com' + } + } + } } } } diff --git a/settings.gradle b/settings.gradle index 03c37e0c..332a86da 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,12 +1,14 @@ include ':bugsnag', - ':bugsnag-spring' + ':bugsnag-spring', + ':bugsnag-spring:javax' -// Exclude examples from JDK7 build as they are written for JDK8 -if (JavaVersion.current().isJava8Compatible()) { - include ':examples:simple', - ':examples:servlet', - ':examples:spring', - ':examples:spring-web', - ':examples:logback' -} +include ':examples:simple', + ':examples:servlet-javax', + ':examples:spring', + ':examples:spring-web', + ':examples:logback' +// jakarta servlet example requires java 11 compatibility for gretty plugin +if (JavaVersion.current().isJava11Compatible()) { + include ':examples:servlet-jakarta' +} \ No newline at end of file