diff --git a/README.md b/README.md index 42b6a9c..381a20d 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,24 @@ 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 records 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 records 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 +``` + ## Grafana Dashboard You can use this dashboard or create yours https://grafana.com/dashboards/10441 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 bad16d0..5481fda 100644 --- a/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEventListener.java +++ b/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEventListener.java @@ -1,9 +1,13 @@ package org.jboss.aerogear.keycloak.metrics; +import java.util.Map; +import java.util.HashMap; import org.jboss.logging.Logger; import org.keycloak.events.Event; import org.keycloak.events.EventListenerProvider; import org.keycloak.events.admin.AdminEvent; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; public class MetricsEventListener implements EventListenerProvider { @@ -11,6 +15,12 @@ public class MetricsEventListener implements EventListenerProvider { private final static Logger logger = Logger.getLogger(MetricsEventListener.class); + private KeycloakSession session; + + public MetricsEventListener (KeycloakSession session) { + this.session = session; + } + @Override public void onEvent(Event event) { logEventDetails(event); @@ -31,6 +41,8 @@ public void onEvent(Event event) { default: PrometheusExporter.instance().recordGenericEvent(event); } + + setSessions(session.realms().getRealm(event.getRealmId())); } @Override @@ -38,6 +50,23 @@ public void onEvent(AdminEvent event, boolean includeRepresentation) { logAdminEventDetails(event); PrometheusExporter.instance().recordGenericAdminEvent(event); + + setSessions(session.realms().getRealm(event.getRealmId())); + } + + private void setSessions(RealmModel realm) { + + Map onlineSessions = new HashMap(); + session.sessions().getActiveClientSessionStats(realm,false).forEach((id, count) -> + onlineSessions.put(realm.getClientById(id).getClientId(), count) + ); + + Map offlineSessions = new HashMap(); + session.sessions().getActiveClientSessionStats(realm,true).forEach((id, count) -> + offlineSessions.put(realm.getClientById(id).getClientId(), count) + ); + + PrometheusExporter.instance().recordSessions(realm.getId(), onlineSessions, offlineSessions); } private void logEventDetails(Event 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 316af16..062dbca 100644 --- a/src/main/java/org/jboss/aerogear/keycloak/metrics/PrometheusExporter.java +++ b/src/main/java/org/jboss/aerogear/keycloak/metrics/PrometheusExporter.java @@ -2,6 +2,7 @@ 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; @@ -37,6 +38,8 @@ public final class PrometheusExporter { final Counter totalRegistrationsErrors; final Counter responseErrors; final Histogram requestDuration; + final Gauge totalOnlineSessions; + final Gauge totalOfflineSessions; final PushGateway PUSH_GATEWAY; private PrometheusExporter() { @@ -89,6 +92,17 @@ private PrometheusExporter() { .labelNames("method") .register(); + totalOnlineSessions = Gauge.build() + .name("keycloak_online_sessions") + .help("Total online sessions") + .labelNames("realm", "client_id") + .register(); + totalOfflineSessions = Gauge.build() + .name("keycloak_offline_sessions") + .help("Total offline sessions") + .labelNames("realm", "client_id") + .register(); + // Counters for all user events for (EventType type : EventType.values()) { if (type.equals(EventType.LOGIN) || type.equals(EventType.LOGIN_ERROR) || type.equals(EventType.REGISTER)) { @@ -208,6 +222,23 @@ public void recordLoginError(final Event event) { pushAsync(); } + /** + * Set sessions number + * + * @param event LoginError event + */ + public void recordSessions(final String realmId, Map onlineSessions, Map offlineSessions) { + + onlineSessions.forEach((clientId, count) -> { + totalOnlineSessions.labels(nullToEmpty(realmId), nullToEmpty(clientId)).set(count); + }); + + offlineSessions.forEach((clientId, count) -> { + totalOfflineSessions.labels(nullToEmpty(realmId), nullToEmpty(clientId)).set(count); + }); + pushAsync(); + } + /** * Record the duration between one request and response * 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 88b9dd3..8877518 100644 --- a/src/test/java/org/jboss/aerogear/keycloak/metrics/PrometheusExporterTest.java +++ b/src/test/java/org/jboss/aerogear/keycloak/metrics/PrometheusExporterTest.java @@ -18,6 +18,7 @@ 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; @@ -177,6 +178,19 @@ public void shouldCorrectlyRecordResponseErrors() throws IOException { assertGenericMetric("keycloak_response_errors", 1, tuple("code", "500"), tuple("method", "POST")); } + @Test + public void shouldCorrectlyRecordSessions() throws IOException { + + Map map1 = Map.of("account", new Long(10), "admin-cli", new Long(0)); + Map map2 = Map.of("account", new Long(0), "admin-cli", new Long(10)); + + PrometheusExporter.instance().recordSessions(DEFAULT_REALM, map1, map2); + assertGenericMetric("keycloak_online_sessions", 10, tuple("realm", DEFAULT_REALM), tuple("client_id", "account")); + assertGenericMetric("keycloak_online_sessions", 0, tuple("realm", DEFAULT_REALM), tuple("client_id", "admin-cli")); + assertGenericMetric("keycloak_offline_sessions", 0, tuple("realm", DEFAULT_REALM), tuple("client_id", "account")); + assertGenericMetric("keycloak_offline_sessions", 10, tuple("realm", DEFAULT_REALM), tuple("client_id", "admin-cli")); + } + @Test public void shouldTolerateNullLabels() throws IOException { final Event nullEvent = new Event();