From e45f273577b1a6c83bb9cdc6b17ffbb220ff8baa Mon Sep 17 00:00:00 2001 From: Ruchika Date: Wed, 15 Jul 2026 12:34:01 +0100 Subject: [PATCH 1/5] Implementation for the ldap connect server metric. Closes #50840 Signed-off-by: Ruchika --- .../storage/ldap/idm/store/ldap/LDAPOperationManager.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java index 96766b639e2c..0716590634aa 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java @@ -774,6 +774,14 @@ private R execute(LdapOperation operation) throws NamingException { private R execute(LdapOperation operation, LDAPOperationDecorator decorator) throws NamingException { try (LDAPContextManager ldapContextManager = LDAPContextManager.create(session, config)) { + long connectStartNanos = System.nanoTime(); + boolean connectSuccess = false; + try { + ldapContextManager.getLdapContext(); + connectSuccess = true; + } finally { + recordLdapRequest("connect", connectSuccess, connectStartNanos); + } return execute(operation, ldapContextManager.getLdapContext(), decorator); } } From c320834b9a7d02586fb18400721faeb40fdbefe6 Mon Sep 17 00:00:00 2001 From: Ruchika Date: Wed, 15 Jul 2026 23:20:51 +0100 Subject: [PATCH 2/5] Implementation for the ldap connect server metric. Closes #50840 Signed-off-by: Ruchika --- .../ldap/idm/query/internal/LDAPQuery.java | 3 +- .../idm/store/ldap/LDAPContextManager.java | 30 ++++++++++++++++++- .../idm/store/ldap/LDAPIdentityStore.java | 6 ++++ .../idm/store/ldap/LDAPOperationManager.java | 10 +------ 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/query/internal/LDAPQuery.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/query/internal/LDAPQuery.java index 9fbf5f614081..faadf0ccc43e 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/query/internal/LDAPQuery.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/query/internal/LDAPQuery.java @@ -219,7 +219,8 @@ public LDAPQuery setLimit(int limit) { public LDAPQuery initPagination() throws NamingException { this.ldapContextManager = LDAPContextManager.create(ldapFedProvider.getSession(), - ldapFedProvider.getLdapIdentityStore().getConfig()); + ldapFedProvider.getLdapIdentityStore().getConfig(), + ldapFedProvider.getLdapIdentityStore().getRequestTimer()); this.paginationContext = new PaginationContext(ldapContextManager.getLdapContext()); return this; } diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPContextManager.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPContextManager.java index 47b2e1db4ca7..768dd4a39d98 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPContextManager.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPContextManager.java @@ -5,6 +5,7 @@ import java.util.Hashtable; import java.util.Map; import java.util.Properties; +import java.util.concurrent.TimeUnit; import javax.naming.AuthenticationException; import javax.naming.Context; import javax.naming.NamingException; @@ -20,6 +21,8 @@ import org.keycloak.truststore.TruststoreProvider; import org.keycloak.vault.VaultStringSecret; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Timer; import org.jboss.logging.Logger; import static javax.naming.Context.SECURITY_CREDENTIALS; @@ -33,22 +36,45 @@ public final class LDAPContextManager implements AutoCloseable { private final KeycloakSession session; private final LDAPConfig ldapConfig; + private final Meter.MeterProvider requestTimer; private StartTlsResponse tlsResponse; private LdapContext ldapContext; public LDAPContextManager(KeycloakSession session, LDAPConfig connectionProperties) { + this(session, connectionProperties, null); + } + + public LDAPContextManager(KeycloakSession session, LDAPConfig connectionProperties, Meter.MeterProvider requestTimer) { this.session = session; this.ldapConfig = connectionProperties; + this.requestTimer = requestTimer; } public static LDAPContextManager create(KeycloakSession session, LDAPConfig connectionProperties) { - return new LDAPContextManager(session, connectionProperties); + return new LDAPContextManager(session, connectionProperties, null); + } + + public static LDAPContextManager create(KeycloakSession session, LDAPConfig connectionProperties, Meter.MeterProvider requestTimer) { + return new LDAPContextManager(session, connectionProperties, requestTimer); + } + + private void recordLdapRequest(boolean success, long startTimeNanos) { + if (requestTimer == null) { + return; + } + long durationNanos = System.nanoTime() - startTimeNanos; + requestTimer.withTags("operation", "connect", "outcome", success ? "success" : "error") + .record(durationNanos, TimeUnit.NANOSECONDS); } // Create connection that is authenticated as admin user. private void createLdapContext() throws NamingException { var tracing = session.getProvider(TracingProvider.class); tracing.startSpan(LDAPContextManager.class, "createLdapContext"); + + long startTimeNanos = System.nanoTime(); + boolean success = false; + try { Hashtable connProp = getNonAuthConnectionProperties(ldapConfig); @@ -82,10 +108,12 @@ private void createLdapContext() throws NamingException { // StartTLS must complete before authenticating, so bind only now. setAdminConnectionAuthProperties(ldapContext); } + success = true; } catch (NamingException e) { tracing.error(e); throw e; } finally { + recordLdapRequest(success, startTimeNanos); tracing.endSpan(); } diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java index 0932a0592482..ab34b2825e6a 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java @@ -81,6 +81,7 @@ public class LDAPIdentityStore implements IdentityStore { private final LDAPConfig config; private final LDAPOperationManager operationManager; + private final Meter.MeterProvider requestTimer; public LDAPIdentityStore(KeycloakSession session, LDAPConfig config) { this(session, config, null); @@ -88,9 +89,14 @@ public LDAPIdentityStore(KeycloakSession session, LDAPConfig config) { public LDAPIdentityStore(KeycloakSession session, LDAPConfig config, Meter.MeterProvider requestTimer) { this.config = config; + this.requestTimer = requestTimer; this.operationManager = new LDAPOperationManager(session, config, requestTimer); } + public Meter.MeterProvider getRequestTimer() { + return requestTimer; + } + @Override public LDAPConfig getConfig() { return this.config; diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java index 0716590634aa..24a037137041 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java @@ -773,15 +773,7 @@ private R execute(LdapOperation operation) throws NamingException { } private R execute(LdapOperation operation, LDAPOperationDecorator decorator) throws NamingException { - try (LDAPContextManager ldapContextManager = LDAPContextManager.create(session, config)) { - long connectStartNanos = System.nanoTime(); - boolean connectSuccess = false; - try { - ldapContextManager.getLdapContext(); - connectSuccess = true; - } finally { - recordLdapRequest("connect", connectSuccess, connectStartNanos); - } + try (LDAPContextManager ldapContextManager = LDAPContextManager.create(session, config, requestTimer)) { return execute(operation, ldapContextManager.getLdapContext(), decorator); } } From c7631923b0984cd95e837df2a406296d98459097 Mon Sep 17 00:00:00 2001 From: Ruchika Date: Thu, 16 Jul 2026 13:46:08 +0100 Subject: [PATCH 3/5] Made the constructor and create method deprecated Closes #50840 Signed-off-by: Ruchika --- .../storage/ldap/idm/store/ldap/LDAPContextManager.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPContextManager.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPContextManager.java index 768dd4a39d98..ef56007ee301 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPContextManager.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPContextManager.java @@ -40,6 +40,7 @@ public final class LDAPContextManager implements AutoCloseable { private StartTlsResponse tlsResponse; private LdapContext ldapContext; + @Deprecated(forRemoval = true) public LDAPContextManager(KeycloakSession session, LDAPConfig connectionProperties) { this(session, connectionProperties, null); } @@ -50,6 +51,7 @@ public LDAPContextManager(KeycloakSession session, LDAPConfig connectionProperti this.requestTimer = requestTimer; } + @Deprecated(forRemoval = true) public static LDAPContextManager create(KeycloakSession session, LDAPConfig connectionProperties) { return new LDAPContextManager(session, connectionProperties, null); } From 89a0505f9389595f593a77cfdb8e86ad7fdaa598 Mon Sep 17 00:00:00 2001 From: Alexander Schwartz Date: Thu, 16 Jul 2026 16:33:19 +0200 Subject: [PATCH 4/5] Review Signed-off-by: Alexander Schwartz --- .../storage/ldap/idm/store/ldap/LDAPContextManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPContextManager.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPContextManager.java index ef56007ee301..fe9a9fed40e7 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPContextManager.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPContextManager.java @@ -40,7 +40,7 @@ public final class LDAPContextManager implements AutoCloseable { private StartTlsResponse tlsResponse; private LdapContext ldapContext; - @Deprecated(forRemoval = true) + @Deprecated(forRemoval = true, since = "26.8") public LDAPContextManager(KeycloakSession session, LDAPConfig connectionProperties) { this(session, connectionProperties, null); } @@ -51,7 +51,7 @@ public LDAPContextManager(KeycloakSession session, LDAPConfig connectionProperti this.requestTimer = requestTimer; } - @Deprecated(forRemoval = true) + @Deprecated(forRemoval = true, since = "26.8") public static LDAPContextManager create(KeycloakSession session, LDAPConfig connectionProperties) { return new LDAPContextManager(session, connectionProperties, null); } From 3db9f2060f3ff7246e7942ce4b18388acaf9b427 Mon Sep 17 00:00:00 2001 From: Alexander Schwartz Date: Thu, 16 Jul 2026 17:30:45 +0200 Subject: [PATCH 5/5] Review Signed-off-by: Alexander Schwartz --- .../storage/ldap/idm/store/ldap/LDAPContextManager.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPContextManager.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPContextManager.java index fe9a9fed40e7..1cddf1bbb898 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPContextManager.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPContextManager.java @@ -51,11 +51,16 @@ public LDAPContextManager(KeycloakSession session, LDAPConfig connectionProperti this.requestTimer = requestTimer; } - @Deprecated(forRemoval = true, since = "26.8") + /** + * Use this method only when the operation should not be tracked by metrics, for example when testing a connection. + */ public static LDAPContextManager create(KeycloakSession session, LDAPConfig connectionProperties) { return new LDAPContextManager(session, connectionProperties, null); } + /** + * This is the default method to create the context manager. It will track metrics for LDAP requests. + */ public static LDAPContextManager create(KeycloakSession session, LDAPConfig connectionProperties, Meter.MeterProvider requestTimer) { return new LDAPContextManager(session, connectionProperties, requestTimer); }