diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 7a74c74..9984b24 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -3,9 +3,12 @@ updates:
- package-ecosystem: maven
directory: /
schedule:
- interval: daily
- allow:
- - dependency-name: org.keycloak:keycloak-parent
+ interval: monthly
+ day: monday
+ # github parses time without quotes to int
+ # yamllint disable-line rule:quoted-strings
+ time: "09:00"
+ timezone: Europe/Berlin
- package-ecosystem: github-actions
directory: /
schedule:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 0ab5ab5..f0a851e 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -61,9 +61,9 @@ jobs:
server-username: SERVER_USERNAME
server-password: SERVER_PASSWORD
- run: mvn -B -ntp dependency:go-offline
- - run: mvn -B -ntp verify -Dcheckstyle.skip
+ - run: mvn -B -ntp verify -Dcheckstyle.skip -Dmaven.test.redirectTestOutputToFile=false
if: ${{ github.ref != 'refs/heads/main' }}
- - run: mvn -B -ntp deploy -Dcheckstyle.skip
+ - run: mvn -B -ntp deploy -Dcheckstyle.skip -Dmaven.test.redirectTestOutputToFile=false
if: ${{ github.ref == 'refs/heads/main' }}
env:
SERVER_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
diff --git a/README.md b/README.md
index 30c1cd6..3451ad6 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,74 @@
# Keycloak Event Metrics
-Provides metrics for Keycloak events.
+Provides metrics for Keycloak user/admin events.
[](http://www.apache.org/licenses/)
-[](https://search.maven.org/search?q=g:%22io.kokuwa.keycloak%22%20AND%20a:%22keycloak-event-metrics%22)
+[](https://central.sonatype.com/search?namespace=io.kokuwa.keycloak&q=keycloak-event-metrics)
[](https://github.com/kokuwaio/keycloak-event-metrics/actions/workflows/ci.yaml?query=branch%3Amain)
+
+## What?
+
+Resuses micrometer from Quarkus distribution to add metrics for Keycloak for events.
+
+### User Events
+
+User events are added with key `keycloak_event_user_total` and tags:
+
+* `type`: [EventType](https://github.com/keycloak/keycloak/blob/main/server-spi-private/src/main/java/org/keycloak/events/EventType.java#L27) from [Event#type](https://github.com/keycloak/keycloak/blob/main/server-spi-private/src/main/java/org/keycloak/events/Event.java#L44)
+* `realm`: realm id from [Event#realmId](https://github.com/keycloak/keycloak/blob/main/server-spi-private/src/main/java/org/keycloak/events/Event.java#L46)
+* `client`: client id from [Event#clientId](https://github.com/keycloak/keycloak/blob/main/server-spi-private/src/main/java/org/keycloak/events/Event.java#L48)
+* `error`: error from [Event#error](https://github.com/keycloak/keycloak/blob/main/server-spi-private/src/main/java/org/keycloak/events/Event.java#L56), only present for error types
+
+Examples:
+
+```txt
+keycloak_event_user_total{client="test",realm="9039a0b5-e8c9-437a-a02e-9d91b04548a4",type="LOGIN",error="",} 2.0
+keycloak_event_user_total{client="test",realm="1fdb3465-1675-49e8-88ad-292e2f42ee72",type="LOGIN",error="",} 1.0
+keycloak_event_user_total{client="test",realm="1fdb3465-1675-49e8-88ad-292e2f42ee72",type="LOGIN_ERROR",error="invalid_user_credentials",} 1.0
+```
+
+### Admin Events
+
+Admin events are added with key `keycloak_event_admin_total` and tags:
+
+* `realm`: realm id from [AdminEvent#realmId](https://github.com/keycloak/keycloak/blob/main/server-spi-private/src/main/java/org/keycloak/events/admin/AdminEvent.java#L44)
+* `operation`: [OperationType](https://github.com/keycloak/keycloak/blob/main/server-spi-private/src/main/java/org/keycloak/events/admin/OperationType.java#L27) from [AdminEvent#operationType](https://github.com/keycloak/keycloak/blob/main/server-spi-private/src/main/java/org/keycloak/events/admin/AdminEvent.java#L53)
+* `resource`: [ResourceType](https://github.com/keycloak/keycloak/blob/main/server-spi-private/src/main/java/org/keycloak/events/admin/ResourceType.java#L24) from [AdminEvent#resourceType](https://github.com/keycloak/keycloak/blob/main/server-spi-private/src/main/java/org/keycloak/events/admin/AdminEvent.java#L51)
+* `error`: error from [AdminEvent#error](https://github.com/keycloak/keycloak/blob/main/server-spi-private/src/main/java/org/keycloak/events/admin/AdminEvent.java#L59), only present for error types
+
+Examples:
+
+```txt
+keycloak_event_admin_total{error="",operation="CREATE",realm="1fdb3465-1675-49e8-88ad-292e2f42ee72",resource="USER",} 1.0
+keycloak_event_admin_total{error="",operation="CREATE",realm="9039a0b5-e8c9-437a-a02e-9d91b04548a4",resource="USER",} 1.0
+```
+
+## Installation
+
+### Testcontainers
+
+For usage in [Testcontainers](https://www.testcontainers.org/) see [KeycloakExtension.java](src/test/java/io/kokuwa/keycloak/metrics/junit/KeycloakExtension.java#L57-L68)
+
+### Docker
+
+Dockerfile:
+
+```Dockerfile
+FROM quay.io/keycloak/keycloak:21.0.1
+
+ENV KEYCLOAK_ADMIN=admin
+ENV KEYCLOAK_ADMIN_PASSWORD=password
+ENV KC_HEALTH_ENABLED=true
+ENV KC_METRICS_ENABLED=true
+ENV KC_LOG_CONSOLE_COLOR=true
+
+ADD target/keycloak-event-metrics-0.0.1-SNAPSHOT.jar /opt/keycloak/providers
+RUN /opt/keycloak/bin/kc.sh build
+```
+
+Run:
+
+```sh
+docker build . --tag keycloak:metrics
+docker run --rm -p8080 keycloak:metrics start-dev
+```
diff --git a/pom.xml b/pom.xml
index 541ce84..ab51bbd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,10 +4,10 @@
io.kokuwa.keycloak
keycloak-event-metrics
- 0.0.1-SNAPSHOT
+ 0.1.0-SNAPSHOT
Keycloak Metrics
- Provides metrics for Keycloak events
+ Provides metrics for Keycloak user/admin events
https://github.com/kokuwaio/keycloak-event-metrics
2023
@@ -210,6 +210,7 @@
${version.org.apache.maven.plugins.surefire}
true
+ ${maven.test.redirectTestOutputToFile}
diff --git a/src/main/java/io/kokuwa/keycloak/metrics/MicrometerEventRecorder.java b/src/main/java/io/kokuwa/keycloak/metrics/MicrometerEventRecorder.java
index 3102a38..99320c6 100644
--- a/src/main/java/io/kokuwa/keycloak/metrics/MicrometerEventRecorder.java
+++ b/src/main/java/io/kokuwa/keycloak/metrics/MicrometerEventRecorder.java
@@ -3,7 +3,6 @@ package io.kokuwa.keycloak.metrics;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
-import java.util.Optional;
import org.keycloak.events.Event;
import org.keycloak.events.admin.AdminEvent;
@@ -18,22 +17,6 @@ import io.micrometer.core.instrument.MeterRegistry;
*/
public class MicrometerEventRecorder {
- private static final String PREFIX = "keycloak_";
- private static final String USER_EVENT_PREFIX = PREFIX + "user_event_";
- private static final String ADMIN_EVENT_PREFIX = PREFIX + "admin_event_";
-
- private static final String LOGIN_ATTEMPTS = PREFIX + "login_attempts";
- private static final String LOGIN_SUCCESS = PREFIX + "logins";
- private static final String LOGIN_FAILURE = PREFIX + "failed_login_attempts";
- private static final String CLIENT_LOGIN_SUCCESS = PREFIX + "client_logins";
- private static final String CLIENT_LOGIN_FAILURE = PREFIX + "failed_client_login_attempts";
- private static final String REGISTER_SUCCESS = PREFIX + "registrations";
- private static final String REGISTER_FAILURE = PREFIX + "registrations_errors";
- private static final String REFRESH_TOKEN_SUCCESS = PREFIX + "refresh_tokens";
- private static final String REFRESH_TOKEN_FAILURE = PREFIX + "refresh_tokens_errors";
- private static final String CODE_TO_TOKEN_SUCCESS = PREFIX + "code_to_tokens";
- private static final String CODE_TO_TOKEN_FAILURE = PREFIX + "code_to_tokens_errors";
-
private final Map counters = new HashMap<>();
private final MeterRegistry registry;
@@ -42,61 +25,27 @@ public class MicrometerEventRecorder {
}
void adminEvent(AdminEvent event) {
- counter(ADMIN_EVENT_PREFIX + event.getOperationType().name(),
- "realm", event.getRealmId(),
- "resource", event.getResourceType() == null ? "" : event.getResourceType().name());
+ counter("keycloak_event_admin",
+ "realm", toBlankIfNull(event.getRealmId()),
+ "resource", toBlankIfNull(event.getResourceType()),
+ "operation", toBlankIfNull(event.getOperationType()),
+ "error", toBlankIfNull(event.getError()));
}
void userEvent(Event event) {
-
- var tags = new String[] {
- "provider", Optional
- .ofNullable(event.getDetails()).orElseGet(Map::of)
- .getOrDefault("identity_provider", "keycloak"),
- "realm", event.getRealmId() == null ? "" : event.getRealmId(),
- "client_id", event.getClientId() == null ? "" : event.getClientId(),
- "error", event.getError() == null ? "" : event.getError() };
-
- switch (event.getType()) {
- case LOGIN:
- counter(LOGIN_ATTEMPTS, tags);
- counter(LOGIN_SUCCESS, tags);
- break;
- case LOGIN_ERROR:
- counter(LOGIN_ATTEMPTS, tags);
- counter(LOGIN_FAILURE, tags);
- break;
- case CLIENT_LOGIN:
- counter(CLIENT_LOGIN_SUCCESS, tags);
- break;
- case CLIENT_LOGIN_ERROR:
- counter(CLIENT_LOGIN_FAILURE, tags);
- break;
- case REGISTER:
- counter(REGISTER_SUCCESS, tags);
- break;
- case REGISTER_ERROR:
- counter(REGISTER_FAILURE, tags);
- break;
- case REFRESH_TOKEN:
- counter(REFRESH_TOKEN_SUCCESS, tags);
- break;
- case REFRESH_TOKEN_ERROR:
- counter(REFRESH_TOKEN_FAILURE, tags);
- break;
- case CODE_TO_TOKEN:
- counter(CODE_TO_TOKEN_SUCCESS, tags);
- break;
- case CODE_TO_TOKEN_ERROR:
- counter(CODE_TO_TOKEN_FAILURE, tags);
- break;
- default:
- counter(USER_EVENT_PREFIX + event.getType().name(), tags);
- }
+ counter("keycloak_event_user",
+ "realm", toBlankIfNull(event.getRealmId()),
+ "type", toBlankIfNull(event.getType()),
+ "client", toBlankIfNull(event.getClientId()),
+ "error", toBlankIfNull(event.getError()));
}
private void counter(String counter, String... tags) {
counters.computeIfAbsent(counter + Arrays.toString(tags), string -> registry.counter(counter, tags))
.increment();
}
+
+ private String toBlankIfNull(Object value) {
+ return value == null ? "" : value.toString();
+ }
}
diff --git a/src/test/java/io/kokuwa/keycloak/metrics/KeycloakIT.java b/src/test/java/io/kokuwa/keycloak/metrics/KeycloakIT.java
index 0d57db0..c3115fe 100644
--- a/src/test/java/io/kokuwa/keycloak/metrics/KeycloakIT.java
+++ b/src/test/java/io/kokuwa/keycloak/metrics/KeycloakIT.java
@@ -10,6 +10,7 @@ import java.util.UUID;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.keycloak.events.EventType;
import io.kokuwa.keycloak.metrics.junit.KeycloakClient;
import io.kokuwa.keycloak.metrics.junit.KeycloakExtension;
@@ -34,15 +35,12 @@ public class KeycloakIT {
keycloak.createUser(realmName2, username2, password2);
prometheus.scrap();
- var loginAttemptsBefore = prometheus.loginAttempts();
- var loginAttemptsBefore1 = prometheus.loginAttempts(realmId1);
- var loginAttemptsBefore2 = prometheus.loginAttempts(realmId2);
- var loginSuccessBefore = prometheus.loginSuccess();
- var loginSuccessBefore1 = prometheus.loginSuccess(realmId1);
- var loginSuccessBefore2 = prometheus.loginSuccess(realmId2);
- var loginFailureBefore = prometheus.loginFailure();
- var loginFailureBefore1 = prometheus.loginFailure(realmId1);
- var loginFailureBefore2 = prometheus.loginFailure(realmId2);
+ var loginBefore = prometheus.userEvent(EventType.LOGIN);
+ var loginBefore1 = prometheus.userEvent(EventType.LOGIN, realmId1);
+ var loginBefore2 = prometheus.userEvent(EventType.LOGIN, realmId2);
+ var loginErrorBefore = prometheus.userEvent(EventType.LOGIN_ERROR);
+ var loginErrorBefore1 = prometheus.userEvent(EventType.LOGIN_ERROR, realmId1);
+ var loginErrorBefore2 = prometheus.userEvent(EventType.LOGIN_ERROR, realmId2);
assertTrue(keycloak.login(realmName1, username1, password1));
assertTrue(keycloak.login(realmName1, username1, password1));
@@ -50,25 +48,19 @@ public class KeycloakIT {
assertFalse(keycloak.login(realmName2, username2, "nope"));
prometheus.scrap();
- var loginAttemptsAfter = prometheus.loginAttempts();
- var loginAttemptsAfter1 = prometheus.loginAttempts(realmId1);
- var loginAttemptsAfter2 = prometheus.loginAttempts(realmId2);
- var loginSuccessAfter = prometheus.loginSuccess();
- var loginSuccessAfter1 = prometheus.loginSuccess(realmId1);
- var loginSuccessAfter2 = prometheus.loginSuccess(realmId2);
- var loginFailureAfter = prometheus.loginFailure();
- var loginFailureAfter1 = prometheus.loginFailure(realmId1);
- var loginFailureAfter2 = prometheus.loginFailure(realmId2);
+ var loginAfter = prometheus.userEvent(EventType.LOGIN);
+ var loginAfter1 = prometheus.userEvent(EventType.LOGIN, realmId1);
+ var loginAfter2 = prometheus.userEvent(EventType.LOGIN, realmId2);
+ var loginErrorAfter = prometheus.userEvent(EventType.LOGIN_ERROR);
+ var loginErrorAfter1 = prometheus.userEvent(EventType.LOGIN_ERROR, realmId1);
+ var loginErrorAfter2 = prometheus.userEvent(EventType.LOGIN_ERROR, realmId2);
- assertAll("promethus",
- () -> assertEquals(loginAttemptsBefore + 4, loginAttemptsAfter, "login attempts total"),
- () -> assertEquals(loginAttemptsBefore1 + 2, loginAttemptsAfter1, "login attempts #1"),
- () -> assertEquals(loginAttemptsBefore2 + 2, loginAttemptsAfter2, "login attempts #2"),
- () -> assertEquals(loginSuccessBefore + 3, loginSuccessAfter, "login success total"),
- () -> assertEquals(loginSuccessBefore1 + 2, loginSuccessAfter1, "login success #1"),
- () -> assertEquals(loginSuccessBefore2 + 1, loginSuccessAfter2, "login success #2"),
- () -> assertEquals(loginFailureBefore + 1, loginFailureAfter, "login failure total"),
- () -> assertEquals(loginFailureBefore1 + 0, loginFailureAfter1, "login failure #1"),
- () -> assertEquals(loginFailureBefore2 + 1, loginFailureAfter2, "login failure #2"));
+ assertAll("prometheus",
+ () -> assertEquals(loginBefore + 3, loginAfter, "login success total"),
+ () -> assertEquals(loginBefore1 + 2, loginAfter1, "login success #1"),
+ () -> assertEquals(loginBefore2 + 1, loginAfter2, "login success #2"),
+ () -> assertEquals(loginErrorBefore + 1, loginErrorAfter, "login failure total"),
+ () -> assertEquals(loginErrorBefore1 + 0, loginErrorAfter1, "login failure #1"),
+ () -> assertEquals(loginErrorBefore2 + 1, loginErrorAfter2, "login failure #2"));
}
}
diff --git a/src/test/java/io/kokuwa/keycloak/metrics/prometheus/Prometheus.java b/src/test/java/io/kokuwa/keycloak/metrics/prometheus/Prometheus.java
index fce4562..d249edd 100644
--- a/src/test/java/io/kokuwa/keycloak/metrics/prometheus/Prometheus.java
+++ b/src/test/java/io/kokuwa/keycloak/metrics/prometheus/Prometheus.java
@@ -7,6 +7,8 @@ import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import org.keycloak.events.EventType;
+
/**
* Client to access Prometheus metric values:
*
@@ -21,28 +23,21 @@ public class Prometheus {
this.client = client;
}
- public int loginAttempts() {
- return scrap("keycloak_login_attempts_total").intValue();
+ public int userEvent(EventType type) {
+ return state.stream()
+ .filter(metric -> Objects.equals(metric.name(), "keycloak_event_user_total"))
+ .filter(metric -> Objects.equals(metric.tags().get("type"), type.toString()))
+ .mapToInt(metric -> metric.value().intValue())
+ .sum();
}
- public int loginAttempts(String realmName) {
- return scrap("keycloak_login_attempts_total", "realm", realmName).intValue();
- }
-
- public int loginSuccess() {
- return scrap("keycloak_logins_total").intValue();
- }
-
- public int loginSuccess(String realmName) {
- return scrap("keycloak_logins_total", "realm", realmName).intValue();
- }
-
- public int loginFailure() {
- return scrap("keycloak_failed_login_attempts_total").intValue();
- }
-
- public int loginFailure(String realmName) {
- return scrap("keycloak_failed_login_attempts_total", "realm", realmName).intValue();
+ public int userEvent(EventType type, String realmName) {
+ return state.stream()
+ .filter(metric -> Objects.equals(metric.name(), "keycloak_event_user_total"))
+ .filter(metric -> Objects.equals(metric.tags().get("type"), type.toString()))
+ .filter(metric -> Objects.equals(metric.tags().get("realm"), realmName))
+ .mapToInt(metric -> metric.value().intValue())
+ .sum();
}
public void scrap() {
@@ -63,19 +58,4 @@ public class Prometheus {
})
.forEach(state::add);
}
-
- private Double scrap(String name) {
- return state.stream()
- .filter(metric -> Objects.equals(metric.name(), name))
- .mapToDouble(PrometheusMetric::value)
- .sum();
- }
-
- private Double scrap(String name, String tag, String value) {
- return state.stream()
- .filter(metric -> Objects.equals(metric.name(), name))
- .filter(metric -> Objects.equals(metric.tags().get(tag), value))
- .mapToDouble(PrometheusMetric::value)
- .sum();
- }
}