From 23bc2ec41d7b2c68e04f0c0ee27d679654ac70e3 Mon Sep 17 00:00:00 2001 From: transcaffeine Date: Tue, 20 Aug 2024 17:58:00 +0200 Subject: [PATCH] feat: initial commit registering in the event-listener- and realm-resource-SPIs --- .gitignore | 38 ++++++ .idea/encodings.xml | 7 ++ .idea/misc.xml | 13 +++ .../keycloak_metrics__package_.xml | 32 ++++++ .idea/vcs.xml | 6 + .idea/workspace.xml | 87 ++++++++++++++ pom.xml | 66 +++++++++++ src/main/java/KeycloakMetricsServer.java | 2 + .../_finally/keycloak_metrics/Metrics.java | 108 ++++++++++++++++++ .../keycloak_metrics/MetricsEndpoint.java | 28 +++++ .../MetricsEndpointFactory.java | 34 ++++++ .../MetricsEventListener.java | 50 ++++++++ .../MetricsEventListenerFactory.java | 34 ++++++ ...ycloak.events.EventListenerProviderFactory | 1 + ...ices.resource.RealmResourceProviderFactory | 1 + 15 files changed, 507 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/encodings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations/keycloak_metrics__package_.xml create mode 100644 .idea/vcs.xml create mode 100644 .idea/workspace.xml create mode 100644 pom.xml create mode 100644 src/main/java/KeycloakMetricsServer.java create mode 100644 src/main/java/coffee/_finally/keycloak_metrics/Metrics.java create mode 100644 src/main/java/coffee/_finally/keycloak_metrics/MetricsEndpoint.java create mode 100644 src/main/java/coffee/_finally/keycloak_metrics/MetricsEndpointFactory.java create mode 100644 src/main/java/coffee/_finally/keycloak_metrics/MetricsEventListener.java create mode 100644 src/main/java/coffee/_finally/keycloak_metrics/MetricsEventListenerFactory.java create mode 100644 src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory create mode 100644 src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..ef2a2f1 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/keycloak_metrics__package_.xml b/.idea/runConfigurations/keycloak_metrics__package_.xml new file mode 100644 index 0000000..931995f --- /dev/null +++ b/.idea/runConfigurations/keycloak_metrics__package_.xml @@ -0,0 +1,32 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..4b1bf9c --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1724001613087 + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..5ecb3b5 --- /dev/null +++ b/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + + org.keycloak + keycloak-parent + 24.0.0 + + + keycloak-metrics + jar + + + + org.keycloak + keycloak-core + provided + + + org.keycloak + keycloak-server-spi + provided + + + org.keycloak + keycloak-server-spi-private + provided + + + org.keycloak + keycloak-common + + + org.keycloak + keycloak-services + provided + + + io.prometheus + simpleclient + 0.16.0 + + + io.prometheus + simpleclient_common + 0.16.0 + + + + + keycloak-metrics + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + \ No newline at end of file diff --git a/src/main/java/KeycloakMetricsServer.java b/src/main/java/KeycloakMetricsServer.java new file mode 100644 index 0000000..e158edb --- /dev/null +++ b/src/main/java/KeycloakMetricsServer.java @@ -0,0 +1,2 @@ +public class KeycloakMetricsServer { +} diff --git a/src/main/java/coffee/_finally/keycloak_metrics/Metrics.java b/src/main/java/coffee/_finally/keycloak_metrics/Metrics.java new file mode 100644 index 0000000..8b9315d --- /dev/null +++ b/src/main/java/coffee/_finally/keycloak_metrics/Metrics.java @@ -0,0 +1,108 @@ +package coffee._finally.keycloak_metrics; + +import io.prometheus.client.Collector; +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.Counter; +import io.prometheus.client.Gauge; +import io.prometheus.client.exporter.common.TextFormat; + +import java.io.*; +import java.util.HashMap; +import java.util.Map; + +public class Metrics { + + private static Metrics METRICS_INSTANCE = null; + private static Map metrics = new HashMap(); + private static CollectorRegistry registry = new CollectorRegistry(); + + private Metrics() { + initializeMetrics(); + } + + private void initializeMetrics() { + metrics.put("realm", Gauge.build() + .namespace("keycloak") + .name("realm_count") + .labelNames("realm") + .help("Number of realms in keycloak") + .register(registry)); + metrics.put("realm_role", Gauge.build() + .namespace("keycloak") + .name("role_count") + .labelNames("realm") + .help("Number of roles in a realm") + .register(registry)); + metrics.put("admin_event", Counter.build() + .namespace("keycloak").name("admin_event_count") + .labelNames("realm", "operation", "resource") + .help("Number and type of admin events since startup") + .register(registry)); + metrics.put("user_event", Counter.build() + .namespace("keycloak").name("user_event_count") + .labelNames("realm", "type") + .help("Number and type of user events since startup") + .register(registry)); + } + + public static synchronized Metrics getInstance() { + if (METRICS_INSTANCE == null) { + METRICS_INSTANCE = new Metrics(); + } + return METRICS_INSTANCE; + } + + public static void increment(String metric) { + Collector collector = metrics.get(metric); + if (collector == null) { + return; + } + if (collector instanceof Gauge) { + ((Gauge) collector).inc(); + return; + } + if (collector instanceof Counter) { + ((Counter) collector).inc(); + } + } + + public static void incrementLabelled(String metric, String... labels) { + Collector collector = metrics.get(metric); + if (collector == null) { + return; + } + if (collector instanceof Gauge) { + ((Gauge) collector).labels(labels).inc(); + return; + } + if (collector instanceof Counter) { + ((Counter) collector).labels(labels).inc(); + } + } + + public static void decrement(String metric) { + Collector collector = metrics.get(metric); + if (collector == null) { + return; + } + if (collector instanceof Gauge) { + ((Gauge) collector).dec(); + } + } + + public static void decrementLabelled(String metric, String... labels) { + Collector collector = metrics.get(metric); + if (collector == null) { + return; + } + if (collector instanceof Gauge) { + ((Gauge) collector).labels(labels).dec(); + } + } + + public void export(final OutputStream stream) throws IOException { + final Writer writer = new BufferedWriter(new OutputStreamWriter(stream)); + TextFormat.writeOpenMetrics100(writer, registry.metricFamilySamples()); + writer.flush(); + } +} diff --git a/src/main/java/coffee/_finally/keycloak_metrics/MetricsEndpoint.java b/src/main/java/coffee/_finally/keycloak_metrics/MetricsEndpoint.java new file mode 100644 index 0000000..56f8e74 --- /dev/null +++ b/src/main/java/coffee/_finally/keycloak_metrics/MetricsEndpoint.java @@ -0,0 +1,28 @@ +package coffee._finally.keycloak_metrics; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.*; +import org.keycloak.services.resource.RealmResourceProvider; + +public class MetricsEndpoint implements RealmResourceProvider { + + public static final String ID = "metrics"; + + @Override + public Object getResource() { + return this; + } + + @Override + public void close() { + + } + + @GET + @Produces(MediaType.TEXT_PLAIN) + public Response get(@Context HttpHeaders headers) { + final StreamingOutput stream = outputStream -> Metrics.getInstance().export(outputStream); + return Response.ok(stream).build(); + } +} diff --git a/src/main/java/coffee/_finally/keycloak_metrics/MetricsEndpointFactory.java b/src/main/java/coffee/_finally/keycloak_metrics/MetricsEndpointFactory.java new file mode 100644 index 0000000..1b102a7 --- /dev/null +++ b/src/main/java/coffee/_finally/keycloak_metrics/MetricsEndpointFactory.java @@ -0,0 +1,34 @@ +package coffee._finally.keycloak_metrics; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.services.resource.RealmResourceProvider; +import org.keycloak.services.resource.RealmResourceProviderFactory; + +public class MetricsEndpointFactory implements RealmResourceProviderFactory { + @Override + public RealmResourceProvider create(KeycloakSession keycloakSession) { + return new MetricsEndpoint(); + } + + @Override + public void init(Config.Scope scope) { + // empty on purpose + } + + @Override + public void postInit(KeycloakSessionFactory keycloakSessionFactory) { + // empty on purpose + } + + @Override + public void close() { + // empty on purpose as nothing needs to be closed manually + } + + @Override + public String getId() { + return MetricsEndpoint.ID; + } +} diff --git a/src/main/java/coffee/_finally/keycloak_metrics/MetricsEventListener.java b/src/main/java/coffee/_finally/keycloak_metrics/MetricsEventListener.java new file mode 100644 index 0000000..a5b1280 --- /dev/null +++ b/src/main/java/coffee/_finally/keycloak_metrics/MetricsEventListener.java @@ -0,0 +1,50 @@ +package coffee._finally.keycloak_metrics; + +import org.keycloak.events.Event; +import org.keycloak.events.EventListenerProvider; +import org.keycloak.events.admin.AdminEvent; + +public class MetricsEventListener implements EventListenerProvider { + + public static final String ID = "metrics-listener"; + + @Override + public void onEvent(Event event) { + Metrics.getInstance().incrementLabelled( + "user_event", + event.getRealmId(), event.getType().toString() + ); + } + + @Override + public void onEvent(AdminEvent adminEvent, boolean b) { + Metrics.getInstance().incrementLabelled( + "admin_event", + adminEvent.getRealmId(), + adminEvent.getOperationType().toString(), + adminEvent.getResourceTypeAsString() + ); + switch (adminEvent.getOperationType()) { + case CREATE: + Metrics.getInstance().incrementLabelled( + adminEvent.getResourceTypeAsString().toLowerCase(), + adminEvent.getRealmId() + ); + break; + case DELETE: + Metrics.getInstance().decrementLabelled( + adminEvent.getResourceTypeAsString().toLowerCase(), + adminEvent.getRealmId() + ); + break; + case UPDATE: + case ACTION: + break; + } + } + + @Override + public void close() { + + } +} diff --git a/src/main/java/coffee/_finally/keycloak_metrics/MetricsEventListenerFactory.java b/src/main/java/coffee/_finally/keycloak_metrics/MetricsEventListenerFactory.java new file mode 100644 index 0000000..442561e --- /dev/null +++ b/src/main/java/coffee/_finally/keycloak_metrics/MetricsEventListenerFactory.java @@ -0,0 +1,34 @@ +package coffee._finally.keycloak_metrics; + +import org.keycloak.Config; +import org.keycloak.events.EventListenerProvider; +import org.keycloak.events.EventListenerProviderFactory; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +public class MetricsEventListenerFactory implements EventListenerProviderFactory { + @Override + public EventListenerProvider create(KeycloakSession keycloakSession) { + return new MetricsEventListener(); + } + + @Override + public void init(Config.Scope scope) { + + } + + @Override + public void postInit(KeycloakSessionFactory keycloakSessionFactory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return MetricsEventListener.ID; + } +} diff --git a/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory b/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory new file mode 100644 index 0000000..04b5ccb --- /dev/null +++ b/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory @@ -0,0 +1 @@ +coffee._finally.keycloak_metrics.MetricsEventListenerFactory \ No newline at end of file diff --git a/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory b/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory new file mode 100644 index 0000000..e851359 --- /dev/null +++ b/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory @@ -0,0 +1 @@ +coffee._finally.keycloak_metrics.MetricsEndpointFactory \ No newline at end of file