diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java index b5b404f9f8120..350a89459e648 100644 --- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java @@ -282,8 +282,10 @@ void setupVertx(InstrumentationRecorder recorder, BeanContainerBuildItem beanCon || capabilities.isPresent(Capability.REACTIVE_MYSQL_CLIENT) || capabilities.isPresent(Capability.REACTIVE_ORACLE_CLIENT) || capabilities.isPresent(Capability.REACTIVE_PG_CLIENT); + boolean redisClientAvailable = capabilities.isPresent(Capability.REDIS_CLIENT); recorder.setupVertxTracer(beanContainerBuildItem.getValue(), sqlClientAvailable, + redisClientAvailable, ConfigProvider.getConfig() .getConfigValue(QUARKUS_OTEL_SEMCONV_STABILITY_OPT_IN) .getValue()); diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/InstrumentRuntimeConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/InstrumentRuntimeConfig.java index f5c5cdddd104b..c781da7e17717 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/InstrumentRuntimeConfig.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/InstrumentRuntimeConfig.java @@ -24,4 +24,10 @@ public interface InstrumentRuntimeConfig { @WithDefault("true") boolean vertxSqlClient(); + /** + * Enables instrumentation for Vert.x Redis Client. + */ + @WithDefault("true") + boolean vertxRedisClient(); + } diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/InstrumentationRecorder.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/InstrumentationRecorder.java index f155facf34c8a..debb921c7350b 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/InstrumentationRecorder.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/InstrumentationRecorder.java @@ -15,6 +15,7 @@ import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.OpenTelemetryVertxMetricsFactory; import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.OpenTelemetryVertxTracer; import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.OpenTelemetryVertxTracingFactory; +import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.RedisClientInstrumenterVertxTracer; import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.SqlClientInstrumenterVertxTracer; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; @@ -42,9 +43,9 @@ public Consumer getVertxTracingOptions() { /* RUNTIME INIT */ public void setupVertxTracer(BeanContainer beanContainer, boolean sqlClientAvailable, - final String semconvStability) { + boolean redisClientAvailable, final String semconvStability) { OpenTelemetry openTelemetry = beanContainer.beanInstance(OpenTelemetry.class); - List> tracers = new ArrayList<>(3); + List> tracers = new ArrayList<>(4); if (config.getValue().instrument().vertxHttp()) { tracers.add(new HttpInstrumenterVertxTracer(openTelemetry, getSemconvStabilityOptin(semconvStability))); } @@ -54,6 +55,9 @@ public void setupVertxTracer(BeanContainer beanContainer, boolean sqlClientAvail if (sqlClientAvailable && config.getValue().instrument().vertxSqlClient()) { tracers.add(new SqlClientInstrumenterVertxTracer(openTelemetry)); } + if (redisClientAvailable && config.getValue().instrument().vertxRedisClient()) { + tracers.add(new RedisClientInstrumenterVertxTracer(openTelemetry)); + } OpenTelemetryVertxTracer openTelemetryVertxTracer = new OpenTelemetryVertxTracer(tracers); FACTORY.getVertxTracerDelegator().setDelegate(openTelemetryVertxTracer); } diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/RedisClientInstrumenterVertxTracer.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/RedisClientInstrumenterVertxTracer.java new file mode 100644 index 0000000000000..445cd66c087a4 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/RedisClientInstrumenterVertxTracer.java @@ -0,0 +1,179 @@ +package io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx; + +import static io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.INSTRUMENTATION_NAME; + +import java.util.Map; +import java.util.function.BiConsumer; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesGetter; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtractor; +import io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil; +import io.opentelemetry.semconv.SemanticAttributes; +import io.vertx.core.Context; +import io.vertx.core.spi.tracing.SpanKind; +import io.vertx.core.spi.tracing.TagExtractor; +import io.vertx.core.tracing.TracingPolicy; + +public class RedisClientInstrumenterVertxTracer implements + InstrumenterVertxTracer { + private final Instrumenter redisClientInstrumenter; + + public RedisClientInstrumenterVertxTracer(final OpenTelemetry openTelemetry) { + InstrumenterBuilder clientInstrumenterBuilder = Instrumenter.builder( + openTelemetry, + INSTRUMENTATION_NAME, + DbClientSpanNameExtractor.create(RedisClientAttributesGetter.INSTANCE)); + + this.redisClientInstrumenter = clientInstrumenterBuilder + .addAttributesExtractor(DbClientAttributesExtractor.create(RedisClientAttributesGetter.INSTANCE)) + .addAttributesExtractor(RedisClientAttributesExtractor.INSTANCE) + .buildInstrumenter(SpanKindExtractor.alwaysClient()); + } + + @Override + public boolean canHandle(R request, TagExtractor tagExtractor) { + if (request instanceof CommandTrace) { + return true; + } + + return "redis".equals(tagExtractor.extract(request).get("db.type")); + } + + @Override + @SuppressWarnings("unchecked") + public OpenTelemetryVertxTracer.SpanOperation sendRequest( + final Context context, + final SpanKind kind, + final TracingPolicy policy, + final R request, + final String operation, + final BiConsumer headers, + final TagExtractor tagExtractor) { + R commandTrace = (R) CommandTrace.commandTrace(tagExtractor.extract(request)); + return InstrumenterVertxTracer.super.sendRequest(context, kind, policy, commandTrace, operation, headers, tagExtractor); + } + + @Override + public void receiveResponse( + final Context context, + final R response, + final OpenTelemetryVertxTracer.SpanOperation spanOperation, + final Throwable failure, + final TagExtractor tagExtractor) { + + InstrumenterVertxTracer.super.receiveResponse(context, response, spanOperation, failure, tagExtractor); + } + + @Override + public Instrumenter getReceiveRequestInstrumenter() { + return null; + } + + @Override + public Instrumenter getSendResponseInstrumenter() { + return null; + } + + @Override + public Instrumenter getSendRequestInstrumenter() { + return redisClientInstrumenter; + } + + @Override + public Instrumenter getReceiveResponseInstrumenter() { + return redisClientInstrumenter; + } + + // From io.vertx.redis.client.impl.CommandReporter + static class CommandTrace { + static final String DB_STATEMENT = "db.statement"; + static final String DB_USER = "db.user"; + static final String PEER_ADDRESS = "peer.address"; + static final String DB_INSTANCE = "db.instance"; + + private final Map attributes; + + CommandTrace(final Map attributes) { + this.attributes = attributes; + } + + static CommandTrace commandTrace(final Map attributes) { + return new CommandTrace(attributes); + } + + public String operation() { + return attributes.get(DB_STATEMENT); + } + + public String user() { + return attributes.get(DB_USER); + } + + public String peerAddress() { + return attributes.get(PEER_ADDRESS); + } + + public long dbIndex() { + return Long.parseLong(attributes.get(DB_INSTANCE)); + } + } + + enum RedisClientAttributesGetter implements DbClientAttributesGetter { + INSTANCE; + + @Override + public String getStatement(final CommandTrace commandTrace) { + return null; + } + + @Override + public String getOperation(final CommandTrace commandTrace) { + return commandTrace.operation(); + } + + @Override + public String getSystem(final CommandTrace commandTrace) { + return SemanticAttributes.DbSystemValues.REDIS; + } + + @Override + public String getUser(final CommandTrace commandTrace) { + return commandTrace.user(); + } + + @Override + public String getName(final CommandTrace commandTrace) { + return null; + } + + @Override + public String getConnectionString(final CommandTrace commandTrace) { + return commandTrace.peerAddress(); + } + } + + enum RedisClientAttributesExtractor implements AttributesExtractor { + INSTANCE; + + @Override + public void onStart(AttributesBuilder attributes, io.opentelemetry.context.Context parentContext, + CommandTrace request) { + AttributesExtractorUtil.internalSet(attributes, SemanticAttributes.DB_REDIS_DATABASE_INDEX, request.dbIndex()); + } + + @Override + public void onEnd(AttributesBuilder attributes, + io.opentelemetry.context.Context context, + CommandTrace request, + Object response, + Throwable error) { + } + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/SqlClientInstrumenterVertxTracer.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/SqlClientInstrumenterVertxTracer.java index f5fc8922af2c9..b8bfdda2a408a 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/SqlClientInstrumenterVertxTracer.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/SqlClientInstrumenterVertxTracer.java @@ -38,7 +38,7 @@ public boolean canHandle(final R request, final TagExtractor tagExtractor return true; } - return tagExtractor.extract(request).containsKey("db.statement"); + return "sql".equals(tagExtractor.extract(request).get("db.type")); } @Override @@ -87,7 +87,7 @@ public Instrumenter getReceiveResponseInstrumenter() { return sqlClientInstrumenter; } - // From io.vertx.sqlclient.impl.tracing.QueryTracer + // From io.vertx.sqlclient.impl.tracing.QueryReporter static class QueryTrace { private final Map attributes; diff --git a/integration-tests/opentelemetry-redis-instrumentation/pom.xml b/integration-tests/opentelemetry-redis-instrumentation/pom.xml new file mode 100644 index 0000000000000..4dd118b520a01 --- /dev/null +++ b/integration-tests/opentelemetry-redis-instrumentation/pom.xml @@ -0,0 +1,159 @@ + + + 4.0.0 + + + io.quarkus + quarkus-integration-tests-parent + 999-SNAPSHOT + + + quarkus-integration-test-opentelemetry-redis-instrumentation + Quarkus - Integration Tests - OpenTelemetry Redis instrumentation + + + + io.quarkus + quarkus-opentelemetry + + + + + io.quarkus + quarkus-rest-jackson + + + + + io.opentelemetry + opentelemetry-sdk-testing + + + + io.quarkus + quarkus-redis-client + + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.awaitility + awaitility + test + + + + + io.quarkus + quarkus-rest-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-opentelemetry-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-redis-client-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + src/main/resources + true + + + + + maven-surefire-plugin + + true + + + + maven-failsafe-plugin + + true + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + + test-redis + + + test-containers + + + + + + maven-surefire-plugin + + false + + + + maven-failsafe-plugin + + false + + + + + + + diff --git a/integration-tests/opentelemetry-redis-instrumentation/src/main/java/io/quarkus/io/opentelemetry/ExporterResource.java b/integration-tests/opentelemetry-redis-instrumentation/src/main/java/io/quarkus/io/opentelemetry/ExporterResource.java new file mode 100644 index 0000000000000..dad9f1b70f8dc --- /dev/null +++ b/integration-tests/opentelemetry-redis-instrumentation/src/main/java/io/quarkus/io/opentelemetry/ExporterResource.java @@ -0,0 +1,46 @@ +package io.quarkus.io.opentelemetry; + +import java.util.List; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; + +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.data.SpanData; + +@Path("/opentelemetry") +public class ExporterResource { + @Inject + InMemorySpanExporter inMemorySpanExporter; + + @GET + @Path("/reset") + public Response reset() { + inMemorySpanExporter.reset(); + return Response.ok().build(); + } + + @GET + @Path("/export") + public List export() { + return inMemorySpanExporter.getFinishedSpanItems() + .stream() + .filter(sd -> !sd.getName().contains("export") && !sd.getName().contains("reset")) + .collect(Collectors.toList()); + } + + @ApplicationScoped + static class InMemorySpanExporterProducer { + @Produces + @Singleton + InMemorySpanExporter inMemorySpanExporter() { + return InMemorySpanExporter.create(); + } + } +} diff --git a/integration-tests/opentelemetry-redis-instrumentation/src/main/java/io/quarkus/io/opentelemetry/RedisResource.java b/integration-tests/opentelemetry-redis-instrumentation/src/main/java/io/quarkus/io/opentelemetry/RedisResource.java new file mode 100644 index 0000000000000..6d54d94df2376 --- /dev/null +++ b/integration-tests/opentelemetry-redis-instrumentation/src/main/java/io/quarkus/io/opentelemetry/RedisResource.java @@ -0,0 +1,71 @@ +package io.quarkus.io.opentelemetry; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; + +import io.quarkus.redis.datasource.ReactiveRedisDataSource; +import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.redis.datasource.value.ReactiveValueCommands; +import io.quarkus.redis.datasource.value.ValueCommands; +import io.smallrye.mutiny.Uni; + +@Path("/redis") +public class RedisResource { + + private final ValueCommands blocking; + private final ReactiveValueCommands reactive; + private final RedisDataSource ds; + private final ReactiveRedisDataSource reactiveDs; + + @Inject + public RedisResource(RedisDataSource ds, + ReactiveRedisDataSource reactiveDs) { + this.blocking = ds.value(String.class); + this.reactive = reactiveDs.value(String.class); + this.ds = ds; + this.reactiveDs = reactiveDs; + } + + // synchronous + @GET + @Path("/sync/{key}") + public String getSync(@PathParam("key") String key) { + return blocking.get(key); + } + + @POST + @Path("/sync/{key}") + public void setSync(@PathParam("key") String key, String value) { + blocking.set(key, value); + } + + @POST + @Path("/sync/invalid-operation") + public void getInvalidOperation() { + ds.execute("bazinga"); + } + + // reactive + @GET + @Path("/reactive/{key}") + public Uni getReactive(@PathParam("key") String key) { + return reactive.get(key); + } + + @POST + @Path("/reactive/{key}") + public Uni setReactive(@PathParam("key") String key, String value) { + return this.reactive.set(key, value); + } + + @POST + @Path("/reactive/invalid-operation") + public Uni getReactiveInvalidOperation() { + return reactiveDs.execute("bazinga") + .replaceWithVoid(); + } + +} diff --git a/integration-tests/opentelemetry-redis-instrumentation/src/main/java/io/quarkus/io/opentelemetry/jackson/ErrorTypeSerializer.java b/integration-tests/opentelemetry-redis-instrumentation/src/main/java/io/quarkus/io/opentelemetry/jackson/ErrorTypeSerializer.java new file mode 100644 index 0000000000000..7c38303038e44 --- /dev/null +++ b/integration-tests/opentelemetry-redis-instrumentation/src/main/java/io/quarkus/io/opentelemetry/jackson/ErrorTypeSerializer.java @@ -0,0 +1,24 @@ +package io.quarkus.io.opentelemetry.jackson; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import io.vertx.redis.client.impl.types.ErrorType; + +public class ErrorTypeSerializer extends StdSerializer { + public ErrorTypeSerializer() { + super(ErrorType.class); + } + + @Override + public void serialize(ErrorType errorType, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) + throws IOException { + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringField("type", errorType.type().name()); + jsonGenerator.writeStringField("message", errorType.getMessage()); + jsonGenerator.writeEndObject(); + } +} diff --git a/integration-tests/opentelemetry-redis-instrumentation/src/main/java/io/quarkus/io/opentelemetry/jackson/RegisterCustomModuleCustomizer.java b/integration-tests/opentelemetry-redis-instrumentation/src/main/java/io/quarkus/io/opentelemetry/jackson/RegisterCustomModuleCustomizer.java new file mode 100644 index 0000000000000..ba6af56077380 --- /dev/null +++ b/integration-tests/opentelemetry-redis-instrumentation/src/main/java/io/quarkus/io/opentelemetry/jackson/RegisterCustomModuleCustomizer.java @@ -0,0 +1,19 @@ +package io.quarkus.io.opentelemetry.jackson; + +import jakarta.inject.Singleton; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import io.quarkus.jackson.ObjectMapperCustomizer; +import io.vertx.redis.client.impl.types.ErrorType; + +@Singleton +public class RegisterCustomModuleCustomizer implements ObjectMapperCustomizer { + + public void customize(ObjectMapper mapper) { + SimpleModule module = new SimpleModule(); + module.addSerializer(ErrorType.class, new ErrorTypeSerializer()); + mapper.registerModule(module); + } +} diff --git a/integration-tests/opentelemetry-redis-instrumentation/src/main/resources/application.properties b/integration-tests/opentelemetry-redis-instrumentation/src/main/resources/application.properties new file mode 100644 index 0000000000000..ed666d4a38ae3 --- /dev/null +++ b/integration-tests/opentelemetry-redis-instrumentation/src/main/resources/application.properties @@ -0,0 +1,10 @@ +# Setting these for tests explicitly. Not required in normal application +quarkus.application.name=opentelemetry-redis-instrumentation-it +quarkus.application.version=999-SNAPSHOT + +# set Redis port +quarkus.redis.devservices.port=16379 + +# speed up build +quarkus.otel.bsp.schedule.delay=100 +quarkus.otel.bsp.export.timeout=5s diff --git a/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisIT.java b/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisIT.java new file mode 100644 index 0000000000000..f67e195d26ce9 --- /dev/null +++ b/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisIT.java @@ -0,0 +1,12 @@ +package io.quarkus.it.opentelemetry; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class QuarkusOpenTelemetryRedisIT extends QuarkusOpenTelemetryRedisTest { + + @Override + String getKey(String k) { + return "native-" + k; + } +} diff --git a/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisTest.java b/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisTest.java new file mode 100644 index 0000000000000..f10a0be951849 --- /dev/null +++ b/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisTest.java @@ -0,0 +1,204 @@ +package io.quarkus.it.opentelemetry; + +import static io.opentelemetry.semconv.SemanticAttributes.DB_CONNECTION_STRING; +import static io.opentelemetry.semconv.SemanticAttributes.DB_OPERATION; +import static io.opentelemetry.semconv.SemanticAttributes.DB_REDIS_DATABASE_INDEX; +import static io.opentelemetry.semconv.SemanticAttributes.DB_SYSTEM; +import static io.opentelemetry.semconv.SemanticAttributes.DbSystemValues.REDIS; +import static io.restassured.RestAssured.get; +import static io.restassured.RestAssured.given; +import static java.net.HttpURLConnection.HTTP_OK; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Predicate; + +import org.awaitility.Awaitility; +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.opentelemetry.api.trace.SpanKind; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; +import io.restassured.common.mapper.TypeRef; +import io.vertx.mutiny.redis.client.Command; + +@QuarkusTest +@SuppressWarnings("unchecked") +class QuarkusOpenTelemetryRedisTest { + static final String SYNC_KEY = "sync-key"; + static final String SYNC_VALUE = "sync-value"; + static final String REACTIVE_KEY = "reactive-key"; + static final String REACTIVE_VALUE = "reactive-value"; + static final String INVALID_OPERATION_PATH = "invalid-operation"; + static final String CONNECTION_STRING = "localhost:16379"; + + String getKey(String k) { + return k; + } + + @BeforeEach + void reset() { + given().get("/opentelemetry/reset").then().statusCode(HTTP_OK); + await().atMost(5, SECONDS).until(() -> getSpans().isEmpty()); + } + + @Test + public void syncValidOperation() { + String path = String.format("redis/sync/%s", getKey(SYNC_KEY)); + String getCommand = Command.GET.toString(); + String setCommand = Command.SET.toString(); + + RestAssured.given() + .body(SYNC_VALUE) + .when() + .post(path) + .then() + .statusCode(204); + + RestAssured.given() + .when() + .get(path) + .then() + .statusCode(200) + .body(CoreMatchers.is(SYNC_VALUE)); + + Awaitility.await().atMost(Duration.ofSeconds(10)).until(() -> getSpans().size() == 4); + + List> spans = getSpans(); + + Map setSpan = findSpan(spans, + m -> SpanKind.CLIENT.name().equals(m.get("kind")) && setCommand.equals(m.get("name"))); + Map getSpan = findSpan(spans, + m -> SpanKind.CLIENT.name().equals(m.get("kind")) && getCommand.equals(m.get("name"))); + + Map setAttributes = (Map) setSpan.get("attributes"); + Map getAttributes = (Map) getSpan.get("attributes"); + + // SET + assertEquals(setCommand, setSpan.get("name")); + assertEquals(setCommand, setAttributes.get(DB_OPERATION.getKey())); + assertEquals(REDIS, setAttributes.get(DB_SYSTEM.getKey())); + assertEquals(CONNECTION_STRING, setAttributes.get(DB_CONNECTION_STRING.getKey())); + assertEquals(0, setAttributes.get(DB_REDIS_DATABASE_INDEX.getKey())); + // GET + assertEquals(getCommand, getSpan.get("name")); + assertEquals(getCommand, getAttributes.get(DB_OPERATION.getKey())); + assertEquals(REDIS, getAttributes.get(DB_SYSTEM.getKey())); + assertEquals(CONNECTION_STRING, getAttributes.get(DB_CONNECTION_STRING.getKey())); + assertEquals(0, getAttributes.get(DB_REDIS_DATABASE_INDEX.getKey())); + } + + @Test + public void syncInvalidOperation() { + String path = String.format("redis/sync/%s", getKey(INVALID_OPERATION_PATH)); + + RestAssured.post(path) + .then() + .statusCode(500); + + Awaitility.await().atMost(Duration.ofSeconds(10)).until(() -> getSpans().size() == 2); + + Map span = findSpan(getSpans(), m -> SpanKind.CLIENT.name().equals(m.get("kind"))); + + Map status = (Map) span.get("status"); + Map event = ((List>) span.get("events")).get(0); + Map exception = (Map) event.get("exception"); + + assertEquals("bazinga", span.get("name")); + assertEquals("ERROR", status.get("statusCode")); + assertEquals("exception", event.get("name")); + assertThat((String) exception.get("message"), containsString("ERR unknown command 'bazinga'")); + } + + @Test + public void reactiveValidOperation() { + String path = String.format("redis/reactive/%s", getKey(REACTIVE_KEY)); + String getCommand = Command.GET.toString(); + String setCommand = Command.SET.toString(); + + RestAssured.given() + .body(REACTIVE_VALUE) + .when() + .post(path) + .then() + .statusCode(204); + + RestAssured.given() + .when() + .get(path) + .then() + .statusCode(200) + .body(CoreMatchers.is(REACTIVE_VALUE)); + + Awaitility.await().atMost(Duration.ofSeconds(10)).until(() -> getSpans().size() == 4); + + List> spans = getSpans(); + + Map setSpan = findSpan(spans, + m -> SpanKind.CLIENT.name().equals(m.get("kind")) && setCommand.equals(m.get("name"))); + Map getSpan = findSpan(spans, + m -> SpanKind.CLIENT.name().equals(m.get("kind")) && getCommand.equals(m.get("name"))); + + Map setAttributes = (Map) setSpan.get("attributes"); + Map getAttributes = (Map) getSpan.get("attributes"); + + // SET + assertEquals(setCommand, setSpan.get("name")); + assertEquals(setCommand, setAttributes.get(DB_OPERATION.getKey())); + assertEquals(REDIS, setAttributes.get(DB_SYSTEM.getKey())); + assertEquals(CONNECTION_STRING, setAttributes.get(DB_CONNECTION_STRING.getKey())); + assertEquals(0, setAttributes.get(DB_REDIS_DATABASE_INDEX.getKey())); + // GET + assertEquals(getCommand, getSpan.get("name")); + assertEquals(getCommand, getAttributes.get(DB_OPERATION.getKey())); + assertEquals(REDIS, getAttributes.get(DB_SYSTEM.getKey())); + assertEquals(CONNECTION_STRING, getAttributes.get(DB_CONNECTION_STRING.getKey())); + assertEquals(0, getAttributes.get(DB_REDIS_DATABASE_INDEX.getKey())); + } + + @Test + public void reactiveInvalidOperation() { + String path = String.format("redis/reactive/%s", getKey(INVALID_OPERATION_PATH)); + + RestAssured.post(path) + .then() + .statusCode(500); + + Awaitility.await().atMost(Duration.ofSeconds(10)).until(() -> getSpans().size() == 2); + + Map span = findSpan(getSpans(), m -> SpanKind.CLIENT.name().equals(m.get("kind"))); + + Map status = (Map) span.get("status"); + Map event = ((List>) span.get("events")).get(0); + Map exception = (Map) event.get("exception"); + + assertEquals("bazinga", span.get("name")); + assertEquals("ERROR", status.get("statusCode")); + assertEquals("exception", event.get("name")); + assertThat((String) exception.get("message"), containsString("ERR unknown command 'bazinga'")); + } + + private List> getSpans() { + return get("/opentelemetry/export").body().as(new TypeRef<>() { + }); + } + + private static Map findSpan(List> spans, + Predicate> spanDataSelector) { + Optional> select = spans.stream().filter(spanDataSelector).findFirst(); + Assertions.assertTrue(select.isPresent()); + Map spanData = select.get(); + Assertions.assertNotNull(spanData.get("spanId")); + return spanData; + } +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index bd23f41c363e2..9db81c251220a 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -356,6 +356,7 @@ opentelemetry-grpc opentelemetry-vertx-exporter opentelemetry-reactive-messaging + opentelemetry-redis-instrumentation logging-json jaxb jaxp