Skip to content
This repository has been archived by the owner on Aug 30, 2023. It is now read-only.

Add Spring Integration. #539

Merged
merged 21 commits into from
Sep 8, 2020
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions buildSrc/src/main/java/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ object Config {
val log4j2Core = "org.apache.logging.log4j:log4j-core:$log4j2Version"

val springBootStarter = "org.springframework.boot:spring-boot-starter:$springBootVersion"
val springBootStarterTest = "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
val springBootStarterWeb = "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
val springBootStarterSecurity = "org.springframework.boot:spring-boot-starter-security:$springBootVersion"

val springWeb = "org.springframework:spring-webmvc"
val servletApi = "javax.servlet:javax.servlet-api"
Expand All @@ -68,17 +71,13 @@ object Config {
val robolectric = "org.robolectric:robolectric:4.3.1"
val mockitoKotlin = "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
val awaitility = "org.awaitility:awaitility-kotlin:4.0.3"
val springBootStarterTest = "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
val springBootStarterWeb = "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
val springBootStarterSecurity = "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
}

object QualityPlugins {
object Jacoco {
val version = "0.8.5"
val minimumCoverage = BigDecimal.valueOf(0.6)
}
val jacocoVersion = "0.8.5"
val spotless = "com.diffplug.spotless"
val spotlessVersion = "5.1.0"
val errorProne = "net.ltgt.errorprone"
Expand All @@ -95,6 +94,7 @@ object Config {
val SENTRY_ANDROID_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.android"
val SENTRY_LOGBACK_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.logback"
val SENTRY_LOG4J2_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.log4j2"
val SENTRY_SPRING_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.spring"
val SENTRY_SPRING_BOOT_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.spring-boot"
val group = "io.sentry"
val description = "SDK for sentry.io"
Expand Down
16 changes: 16 additions & 0 deletions sentry-core/src/main/java/io/sentry/core/Breadcrumb.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,22 @@ public final class Breadcrumb implements Cloneable, IUnknownPropertiesConsumer {
this.timestamp = timestamp;
}

/**
* Creates HTTP breadcrumb.
*
* @param url - the request URL
* @param method - the request method
* @return the breadcrumb
*/
public static @NotNull Breadcrumb http(final @NotNull String url, final @NotNull String method) {
maciejwalkowiak marked this conversation as resolved.
Show resolved Hide resolved
final Breadcrumb breadcrumb = new Breadcrumb();
breadcrumb.setType("http");
breadcrumb.setCategory("http");
breadcrumb.setData("url", url);
breadcrumb.setData("method", method.toUpperCase(Locale.getDefault()));
return breadcrumb;
}

/** Breadcrumb ctor */
public Breadcrumb() {
this(DateUtils.getCurrentDateTimeOrNull());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package io.sentry.core;

import io.sentry.core.exception.ExceptionMechanismException;
import io.sentry.core.util.Objects;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/** Deduplicates events containing throwable that has been already processed. */
public final class DuplicateEventDetectionEventProcessor implements EventProcessor {
private final WeakHashMap<Throwable, Object> capturedObjects = new WeakHashMap<>();
private final SentryOptions options;

public DuplicateEventDetectionEventProcessor(final @NotNull SentryOptions options) {
this.options = Objects.requireNonNull(options, "options are required");
}

@Override
public SentryEvent process(final @NotNull SentryEvent event, final @Nullable Object hint) {
final Throwable throwable = event.getThrowable();
if (throwable != null) {
if (throwable instanceof ExceptionMechanismException) {
final ExceptionMechanismException ex = (ExceptionMechanismException) throwable;
if (capturedObjects.containsKey(ex.getThrowable())) {
options
.getLogger()
.log(
SentryLevel.DEBUG,
"Duplicate Exception detected. Event %s will be discarded.",
event.getEventId());
return null;
} else {
capturedObjects.put(ex.getThrowable(), null);
}
} else {
if (capturedObjects.containsKey(throwable)
|| containsAnyKey(capturedObjects, allCauses(throwable))) {
options
.getLogger()
.log(
SentryLevel.DEBUG,
"Duplicate Exception detected. Event %s will be discarded.",
event.getEventId());
return null;
} else {
capturedObjects.put(throwable, null);
}
}
}
return event;
}

private static <T> boolean containsAnyKey(
final @NotNull Map<T, Object> map, final @NotNull List<T> list) {
for (T entry : list) {
if (map.containsKey(entry)) {
return true;
}
}
return false;
}

private static @NotNull List<Throwable> allCauses(final @NotNull Throwable throwable) {
final List<Throwable> causes = new ArrayList<>();
Throwable ex = throwable;
while (ex.getCause() != null) {
causes.add(ex.getCause());
ex = ex.getCause();
}
return causes;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1047,6 +1047,7 @@ public SentryOptions() {
integrations.add(new ShutdownHookIntegration());

eventProcessors.add(new MainEventProcessor(this));
eventProcessors.add(new DuplicateEventDetectionEventProcessor(this));

setSentryClientName(BuildConfig.SENTRY_JAVA_SDK_NAME + "/" + BuildConfig.VERSION_NAME);
setSdkVersion(createSdkVersion());
Expand Down
9 changes: 9 additions & 0 deletions sentry-core/src/test/java/io/sentry/core/BreadcrumbTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,13 @@ class BreadcrumbTest {
val breadcrumb = Breadcrumb("this is a test")
assertEquals("this is a test", breadcrumb.message)
}

@Test
fun `creates HTTP breadcrumb`() {
val breadcrumb = Breadcrumb.http("http://example.com", "POST")
assertEquals("http://example.com", breadcrumb.data["url"])
assertEquals("POST", breadcrumb.data["method"])
assertEquals("http", breadcrumb.type)
assertEquals("http", breadcrumb.category)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package io.sentry.core

import io.sentry.core.exception.ExceptionMechanismException
import io.sentry.core.protocol.Mechanism
import java.lang.RuntimeException
import kotlin.test.Test
import kotlin.test.assertNotNull
import kotlin.test.assertNull

class DuplicateEventDetectionEventProcessorTest {

val processor = DuplicateEventDetectionEventProcessor(SentryOptions())

@Test
fun `does not drop event if no previous event with same exception was processed`() {
processor.process(SentryEvent(), null)

val result = processor.process(SentryEvent(RuntimeException()), null)

assertNotNull(result)
}

@Test
fun `drops event with the same exception`() {
val event = SentryEvent(RuntimeException())
processor.process(event, null)

val result = processor.process(event, null)
assertNull(result)
}

@Test
fun `drops event with mechanism exception having an exception that has already been processed`() {
val event = SentryEvent(RuntimeException())
processor.process(event, null)

val result = processor.process(SentryEvent(ExceptionMechanismException(Mechanism(), event.throwable, null)), null)
assertNull(result)
}

@Test
fun `drops event with exception that has already been processed with event with mechanism exception`() {
val sentryEvent = SentryEvent(ExceptionMechanismException(Mechanism(), RuntimeException(), null))
processor.process(sentryEvent, null)

val result = processor.process(SentryEvent((sentryEvent.throwable as ExceptionMechanismException).throwable), null)

assertNull(result)
}

@Test
fun `drops event with the cause equal to exception in already processed event`() {
val event = SentryEvent(RuntimeException())
processor.process(event, null)

val result = processor.process(SentryEvent(RuntimeException(event.throwable)), null)

assertNull(result)
}

@Test
fun `drops event with any of the causes has been already processed`() {
val event = SentryEvent(RuntimeException())
processor.process(event, null)

val result = processor.process(SentryEvent(RuntimeException(RuntimeException(event.throwable))), null)

assertNull(result)
}
}
8 changes: 4 additions & 4 deletions sentry-samples/sentry-samples-spring-boot/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ repositories {
}

dependencies {
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter")
implementation(Config.Libs.springBootStarterSecurity)
implementation(Config.Libs.springBootStarterWeb)
implementation(Config.Libs.springBootStarter)
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
maciejwalkowiak marked this conversation as resolved.
Show resolved Hide resolved
implementation(project(":sentry-spring-boot-starter"))
implementation(project(":sentry-logback"))
testImplementation("org.springframework.boot:spring-boot-starter-test") {
testImplementation(Config.Libs.springBootStarterTest) {
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.sentry.samples.spring.boot;

import io.sentry.core.EventProcessor;
import io.sentry.core.SentryEvent;
import io.sentry.core.protocol.SentryRuntime;
import org.jetbrains.annotations.Nullable;
import org.springframework.stereotype.Component;

/**
* Custom {@link EventProcessor} implementation lets modifying {@link SentryEvent}s before they are
* sent to Sentry.
*/
@Component
public class CustomEventProcessor implements EventProcessor {
private final String javaVersion;
private final String javaVendor;

public CustomEventProcessor(String javaVersion, String javaVendor) {
this.javaVersion = javaVersion;
this.javaVendor = javaVendor;
}

public CustomEventProcessor() {
this(System.getProperty("java.version"), System.getProperty("java.vendor"));
}

@Override
public SentryEvent process(SentryEvent event, @Nullable Object hint) {
final SentryRuntime runtime = new SentryRuntime();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to do this to all events (put it in the main event processor)
Likely not going to work for Android though

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it won't, we could have the processor on sentry-core but either it gets added to the processor list only if its non-Android or we do a conditional flag.

runtime.setVersion(javaVersion);
runtime.setName(javaVendor);
event.getContexts().setRuntime(runtime);
return event;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.sentry.samples.spring.boot;

public class Person {
private final String firstName;
private final String lastName;

public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

public String getFirstName() {
return firstName;
}

public String getLastName() {
return lastName;
}

@Override
public String toString() {
return "Person{" + "firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + '}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.sentry.samples.spring.boot;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/person/")
public class PersonController {
private static final Logger LOGGER = LoggerFactory.getLogger(PersonController.class);

@GetMapping("{id}")
Person person(@PathVariable Long id) {
LOGGER.info("Loading person with id={}", id);
throw new IllegalArgumentException("Something went wrong [id=" + id + "]");
}

@PostMapping
Person create(@RequestBody Person person) {
LOGGER.warn("Creating person: {}", person);
return person;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.sentry.samples.spring.boot;

import io.sentry.core.IHub;
import io.sentry.core.SentryOptions;
import io.sentry.spring.SentrySecurityFilter;
import org.jetbrains.annotations.NotNull;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

private final @NotNull IHub hub;
private final @NotNull SentryOptions options;

public SecurityConfiguration(final @NotNull IHub hub, final @NotNull SentryOptions options) {
this.hub = hub;
this.options = options;
}

// this API is meant to be consumed by non-browser clients thus the CSRF protection is not needed.
@Override
@SuppressWarnings("lgtm[java/spring-disabled-csrf-protection]")
protected void configure(final @NotNull HttpSecurity http) throws Exception {
// register SentrySecurityFilter to attach user information to SentryEvents
http.addFilterAfter(new SentrySecurityFilter(hub, options), AnonymousAuthenticationFilter.class)
.csrf()
.disable()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.httpBasic();
}

@Bean
@Override
public @NotNull UserDetailsService userDetailsService() {
final PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

final UserDetails user =
User.builder()
.passwordEncoder(encoder::encode)
.username("user")
.password("password")
.roles("USER")
.build();

return new InMemoryUserDetailsManager(user);
}
}
Loading