From e58c3581d07e33eb7e224e139050e4efc9486658 Mon Sep 17 00:00:00 2001 From: Thibault Le Reste Date: Fri, 23 Oct 2020 10:57:06 +0200 Subject: [PATCH 1/2] add counter metrics for client_login, refresh_token and code_to_token events --- .../metrics/MetricsEventListener.java | 18 +++ .../keycloak/metrics/PrometheusExporter.java | 121 ++++++++++++++++++ .../metrics/PrometheusExporterTest.java | 84 ++++++++++++ 3 files changed, 223 insertions(+) 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..a92c04d 100644 --- a/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEventListener.java +++ b/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEventListener.java @@ -19,15 +19,33 @@ public void onEvent(Event event) { case LOGIN: PrometheusExporter.instance().recordLogin(event); break; + case CLIENT_LOGIN: + PrometheusExporter.instance().recordClientLogin(event); + break; case REGISTER: PrometheusExporter.instance().recordRegistration(event); break; + case REFRESH_TOKEN: + PrometheusExporter.instance().recordRefreshToken(event); + break; + case CODE_TO_TOKEN: + PrometheusExporter.instance().recordCodeToToken(event); + break; case REGISTER_ERROR: PrometheusExporter.instance().recordRegistrationError(event); break; case LOGIN_ERROR: PrometheusExporter.instance().recordLoginError(event); break; + case CLIENT_LOGIN_ERROR: + PrometheusExporter.instance().recordClientLoginError(event); + break; + case REFRESH_TOKEN_ERROR: + PrometheusExporter.instance().recordRefreshTokenError(event); + break; + case CODE_TO_TOKEN_ERROR: + PrometheusExporter.instance().recordCodeToTokenError(event); + break; default: PrometheusExporter.instance().recordGenericEvent(event); } 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 9c9db39..38962d4 100644 --- a/src/main/java/org/jboss/aerogear/keycloak/metrics/PrometheusExporter.java +++ b/src/main/java/org/jboss/aerogear/keycloak/metrics/PrometheusExporter.java @@ -37,6 +37,12 @@ public final class PrometheusExporter { final Counter totalFailedLoginAttempts; final Counter totalRegistrations; final Counter totalRegistrationsErrors; + final Counter totalRefreshTokens; + final Counter totalRefreshTokensErrors; + final Counter totalClientLogins; + final Counter totalFailedClientLoginAttempts; + final Counter totalCodeToTokens; + final Counter totalCodeToTokensErrors; final Counter responseErrors; final Histogram requestDuration; final PushGateway PUSH_GATEWAY; @@ -72,12 +78,55 @@ private PrometheusExporter() { .labelNames("realm", "provider", "client_id") .register(); + // package private on purpose totalRegistrationsErrors = Counter.build() .name("keycloak_registrations_errors") .help("Total errors on registrations") .labelNames("realm", "provider", "error", "client_id") .register(); + // package private on purpose + totalRefreshTokens = Counter.build() + .name("keycloak_refresh_tokens") + .help("Total number of successful token refreshes") + .labelNames("realm", "provider", "client_id") + .register(); + + // package private on purpose + totalRefreshTokensErrors = Counter.build() + .name("keycloak_refresh_tokens_errors") + .help("Total number of failed token refreshes") + .labelNames("realm", "provider", "error", "client_id") + .register(); + + // package private on purpose + totalClientLogins = Counter.build() + .name("keycloak_client_logins") + .help("Total successful client logins") + .labelNames("realm", "provider", "client_id") + .register(); + + // package private on purpose + totalFailedClientLoginAttempts = Counter.build() + .name("keycloak_failed_client_login_attempts") + .help("Total failed client login attempts") + .labelNames("realm", "provider", "error", "client_id") + .register(); + + // package private on purpose + totalCodeToTokens = Counter.build() + .name("keycloak_code_to_tokens") + .help("Total number of successful code to token") + .labelNames("realm", "provider", "client_id") + .register(); + + // package private on purpose + totalCodeToTokensErrors = Counter.build() + .name("keycloak_code_to_tokens_errors") + .help("Total number of failed code to token") + .labelNames("realm", "provider", "error", "client_id") + .register(); + responseErrors = Counter.build() .name("keycloak_response_errors") .help("Total number of error responses") @@ -210,6 +259,78 @@ public void recordLoginError(final Event event) { pushAsync(); } + /** + * Increase the number of currently client logged + * + * @param event ClientLogin event + */ + public void recordClientLogin(final Event event) { + final String provider = getIdentityProvider(event); + + totalClientLogins.labels(nullToEmpty(event.getRealmId()), provider, nullToEmpty(event.getClientId())).inc(); + pushAsync(); + } + + /** + * Increase the number of failed login attempts + * + * @param event ClientLoginError event + */ + public void recordClientLoginError(final Event event) { + final String provider = getIdentityProvider(event); + + totalFailedClientLoginAttempts.labels(nullToEmpty(event.getRealmId()), provider, nullToEmpty(event.getError()), nullToEmpty(event.getClientId())).inc(); + pushAsync(); + } + + /** + * Increase the number of refreshes tokens + * + * @param event RefreshToken event + */ + public void recordRefreshToken(final Event event) { + final String provider = getIdentityProvider(event); + + totalRefreshTokens.labels(nullToEmpty(event.getRealmId()), provider, nullToEmpty(event.getClientId())).inc(); + pushAsync(); + } + + /** + * Increase the number of failed refreshes tokens attempts + * + * @param event RefreshTokenError event + */ + public void recordRefreshTokenError(final Event event) { + final String provider = getIdentityProvider(event); + + totalRefreshTokensErrors.labels(nullToEmpty(event.getRealmId()), provider, nullToEmpty(event.getError()), nullToEmpty(event.getClientId())).inc(); + pushAsync(); + } + + /** + * Increase the number of code to tokens + * + * @param event CodeToToken event + */ + public void recordCodeToToken(final Event event) { + final String provider = getIdentityProvider(event); + + totalCodeToTokens.labels(nullToEmpty(event.getRealmId()), provider, nullToEmpty(event.getClientId())).inc(); + pushAsync(); + } + + /** + * Increase the number of failed code to tokens attempts + * + * @param event CodeToTokenError event + */ + public void recordCodeToTokenError(final Event event) { + final String provider = getIdentityProvider(event); + + totalCodeToTokensErrors.labels(nullToEmpty(event.getRealmId()), provider, nullToEmpty(event.getError()), nullToEmpty(event.getClientId())).inc(); + 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 4a682fa..8a3353a 100644 --- a/src/test/java/org/jboss/aerogear/keycloak/metrics/PrometheusExporterTest.java +++ b/src/test/java/org/jboss/aerogear/keycloak/metrics/PrometheusExporterTest.java @@ -128,6 +128,90 @@ public void shouldCorrectlyCountRegister() throws IOException { assertMetric("keycloak_registrations", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", "THE_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")); + PrometheusExporter.instance().recordRefreshToken(event1); + assertMetric("keycloak_refresh_tokens", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", "THE_CLIENT_ID")); + + // without id provider defined + final Event event2 = createEvent(EventType.REFRESH_TOKEN, DEFAULT_REALM, "THE_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")); + } + + @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")); + 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")); + + // without id provider defined + final Event event2 = createEvent(EventType.REFRESH_TOKEN_ERROR, DEFAULT_REALM, "THE_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")); + } + + @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")); + PrometheusExporter.instance().recordClientLogin(event1); + assertMetric("keycloak_client_logins", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", "THE_CLIENT_ID")); + + // without id provider defined + final Event event2 = createEvent(EventType.CLIENT_LOGIN, DEFAULT_REALM, "THE_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")); + } + + @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")); + 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")); + + // without id provider defined + final Event event2 = createEvent(EventType.CLIENT_LOGIN_ERROR, DEFAULT_REALM, "THE_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")); + } + + @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")); + PrometheusExporter.instance().recordCodeToToken(event1); + assertMetric("keycloak_code_to_tokens", 1, tuple("provider", "THE_ID_PROVIDER"), tuple("client_id", "THE_CLIENT_ID")); + + // without id provider defined + final Event event2 = createEvent(EventType.CODE_TO_TOKEN, DEFAULT_REALM, "THE_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")); + } + + @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")); + 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")); + + // without id provider defined + final Event event2 = createEvent(EventType.CODE_TO_TOKEN_ERROR, DEFAULT_REALM, "THE_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")); + } + @Test public void shouldCorrectlyRecordGenericEvents() throws IOException { final Event event1 = createEvent(EventType.UPDATE_EMAIL); From 39aab79edfa4a6096cd01afeb0c6e6f08bc7aef1 Mon Sep 17 00:00:00 2001 From: Thibault Le Reste Date: Fri, 23 Oct 2020 14:26:16 +0200 Subject: [PATCH 2/2] add documentation for client_login, refresh_token and code_to_token counter metrics --- README.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/README.md b/README.md index 438ed05..8137f86 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,44 @@ keycloak_failed_login_attempts{realm="test",provider="keycloak",error="invalid_u keycloak_failed_login_attempts{realm="test",provider="keycloak",error="user_not_found",client_id="application1"} 2.0 ``` +##### keycloak_client_logins +This counter counts every client login. + +```c +# HELP keycloak_client_logins Total successful client logins +# TYPE keycloak_client_logins gauge +keycloak_client_logins{realm="test",provider="keycloak",client_id="account"} 4.0 +keycloak_client_logins{realm="test",provider="github",client_id="application2"} 7.0 +``` + +##### keycloak_failed_client_login_attempts +This counter counts every client login performed that fails, being the error described by the label **error**. +```c +# HELP keycloak_failed_client_login_attempts Total failed client login attempts +# TYPE keycloak_failed_client_login_attempts gauge +keycloak_failed_client_login_attempts{realm="test2",provider="keycloak",error="invalid_client_credentials",client_id="application2"} 5.0 +keycloak_failed_client_login_attempts{realm="test2",provider="keycloak",error="client_not_found",client_id="application2"} 3.0 +``` + +##### keycloak_refresh_tokens +This counter counts every refresh token. + +```c +# HELP keycloak_refresh_tokens Total number of successful token refreshes +# TYPE keycloak_refresh_tokens gauge +keycloak_refresh_tokens{realm="test3",provider="keycloak",client_id="account"} 1.0 +keycloak_refresh_tokens{realm="test3",provider="github",client_id="application3"} 2.0 +``` + +##### keycloak_refresh_tokens_errors +This counter counts every refresh token that fails. + +```c +# HELP keycloak_refresh_tokens_errors Total number of failed token refreshes +# TYPE keycloak_refresh_tokens_errors gauge +keycloak_refresh_tokens_errors{realm="test3",provider="keycloak",error="invalid_token",client_id="application3"} 3.0 +``` + ##### keycloak_registrations This counter counts every new user registration. It also distinguishes registrations by the identity provider used by means of the label **provider** and by client with the label **client_id**.. @@ -145,6 +183,25 @@ keycloak_registrations_errors{realm="test",provider="keycloak",error="invalid_re keycloak_registrations_errors{realm="test",provider="keycloak",error="email_in_use",client_id="application1",} 3.0 ``` +##### keycloak_code_to_tokens +This counter counts every code to token. + +```c +# HELP keycloak_code_to_tokens Total number of successful code to token +# TYPE keycloak_code_to_tokens gauge +keycloak_code_to_tokens{realm="test4",provider="keycloak",client_id="account"} 3.0 +keycloak_code_to_tokens{realm="test4",provider="github",client_id="application4"} 1.0 +``` + +##### keycloak_code_to_tokens_errors +This counter counts every code to token performed that fails, being the error described by the label **error**. + +```c +# HELP keycloak_code_to_tokens_errors Total number of failed code to token +# TYPE keycloak_code_to_tokens_errors gauge +keycloak_code_to_tokens_errors{realm="test4",provider="keycloak",error="invalid_client_credentials",client_id="application4"} 7.0 +``` + ##### keycloak_request_duration This histogram records the response times per http method and puts them in one of nine buckets: