diff --git a/README.md b/README.md index 0d06359..3ccd505 100644 --- a/README.md +++ b/README.md @@ -333,6 +333,25 @@ This counter counts the number of response errors (responses where the http stat keycloak_response_errors{code="500",method="GET",} 1 ``` +##### keycloak_online_sessions +This gauge indicates the number of online sessions. + +```c +# HELP keycloak_online_sessions Total online sessions +# TYPE keycloak_online_sessions gauge +keycloak_online_sessions{realm="test",client_id="application1",} 1.0 +``` + +##### keycloak_offline_sessions +This gauge indicates the number of offline sessions. + +```c +# HELP keycloak_offline_sessions Total offline sessions +# TYPE keycloak_offline_sessions gauge +keycloak_offline_sessions{realm="test",client_id="application1",} 1.0 +``` + + #### Metrics URI The URI can be added to the metrics by setting the environment variable ```URI_METRICS_ENABLED``` to `true`. This will output a consolidated realm URI value to the metrics. The realm value is replaced with a generic `{realm}` value diff --git a/pom.xml b/pom.xml index c57dfe6..91abf63 100644 --- a/pom.xml +++ b/pom.xml @@ -64,6 +64,17 @@ 1.19.0 test + + org.apache.commons + commons-collections4 + 4.4 + + + org.mockito + mockito-core + 4.11.0 + test + diff --git a/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEventListener.java b/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEventListener.java index d9be8b8..21fd8e3 100644 --- a/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEventListener.java +++ b/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEventListener.java @@ -4,6 +4,7 @@ import org.keycloak.events.Event; import org.keycloak.events.EventListenerProvider; import org.keycloak.events.admin.AdminEvent; +import org.keycloak.models.KeycloakSession; public class MetricsEventListener implements EventListenerProvider { @@ -11,6 +12,12 @@ public class MetricsEventListener implements EventListenerProvider { private final static Logger logger = Logger.getLogger(MetricsEventListener.class); + private final KeycloakSession keycloakSession; + + public MetricsEventListener(KeycloakSession keycloakSession) { + this.keycloakSession = keycloakSession; + } + @Override public void onEvent(Event event) { logEventDetails(event); @@ -18,9 +25,14 @@ public void onEvent(Event event) { switch (event.getType()) { case LOGIN: PrometheusExporter.instance().recordLogin(event); + PrometheusExporter.instance().recordSessions(event, keycloakSession); break; case CLIENT_LOGIN: PrometheusExporter.instance().recordClientLogin(event); + PrometheusExporter.instance().recordSessions(event, keycloakSession); + break; + case LOGOUT: + PrometheusExporter.instance().recordSessions(event, keycloakSession); break; case REGISTER: PrometheusExporter.instance().recordRegistration(event); diff --git a/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEventListenerFactory.java b/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEventListenerFactory.java index 9da066b..ed8f56f 100644 --- a/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEventListenerFactory.java +++ b/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEventListenerFactory.java @@ -10,7 +10,7 @@ public class MetricsEventListenerFactory implements EventListenerProviderFactory @Override public EventListenerProvider create(KeycloakSession session) { - return new MetricsEventListener(); + return new MetricsEventListener(session); } @Override diff --git a/src/main/java/org/jboss/aerogear/keycloak/metrics/PrometheusExporter.java b/src/main/java/org/jboss/aerogear/keycloak/metrics/PrometheusExporter.java index ed05a29..4a6dd88 100644 --- a/src/main/java/org/jboss/aerogear/keycloak/metrics/PrometheusExporter.java +++ b/src/main/java/org/jboss/aerogear/keycloak/metrics/PrometheusExporter.java @@ -2,15 +2,20 @@ import io.prometheus.client.CollectorRegistry; import io.prometheus.client.Counter; +import io.prometheus.client.Gauge; import io.prometheus.client.Histogram; import io.prometheus.client.exporter.PushGateway; import io.prometheus.client.exporter.common.TextFormat; import io.prometheus.client.hotspot.DefaultExports; +import org.apache.commons.collections4.MapUtils; import org.jboss.logging.Logger; import org.keycloak.events.Event; import org.keycloak.events.EventType; import org.keycloak.events.admin.AdminEvent; import org.keycloak.events.admin.OperationType; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; import java.io.BufferedWriter; import java.io.IOException; @@ -60,6 +65,8 @@ public final class PrometheusExporter { final Counter responseTotal; final Counter responseErrors; final Histogram requestDuration; + final Gauge totalOnlineSessions; + final Gauge totalOfflineSessions; final PushGateway PUSH_GATEWAY; private PrometheusExporter() { @@ -149,6 +156,18 @@ private PrometheusExporter() { .labelNames("realm", "provider", "error", "client_id") .register(); + totalOnlineSessions = Gauge.build() + .name("keycloak_online_sessions") + .help("Total online sessions") + .labelNames("realm", "provider", "client_id") + .register(); + + totalOfflineSessions = Gauge.build() + .name("keycloak_offline_sessions") + .help("Total offline sessions") + .labelNames("realm", "provider", "client_id") + .register(); + final boolean URI_METRICS_ENABLED = Boolean.parseBoolean(System.getenv("URI_METRICS_ENABLED")); if (URI_METRICS_ENABLED){ responseTotal = Counter.build() @@ -274,6 +293,25 @@ public void recordLogin(final Event event) { pushAsync(); } + public void recordSessions(final Event event, final KeycloakSession keycloakSession) { + final String provider = getIdentityProvider(event); + final RealmModel realmModel = keycloakSession.realms().getRealm(event.getRealmId()); + final ClientModel clientModel = keycloakSession.clients().getClientByClientId(realmModel, event.getClientId()); + + if(clientModel != null) { + final Map onlineClientSessionStats = keycloakSession.sessions().getActiveClientSessionStats(realmModel, false); + final Optional onlineSessionCount = Optional.ofNullable(MapUtils.emptyIfNull(onlineClientSessionStats).get(clientModel.getId())); + + final Map offlineClientSessionStats = keycloakSession.sessions().getActiveClientSessionStats(realmModel, true); + final Optional offlineSessionCount = Optional.ofNullable(MapUtils.emptyIfNull(offlineClientSessionStats).get(clientModel.getId())); + + totalOnlineSessions.labels(nullToEmpty(event.getRealmId()), provider, nullToEmpty(event.getClientId())).set(onlineSessionCount.orElse(0L)); + totalOfflineSessions.labels(nullToEmpty(event.getRealmId()), provider, nullToEmpty(event.getClientId())).set(offlineSessionCount.orElse(0L)); + + pushAsync(); + } + } + /** * Increase the number registered users * diff --git a/src/test/java/org/jboss/aerogear/keycloak/metrics/PrometheusExporterTest.java b/src/test/java/org/jboss/aerogear/keycloak/metrics/PrometheusExporterTest.java index 0f142a0..fee1d92 100644 --- a/src/test/java/org/jboss/aerogear/keycloak/metrics/PrometheusExporterTest.java +++ b/src/test/java/org/jboss/aerogear/keycloak/metrics/PrometheusExporterTest.java @@ -7,25 +7,57 @@ import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.EnvironmentVariables; +import org.junit.runner.RunWith; import org.keycloak.events.Event; import org.keycloak.events.EventType; import org.keycloak.events.admin.AdminEvent; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RealmProvider; +import org.keycloak.models.UserSessionProvider; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.reflect.Field; import java.util.Collections; import java.util.HashMap; +import java.util.Map; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; @SuppressWarnings("unchecked") +@RunWith(MockitoJUnitRunner.class) public class PrometheusExporterTest { private static final String DEFAULT_REALM = "myrealm"; + private static final String DEFAULT_CLIENT_CLIENT_ID = "THE_CLIENT_CLIENT_ID"; + private static final String DEFAULT_CLIENT_ID = "THE_CLIENT_ID"; + private static final String OTHER_CLIENT_CLIENT_ID = "OTHER_CLIENT_CLIENT_ID"; + private static final String OTHER_CLIENT_ID = "OTHER_CLIENT_ID"; + + @Mock + private KeycloakSession keycloakSession; + @Mock + private UserSessionProvider userSessionProvider; + @Mock + private RealmProvider realmProvider; + @Mock + private ClientProvider clientProvider; + @Mock + private RealmModel realm; + @Mock + private ClientModel client1; + @Mock + private ClientModel client2; @Rule public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); @@ -36,6 +68,15 @@ public void resetSingleton() throws SecurityException, NoSuchFieldException, Ill instance.setAccessible(true); instance.set(null, null); CollectorRegistry.defaultRegistry.clear(); + + Mockito.when(keycloakSession.sessions()).thenReturn(userSessionProvider); + Mockito.when(keycloakSession.realms()).thenReturn(realmProvider); + Mockito.when(realmProvider.getRealm(DEFAULT_REALM)).thenReturn(realm); + Mockito.when(keycloakSession.clients()).thenReturn(clientProvider); + Mockito.when(clientProvider.getClientByClientId(realm, DEFAULT_CLIENT_CLIENT_ID)).thenReturn(client1); + Mockito.when(clientProvider.getClientByClientId(realm, OTHER_CLIENT_CLIENT_ID)).thenReturn(client2); + Mockito.when(client1.getId()).thenReturn(DEFAULT_CLIENT_ID); + Mockito.when(client2.getId()).thenReturn(OTHER_CLIENT_ID); } @Test @@ -53,178 +94,178 @@ public void shouldRegisterCountersForAllKeycloakEvents() { @Test public void shouldCorrectlyCountLoginAttemptsForSuccessfulAndFailedAttempts() throws IOException { // with LOGIN event - final Event login1 = createEvent(EventType.LOGIN, DEFAULT_REALM, "THE_CLIENT_ID"); + final Event login1 = createEvent(EventType.LOGIN, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID); PrometheusExporter.instance().recordLogin(login1); - assertMetric("keycloak_login_attempts", 1, tuple("provider", "keycloak"), tuple("client_id", "THE_CLIENT_ID")); - assertMetric("keycloak_logins", 1, tuple("provider", "keycloak"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_login_attempts", 1, tuple("provider", "keycloak"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); + assertMetric("keycloak_logins", 1, tuple("provider", "keycloak"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); // with LOGIN_ERROR event - final Event event2 = createEvent(EventType.LOGIN_ERROR, DEFAULT_REALM, "THE_CLIENT_ID", "user_not_found"); + final Event event2 = createEvent(EventType.LOGIN_ERROR, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID, "user_not_found"); PrometheusExporter.instance().recordLoginError(event2); - assertMetric("keycloak_login_attempts", 2, tuple("provider", "keycloak"), tuple("client_id", "THE_CLIENT_ID")); - assertMetric("keycloak_failed_login_attempts", 1, tuple("provider", "keycloak"), tuple("error", "user_not_found"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_login_attempts", 2, tuple("provider", "keycloak"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); + assertMetric("keycloak_failed_login_attempts", 1, tuple("provider", "keycloak"), tuple("error", "user_not_found"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); } @Test public void shouldCorrectlyCountLoginWhenIdentityProviderIsDefined() throws IOException { - final Event login1 = createEvent(EventType.LOGIN, DEFAULT_REALM, "THE_CLIENT_ID", tuple("identity_provider", "THE_ID_PROVIDER")); + final Event login1 = createEvent(EventType.LOGIN, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID, tuple("identity_provider", "THE_ID_PROVIDER")); PrometheusExporter.instance().recordLogin(login1); - assertMetric("keycloak_logins", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_logins", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); - final Event login2 = createEvent(EventType.LOGIN, DEFAULT_REALM, "THE_CLIENT_ID", tuple("identity_provider", "THE_ID_PROVIDER")); + final Event login2 = createEvent(EventType.LOGIN, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID, tuple("identity_provider", "THE_ID_PROVIDER")); PrometheusExporter.instance().recordLogin(login2); - assertMetric("keycloak_logins", 2, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_logins", 2, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); } @Test public void shouldCorrectlyCountLoginWhenIdentityProviderIsNotDefined() throws IOException { final Event login1 = createEvent(EventType.LOGIN); PrometheusExporter.instance().recordLogin(login1); - assertMetric("keycloak_logins", 1, tuple("provider", "keycloak"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_logins", 1, tuple("provider", "keycloak"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); final Event login2 = createEvent(EventType.LOGIN); PrometheusExporter.instance().recordLogin(login2); - assertMetric("keycloak_logins", 2, tuple("provider", "keycloak"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_logins", 2, tuple("provider", "keycloak"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); } @Test public void shouldCorrectlyCountLoginsFromDifferentProviders() throws IOException { // with id provider defined - final Event login1 = createEvent(EventType.LOGIN, DEFAULT_REALM, "THE_CLIENT_ID", tuple("identity_provider", "THE_ID_PROVIDER")); + final Event login1 = createEvent(EventType.LOGIN, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID, tuple("identity_provider", "THE_ID_PROVIDER")); PrometheusExporter.instance().recordLogin(login1); - assertMetric("keycloak_logins", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_logins", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); // without id provider defined - final Event login2 = createEvent(EventType.LOGIN, DEFAULT_REALM, "THE_CLIENT_ID"); + final Event login2 = createEvent(EventType.LOGIN, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID); PrometheusExporter.instance().recordLogin(login2); - assertMetric("keycloak_logins", 1, tuple("provider", "keycloak"), tuple("client_id", "THE_CLIENT_ID")); - assertMetric("keycloak_logins", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_logins", 1, tuple("provider", "keycloak"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); + assertMetric("keycloak_logins", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); } @Test public void shouldRecordLoginsPerRealm() throws IOException { // realm 1 - final Event login1 = createEvent(EventType.LOGIN, DEFAULT_REALM, "THE_CLIENT_ID", tuple("identity_provider", "THE_ID_PROVIDER")); + final Event login1 = createEvent(EventType.LOGIN, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID, tuple("identity_provider", "THE_ID_PROVIDER")); PrometheusExporter.instance().recordLogin(login1); // realm 2 - final Event login2 = createEvent(EventType.LOGIN, "OTHER_REALM", "THE_CLIENT_ID", tuple("identity_provider", "THE_ID_PROVIDER")); + final Event login2 = createEvent(EventType.LOGIN, "OTHER_REALM", DEFAULT_CLIENT_CLIENT_ID, tuple("identity_provider", "THE_ID_PROVIDER")); PrometheusExporter.instance().recordLogin(login2); - assertMetric("keycloak_logins", 1, DEFAULT_REALM, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", "THE_CLIENT_ID")); - assertMetric("keycloak_logins", 1, "OTHER_REALM", tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_logins", 1, DEFAULT_REALM, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); + assertMetric("keycloak_logins", 1, "OTHER_REALM", tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); } @Test public void shouldCorrectlyCountLoginError() throws IOException { // with id provider defined - final Event event1 = createEvent(EventType.LOGIN_ERROR, DEFAULT_REALM, "THE_CLIENT_ID", "user_not_found", tuple("identity_provider", "THE_ID_PROVIDER")); + final Event event1 = createEvent(EventType.LOGIN_ERROR, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID, "user_not_found", tuple("identity_provider", "THE_ID_PROVIDER")); PrometheusExporter.instance().recordLoginError(event1); - assertMetric("keycloak_failed_login_attempts", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("error", "user_not_found"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_failed_login_attempts", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("error", "user_not_found"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); // without id provider defined - final Event event2 = createEvent(EventType.LOGIN_ERROR, DEFAULT_REALM, "THE_CLIENT_ID", "user_not_found"); + final Event event2 = createEvent(EventType.LOGIN_ERROR, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID, "user_not_found"); PrometheusExporter.instance().recordLoginError(event2); - assertMetric("keycloak_failed_login_attempts", 1, tuple("provider", "keycloak"), tuple("error", "user_not_found"), tuple("client_id", "THE_CLIENT_ID")); - assertMetric("keycloak_failed_login_attempts", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("error", "user_not_found"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_failed_login_attempts", 1, tuple("provider", "keycloak"), tuple("error", "user_not_found"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); + assertMetric("keycloak_failed_login_attempts", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("error", "user_not_found"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); } @Test public void shouldCorrectlyCountRegister() throws IOException { // with id provider defined - final Event event1 = createEvent(EventType.REGISTER, DEFAULT_REALM, "THE_CLIENT_ID", tuple("identity_provider", "THE_ID_PROVIDER")); + final Event event1 = createEvent(EventType.REGISTER, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID, tuple("identity_provider", "THE_ID_PROVIDER")); PrometheusExporter.instance().recordRegistration(event1); - assertMetric("keycloak_registrations", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_registrations", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); // without id provider defined - final Event event2 = createEvent(EventType.REGISTER, DEFAULT_REALM, "THE_CLIENT_ID"); + final Event event2 = createEvent(EventType.REGISTER, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID); PrometheusExporter.instance().recordRegistration(event2); - assertMetric("keycloak_registrations", 1, tuple("provider", "keycloak"), tuple("client_id", "THE_CLIENT_ID")); - assertMetric("keycloak_registrations", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_registrations", 1, tuple("provider", "keycloak"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); + assertMetric("keycloak_registrations", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); } @Test public void shouldCorrectlyCountRefreshTokens() throws IOException { // with id provider defined - final Event event1 = createEvent(EventType.REFRESH_TOKEN, DEFAULT_REALM, "THE_CLIENT_ID", tuple("identity_provider", "THE_ID_PROVIDER")); + final Event event1 = createEvent(EventType.REFRESH_TOKEN, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID, tuple("identity_provider", "THE_ID_PROVIDER")); PrometheusExporter.instance().recordRefreshToken(event1); - assertMetric("keycloak_refresh_tokens", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_refresh_tokens", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); // without id provider defined - final Event event2 = createEvent(EventType.REFRESH_TOKEN, DEFAULT_REALM, "THE_CLIENT_ID"); + final Event event2 = createEvent(EventType.REFRESH_TOKEN, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID); PrometheusExporter.instance().recordRefreshToken(event2); - assertMetric("keycloak_refresh_tokens", 1, tuple("provider", "keycloak"), tuple("client_id", "THE_CLIENT_ID")); - assertMetric("keycloak_refresh_tokens", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_refresh_tokens", 1, tuple("provider", "keycloak"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); + assertMetric("keycloak_refresh_tokens", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); } @Test public void shouldCorrectlyCountRefreshTokensErrors() throws IOException { // with id provider defined - final Event event1 = createEvent(EventType.REFRESH_TOKEN_ERROR, DEFAULT_REALM, "THE_CLIENT_ID", "user_not_found", tuple("identity_provider", "THE_ID_PROVIDER")); + final Event event1 = createEvent(EventType.REFRESH_TOKEN_ERROR, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID, "user_not_found", tuple("identity_provider", "THE_ID_PROVIDER")); PrometheusExporter.instance().recordRefreshTokenError(event1); - assertMetric("keycloak_refresh_tokens_errors", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("error", "user_not_found"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_refresh_tokens_errors", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("error", "user_not_found"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); // without id provider defined - final Event event2 = createEvent(EventType.REFRESH_TOKEN_ERROR, DEFAULT_REALM, "THE_CLIENT_ID", "user_not_found"); + final Event event2 = createEvent(EventType.REFRESH_TOKEN_ERROR, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID, "user_not_found"); PrometheusExporter.instance().recordRefreshTokenError(event2); - assertMetric("keycloak_refresh_tokens_errors", 1, tuple("provider", "keycloak"), tuple("error", "user_not_found"), tuple("client_id", "THE_CLIENT_ID")); - assertMetric("keycloak_refresh_tokens_errors", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("error", "user_not_found"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_refresh_tokens_errors", 1, tuple("provider", "keycloak"), tuple("error", "user_not_found"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); + assertMetric("keycloak_refresh_tokens_errors", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("error", "user_not_found"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); } @Test public void shouldCorrectlyCountClientLogins() throws IOException { // with id provider defined - final Event event1 = createEvent(EventType.CLIENT_LOGIN, DEFAULT_REALM, "THE_CLIENT_ID", tuple("identity_provider", "THE_ID_PROVIDER")); + final Event event1 = createEvent(EventType.CLIENT_LOGIN, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID, tuple("identity_provider", "THE_ID_PROVIDER")); PrometheusExporter.instance().recordClientLogin(event1); - assertMetric("keycloak_client_logins", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_client_logins", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); // without id provider defined - final Event event2 = createEvent(EventType.CLIENT_LOGIN, DEFAULT_REALM, "THE_CLIENT_ID"); + final Event event2 = createEvent(EventType.CLIENT_LOGIN, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID); PrometheusExporter.instance().recordClientLogin(event2); - assertMetric("keycloak_client_logins", 1, tuple("provider", "keycloak"), tuple("client_id", "THE_CLIENT_ID")); - assertMetric("keycloak_client_logins", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_client_logins", 1, tuple("provider", "keycloak"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); + assertMetric("keycloak_client_logins", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); } @Test public void shouldCorrectlyCountClientLoginAttempts() throws IOException { // with id provider defined - final Event event1 = createEvent(EventType.CLIENT_LOGIN_ERROR, DEFAULT_REALM, "THE_CLIENT_ID", "user_not_found", tuple("identity_provider", "THE_ID_PROVIDER")); + final Event event1 = createEvent(EventType.CLIENT_LOGIN_ERROR, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID, "user_not_found", tuple("identity_provider", "THE_ID_PROVIDER")); PrometheusExporter.instance().recordClientLoginError(event1); - assertMetric("keycloak_failed_client_login_attempts", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("error", "user_not_found"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_failed_client_login_attempts", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("error", "user_not_found"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); // without id provider defined - final Event event2 = createEvent(EventType.CLIENT_LOGIN_ERROR, DEFAULT_REALM, "THE_CLIENT_ID", "user_not_found"); + final Event event2 = createEvent(EventType.CLIENT_LOGIN_ERROR, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID, "user_not_found"); PrometheusExporter.instance().recordClientLoginError(event2); - assertMetric("keycloak_failed_client_login_attempts", 1, tuple("provider", "keycloak"), tuple("error", "user_not_found"), tuple("client_id", "THE_CLIENT_ID")); - assertMetric("keycloak_failed_client_login_attempts", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("error", "user_not_found"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_failed_client_login_attempts", 1, tuple("provider", "keycloak"), tuple("error", "user_not_found"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); + assertMetric("keycloak_failed_client_login_attempts", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("error", "user_not_found"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); } @Test public void shouldCorrectlyCountCodeToTokens() throws IOException { // with id provider defined - final Event event1 = createEvent(EventType.CODE_TO_TOKEN, DEFAULT_REALM, "THE_CLIENT_ID", tuple("identity_provider", "THE_ID_PROVIDER")); + final Event event1 = createEvent(EventType.CODE_TO_TOKEN, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID, tuple("identity_provider", "THE_ID_PROVIDER")); PrometheusExporter.instance().recordCodeToToken(event1); - assertMetric("keycloak_code_to_tokens", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_code_to_tokens", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); // without id provider defined - final Event event2 = createEvent(EventType.CODE_TO_TOKEN, DEFAULT_REALM, "THE_CLIENT_ID"); + final Event event2 = createEvent(EventType.CODE_TO_TOKEN, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID); PrometheusExporter.instance().recordCodeToToken(event2); - assertMetric("keycloak_code_to_tokens", 1, tuple("provider", "keycloak"), tuple("client_id", "THE_CLIENT_ID")); - assertMetric("keycloak_code_to_tokens", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_code_to_tokens", 1, tuple("provider", "keycloak"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); + assertMetric("keycloak_code_to_tokens", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); } @Test public void shouldCorrectlyCountCodeToTokensErrors() throws IOException { // with id provider defined - final Event event1 = createEvent(EventType.CODE_TO_TOKEN_ERROR, DEFAULT_REALM, "THE_CLIENT_ID", "user_not_found", tuple("identity_provider", "THE_ID_PROVIDER")); + final Event event1 = createEvent(EventType.CODE_TO_TOKEN_ERROR, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID, "user_not_found", tuple("identity_provider", "THE_ID_PROVIDER")); PrometheusExporter.instance().recordCodeToTokenError(event1); - assertMetric("keycloak_code_to_tokens_errors", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("error", "user_not_found"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_code_to_tokens_errors", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("error", "user_not_found"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); // without id provider defined - final Event event2 = createEvent(EventType.CODE_TO_TOKEN_ERROR, DEFAULT_REALM, "THE_CLIENT_ID", "user_not_found"); + final Event event2 = createEvent(EventType.CODE_TO_TOKEN_ERROR, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID, "user_not_found"); PrometheusExporter.instance().recordCodeToTokenError(event2); - assertMetric("keycloak_code_to_tokens_errors", 1, tuple("provider", "keycloak"), tuple("error", "user_not_found"), tuple("client_id", "THE_CLIENT_ID")); - assertMetric("keycloak_code_to_tokens_errors", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("error", "user_not_found"), tuple("client_id", "THE_CLIENT_ID")); + assertMetric("keycloak_code_to_tokens_errors", 1, tuple("provider", "keycloak"), tuple("error", "user_not_found"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); + assertMetric("keycloak_code_to_tokens_errors", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("error", "user_not_found"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); } @Test @@ -292,6 +333,45 @@ public void shouldCorrectlyRecordResponseErrors() throws IOException { tuple("code", "500"), tuple("method", "POST"), tuple("resource", "admin,admin/serverinfo"), tuple("uri", "auth/realm")); } + @Test + public void shouldUpdateSessionsOnlyForEventClient() throws IOException { + final Event event = createEvent(EventType.LOGIN, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID); + + final Map onlineSessionsMap = new HashMap<>(); + final Map offlineSessionsMap = new HashMap<>(); + Mockito.when(userSessionProvider.getActiveClientSessionStats(realm, false)).thenReturn(onlineSessionsMap); + Mockito.when(userSessionProvider.getActiveClientSessionStats(realm, true)).thenReturn(offlineSessionsMap); + + onlineSessionsMap.put(DEFAULT_CLIENT_ID, 3L); + onlineSessionsMap.put(OTHER_CLIENT_ID, 10L); + + PrometheusExporter.instance().recordSessions(event, keycloakSession); + assertMetric("keycloak_online_sessions", 3.0, DEFAULT_REALM, tuple("provider", "keycloak"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); + assertNoMetric("keycloak_online_sessions", 10.0, DEFAULT_REALM, tuple("provider", "keycloak"), tuple("client_id", OTHER_CLIENT_CLIENT_ID)); + } + + @Test + public void shouldCorrectlyRecordSessions() throws IOException { + final Event event1 = createEvent(EventType.LOGIN, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID); + final Event event2 = createEvent(EventType.LOGIN, DEFAULT_REALM, OTHER_CLIENT_CLIENT_ID); + + final Map onlineSessionsMap = new HashMap<>(); + final Map offlineSessionsMap = new HashMap<>(); + Mockito.when(userSessionProvider.getActiveClientSessionStats(realm, false)).thenReturn(onlineSessionsMap); + Mockito.when(userSessionProvider.getActiveClientSessionStats(realm, true)).thenReturn(offlineSessionsMap); + + onlineSessionsMap.put(DEFAULT_CLIENT_ID, 3L); + onlineSessionsMap.put(OTHER_CLIENT_ID, 0L); + offlineSessionsMap.put(OTHER_CLIENT_ID, 5L); + + PrometheusExporter.instance().recordSessions(event1, keycloakSession); + PrometheusExporter.instance().recordSessions(event2, keycloakSession); + assertMetric("keycloak_online_sessions", 3.0, DEFAULT_REALM, tuple("provider", "keycloak"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); + assertMetric("keycloak_offline_sessions", 0.0, DEFAULT_REALM, tuple("provider", "keycloak"), tuple("client_id", DEFAULT_CLIENT_CLIENT_ID)); + assertMetric("keycloak_online_sessions", 0.0, DEFAULT_REALM, tuple("provider", "keycloak"), tuple("client_id", OTHER_CLIENT_CLIENT_ID)); + assertMetric("keycloak_offline_sessions", 5.0, DEFAULT_REALM, tuple("provider", "keycloak"), tuple("client_id", OTHER_CLIENT_CLIENT_ID)); + } + @Test public void shouldTolerateNullLabels() throws IOException { final Event nullEvent = new Event(); @@ -362,6 +442,26 @@ private void assertMetric(String metricName, double metricValue, String realm, T } } + private void assertNoMetric(String metricName, double metricValue, String realm, Tuple... labels) throws IOException { + try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) { + PrometheusExporter.instance().export(stream); + String result = new String(stream.toByteArray()); + + final StringBuilder builder = new StringBuilder(); + + builder.append(metricName).append("{"); + builder.append("realm").append("=\"").append(realm).append("\","); + + for (Tuple label : labels) { + builder.append(label.left).append("=\"").append(label.right).append("\","); + } + + builder.append("} ").append(metricValue); + + MatcherAssert.assertThat(result, not(containsString(builder.toString()))); + } + } + private void assertMetric(String metricName, double metricValue, Tuple... labels) throws IOException { this.assertMetric(metricName, metricValue, DEFAULT_REALM, labels); } @@ -387,7 +487,7 @@ private Event createEvent(EventType type, String realm, String clientId, String } private Event createEvent(EventType type, Tuple... tuples) { - return this.createEvent(type, DEFAULT_REALM, "THE_CLIENT_ID", (String) null, tuples); + return this.createEvent(type, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID, (String) null, tuples); } private Event createEvent(EventType type, String realm, String clientId, Tuple... tuples) { @@ -395,11 +495,11 @@ private Event createEvent(EventType type, String realm, String clientId, Tuple... tuples) { - return this.createEvent(type, realm, "THE_CLIENT_ID", (String) null, tuples); + return this.createEvent(type, realm, DEFAULT_CLIENT_CLIENT_ID, (String) null, tuples); } private Event createEvent(EventType type) { - return createEvent(type, DEFAULT_REALM, "THE_CLIENT_ID",(String) null); + return createEvent(type, DEFAULT_REALM, DEFAULT_CLIENT_CLIENT_ID,(String) null); } private static Tuple tuple(L left, R right) {