Skip to content

Commit

Permalink
Dev UI - add CDI fired events view
Browse files Browse the repository at this point in the history
  • Loading branch information
mkouba committed Feb 12, 2021
1 parent 9046f48 commit acf51a0
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@
import org.jboss.jandex.FieldInfo;
import org.jboss.jandex.MethodInfo;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.ValidationPhaseBuildItem;
import io.quarkus.arc.processor.BeanDeploymentValidator;
import io.quarkus.arc.processor.BeanInfo;
import io.quarkus.arc.processor.BuildExtension;
import io.quarkus.arc.processor.ObserverInfo;
import io.quarkus.arc.runtime.ArcContainerSupplier;
import io.quarkus.arc.runtime.ArcRecorder;
import io.quarkus.arc.runtime.BeanLookupSupplier;
import io.quarkus.arc.runtime.devconsole.EventsMonitor;
import io.quarkus.deployment.IsDevelopment;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
Expand All @@ -29,11 +33,19 @@ public class DevConsoleProcessor {

@BuildStep(onlyIf = IsDevelopment.class)
@Record(ExecutionTime.STATIC_INIT)
public DevConsoleRuntimeTemplateInfoBuildItem collectBeanInfo(ArcRecorder recorder) {
public DevConsoleRuntimeTemplateInfoBuildItem exposeArcContainer(ArcRecorder recorder) {
return new DevConsoleRuntimeTemplateInfoBuildItem("arcContainer",
new ArcContainerSupplier());
}

@BuildStep(onlyIf = IsDevelopment.class)
void monitorEvents(BuildProducer<DevConsoleRuntimeTemplateInfoBuildItem> runtimeInfos,
BuildProducer<AdditionalBeanBuildItem> beans) {
runtimeInfos.produce(new DevConsoleRuntimeTemplateInfoBuildItem("eventsMonitor",
new BeanLookupSupplier(EventsMonitor.class)));
beans.produce(AdditionalBeanBuildItem.unremovableOf(EventsMonitor.class));
}

@BuildStep(onlyIf = IsDevelopment.class)
public DevConsoleTemplateInfoBuildItem collectBeanInfo(ValidationPhaseBuildItem validationPhaseBuildItem) {
BeanDeploymentValidator.ValidationContext validationContext = validationPhaseBuildItem.getContext();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
<a href="{urlbase}/beans" class="badge badge-light">
<i class="fa fa-egg fa-fw"></i>
Beans <span class="badge badge-light">{info:devBeanInfos.beans.size()}</span></a>
Beans <span class="badge badge-light">{info:devBeanInfos.beans.size}</span></a>
<br>
<a href="{urlbase}/observers" class="badge badge-light">
<i class="fa fa-eye fa-fw"></i>
Observers <span class="badge badge-light">{info:arcContainer.observers.size()}</span></a>
Observers <span class="badge badge-light">{info:arcContainer.observers.size}</span></a>
<br>
<a href="{urlbase}/events" class="badge badge-light">
<i class="far fa-eye"></i>
Fired Events</a>
<br>
<a href="{urlbase}/removed-beans" class="badge badge-light">
<i class="fa fa-trash-alt fa-fw"></i>
Removed Beans <span class="badge badge-light">{info:arcContainer.removedBeans.size()}</span></a>
Removed Beans <span class="badge badge-light">{info:arcContainer.removedBeans.size}</span></a>
<br>
<span class="badge badge-light">
<i class="fa fa-traffic-light fa-fw"></i>
Interceptors <span class="badge badge-light">{info:arcContainer.interceptors.size()}</span></span>
Interceptors <span class="badge badge-light">{info:arcContainer.interceptors.size}</span></span>
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{#include main fluid=true}
{#style}
.nav-item {
padding: .5rem .3rem;
}
{/style}
{#title}Fired Events{/title}
{#body}
<ul class="nav">
<li class="nav-item">
<input id="clear" type="submit" class="btn btn-primary btn-sm" value="Refresh"
onClick="window.location.reload();">
</li>
<li class="nav-item">
<form method="post" enctype="application/x-www-form-urlencoded" class="form-inline">
<input id="clear" type="submit" class="btn btn-primary btn-sm" value="Clear">
</form>
</li>
<li class="nav-item">
<form method="post" enctype="application/x-www-form-urlencoded" class="form-inline">
<input id="skipContextsCheck" type="checkbox" class="form-check-input" disabled{#if info:eventsMonitor.skipContextEvents} checked{/if}>
<label for="skipContextsCheck">Skip context lifecycle events</label>
<input type="hidden" name="action" value="skipContext">
&nbsp;
<input id="toggle" type="submit" class="btn btn-primary btn-sm" value="Toggle">
</form>
</li>
</ul>
<table class="table table-striped">
<thead class="thead-dark">
<tr>
<th scope="col">#</th>
<th scope="col">Event Type</th>
<th scope="col">Qualifiers</th>
<th scope="col">Timestamp</th>
</tr>
</thead>
<tbody>
{#for event in info:eventsMonitor.lastEvents}
<tr>
<td>{count}.</td>
<td>
<code>{event.type}</code>
</td>
<td>
{#when event.qualifiers.size}
{#is 1}
<code>{event.qualifiers.iterator.next}</code>
{#is > 1}
<ul>
{#for q in event.qualifiers}
<li>{q}</li>
{/for}
</ul>
{/when}
</td>
<td>
{event.timestamp}
</td>
</tr>
{/for}
</tbody>
</table>
{/body}
{/include}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package io.quarkus.arc.runtime.devconsole;

import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;

import javax.enterprise.context.BeforeDestroyed;
import javax.enterprise.context.Destroyed;
import javax.enterprise.context.Initialized;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.Any;
import javax.enterprise.inject.Default;
import javax.enterprise.inject.spi.EventMetadata;
import javax.inject.Singleton;

// This bean is only registered in the dev mode
@Singleton
public class EventsMonitor {

private static final int DEFAULT_EVENTS_LIMIT = 500;

private volatile boolean skipContextEvents = true;
private final List<EventInfo> events = Collections.synchronizedList(new ArrayList<EventInfo>(DEFAULT_EVENTS_LIMIT));

void notify(@Observes Object payload, EventMetadata eventMetadata) {
if (skipContextEvents && isContextEvent(eventMetadata)) {
return;
}
if (events.size() > DEFAULT_EVENTS_LIMIT) {
// Remove some old data if the limit is exceeded
synchronized (events) {
if (events.size() > DEFAULT_EVENTS_LIMIT) {
events.subList(0, DEFAULT_EVENTS_LIMIT / 2).clear();
}
}
}
events.add(EventInfo.from(eventMetadata));
}

public void clear() {
events.clear();
}

/**
* @return mutable copy of the captured event information in reverse order
*/
public List<EventInfo> getLastEvents() {
synchronized (events) {
List<EventInfo> result = new ArrayList<>(events.size());
for (ListIterator<EventInfo> iterator = events.listIterator(events.size()); iterator.hasPrevious();) {
result.add(iterator.previous());
}
return result;
}
}

public boolean isSkipContextEvents() {
return skipContextEvents;
}

public void toggleSkipContextEvents() {
// This is not thread-safe but we don't expect concurrent actions from dev ui
skipContextEvents = !skipContextEvents;
}

boolean isContextEvent(EventMetadata eventMetadata) {
if (!eventMetadata.getType().equals(Object.class) || eventMetadata.getQualifiers().size() != 2) {
return false;
}
for (Annotation qualifier : eventMetadata.getQualifiers()) {
Type qualifierType = qualifier.annotationType();
// @Any, @Initialized, @BeforeDestroyed or @Destroyed
if (!qualifierType.equals(Any.class) && !qualifierType.equals(Initialized.class)
&& !qualifierType.equals(BeforeDestroyed.class) && !qualifierType.equals(Destroyed.class)) {

return false;
}
}
return true;
}

static class EventInfo {

static EventInfo from(EventMetadata eventMetadata) {
List<Annotation> qualifiers;
if (eventMetadata.getQualifiers().size() == 1) {
// Just @Any
qualifiers = Collections.emptyList();
} else {
qualifiers = new ArrayList<>(1);
for (Annotation qualifier : eventMetadata.getQualifiers()) {
// Skip @Any and @Default
if (!qualifier.annotationType().equals(Any.class) && !qualifier.annotationType().equals(Default.class)) {
qualifiers.add(qualifier);
}
}
}
return new EventInfo(eventMetadata.getType(), qualifiers);
}

private final LocalDateTime timestamp;
private final Type type;
private final List<Annotation> qualifiers;

EventInfo(Type type, List<Annotation> qualifiers) {
this.timestamp = LocalDateTime.now();
this.type = type;
this.qualifiers = qualifiers;
}

public LocalDateTime getTimestamp() {
return timestamp;
}

public String getType() {
return type.getTypeName();
}

public List<Annotation> getQualifiers() {
return qualifiers;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.devconsole.spi.DevConsoleRouteBuildItem;
import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem;
import io.quarkus.vertx.http.deployment.RouteBuildItem;
import io.quarkus.vertx.http.runtime.devmode.ArcEndpointRecorder;
import io.quarkus.vertx.http.runtime.devmode.ArcDevRecorder;

public class ArcEndpointProcessor {
public class ArcDevProcessor {

@Record(ExecutionTime.RUNTIME_INIT)
@BuildStep(onlyIf = IsDevelopment.class)
void registerRoutes(ArcConfig arcConfig, ArcEndpointRecorder recorder,
void registerRoutes(ArcConfig arcConfig, ArcDevRecorder recorder,
BuildProducer<RouteBuildItem> routes,
BuildProducer<NotFoundPageDisplayableEndpointBuildItem> displayableEndpoints,
NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) {
Expand Down Expand Up @@ -53,4 +54,12 @@ private Map<String, String> getConfigProperties(ArcConfig arcConfig) {
return props;
}

// NOTE: we can't add this build step to the ArC extension as it would cause a cyclic dependency
@BuildStep
@Record(value = ExecutionTime.STATIC_INIT, optional = true)
DevConsoleRouteBuildItem invokeEndpoint(ArcDevRecorder recorder) {
return new DevConsoleRouteBuildItem("io.quarkus", "quarkus-arc", "events", "POST",
recorder.events());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,22 @@
import javax.enterprise.inject.Any;
import javax.enterprise.inject.Default;

import io.quarkus.arc.Arc;
import io.quarkus.arc.InjectableBean;
import io.quarkus.arc.InjectableObserverMethod;
import io.quarkus.arc.RemovedBean;
import io.quarkus.arc.impl.ArcContainerImpl;
import io.quarkus.arc.runtime.devconsole.EventsMonitor;
import io.quarkus.devconsole.runtime.spi.DevConsolePostHandler;
import io.quarkus.runtime.annotations.Recorder;
import io.quarkus.vertx.http.runtime.devmode.Json.JsonArrayBuilder;
import io.quarkus.vertx.http.runtime.devmode.Json.JsonObjectBuilder;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.ext.web.RoutingContext;

@Recorder
public class ArcEndpointRecorder {
public class ArcDevRecorder {

public Handler<RoutingContext> createSummaryHandler(Map<String, String> configProperties) {
return new Handler<RoutingContext>() {
Expand Down Expand Up @@ -200,4 +204,20 @@ public void handle(RoutingContext ctx) {
};
}

// NOTE: we can't add this recorder to the ArC extension as it would cause a cyclic dependency
public Handler<RoutingContext> events() {
return new DevConsolePostHandler() {
@Override
protected void handlePost(RoutingContext event, MultiMap form)
throws Exception {
String action = form.get("action");
if ("skipContext".equals(action)) {
Arc.container().instance(EventsMonitor.class).get().toggleSkipContextEvents();
} else {
Arc.container().instance(EventsMonitor.class).get().clear();
}
}
};
}

}

0 comments on commit acf51a0

Please sign in to comment.