Skip to content
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
29 changes: 23 additions & 6 deletions .github/quarkus-github-bot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,21 +116,28 @@ triage:
- devtools/platform-descriptor-json/src/main/resources/templates/
- id: hibernate-reactive
labels: [area/hibernate-reactive]
title: "hibernate.reactive"
expression: |
// Fuzzy match; to avoid false positives we match on title and add exclusions on title+body
matches("hibernate", title) && matches("reactive", title)
&& !matches("hibernate.validator", title)
&& !matches("hibernate.validator", titleBody)
&& !matches("hibernate.search", titleBody)
// Stricter match; lesser risk of false positive so we match on title+body and don't add exclusions
|| matches("hibernate.reactive", titleBody)
notify: [DavideD, gavinking]
directories:
- extensions/hibernate-reactive
- id: hibernate-orm
labels: [area/hibernate-orm]
expression: |
matches("hibernate", title) && !matches("reactive", title)
&& !matches("hibernate.validator", title)
// Fuzzy match; to avoid false positives we match on title and add exclusions on title+body
matches("hibernate", title)
&& !matches("reactive", title)
&& !matches("hibernate.validator", titleBody)
&& !matches("hibernate.search", titleBody)
&& !matches("hibernate.reactive", titleBody)
// Stricter match; lesser risk of false positive so we match on title+body and don't add exclusions
|| matches("hibernate.orm", titleBody)
|| matches("hibernate-core", titleBody)
notify: [gsmet]
notifyInPullRequest: true
directories:
Expand All @@ -144,7 +151,11 @@ triage:
- integration-tests/infinispan-cache-jpa
- id: hibernate-search
labels: [area/hibernate-search]
title: "hibernate.search"
expression: |
// Fuzzy match; to avoid false positives we match on title
matches("hibernate", title) && matches("search", title)
// Stricter match; lesser risk of false positive so we match on title+body and don't add exclusions
|| matches("hibernate.search", titleBody)
notify: [gsmet, marko-bekhta]
notifyInPullRequest: true
directories:
Expand All @@ -162,7 +173,13 @@ triage:
- integration-tests/elasticsearch
- id: hibernate-validator
labels: [area/hibernate-validator]
title: "hibernate.validator"
expression: |
// Fuzzy match; to avoid false positives we match on title
(matches("hibernate", title) && matches("validator", title)
|| matches("jakarta", title) && matches("validation", title))
// Stricter match; lesser risk of false positive so we match on title+body and don't add exclusions
|| matches("hibernate.validator", titleBody)
|| matches("jakarta.validation", titleBody)
notify: [gsmet, marko-bekhta]
directories:
# No trailing slashes: we also match sibling directories starting with these names
Expand Down
2 changes: 1 addition & 1 deletion devtools/gradle/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
plugin-publish = "2.1.0"
plugin-publish = "2.1.1"

kotlin = "2.3.10"
smallrye-config = "3.16.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,4 +220,13 @@ public interface KeycloakDevServicesConfig {
@WithDefault("4S")
Duration webClientTimeout();

/**
* Specifies whether to disable HTTPS on the master realm by setting {@code sslRequired=NONE}.
*
* This is useful when the Keycloak container is started without HTTPS support and the master realm's
* default SSL requirement prevents HTTP access.
*/
@WithDefault("false")
boolean disableHttps();

}
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@ private static String getServiceConfigIdentifier(KeycloakDevServicesConfig confi
config.port(),
safeMapHash(config.containerEnv()),
config.containerMemoryLimit(),
config.webClientTimeout());
config.webClientTimeout(),
config.disableHttps());
serviceConfigIdentifier.append(configHashCode);

for (int fileTimeHashCode : getRealmFileLastModifiedDateHashCode(config.realmPath())) {
Expand Down Expand Up @@ -511,9 +512,31 @@ private QuarkusOidcContainer(KeycloakDevServicesConfig config, DockerImageName d
@Override
public void start() {
super.start();
if (config.disableHttps()) {
disableMasterRealmHttpsRequirement();
}
configPropertiesContext = createConfigPropertiesContext();
}

private void disableMasterRealmHttpsRequirement() {
try {
var configResult = execInContainer("/opt/keycloak/bin/kcadm.sh",
"config", "credentials", "--server", "http://localhost:8080",
"--realm", "master", "--user", KEYCLOAK_ADMIN_USER, "--password", KEYCLOAK_ADMIN_PASSWORD);
if (configResult.getExitCode() != 0) {
LOG.errorf("Failed to configure kcadm.sh credentials: %s", configResult.getStderr());
return;
}
var updateResult = execInContainer("/opt/keycloak/bin/kcadm.sh",
"update", "realms/master", "-s", "sslRequired=NONE");
if (updateResult.getExitCode() != 0) {
LOG.errorf("Failed to disable HTTPS on the master realm: %s", updateResult.getStderr());
}
} catch (IOException | InterruptedException e) {
LOG.error("Failed to disable HTTPS on the master realm", e);
}
}

@Override
protected void configure() {
super.configure();
Expand Down
17 changes: 17 additions & 0 deletions extensions/devui/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,23 @@
</capabilities>
</configuration>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
<version>${project.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.quarkus.devui.runtime.mcp;

import java.util.Optional;

import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
import io.smallrye.config.ConfigMapping;

@ConfigRoot(phase = ConfigPhase.RUN_TIME)
@ConfigMapping(prefix = "quarkus.dev-mcp")
public interface DevMcpConfig {

/**
* Enable/Disable the Dev MCP server.
* This overrides the value in ~/.quarkus/dev-mcp.properties.
*/
Optional<Boolean> enabled();
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Inject;

import org.jboss.logging.Logger;

Expand All @@ -38,9 +39,16 @@ public class McpDevUIJsonRpcService {
private final Set<McpClientInfo> connectedClients = new HashSet<>();
private McpServerConfiguration mcpServerConfiguration;

@Inject
DevMcpConfig devMcpConfig;

@PostConstruct
public void init() {
this.mcpServerConfiguration = new McpServerConfiguration(loadConfiguration());

// Allow SmallRye Config sources (system properties, env vars, application.properties)
// to override the ~/.quarkus/dev-mcp.properties file setting
devMcpConfig.enabled().ifPresent(this.mcpServerConfiguration::setEnable);
}

public Set<McpClientInfo> getConnectedClients() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ public void build(
CombinedIndexBuildItem indexBuildItem,
BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
BuildProducer<ReflectiveMethodBuildItem> reflectiveMethod,
BuildProducer<ReflectiveClassConditionBuildItem> reflectiveClassCondition,
BuildProducer<ServiceProviderBuildItem> serviceProviders,
BuildProducer<NativeImageProxyDefinitionBuildItem> proxies,
Capabilities capabilities,
Expand Down Expand Up @@ -249,9 +250,11 @@ public void build(
.reason(getClass().getName() + " OAuthBearerSaslClient classes")
.build());

// This is done to avoid loading jose4j classes when not needed, as DefaultJwtValidator is the default validator used by Kafka clients if no other validator is specified.
reflectiveMethod.produce(new ReflectiveMethodBuildItem(getClass().getName() + " DefaultJwtValidator class",
DefaultJwtValidator.class.getName(), "<init>", new String[0]));
// Register DefaultJwtValidator only when jose4j is present to avoid NoClassDefFoundError
// with GraalVM 25's --future-defaults=complete-reflection-types flag
reflectiveClassCondition.produce(new ReflectiveClassConditionBuildItem(
DefaultJwtValidator.class.getName(),
"org.jose4j.keys.resolvers.VerificationKeyResolver"));

for (Class<?> i : BUILT_INS) {
reflectiveClass.produce(ReflectiveClassBuildItem.builder(i.getName())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package io.quarkus.opentelemetry.deployment.instrumentation;

import static io.opentelemetry.api.trace.SpanKind.SERVER;
import static io.restassured.RestAssured.given;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;

import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.slf4j.LoggerFactory;

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.trace.data.SpanData;
import io.quarkus.opentelemetry.deployment.common.SemconvResolver;
import io.quarkus.opentelemetry.deployment.common.exporter.InMemoryLogRecordExporterProvider;
import io.quarkus.opentelemetry.deployment.common.exporter.InMemoryMetricExporterProvider;
import io.quarkus.opentelemetry.deployment.common.exporter.TestSpanExporter;
import io.quarkus.opentelemetry.deployment.common.exporter.TestSpanExporterProvider;
import io.quarkus.opentelemetry.runtime.OpenTelemetryUtil;
import io.quarkus.test.QuarkusUnitTest;

/**
* Regression test for <a href="https://github.com/quarkusio/quarkus/issues/52239">#52239</a>.
*/
public class RestClientReadTimeoutOpenTelemetryTest {

static final AtomicReference<String> TRACE_ID_AFTER_SLEEP = new AtomicReference<>();
static final CountDownLatch SLOW_HANDLER_DONE = new CountDownLatch(1);

@RegisterExtension
static final QuarkusUnitTest TEST = new QuarkusUnitTest().withApplicationRoot((jar) -> jar
.addPackage(TestSpanExporter.class.getPackage())
.addClasses(SemconvResolver.class)
.addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()),
"META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider")
.addAsResource(new StringAsset(InMemoryMetricExporterProvider.class.getCanonicalName()),
"META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider")
.addAsResource(new StringAsset(InMemoryLogRecordExporterProvider.class.getCanonicalName()),
"META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider"))
.withConfigurationResource("application-default.properties")
.overrideConfigKey("quarkus.log.console.format",
"%d{HH:mm:ss} %-5p traceId=%X{traceId}, spanId=%X{spanId} [%c{2.}] (%t) %s%e%n")
.overrideConfigKey("quarkus.log.category.\"io.quarkus.opentelemetry\".level", "DEBUG")
.overrideConfigKey("quarkus.rest-client.slow-client.url", "${test.url}")
.overrideConfigKey("quarkus.rest-client.slow-client.read-timeout", "3000");
private static final org.slf4j.Logger log = LoggerFactory.getLogger(RestClientReadTimeoutOpenTelemetryTest.class);

@Inject
TestSpanExporter spanExporter;

@AfterEach
void tearDown() {
spanExporter.reset();
TRACE_ID_AFTER_SLEEP.set(null);
}

@Test
void readTimeoutDoesNotLoseServerOtelContext() throws InterruptedException {
given().get("/caller").then().statusCode(200);

// Wait for the slow handler to complete (it sleeps 1s, timeout is 0.5s)
SLOW_HANDLER_DONE.await(3, SECONDS);

// Wait for spans to be exported. We expect at least:
// 1. server span for GET /caller
// 2. server span for GET /slow (ended when connection was reset)
List<SpanData> spans = spanExporter.getFinishedSpanItemsAtLeast(2);

// Verify exactly one server span for /slow (no duplicates from double sendResponse)
long slowServerSpanCount = spans.stream()
.filter(s -> s.getKind() == SERVER && s.getName().contains("slow"))
.count();
assertEquals(1, slowServerSpanCount, "Expected exactly one server span for /slow, got: " + spans);

// Verify the server span for /slow has an error (connection closed)
SpanData slowServerSpan = spans.stream()
.filter(s -> s.getKind() == SERVER && s.getName().contains("slow"))
.findFirst()
.orElseThrow();
assertEquals(StatusCode.ERROR, slowServerSpan.getStatus().getStatusCode());

// Verify the traceId was still valid after the sleep (the core assertion for #52239)
String traceIdAfterSleep = TRACE_ID_AFTER_SLEEP.get();
assertNotEquals(null, traceIdAfterSleep, "Slow handler did not capture traceId after sleep");
assertNotEquals("00000000000000000000000000000000", traceIdAfterSleep,
"OTel context was lost: Span.current() returned invalid traceId after client disconnect");

// Verify the traceId matches the exported server span
assertEquals(slowServerSpan.getTraceId(), traceIdAfterSleep,
"TraceId after sleep should match the exported server span's traceId");
}

@Path("/caller")
public static class CallerResource {
private static final Logger logger = Logger.getLogger(CallerResource.class);

@Inject
@RestClient
SlowClient slowClient;

@GET
public String call() {
try {
logger.infov("Calling Slow");
String slow = slowClient.slow();
logger.infov("client received: {0}", slow);
return slow;
} catch (Exception e) {
return "timeout";
}
}
}

@RegisterRestClient(configKey = "slow-client")
@Path("/slow")
public interface SlowClient {
@GET
String slow();
}

@Path("/slow")
public static class SlowResource {
private static final Logger logger = Logger.getLogger(SlowResource.class);

@GET
public String slow() throws InterruptedException {
logger.infov("Span before sleep: {0}", OpenTelemetryUtil.getSpanData(Context.current()));

try {
// Sleep longer than the client read timeout (1s) to trigger a connection reset
Thread.sleep(5000);
// Capture the traceId AFTER the client has disconnected
TRACE_ID_AFTER_SLEEP.set(Span.current().getSpanContext().getTraceId());
logger.infov("Span after sleep: {0}", OpenTelemetryUtil.getSpanData(Context.current()));
return "slow response";
} finally {
SLOW_HANDLER_DONE.countDown();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ public class OtelLoggingTest {
"META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider")
.add(new StringAsset(
"quarkus.otel.logs.enabled=true\n" +
"quarkus.otel.traces.enabled=true\n"),
"quarkus.otel.traces.enabled=true\n" +
"quarkus.log.category.\"io.quarkus.opentelemetry\".level=INFO\n"),
"application.properties"));

@Inject
Expand Down
Loading
Loading