Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

add spring starter r2dbc support #11221

Merged
merged 9 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ dependencies {

tasks {
shadowJar {
exclude("META-INF/**/*")
exclude {
it.path.startsWith("META-INF") && !it.path.startsWith("META-INF/io/opentelemetry/instrumentation/")
}

dependencies {
// including only :r2dbc-1.0:library excludes its transitive dependencies
Expand All @@ -21,13 +23,20 @@ tasks {
}
relocate(
"io.r2dbc.proxy",
"io.opentelemetry.javaagent.instrumentation.r2dbc.v1_0.shaded.io.r2dbc.proxy"
"io.opentelemetry.instrumentation.r2dbc.v1_0.shaded.io.r2dbc.proxy"
)
}

val extractShadowJar by registering(Copy::class) {
dependsOn(shadowJar)
from(zipTree(shadowJar.get().archiveFile))
exclude("META-INF/**")
into("build/extracted/shadow")
}

val extractShadowJarSpring by registering(Copy::class) {
dependsOn(shadowJar)
from(zipTree(shadowJar.get().archiveFile))
into("build/extracted/shadow-spring")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ public R2dbcInstrumenterBuilder addAttributeExtractor(
}

public Instrumenter<DbExecution, Void> build(boolean statementSanitizationEnabled) {

return Instrumenter.<DbExecution, Void>builder(
openTelemetry,
INSTRUMENTATION_NAME,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ group = "io.opentelemetry.instrumentation"
val versions: Map<String, String> by project
val springBootVersion = versions["org.springframework.boot"]

// r2dbc-proxy is shadowed to prevent org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration
// from being loaded by Spring Boot (by the presence of META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider) - even if the user doesn't want to use R2DBC.
sourceSets {
main {
val shadedDep = project(":instrumentation:r2dbc-1.0:library-instrumentation-shaded")
output.dir(
shadedDep.file("build/extracted/shadow-spring"),
"builtBy" to ":instrumentation:r2dbc-1.0:library-instrumentation-shaded:extractShadowJarSpring",
)
}
}

dependencies {
implementation("org.springframework.boot:spring-boot-autoconfigure:$springBootVersion")
annotationProcessor("org.springframework.boot:spring-boot-autoconfigure-processor:$springBootVersion")
Expand All @@ -17,6 +29,7 @@ dependencies {

implementation(project(":instrumentation-annotations-support"))
implementation(project(":instrumentation:kafka:kafka-clients:kafka-clients-2.6:library"))
compileOnly(project(path = ":instrumentation:r2dbc-1.0:library-instrumentation-shaded", configuration = "shadow"))
implementation(project(":instrumentation:spring:spring-kafka-2.7:library"))
implementation(project(":instrumentation:spring:spring-web:spring-web-3.1:library"))
implementation(project(":instrumentation:spring:spring-webmvc:spring-webmvc-5.3:library"))
Expand All @@ -36,6 +49,7 @@ dependencies {
library("org.springframework.boot:spring-boot-starter-aop:$springBootVersion")
library("org.springframework.boot:spring-boot-starter-web:$springBootVersion")
library("org.springframework.boot:spring-boot-starter-webflux:$springBootVersion")
library("org.springframework.boot:spring-boot-starter-data-r2dbc:$springBootVersion")

implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
implementation(project(":sdk-autoconfigure-support"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.r2dbc;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.spring.autoconfigure.internal.SdkEnabled;
import io.r2dbc.spi.ConnectionFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

@ConditionalOnBean(OpenTelemetry.class)
@ConditionalOnClass(ConnectionFactory.class)
@ConditionalOnProperty(name = "otel.instrumentation.r2dbc.enabled", matchIfMissing = true)
@Conditional(SdkEnabled.class)
@Configuration(proxyBeanMethods = false)
public class R2dbcAutoConfiguration {

public R2dbcAutoConfiguration() {}

@Bean
// static to avoid "is not eligible for getting processed by all BeanPostProcessors" warning
static R2dbcInstrumentingPostProcessor r2dbcInstrumentingPostProcessor(
zeitlinger marked this conversation as resolved.
Show resolved Hide resolved
ObjectProvider<OpenTelemetry> openTelemetryProvider,
@Value("${otel.instrumentation.common.db-statement-sanitizer.enabled:true}")
boolean statementSanitizationEnabled) {
return new R2dbcInstrumentingPostProcessor(openTelemetryProvider, statementSanitizationEnabled);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.r2dbc;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.r2dbc.v1_0.R2dbcTelemetry;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactoryOptions;
import org.springframework.aop.scope.ScopedProxyUtils;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.r2dbc.OptionsCapableConnectionFactory;

class R2dbcInstrumentingPostProcessor implements BeanPostProcessor {

private final ObjectProvider<OpenTelemetry> openTelemetryProvider;
private final boolean statementSanitizationEnabled;

R2dbcInstrumentingPostProcessor(
ObjectProvider<OpenTelemetry> openTelemetryProvider, boolean statementSanitizationEnabled) {
this.openTelemetryProvider = openTelemetryProvider;
this.statementSanitizationEnabled = statementSanitizationEnabled;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean instanceof ConnectionFactory && !ScopedProxyUtils.isScopedTarget(beanName)) {
ConnectionFactory connectionFactory = (ConnectionFactory) bean;
return R2dbcTelemetry.builder(openTelemetryProvider.getObject())
.setStatementSanitizationEnabled(statementSanitizationEnabled)
.build()
.wrapConnectionFactory(connectionFactory, getConnectionFactoryOptions(connectionFactory));
}
return bean;
}

private static ConnectionFactoryOptions getConnectionFactoryOptions(
ConnectionFactory connectionFactory) {
OptionsCapableConnectionFactory optionsCapableConnectionFactory =
OptionsCapableConnectionFactory.unwrapFrom(connectionFactory);
if (optionsCapableConnectionFactory != null) {
return optionsCapableConnectionFactory.getOptions();
} else {
// in practice should never happen
// fall back to empty options; or reconstruct them from the R2dbcProperties
return ConnectionFactoryOptions.builder().build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,12 @@
"description": "Enable the <code>@WithSpan</code> annotation.",
"defaultValue": true
},
{
"name": "otel.instrumentation.common.db-statement-sanitizer.enabled",
"type": "java.lang.Boolean",
"description": "Enables the DB statement sanitization for R2DBC.",
"defaultValue": true
Comment on lines +277 to +280
Copy link
Contributor

Choose a reason for hiding this comment

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

it is weird that this applies only to r2dbc, I'd assume that this would also apply to jdbc. I'd make the description more generic.

Copy link
Member Author

Choose a reason for hiding this comment

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

no, this feature is missing for JDBC currently - but I'm happy to add that in a follow up PR 😄

},
{
"name": "otel.instrumentation.kafka.enabled",
"type": "java.lang.Boolean",
Expand Down Expand Up @@ -332,6 +338,12 @@
"description": "Enable the Micrometer instrumentation.",
"defaultValue": false
},
{
"name": "otel.instrumentation.r2dbc.enabled",
"type": "java.lang.Boolean",
"description": "Enable the R2DBC (reactive JDBC) instrumentation. Also see <code>otel.instrumentation.common.db-statement-sanitizer.enabled</code>.",
"defaultValue": true
},
{
"name": "otel.instrumentation.spring-web.enabled",
"type": "java.lang.Boolean",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.kafka.Kafk
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.logging.OpenTelemetryAppenderAutoConfiguration,\
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.jdbc.JdbcInstrumentationAutoConfiguration,\
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.micrometer.MicrometerBridgeAutoConfiguration,\
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.r2dbc.R2dbcAutoConfiguration,\
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.web.SpringWebInstrumentationAutoConfiguration,\
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.webflux.SpringWebfluxInstrumentationAutoConfiguration,\
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.webmvc.SpringWebMvc5InstrumentationAutoConfiguration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.kafka.Kafk
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.logging.OpenTelemetryAppenderAutoConfiguration
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.jdbc.JdbcInstrumentationAutoConfiguration
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.micrometer.MicrometerBridgeAutoConfiguration
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.r2dbc.R2dbcAutoConfiguration
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.web.SpringWebInstrumentationAutoConfiguration
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.webflux.SpringWebfluxInstrumentationAutoConfiguration
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.webmvc.SpringWebMvc6InstrumentationAutoConfiguration
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ dependencies {

implementation(project(":smoke-tests-otel-starter:spring-boot-reactive-common"))
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")

runtimeOnly("com.h2database:h2")
runtimeOnly("io.r2dbc:r2dbc-h2")

testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.projectreactor:reactor-test")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ dependencies {

implementation(project(":smoke-tests-otel-starter:spring-boot-reactive-common"))
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")

runtimeOnly("com.h2database:h2")
runtimeOnly("io.r2dbc:r2dbc-h2")

testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.projectreactor:reactor-test")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dependencies {
compileOnly("org.springframework.boot:spring-boot-starter-web")
compileOnly("org.springframework.boot:spring-boot-starter-webflux")
compileOnly("org.springframework.boot:spring-boot-starter-test")
compileOnly("org.springframework.boot:spring-boot-starter-data-r2dbc")
api(project(":smoke-tests-otel-starter:spring-smoke-testing"))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.semconv.HttpAttributes;
import io.opentelemetry.semconv.UrlAttributes;
import io.opentelemetry.semconv.incubating.DbIncubatingAttributes;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down Expand Up @@ -45,6 +46,8 @@ void webClientAndWebFluxAndR2dbc() {
.blockLast();

testing.waitAndAssertTraces(
trace ->
trace.hasSpansSatisfyingExactly(span -> span.hasName("CREATE TABLE testdb.player")),
trace ->
trace.hasSpansSatisfyingExactly(
span ->
Expand All @@ -57,6 +60,24 @@ void webClientAndWebFluxAndR2dbc() {
.hasName("GET /webflux")
.hasAttribute(HttpAttributes.HTTP_REQUEST_METHOD, "GET")
.hasAttribute(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L)
.hasAttribute(HttpAttributes.HTTP_ROUTE, "/webflux")));
.hasAttribute(HttpAttributes.HTTP_ROUTE, "/webflux"),
span ->
span.hasKind(SpanKind.CLIENT)
.satisfies(
s ->
assertThat(s.getName())
.isEqualToIgnoringCase("SELECT testdb.PLAYER"))
.hasAttribute(DbIncubatingAttributes.DB_NAME, "testdb")
.hasAttributesSatisfying(
a ->
assertThat(a.get(DbIncubatingAttributes.DB_SQL_TABLE))
.isEqualToIgnoringCase("PLAYER"))
.hasAttribute(DbIncubatingAttributes.DB_OPERATION, "SELECT")
.hasAttributesSatisfying(
a ->
assertThat(a.get(DbIncubatingAttributes.DB_STATEMENT))
.isEqualToIgnoringCase(
"SELECT PLAYER.* FROM PLAYER WHERE PLAYER.ID = $? LIMIT ?"))
.hasAttribute(DbIncubatingAttributes.DB_SYSTEM, "h2")));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,16 @@
public class OtelReactiveSpringStarterSmokeTestController {

public static final String WEBFLUX = "/webflux";
private final PlayerRepository playerRepository;

public OtelReactiveSpringStarterSmokeTestController(PlayerRepository playerRepository) {
this.playerRepository = playerRepository;
}

@GetMapping(WEBFLUX)
public Mono<String> webflux() {
return Mono.just("webflux");
return playerRepository
.findById(1)
.map(player -> "Player: " + player.getName() + " Age: " + player.getAge());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.spring.smoketest;

import org.springframework.data.annotation.Id;

public class Player {
@Id Integer id;
String name;
Integer age;

public Player() {}

public Player(Integer id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}

public Integer getId() {
return id;
}

public String getName() {
return name;
}

public Integer getAge() {
return age;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.spring.smoketest;

import org.springframework.data.repository.reactive.ReactiveCrudRepository;

public interface PlayerRepository extends ReactiveCrudRepository<Player, Integer> {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
spring:
r2dbc:
url: r2dbc:h2:mem:///testdb
jpa:
hibernate:
ddl-auto: create
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE TABLE IF NOT EXISTS player(id INT NOT NULL AUTO_INCREMENT, name VARCHAR(255), age INT, PRIMARY KEY (id));
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,17 @@ void setUpTesting() {
void checkSpringLogs(CapturedOutput output) {
// warnings are emitted if the auto-configuration have non-fatal problems
assertThat(output)
// not a warning in Spring Boot 2
.doesNotContain("is not eligible for getting processed by all BeanPostProcessors")
// only look for WARN and ERROR log level, e.g. [Test worker] WARN
.doesNotContain("] WARN")
.doesNotContain("] ERROR")
// not a warning in Spring Boot 2
.doesNotContain("is not eligible for getting processed by all BeanPostProcessors");
.satisfies(
s -> {
if (!s.toString()
.contains(
"Unable to load io.netty.resolver.dns.macos.MacOSDnsServerAddressStreamProvider")) {
assertThat(s).doesNotContain("] ERROR");
}
});
}
}
Loading