diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml
index b26632f7f65..f0ed81b44db 100644
--- a/.github/workflows/system-tests-backend.yml
+++ b/.github/workflows/system-tests-backend.yml
@@ -73,6 +73,9 @@ jobs:
- sample: "sentry-samples-spring-boot-4"
agent: "false"
agent-auto-init: "true"
+ - sample: "sentry-samples-spring-boot-4-log4j2"
+ agent: "false"
+ agent-auto-init: "true"
- sample: "sentry-samples-spring-boot-4-webflux"
agent: "false"
agent-auto-init: "true"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a6600fda921..fb4d450f4ec 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,33 @@
import
```
+ - Sentry can now configure Log4j2 automatically for Spring Boot 4 when `sentry-log4j2` is on the classpath and Log4j2 Core is the active logging backend ([#5403](https://github.com/getsentry/sentry-java/pull/5403))
+ - Enable automatic appender registration with:
+ ```properties
+ sentry.logging.enabled=true
+ ```
+ Automatic registration is disabled by default for now and will be enabled by default in the next major release.
+ - The appender is attached to the root logger by default. Configure one or more logger names with:
+ ```properties
+ sentry.logging.loggers[0]=ROOT
+ sentry.logging.loggers[1]=com.example
+ ```
+ - Configure the minimum level for creating breadcrumbs. The default is `INFO`:
+ ```properties
+ sentry.logging.minimum-breadcrumb-level=INFO
+ ```
+ - Configure the minimum level for creating Sentry error events. The default is `ERROR`:
+ ```properties
+ sentry.logging.minimum-event-level=ERROR
+ ```
+ - Configure the minimum level for sending Sentry structured logs. The default is `INFO`:
+ ```properties
+ sentry.logging.minimum-level=INFO
+ ```
+ Structured logs must also be enabled:
+ ```properties
+ sentry.logs.enabled=true
+ ```
### Fixes
diff --git a/build.gradle.kts b/build.gradle.kts
index 55b5a71a1e5..e99dba65350 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -71,6 +71,7 @@ apiValidation {
"sentry-samples-spring-boot-webflux",
"sentry-samples-spring-boot-webflux-jakarta",
"sentry-samples-spring-boot-4",
+ "sentry-samples-spring-boot-4-log4j2",
"sentry-samples-spring-boot-4-opentelemetry",
"sentry-samples-spring-boot-4-opentelemetry-noagent",
"sentry-samples-spring-boot-4-otlp",
diff --git a/sentry-log4j2/api/sentry-log4j2.api b/sentry-log4j2/api/sentry-log4j2.api
index 2a5d4bf7895..7eebea7136e 100644
--- a/sentry-log4j2/api/sentry-log4j2.api
+++ b/sentry-log4j2/api/sentry-log4j2.api
@@ -12,6 +12,9 @@ public class io/sentry/log4j2/SentryAppender : org/apache/logging/log4j/core/app
public static fun createAppender (Ljava/lang/String;Lorg/apache/logging/log4j/Level;Lorg/apache/logging/log4j/Level;Lorg/apache/logging/log4j/Level;Ljava/lang/String;Ljava/lang/Boolean;Lorg/apache/logging/log4j/core/Filter;Ljava/lang/String;)Lio/sentry/log4j2/SentryAppender;
protected fun createBreadcrumb (Lorg/apache/logging/log4j/core/LogEvent;)Lio/sentry/Breadcrumb;
protected fun createEvent (Lorg/apache/logging/log4j/core/LogEvent;)Lio/sentry/SentryEvent;
+ public fun getMinimumBreadcrumbLevel ()Lorg/apache/logging/log4j/Level;
+ public fun getMinimumEventLevel ()Lorg/apache/logging/log4j/Level;
+ public fun getMinimumLevel ()Lorg/apache/logging/log4j/Level;
public fun start ()V
}
diff --git a/sentry-log4j2/src/main/java/io/sentry/log4j2/SentryAppender.java b/sentry-log4j2/src/main/java/io/sentry/log4j2/SentryAppender.java
index df0f9eeb2d2..9bc8e90bfe0 100644
--- a/sentry-log4j2/src/main/java/io/sentry/log4j2/SentryAppender.java
+++ b/sentry-log4j2/src/main/java/io/sentry/log4j2/SentryAppender.java
@@ -167,6 +167,18 @@ public void start() {
start(getOptionsConfiguration(null));
}
+ public @NotNull Level getMinimumBreadcrumbLevel() {
+ return minimumBreadcrumbLevel;
+ }
+
+ public @NotNull Level getMinimumEventLevel() {
+ return minimumEventLevel;
+ }
+
+ public @NotNull Level getMinimumLevel() {
+ return minimumLevel;
+ }
+
@NotNull
Sentry.OptionsConfiguration getOptionsConfiguration(
final @Nullable Sentry.OptionsConfiguration additionalOptionsConfiguration) {
diff --git a/sentry-samples/sentry-samples-spring-boot-4-log4j2/README.md b/sentry-samples/sentry-samples-spring-boot-4-log4j2/README.md
new file mode 100644
index 00000000000..dfe080e91df
--- /dev/null
+++ b/sentry-samples/sentry-samples-spring-boot-4-log4j2/README.md
@@ -0,0 +1,122 @@
+# Sentry Sample Spring Boot 4 with Log4j2
+
+Sample application showing how to use Sentry with [Spring Boot 4](https://spring.io/projects/spring-boot) and [Log4j2](https://logging.apache.org/log4j/2.x/).
+
+## How to run?
+
+To see events triggered in this sample application in your Sentry dashboard, go to `src/main/resources/application.properties` and replace the test DSN with your own DSN.
+
+Then, execute a command from the module directory:
+
+```
+../../gradlew bootRun
+```
+
+Make an HTTP request that will trigger events:
+
+```
+curl -XPOST --user user:password http://localhost:8080/person/ -H "Content-Type:application/json" -d '{"firstName":"John","lastName":"Smith"}'
+```
+
+## GraphQL
+
+The following queries can be used to test the GraphQL integration.
+
+### Greeting
+```
+{
+ greeting(name: "crash")
+}
+```
+
+### Greeting with variables
+
+```
+query GreetingQuery($name: String) {
+ greeting(name: $name)
+}
+```
+variables:
+```
+{
+ "name": "crash"
+}
+```
+
+### Project
+
+```
+query ProjectQuery($slug: ID!) {
+ project(slug: $slug) {
+ slug
+ name
+ repositoryUrl
+ status
+ }
+}
+```
+variables:
+```
+{
+ "slug": "statuscrash"
+}
+```
+
+### Mutation
+
+```
+mutation AddProjectMutation($slug: ID!) {
+ addProject(slug: $slug)
+}
+```
+variables:
+```
+{
+ "slug": "nocrash",
+ "name": "nocrash"
+}
+```
+
+### Subscription
+
+```
+subscription SubscriptionNotifyNewTask($slug: ID!) {
+ notifyNewTask(projectSlug: $slug) {
+ id
+ name
+ assigneeId
+ assignee {
+ id
+ name
+ }
+ }
+}
+```
+variables:
+```
+{
+ "slug": "crash"
+}
+```
+
+### Data loader
+
+```
+query TasksAndAssigneesQuery($slug: ID!) {
+ tasks(projectSlug: $slug) {
+ id
+ name
+ assigneeId
+ assignee {
+ id
+ name
+ }
+ }
+}
+```
+variables:
+```
+{
+ "slug": "crash"
+}
+```
diff --git a/sentry-samples/sentry-samples-spring-boot-4-log4j2/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-4-log4j2/build.gradle.kts
new file mode 100644
index 00000000000..7686edade78
--- /dev/null
+++ b/sentry-samples/sentry-samples-spring-boot-4-log4j2/build.gradle.kts
@@ -0,0 +1,113 @@
+import org.jetbrains.kotlin.config.KotlinCompilerVersion
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+ alias(libs.plugins.springboot4)
+ alias(libs.plugins.spring.dependency.management)
+ alias(libs.plugins.kotlin.jvm)
+ alias(libs.plugins.kotlin.spring)
+ id("io.sentry.systemtest")
+}
+
+group = "io.sentry.sample.spring-boot-4-log4j2"
+
+version = "0.0.1-SNAPSHOT"
+
+java.sourceCompatibility = JavaVersion.VERSION_17
+
+java.targetCompatibility = JavaVersion.VERSION_17
+
+configure {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+}
+
+tasks.withType().configureEach {
+ kotlin {
+ explicitApi()
+ // skip metadata version check, as Spring 7 / Spring Boot 4 is
+ // compiled against a newer version of Kotlin
+ compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict", "-Xskip-metadata-version-check")
+ compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
+ compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9
+ compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9
+ }
+}
+
+dependencies {
+ implementation(libs.springboot4.starter)
+ implementation(libs.springboot4.starter.actuator)
+ implementation(libs.springboot4.starter.aspectj)
+ implementation(libs.springboot4.starter.graphql)
+ implementation(libs.springboot4.starter.jdbc)
+ implementation(libs.springboot4.starter.quartz)
+ implementation(libs.springboot4.starter.security)
+ implementation(libs.springboot4.starter.web)
+ implementation(libs.springboot4.starter.webflux)
+ implementation(libs.springboot4.starter.websocket)
+ implementation(libs.springboot4.starter.restclient)
+ implementation(libs.springboot4.starter.webclient)
+ implementation(Config.Libs.aspectj)
+ implementation(Config.Libs.kotlinReflect)
+ implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION))
+ implementation(projects.sentrySpringBoot4Starter)
+ implementation(projects.sentryGraphql22)
+ implementation(projects.sentryQuartz)
+ implementation(projects.sentryAsyncProfiler)
+
+ implementation(projects.sentryLog4j2)
+ implementation("org.springframework.boot:spring-boot-starter-log4j2")
+ modules {
+ module("org.springframework.boot:spring-boot-starter-logging") {
+ replacedBy(
+ "org.springframework.boot:spring-boot-starter-log4j2",
+ "Use Log4j2 instead of Logback",
+ )
+ }
+ }
+
+ // cache tracing
+ implementation(libs.springboot4.starter.cache)
+ implementation(libs.caffeine)
+
+ // kafka
+ implementation(libs.springboot4.starter.kafka)
+ implementation(projects.sentryKafka)
+
+ // database query tracing
+ implementation(projects.sentryJdbc)
+ runtimeOnly(libs.hsqldb)
+
+ testImplementation(kotlin(Config.kotlinStdLib))
+ testImplementation(projects.sentry)
+ testImplementation(projects.sentrySystemTestSupport)
+ testImplementation(libs.apollo3.kotlin)
+ testImplementation(libs.kotlin.test.junit)
+ testImplementation(libs.slf4j2.api)
+ testImplementation(libs.springboot4.starter.test) {
+ exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
+ }
+}
+
+tasks.register("systemTest").configure {
+ group = "verification"
+ description = "Runs the System tests"
+
+ val test = project.extensions.getByType()["test"]
+ testClassesDirs = test.output.classesDirs
+ classpath = test.runtimeClasspath
+
+ maxParallelForks = 1
+
+ // Cap JVM args per test
+ minHeapSize = "128m"
+ maxHeapSize = "1g"
+
+ filter { includeTestsMatching("io.sentry.systemtest*") }
+}
+
+tasks.named("test").configure {
+ require(this is Test)
+
+ filter { excludeTestsMatching("io.sentry.systemtest.*") }
+}
diff --git a/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/CacheController.java b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/CacheController.java
new file mode 100644
index 00000000000..3c2e66442de
--- /dev/null
+++ b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/CacheController.java
@@ -0,0 +1,34 @@
+package io.sentry.samples.spring.boot4;
+
+import org.springframework.web.bind.annotation.DeleteMapping;
+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("/cache/")
+public class CacheController {
+ private final TodoService todoService;
+
+ public CacheController(TodoService todoService) {
+ this.todoService = todoService;
+ }
+
+ @GetMapping("{id}")
+ Todo get(@PathVariable Long id) {
+ return todoService.get(id);
+ }
+
+ @PostMapping
+ Todo save(@RequestBody Todo todo) {
+ return todoService.save(todo);
+ }
+
+ @DeleteMapping("{id}")
+ void delete(@PathVariable Long id) {
+ todoService.delete(id);
+ }
+}
diff --git a/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/CustomEventProcessor.java b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/CustomEventProcessor.java
new file mode 100644
index 00000000000..723d9683d31
--- /dev/null
+++ b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/CustomEventProcessor.java
@@ -0,0 +1,35 @@
+package io.sentry.samples.spring.boot4;
+
+import io.sentry.EventProcessor;
+import io.sentry.Hint;
+import io.sentry.SentryEvent;
+import io.sentry.protocol.SentryRuntime;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.boot.SpringBootVersion;
+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 springBootVersion;
+
+ public CustomEventProcessor(String springBootVersion) {
+ this.springBootVersion = springBootVersion;
+ }
+
+ public CustomEventProcessor() {
+ this(SpringBootVersion.getVersion());
+ }
+
+ @Override
+ public @NotNull SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) {
+ final SentryRuntime runtime = new SentryRuntime();
+ runtime.setVersion(springBootVersion);
+ runtime.setName("Spring Boot");
+ event.getContexts().setRuntime(runtime);
+ return event;
+ }
+}
diff --git a/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/CustomJob.java b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/CustomJob.java
new file mode 100644
index 00000000000..b96d2a6c431
--- /dev/null
+++ b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/CustomJob.java
@@ -0,0 +1,25 @@
+package io.sentry.samples.spring.boot4;
+
+import io.sentry.spring7.checkin.SentryCheckIn;
+import io.sentry.spring7.tracing.SentryTransaction;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+/**
+ * {@link SentryTransaction} added on the class level, creates transaction around each method
+ * execution of every method of the annotated class.
+ */
+@Component
+@SentryTransaction(operation = "scheduled")
+public class CustomJob {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(CustomJob.class);
+
+ @SentryCheckIn("monitor_slug_1")
+ // @Scheduled(fixedRate = 3 * 60 * 1000L)
+ void execute() throws InterruptedException {
+ LOGGER.info("Executing scheduled job");
+ Thread.sleep(2000L);
+ }
+}
diff --git a/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/DistributedTracingController.java b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/DistributedTracingController.java
new file mode 100644
index 00000000000..9018e4c2184
--- /dev/null
+++ b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/DistributedTracingController.java
@@ -0,0 +1,49 @@
+package io.sentry.samples.spring.boot4;
+
+import java.nio.charset.Charset;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpHeaders;
+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;
+import org.springframework.web.client.RestClient;
+
+@RestController
+@RequestMapping("/tracing/")
+public class DistributedTracingController {
+ private static final Logger LOGGER = LoggerFactory.getLogger(DistributedTracingController.class);
+ private final RestClient restClient;
+
+ public DistributedTracingController(RestClient restClient) {
+ this.restClient = restClient;
+ }
+
+ @GetMapping("{id}")
+ Person person(@PathVariable Long id) {
+ return restClient
+ .get()
+ .uri("http://localhost:8080/person/{id}", id)
+ .header(
+ HttpHeaders.AUTHORIZATION,
+ "Basic " + HttpHeaders.encodeBasicAuth("user", "password", Charset.defaultCharset()))
+ .retrieve()
+ .body(Person.class);
+ }
+
+ @PostMapping
+ Person create(@RequestBody Person person) {
+ return restClient
+ .post()
+ .uri("http://localhost:8080/person/")
+ .body(person)
+ .header(
+ HttpHeaders.AUTHORIZATION,
+ "Basic " + HttpHeaders.encodeBasicAuth("user", "password", Charset.defaultCharset()))
+ .retrieve()
+ .body(Person.class);
+ }
+}
diff --git a/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/MetricController.java b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/MetricController.java
new file mode 100644
index 00000000000..be75f5e3002
--- /dev/null
+++ b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/MetricController.java
@@ -0,0 +1,37 @@
+package io.sentry.samples.spring.boot4;
+
+import io.sentry.Sentry;
+import io.sentry.metrics.MetricsUnit;
+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.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/metric/")
+public class MetricController {
+ private static final Logger LOGGER = LoggerFactory.getLogger(MetricController.class);
+
+ @GetMapping("count")
+ String count() {
+ Sentry.setAttribute("user.type", "admin");
+ Sentry.setAttribute("feature.version", 2);
+ Sentry.metrics().count("countMetric");
+ return "count metric increased";
+ }
+
+ @GetMapping("gauge/{value}")
+ String gauge(@PathVariable("value") Long value) {
+ Sentry.metrics().gauge("memory.free", value.doubleValue(), MetricsUnit.Information.BYTE);
+ return "gauge metric tracked";
+ }
+
+ @GetMapping("distribution/{value}")
+ String distribution(@PathVariable("value") Long value) {
+ Sentry.metrics()
+ .distribution("distributionMetric", value.doubleValue(), MetricsUnit.Duration.MILLISECOND);
+ return "distribution metric tracked";
+ }
+}
diff --git a/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/Person.java b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/Person.java
new file mode 100644
index 00000000000..a12881fb346
--- /dev/null
+++ b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/Person.java
@@ -0,0 +1,24 @@
+package io.sentry.samples.spring.boot4;
+
+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 + '\'' + '}';
+ }
+}
diff --git a/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/PersonController.java b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/PersonController.java
new file mode 100644
index 00000000000..489dc629d28
--- /dev/null
+++ b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/PersonController.java
@@ -0,0 +1,55 @@
+package io.sentry.samples.spring.boot4;
+
+import io.sentry.ISpan;
+import io.sentry.Sentry;
+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 final PersonService personService;
+ private static final Logger LOGGER = LoggerFactory.getLogger(PersonController.class);
+
+ public PersonController(PersonService personService) {
+ this.personService = personService;
+ }
+
+ @GetMapping("{id}")
+ Person person(@PathVariable Long id) {
+ Sentry.addFeatureFlag("transaction-feature-flag", true);
+ ISpan currentSpan = Sentry.getSpan();
+ ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi");
+ try {
+ Sentry.setAttribute("user.type", "admin");
+ Sentry.setAttribute("feature.version", 2);
+ Sentry.setAttribute("debug.enabled", true);
+
+ Sentry.logger().warn("warn Sentry logging");
+ Sentry.logger().error("error Sentry logging");
+ Sentry.logger().info("hello %s %s", "there", "world!");
+ Sentry.addFeatureFlag("my-feature-flag", true);
+ LOGGER.error("Trying person with id={}", id, new RuntimeException("error while loading"));
+ throw new IllegalArgumentException("Something went wrong [id=" + id + "]");
+ } finally {
+ sentrySpan.finish();
+ }
+ }
+
+ @PostMapping
+ Person create(@RequestBody Person person) {
+ ISpan currentSpan = Sentry.getSpan();
+ ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi");
+ try {
+ return personService.create(person);
+ } finally {
+ sentrySpan.finish();
+ }
+ }
+}
diff --git a/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/PersonService.java b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/PersonService.java
new file mode 100644
index 00000000000..de2e684c920
--- /dev/null
+++ b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/PersonService.java
@@ -0,0 +1,41 @@
+package io.sentry.samples.spring.boot4;
+
+import io.sentry.ISpan;
+import io.sentry.Sentry;
+import io.sentry.spring7.tracing.SentrySpan;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+
+/**
+ * {@link SentrySpan} can be added either on the class or the method to create spans around method
+ * executions.
+ */
+@Service
+@SentrySpan
+public class PersonService {
+ private static final Logger LOGGER = LoggerFactory.getLogger(PersonService.class);
+
+ private final JdbcTemplate jdbcTemplate;
+ private int createCount = 0;
+
+ public PersonService(JdbcTemplate jdbcTemplate) {
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ Person create(Person person) {
+ createCount++;
+ final ISpan span = Sentry.getSpan();
+ if (span != null) {
+ span.setMeasurement("create_count", createCount);
+ }
+
+ jdbcTemplate.update(
+ "insert into person (firstName, lastName) values (?, ?)",
+ person.getFirstName(),
+ person.getLastName());
+
+ return person;
+ }
+}
diff --git a/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/SecurityConfiguration.java b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/SecurityConfiguration.java
new file mode 100644
index 00000000000..d12a40fb51d
--- /dev/null
+++ b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/SecurityConfiguration.java
@@ -0,0 +1,41 @@
+package io.sentry.samples.spring.boot4;
+
+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.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+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.SecurityFilterChain;
+
+@Configuration
+public class SecurityConfiguration {
+
+ // this API is meant to be consumed by non-browser clients thus the CSRF protection is not needed.
+ @SuppressWarnings({"lgtm[java/spring-disabled-csrf-protection]", "removal"})
+ @Bean
+ public SecurityFilterChain filterChain(final @NotNull HttpSecurity http) throws Exception {
+ return http.csrf((csrf) -> csrf.disable())
+ .authorizeHttpRequests((r) -> r.anyRequest().authenticated())
+ .httpBasic((h) -> {})
+ .build();
+ }
+
+ @Bean
+ public @NotNull InMemoryUserDetailsManager 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);
+ }
+}
diff --git a/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java
new file mode 100644
index 00000000000..13d97fa8442
--- /dev/null
+++ b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java
@@ -0,0 +1,73 @@
+package io.sentry.samples.spring.boot4;
+
+import static io.sentry.quartz.SentryJobListener.SENTRY_SLUG_KEY;
+
+import io.sentry.samples.spring.boot4.quartz.SampleJob;
+import java.util.Collections;
+import org.quartz.JobDetail;
+import org.quartz.SimpleTrigger;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.restclient.RestTemplateBuilder;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
+import org.springframework.scheduling.quartz.JobDetailFactoryBean;
+import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean;
+import org.springframework.web.client.RestClient;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.reactive.function.client.WebClient;
+
+@SpringBootApplication
+@EnableCaching
+@EnableScheduling
+public class SentryDemoApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(SentryDemoApplication.class, args);
+ }
+
+ @Bean
+ RestTemplate restTemplate(RestTemplateBuilder builder) {
+ return builder.build();
+ }
+
+ @Bean
+ WebClient webClient(WebClient.Builder builder) {
+ return builder.build();
+ }
+
+ @Bean
+ RestClient restClient(RestClient.Builder builder) {
+ return builder.build();
+ }
+
+ @Bean
+ public JobDetailFactoryBean jobDetail() {
+ JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean();
+ jobDetailFactory.setJobClass(SampleJob.class);
+ jobDetailFactory.setDurability(true);
+ jobDetailFactory.setJobDataAsMap(
+ Collections.singletonMap(SENTRY_SLUG_KEY, "monitor_slug_job_detail"));
+ return jobDetailFactory;
+ }
+
+ @Bean
+ public SimpleTriggerFactoryBean trigger(JobDetail job) {
+ SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean();
+ trigger.setJobDetail(job);
+ trigger.setRepeatInterval(2 * 60 * 1000); // every two minutes
+ trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY);
+ trigger.setJobDataAsMap(
+ Collections.singletonMap(SENTRY_SLUG_KEY, "monitor_slug_simple_trigger"));
+ return trigger;
+ }
+
+ @Bean
+ public CronTriggerFactoryBean cronTrigger(JobDetail job) {
+ CronTriggerFactoryBean trigger = new CronTriggerFactoryBean();
+ trigger.setJobDetail(job);
+ trigger.setCronExpression("0 0/5 * ? * *"); // every five minutes
+ return trigger;
+ }
+}
diff --git a/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/Todo.java b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/Todo.java
new file mode 100644
index 00000000000..ae3d128d6b9
--- /dev/null
+++ b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/Todo.java
@@ -0,0 +1,25 @@
+package io.sentry.samples.spring.boot4;
+
+public class Todo {
+ private final Long id;
+ private final String title;
+ private final boolean completed;
+
+ public Todo(Long id, String title, boolean completed) {
+ this.id = id;
+ this.title = title;
+ this.completed = completed;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public boolean isCompleted() {
+ return completed;
+ }
+}
diff --git a/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/TodoController.java b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/TodoController.java
new file mode 100644
index 00000000000..0f71cca0419
--- /dev/null
+++ b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/TodoController.java
@@ -0,0 +1,57 @@
+package io.sentry.samples.spring.boot4;
+
+import io.sentry.reactor.SentryReactorUtils;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.client.RestClient;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Hooks;
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Schedulers;
+
+@RestController
+public class TodoController {
+ private final RestTemplate restTemplate;
+ private final WebClient webClient;
+ private final RestClient restClient;
+
+ public TodoController(RestTemplate restTemplate, WebClient webClient, RestClient restClient) {
+ this.restTemplate = restTemplate;
+ this.webClient = webClient;
+ this.restClient = restClient;
+ }
+
+ @GetMapping("/todo/{id}")
+ Todo todo(@PathVariable Long id) {
+ return restTemplate.getForObject(
+ "https://jsonplaceholder.typicode.com/todos/{id}", Todo.class, id);
+ }
+
+ @GetMapping("/todo-webclient/{id}")
+ Todo todoWebClient(@PathVariable Long id) {
+ Hooks.enableAutomaticContextPropagation();
+ return SentryReactorUtils.withSentry(
+ Mono.just(true)
+ .publishOn(Schedulers.boundedElastic())
+ .flatMap(
+ x ->
+ webClient
+ .get()
+ .uri("https://jsonplaceholder.typicode.com/todos/{id}", id)
+ .retrieve()
+ .bodyToMono(Todo.class)
+ .map(response -> response)))
+ .block();
+ }
+
+ @GetMapping("/todo-restclient/{id}")
+ Todo todoRestClient(@PathVariable Long id) {
+ return restClient
+ .get()
+ .uri("https://jsonplaceholder.typicode.com/todos/{id}", id)
+ .retrieve()
+ .body(Todo.class);
+ }
+}
diff --git a/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/TodoService.java b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/TodoService.java
new file mode 100644
index 00000000000..c837ab8398a
--- /dev/null
+++ b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/TodoService.java
@@ -0,0 +1,29 @@
+package io.sentry.samples.spring.boot4;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.CachePut;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+
+@Service
+public class TodoService {
+ private final Map store = new ConcurrentHashMap<>();
+
+ @Cacheable(value = "todos", key = "#id")
+ public Todo get(Long id) {
+ return store.get(id);
+ }
+
+ @CachePut(value = "todos", key = "#todo.id")
+ public Todo save(Todo todo) {
+ store.put(todo.getId(), todo);
+ return todo;
+ }
+
+ @CacheEvict(value = "todos", key = "#id")
+ public void delete(Long id) {
+ store.remove(id);
+ }
+}
diff --git a/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/graphql/AssigneeController.java b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/graphql/AssigneeController.java
new file mode 100644
index 00000000000..d43cde143d1
--- /dev/null
+++ b/sentry-samples/sentry-samples-spring-boot-4-log4j2/src/main/java/io/sentry/samples/spring/boot4/graphql/AssigneeController.java
@@ -0,0 +1,34 @@
+package io.sentry.samples.spring.boot4.graphql;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.graphql.data.method.annotation.BatchMapping;
+import org.springframework.stereotype.Controller;
+import reactor.core.publisher.Mono;
+
+@Controller
+public class AssigneeController {
+
+ @BatchMapping(typeName = "Task", field = "assignee")
+ public Mono