Add automatic MongoDB instrumentation to the OTel starter (#11211)

Co-authored-by: Jean Bisutti <jean.bisutti@gmail.com>
This commit is contained in:
Gregor Zeitlinger 2024-05-16 18:20:38 +02:00 committed by GitHub
parent 39c373e5bd
commit 6b66434258
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 270 additions and 29 deletions

View File

@ -1,4 +1,9 @@
services: services:
mongodb:
image: mongo:4.0
ports:
- "27017:27017"
zookeeper: zookeeper:
image: confluentinc/cp-zookeeper:6.2.10 image: confluentinc/cp-zookeeper:6.2.10
environment: environment:

View File

@ -29,6 +29,7 @@ dependencies {
implementation(project(":instrumentation-annotations-support")) implementation(project(":instrumentation-annotations-support"))
implementation(project(":instrumentation:kafka:kafka-clients:kafka-clients-2.6:library")) implementation(project(":instrumentation:kafka:kafka-clients:kafka-clients-2.6:library"))
implementation(project(":instrumentation:mongo:mongo-3.1:library"))
compileOnly(project(path = ":instrumentation:r2dbc-1.0:library-instrumentation-shaded", configuration = "shadow")) 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-kafka-2.7:library"))
implementation(project(":instrumentation:spring:spring-web:spring-web-3.1:library")) implementation(project(":instrumentation:spring:spring-web:spring-web-3.1:library"))
@ -49,6 +50,7 @@ dependencies {
library("org.springframework.boot:spring-boot-starter-aop:$springBootVersion") 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-web:$springBootVersion")
library("org.springframework.boot:spring-boot-starter-webflux:$springBootVersion") library("org.springframework.boot:spring-boot-starter-webflux:$springBootVersion")
library("org.springframework.boot:spring-boot-starter-data-mongodb:$springBootVersion")
library("org.springframework.boot:spring-boot-starter-data-r2dbc:$springBootVersion") library("org.springframework.boot:spring-boot-starter-data-r2dbc:$springBootVersion")
implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")

View File

@ -0,0 +1,40 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.mongo;
import com.mongodb.MongoClientSettings;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.mongo.v3_1.MongoTelemetry;
import io.opentelemetry.instrumentation.spring.autoconfigure.internal.SdkEnabled;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.mongo.MongoClientSettingsBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
@ConditionalOnBean(OpenTelemetry.class)
@ConditionalOnClass(MongoClientSettings.class)
@ConditionalOnProperty(name = "otel.instrumentation.mongo.enabled", matchIfMissing = true)
@Conditional(SdkEnabled.class)
@Configuration
public class MongoClientInstrumentationAutoConfiguration {
@Bean
public MongoClientSettingsBuilderCustomizer customizer(
OpenTelemetry openTelemetry, ConfigProperties config) {
return builder ->
builder.addCommandListener(
MongoTelemetry.builder(openTelemetry)
.setStatementSanitizationEnabled(
config.getBoolean(
"otel.instrumentation.mongo.statement-sanitizer.enabled", true))
.build()
.newCommandListener());
}
}

View File

@ -291,6 +291,18 @@
"description": "Enable the capture of experimental Kafka span attributes.", "description": "Enable the capture of experimental Kafka span attributes.",
"defaultValue": false "defaultValue": false
}, },
{
"name": "otel.instrumentation.mongo.enabled",
"type": "java.lang.Boolean",
"description": "Enable the Mongo client instrumentation.",
"defaultValue": true
},
{
"name": "otel.instrumentation.mongo.statement-sanitizer.enabled",
"type": "java.lang.Boolean",
"description": "Enables the DB statement sanitization.",
"defaultValue": true
},
{ {
"name": "otel.instrumentation.log4j-appender.enabled", "name": "otel.instrumentation.log4j-appender.enabled",
"type": "java.lang.Boolean", "type": "java.lang.Boolean",

View File

@ -2,6 +2,7 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration,\ io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration,\
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.annotations.InstrumentationAnnotationsAutoConfiguration,\ io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.annotations.InstrumentationAnnotationsAutoConfiguration,\
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.kafka.KafkaInstrumentationAutoConfiguration,\ io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.kafka.KafkaInstrumentationAutoConfiguration,\
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.mongo.MongoClientInstrumentationAutoConfiguration,\
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.logging.OpenTelemetryAppenderAutoConfiguration,\ io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.logging.OpenTelemetryAppenderAutoConfiguration,\
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.jdbc.JdbcInstrumentationAutoConfiguration,\ io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.jdbc.JdbcInstrumentationAutoConfiguration,\
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.micrometer.MicrometerBridgeAutoConfiguration,\ io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.micrometer.MicrometerBridgeAutoConfiguration,\

View File

@ -1,6 +1,7 @@
io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.annotations.InstrumentationAnnotationsAutoConfiguration io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.annotations.InstrumentationAnnotationsAutoConfiguration
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.kafka.KafkaInstrumentationAutoConfiguration io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.kafka.KafkaInstrumentationAutoConfiguration
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.mongo.MongoClientInstrumentationAutoConfiguration
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.logging.OpenTelemetryAppenderAutoConfiguration io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.logging.OpenTelemetryAppenderAutoConfiguration
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.jdbc.JdbcInstrumentationAutoConfiguration io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.jdbc.JdbcInstrumentationAutoConfiguration
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.micrometer.MicrometerBridgeAutoConfiguration io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.micrometer.MicrometerBridgeAutoConfiguration

View File

@ -11,12 +11,14 @@ dependencies {
runtimeOnly("com.h2database:h2") runtimeOnly("com.h2database:h2")
implementation("org.apache.commons:commons-dbcp2") implementation("org.apache.commons:commons-dbcp2")
implementation("org.springframework.kafka:spring-kafka") implementation("org.springframework.kafka:spring-kafka")
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
implementation(project(":smoke-tests-otel-starter:spring-boot-common")) implementation(project(":smoke-tests-otel-starter:spring-boot-common"))
testImplementation("org.testcontainers:junit-jupiter") testImplementation("org.testcontainers:junit-jupiter")
testImplementation("org.testcontainers:kafka") testImplementation("org.testcontainers:kafka")
testImplementation("org.testcontainers:mongodb")
testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.boot:spring-boot-starter-test")
} }

View File

@ -0,0 +1,11 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.spring.smoketest;
import org.junit.jupiter.api.condition.DisabledInNativeImage;
@DisabledInNativeImage // See GraalVmNativeMongodbSpringStarterSmokeTest for the GraalVM native test
public class MongoSpringStarterSmokeTest extends AbstractJvmMongodbSpringStarterSmokeTest {}

View File

@ -16,12 +16,13 @@ dependencies {
runtimeOnly("com.h2database:h2") runtimeOnly("com.h2database:h2")
implementation("org.apache.commons:commons-dbcp2") implementation("org.apache.commons:commons-dbcp2")
implementation("org.springframework.kafka:spring-kafka") implementation("org.springframework.kafka:spring-kafka")
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
implementation(project(":smoke-tests-otel-starter:spring-boot-common")) implementation(project(":smoke-tests-otel-starter:spring-boot-common"))
testImplementation("org.testcontainers:junit-jupiter") testImplementation("org.testcontainers:junit-jupiter")
testImplementation("org.testcontainers:kafka") testImplementation("org.testcontainers:kafka")
testImplementation("org.testcontainers:mongodb")
testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.boot:spring-boot-starter-test")
} }

View File

@ -5,7 +5,9 @@
package io.opentelemetry.spring.smoketest; package io.opentelemetry.spring.smoketest;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
// Necessary for GraalVM native test // Necessary for GraalVM native test
public class RuntimeHints implements RuntimeHintsRegistrar { public class RuntimeHints implements RuntimeHintsRegistrar {
@ -14,5 +16,16 @@ public class RuntimeHints implements RuntimeHintsRegistrar {
public void registerHints( public void registerHints(
org.springframework.aot.hint.RuntimeHints hints, ClassLoader classLoader) { org.springframework.aot.hint.RuntimeHints hints, ClassLoader classLoader) {
hints.resources().registerResourceBundle("org.apache.commons.dbcp2.LocalStrings"); hints.resources().registerResourceBundle("org.apache.commons.dbcp2.LocalStrings");
// To avoid Spring native issue with MongoDB: java.lang.ClassNotFoundException:
// org.springframework.data.mongodb.core.aggregation.AggregationOperation
hints
.reflection()
.registerType(
TypeReference.of(
"org.springframework.data.mongodb.core.aggregation.AggregationOperation"),
hint -> {
hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
});
} }
} }

View File

@ -0,0 +1,29 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.spring.smoketest;
import org.junit.jupiter.api.condition.EnabledInNativeImage;
import org.springframework.boot.test.context.SpringBootTest;
/**
* GraalVM native image doesn't support Testcontainers in our case, so the docker container is
* started manually before running the tests.
*
* <p>In other cases, it does work, e.g. <a
* href="https://info.michael-simons.eu/2023/10/25/run-your-integration-tests-against-testcontainers-with-graalvm-native-image/">here</a>,
* it's not yet clear why it doesn't work in our case.
*
* <p>In CI, this is done in reusable-native-tests.yml. If you want to run the tests locally, you
* need to start the container manually: see .github/workflows/reusable-native-tests.yml for the
* command.
*/
@SpringBootTest(
classes = {OtelSpringStarterSmokeTestApplication.class, SpringSmokeOtelConfiguration.class},
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@EnabledInNativeImage // see JvmMongodbSpringStarterSmokeTest for the JVM test
@EnabledInGithubActions
public class GraalVmNativeMongodbSpringStarterSmokeTest
extends AbstractMongodbSpringStarterSmokeTest {}

View File

@ -0,0 +1,11 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.spring.smoketest;
import org.junit.jupiter.api.condition.DisabledInNativeImage;
@DisabledInNativeImage // See GraalVmNativeMongodbSpringStarterSmokeTest for the GraalVM native test
public class MongoSpringStarterSmokeTest extends AbstractJvmMongodbSpringStarterSmokeTest {}

View File

@ -15,8 +15,10 @@ dependencies {
compileOnly("org.springframework.boot:spring-boot-starter-data-jdbc") compileOnly("org.springframework.boot:spring-boot-starter-data-jdbc")
compileOnly("org.apache.commons:commons-dbcp2") compileOnly("org.apache.commons:commons-dbcp2")
compileOnly("org.springframework.kafka:spring-kafka") compileOnly("org.springframework.kafka:spring-kafka")
compileOnly("org.springframework.boot:spring-boot-starter-data-mongodb")
compileOnly("org.testcontainers:junit-jupiter") compileOnly("org.testcontainers:junit-jupiter")
compileOnly("org.testcontainers:kafka") compileOnly("org.testcontainers:kafka")
compileOnly("org.testcontainers:mongodb")
api(project(":smoke-tests-otel-starter:spring-smoke-testing")) api(project(":smoke-tests-otel-starter:spring-smoke-testing"))

View File

@ -0,0 +1,65 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.spring.smoketest;
import com.mongodb.client.MongoClient;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration;
import io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.mongo.MongoClientInstrumentationAutoConfiguration;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.junit.jupiter.Container;
/** Spring has a test container integration, but that doesn't work for Spring Boot 2 */
public class AbstractJvmMongodbSpringStarterSmokeTest
extends AbstractMongodbSpringStarterSmokeTest {
@Container static MongoDBContainer container;
private ApplicationContextRunner contextRunner;
@BeforeAll
static void setUpContainer() {
container = new MongoDBContainer("mongo:4.0");
container.start();
}
@AfterAll
static void tearDownContainer() {
container.stop();
}
@BeforeEach
void setUpContext() {
contextRunner =
new ApplicationContextRunner()
.withAllowBeanDefinitionOverriding(true)
.withConfiguration(
AutoConfigurations.of(
OpenTelemetryAutoConfiguration.class,
SpringSmokeOtelConfiguration.class,
MongoAutoConfiguration.class,
MongoClientInstrumentationAutoConfiguration.class))
.withPropertyValues("spring.data.mongodb.uri=" + container.getReplicaSetUrl());
}
@Override
@Test
void mongodb() {
contextRunner.run(
applicationContext -> {
testing = new SpringSmokeTestRunner(applicationContext.getBean(OpenTelemetry.class));
mongoClient = applicationContext.getBean(MongoClient.class);
super.mongodb();
});
}
}

View File

@ -30,8 +30,6 @@ abstract class AbstractKafkaSpringStarterSmokeTest extends AbstractSpringStarter
@Test @Test
void shouldInstrumentProducerAndConsumer() { void shouldInstrumentProducerAndConsumer() {
testing.clearAllExportedData(); // ignore data from application startup
testing.runWithSpan( testing.runWithSpan(
"producer", "producer",
() -> { () -> {

View File

@ -0,0 +1,38 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.spring.smoketest;
import com.mongodb.client.MongoClient;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.semconv.incubating.DbIncubatingAttributes;
import java.util.ArrayList;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
abstract class AbstractMongodbSpringStarterSmokeTest extends AbstractSpringStarterSmokeTest {
@Autowired protected MongoClient mongoClient;
@Test
void mongodb() {
testing.runWithSpan(
"server",
() -> {
mongoClient.listDatabaseNames().into(new ArrayList<>());
});
testing.waitAndAssertTraces(
trace ->
trace.hasSpansSatisfyingExactly(
span -> span.hasName("server"),
span ->
span.hasKind(SpanKind.CLIENT)
.hasName("listDatabases admin")
.hasAttribute(
DbIncubatingAttributes.DB_SYSTEM,
DbIncubatingAttributes.DbSystemValues.MONGODB)));
}
}

View File

@ -31,12 +31,16 @@ import org.assertj.core.api.AbstractIterableAssert;
import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.jdbc.core.JdbcTemplate;
/** /**
* This test class enforces the order of the tests to make sure that {@link #shouldSendTelemetry()}, * This test class enforces the order of the tests to make sure that {@link #shouldSendTelemetry()},
@ -54,6 +58,14 @@ class AbstractOtelSpringStarterSmokeTest extends AbstractSpringStarterSmokeTest
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
static class TestConfiguration { static class TestConfiguration {
@Autowired private ObjectProvider<JdbcTemplate> jdbcTemplate;
@EventListener(ApplicationReadyEvent.class)
public void loadData() {
jdbcTemplate
.getObject()
.execute("create table test_table (id bigint not null, primary key (id))");
}
@Bean @Bean
@Order(1) @Order(1)

View File

@ -1,26 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.spring.smoketest;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
@Component
public class SqlExecutor {
private final JdbcTemplate jdbcTemplate;
public SqlExecutor(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@EventListener(ApplicationReadyEvent.class)
public void loadData() {
jdbcTemplate.execute("create table test_table (id bigint not null, primary key (id))");
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.spring.smoketest;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EnabledIfEnvironmentVariable(
named = "GITHUB_ACTIONS",
matches = "true",
disabledReason =
"Not currently executing within GitHub actions where the required external "
+ "services (e.g. mongodb) are started using docker.")
public @interface EnabledInGithubActions {}