Skip to content

Commit

Permalink
Add missing event metrics from aerogear/keycloak-metrics-spi to a key…
Browse files Browse the repository at this point in the history
  • Loading branch information
bohmber committed Oct 4, 2024
1 parent 84b0afe commit 85b1190
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 237 deletions.
28 changes: 11 additions & 17 deletions docs/guides/server/event-metrics.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,17 @@ The snippet below is an example of a response:
[source]
----
# TYPE keycloak_admin_event counter
# HELP keycloak_admin_event
keycloak_admin_event_total{operation="CREATE",realm="master",resource="CLIENT"} 1.0
keycloak_admin_event_total{operation="DELETE",realm="master",resource="CLIENT"} 1.0
# TYPE keycloak_event_logout counter
# HELP keycloak_event_logout
keycloak_event_logout_total{realm="master",result="success"} 1.0
# TYPE keycloak_event_code_to_token counter
# HELP keycloak_event_code_to_token
keycloak_event_code_to_token_total{client_id="security-admin-console",error="",provider="keycloak",realm="master",result="success"} 1.0
# TYPE keycloak_event_login counter
# HELP keycloak_event_login
keycloak_event_login_total{client_id="security-admin-console",error="",provider="keycloak",realm="master",result="success"} 1.0
keycloak_event_login_total{client_id="client_not_found",error="client_not_found",provider="keycloak",realm="master",result="error"} 1.0
# TYPE keycloak_event_refresh_token counter
# HELP keycloak_event_refresh_token
keycloak_event_refresh_token_total{client_id="security-admin-console",error="",provider="keycloak",realm="master",result="success"} 2.0
# TYPE keycloak_simple_events counter
# HELP keycloak_simple_events The total number of Keycloak events
keycloak_simple_events_total{event="update_password",realm="master"} 1.0
keycloak_simple_events_total{event="update_credential",realm="master"} 1.0
# TYPE keycloak_events counter
# HELP keycloak_events The total number of Keycloak events
keycloak_events_total{client_id="security-admin-console",error="",event="code_to_token",idp="",realm="master"} 6.0
keycloak_events_total{client_id="security-admin-console",error="",event="logout",idp="",realm="master"} 2.0
keycloak_events_total{client_id="security-admin-console",error="",event="login",idp="",realm="master"} 6.0
keycloak_events_total{client_id="security-admin-console",error="",event="refresh_token",idp="",realm="master"} 2.0
keycloak_events_total{client_id="client_not_found",error="client_not_found",event="login_error",idp="",realm="master"} 1.0
...
----

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@

package org.keycloak.quarkus.runtime.services.metrics.events;

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.binder.BaseUnits;
import org.jboss.logging.Logger;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
Expand All @@ -38,40 +41,34 @@ public class MicrometerMetricsEventListener implements EventListenerProvider {

private static final Logger logger = Logger.getLogger(MicrometerMetricsEventListener.class);

private static final String PROVIDER_KEYCLOAK_OPENID = "keycloak";
private static final String EMPTY_IDP = "";
private static final String REALM_TAG = "realm";
private static final String PROVIDER_TAG = "provider";
private static final String IDP_TAG = "idp";
private static final String CLIENT_ID_TAG = "client.id";
private static final String ERROR_TAG = "error";
private static final String RESOURCE_TAG = "resource";
private static final String OPERATION_TAG = "operation";
private static final String RESULT_TAG = "result";
private static final String SUCCESS = "success";
private static final String ERROR = "error";
public static final String EVENT_TYPE_ERROR_SUFFIX = "_ERROR";

private static final String KEYLOAK_METER_NAME_PREFIX = "keycloak.";
private static final String EVENT_PREFIX = KEYLOAK_METER_NAME_PREFIX + "event.";
private static final String ADMIN_EVENT_COUNTER_NAME = KEYLOAK_METER_NAME_PREFIX + "admin.event";

private static final Map<EventType, String> EVENT_TYPE_TO_NAME =
private static final String EVENT_TAG = "event";
// TODO better description
private static final String DESCRIPTION_OF_EVENT_METER = "The total number of Keycloak events";
private static final String KEYLOAK_METER_NAME = "keycloak";
// TODO better name for simple
private static final String SIMPLE_EVENT_METER_NAME = KEYLOAK_METER_NAME + ".simple";

private static final Map<EventType, String> EVENT_TYPE_TO_TAG_VALUE =
Arrays.stream(EventType.values())
.collect(Collectors.toMap(e -> e, MicrometerMetricsEventListener::buildCounterName));
.collect(Collectors.toMap(e -> e, MicrometerMetricsEventListenerFactory::format));

private final EventListenerTransaction tx =
new EventListenerTransaction(this::countRealmResourceTagsFromGenericAdminEvent, this::countEvent);
new EventListenerTransaction(null, this::countEvent);

private final MeterRegistry meterRegistry = Metrics.globalRegistry;
private final EnumSet<EventType> includedEvents;
private final EnumSet<EventType> eventsWithAdditionalTags;
private final boolean adminEventEnabled;

public MicrometerMetricsEventListener(KeycloakSession session, EnumSet<EventType> includedEvents,
EnumSet<EventType> eventsWithAdditionalTags, boolean adminEventEnabled) {
public MicrometerMetricsEventListener(KeycloakSession session,
EnumSet<EventType> includedEvents,
EnumSet<EventType> eventsWithAdditionalTags) {
session.getTransactionManager().enlistAfterCompletion(tx);
this.includedEvents = includedEvents;
this.eventsWithAdditionalTags = eventsWithAdditionalTags;
this.adminEventEnabled = adminEventEnabled;
}

@Override
Expand All @@ -85,41 +82,38 @@ private void countEvent(Event event) {
logger.debugf("Received user event of type %s in realm %s",
event.getType().name(), event.getRealmName());
if (eventsWithAdditionalTags.contains(event.getType())) {
countRealmProviderClientIdResultErrorTagsFromEvent(event);
countRealmEventIdpClientIdErrorTagsFromEvent(event);
} else {
countRealmResultTagsFromEvent(event);
countRealmEventTagsFromEvent(event);
}
}

@Override
public void onEvent(AdminEvent event, boolean includeRepresentation) {
if (adminEventEnabled) {
tx.addAdminEvent(event, includeRepresentation);
}
}

private void countRealmResultTagsFromEvent(final Event event) {
meterRegistry.counter(EVENT_TYPE_TO_NAME.get(event.getType()),
REALM_TAG, nullToEmpty(event.getRealmName()),
RESULT_TAG, getResultCode(event)).increment();
// do nothing
}

private void countRealmProviderClientIdResultErrorTagsFromEvent(final Event event) {
meterRegistry.counter(EVENT_TYPE_TO_NAME.get(event.getType()),
REALM_TAG, nullToEmpty(event.getRealmName()),
PROVIDER_TAG, getIdentityProvider(event),
CLIENT_ID_TAG, getErrorClientId(event),
RESULT_TAG, getResultCode(event),
ERROR_TAG, nullToEmpty(event.getError())).increment();
private void countRealmEventTagsFromEvent(final Event event) {
Counter.builder(SIMPLE_EVENT_METER_NAME)
.description(DESCRIPTION_OF_EVENT_METER)
.tags(Tags.of(Tag.of(REALM_TAG, nullToEmpty(event.getRealmName())),
Tag.of(EVENT_TAG, EVENT_TYPE_TO_TAG_VALUE.get(event.getType()))))
.baseUnit(BaseUnits.EVENTS)
.register(Metrics.globalRegistry)
.increment();
}

private void countRealmResourceTagsFromGenericAdminEvent(final AdminEvent event, boolean includeRepresentation) {
logger.debugf("Received admin event of type %s (%s) in realm %s",
event.getOperationType().name(), event.getResourceType().name(), event.getRealmName());
meterRegistry.counter(ADMIN_EVENT_COUNTER_NAME,
REALM_TAG, nullToEmpty(event.getRealmName()),
RESOURCE_TAG, event.getResourceType().name(),
OPERATION_TAG, event.getOperationType().name()).increment();
private void countRealmEventIdpClientIdErrorTagsFromEvent(final Event event) {
Counter.builder(KEYLOAK_METER_NAME)
.description(DESCRIPTION_OF_EVENT_METER)
.tags(Tags.of(Tag.of(REALM_TAG, nullToEmpty(event.getRealmName())),
Tag.of(EVENT_TAG, EVENT_TYPE_TO_TAG_VALUE.get(event.getType())),
Tag.of(IDP_TAG, getIdentityProvider(event)),
Tag.of(CLIENT_ID_TAG, getErrorClientId(event)),
Tag.of(ERROR_TAG, nullToEmpty(event.getError()))))
.baseUnit(BaseUnits.EVENTS)
.register(Metrics.globalRegistry)
.increment();
}

private String getIdentityProvider(Event event) {
Expand All @@ -128,18 +122,12 @@ private String getIdentityProvider(Event event) {
identityProvider = event.getDetails().get(Details.IDENTITY_PROVIDER);
}
if (identityProvider == null) {
identityProvider = PROVIDER_KEYCLOAK_OPENID;
identityProvider = EMPTY_IDP;
}
return identityProvider;
}

private String getResultCode(Event event) {
return isError(event) ? ERROR : SUCCESS;
}

private boolean isError(Event event) {
return event.getType().name().endsWith(EVENT_TYPE_ERROR_SUFFIX);
}
private String getErrorClientId(Event event) {
return nullToEmpty(Errors.CLIENT_NOT_FOUND.equals(event.getError())
? Errors.CLIENT_NOT_FOUND : event.getClientId());
Expand All @@ -149,14 +137,6 @@ private String nullToEmpty(String value) {
return value == null ? "" : value;
}

private static String buildCounterName(EventType type) {
String name = type.name();
if (name.endsWith(EVENT_TYPE_ERROR_SUFFIX)) {
name = name.substring(0, name.length() - EVENT_TYPE_ERROR_SUFFIX.length());
}
return EVENT_PREFIX + name.toLowerCase().replace("_", ".");
}

@Override
public void close() {
// unused
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import java.util.Comparator;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.stream.Stream;

public class MicrometerMetricsEventListenerFactory implements EventListenerProviderFactory {
Expand All @@ -44,24 +45,25 @@ public class MicrometerMetricsEventListenerFactory implements EventListenerProvi
private static final String ID = "micrometer-metrics";
private static final String EXCLUDED_EVENTS_OPTION = "excluded-events";
private static final String EVENTS_WITH_ADDITIONAL_TAGS_OPTION = "events-with-additional-tags";
private static final String ADMIN_EVENTS_ENABLED_OPTION = "admin-events-enabled";
private static final String[] EVENTS_WITH_ADDITIONAL_TAGS_DEFAULT =
Stream.of(EventType.LOGIN, EventType.LOGOUT, EventType.CLIENT_LOGIN,
EventType.CODE_TO_TOKEN, EventType.REFRESH_TOKEN, EventType.REGISTER)
.map(EventType::name)
.map(String::toLowerCase)
Stream.of(EventType.LOGIN, EventType.LOGIN_ERROR,
EventType.LOGOUT, EventType.LOGOUT_ERROR,
EventType.CLIENT_LOGIN, EventType.CLIENT_LOGIN_ERROR,
EventType.CODE_TO_TOKEN, EventType.CODE_TO_TOKEN_ERROR,
EventType.REFRESH_TOKEN, EventType.REFRESH_TOKEN_ERROR,
EventType.REGISTER, EventType.REGISTER_ERROR)
.map(MicrometerMetricsEventListenerFactory::format)
.toArray(String[]::new);

private final EnumSet<EventType> includedEvents = EnumSet.allOf(EventType.class);
private final EnumSet<EventType> eventsWithAdditionalTags = EnumSet.noneOf(EventType.class);
private boolean adminEventsEnabled;

private boolean metricsEnabled;

@Override
public EventListenerProvider create(KeycloakSession session) {
if (metricsEnabled) {
return new MicrometerMetricsEventListener(session, includedEvents,
eventsWithAdditionalTags, adminEventsEnabled);
eventsWithAdditionalTags);
} else {
return NO_OP_LISTENER;
}
Expand All @@ -72,20 +74,17 @@ public void init(Config.Scope config) {
String[] excluded = config.getArray(EXCLUDED_EVENTS_OPTION);
if (excluded != null) {
for (String e : excluded) {
includedEvents.remove(EventType.valueOf(e.toUpperCase()));
includedEvents.remove(EventType.valueOf(e.toUpperCase(Locale.ROOT)));
}
}
String[] eventWithAdditionalDetailsOption = config.getArray(EVENTS_WITH_ADDITIONAL_TAGS_OPTION);
if (eventWithAdditionalDetailsOption == null) {
eventWithAdditionalDetailsOption = EVENTS_WITH_ADDITIONAL_TAGS_DEFAULT;
}
for (String e : eventWithAdditionalDetailsOption) {
String name = e.toUpperCase();
String name = e.toUpperCase(Locale.ROOT);
eventsWithAdditionalTags.add(EventType.valueOf(name));
eventsWithAdditionalTags.add(EventType.valueOf(name
+ MicrometerMetricsEventListener.EVENT_TYPE_ERROR_SUFFIX));
}
adminEventsEnabled = config.getBoolean(ADMIN_EVENTS_ENABLED_OPTION, true);
}

@Override
Expand All @@ -104,14 +103,7 @@ public void close() {
@Override
public List<ProviderConfigProperty> getConfigMetadata() {
String[] supportedEvents = Arrays.stream(EventType.values())
.map(EventType::name)
.map(String::toLowerCase)
.sorted(Comparator.naturalOrder())
.toArray(String[]::new);
String[] supportedEventsWithAdditionalTags = Arrays.stream(EventType.values())
.map(EventType::name)
.filter(n -> !n.endsWith(MicrometerMetricsEventListener.EVENT_TYPE_ERROR_SUFFIX))
.map(String::toLowerCase)
.map(MicrometerMetricsEventListenerFactory::format)
.sorted(Comparator.naturalOrder())
.toArray(String[]::new);
String eventsWithAdditionalTagsDefault = String.join(",", EVENTS_WITH_ADDITIONAL_TAGS_DEFAULT);
Expand All @@ -126,15 +118,9 @@ public List<ProviderConfigProperty> getConfigMetadata() {
.name(EVENTS_WITH_ADDITIONAL_TAGS_OPTION)
.type(ProviderConfigProperty.MULTIVALUED_STRING_TYPE)
.helpText("A comma-separated list of events that are counted with additional labels/tags (provider,client_id,error)."
+ "The corresponding Error Events are added automatically. Default value: "
+ " Default value: "
+ eventsWithAdditionalTagsDefault)
.options(supportedEventsWithAdditionalTags)
.add()
.property()
.name(ADMIN_EVENTS_ENABLED_OPTION)
.type(ProviderConfigProperty.BOOLEAN_TYPE)
.helpText("AdminEvents should be collected as a metric.")
.defaultValue(true)
.options(supportedEvents)
.add()
.build();
}
Expand All @@ -144,6 +130,11 @@ public String getId() {
return ID;
}

public static String format(EventType type) {
String name = type.name();
return name.toLowerCase(Locale.ROOT);
}

private static class NoOpEventListenerProvider implements EventListenerProvider {
@Override
public void onEvent(Event event) {
Expand Down
Loading

0 comments on commit 85b1190

Please sign in to comment.