diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/DevConsoleProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/DevConsoleProcessor.java index 8fa5826bd7dc48..9f886378bdb621 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/DevConsoleProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/DevConsoleProcessor.java @@ -11,6 +11,7 @@ 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; @@ -18,7 +19,10 @@ 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; @@ -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 runtimeInfos, + BuildProducer 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(); diff --git a/extensions/arc/deployment/src/main/resources/dev-templates/embedded.html b/extensions/arc/deployment/src/main/resources/dev-templates/embedded.html index 5053fe5bbaae8b..94ac5bedc11fe3 100644 --- a/extensions/arc/deployment/src/main/resources/dev-templates/embedded.html +++ b/extensions/arc/deployment/src/main/resources/dev-templates/embedded.html @@ -1,15 +1,19 @@ - Beans {info:devBeanInfos.beans.size()} + Beans {info:devBeanInfos.beans.size}
- Observers {info:arcContainer.observers.size()} + Observers {info:arcContainer.observers.size} +
+ + + Fired Events
- Removed Beans {info:arcContainer.removedBeans.size()} + Removed Beans {info:arcContainer.removedBeans.size}
- Interceptors {info:arcContainer.interceptors.size()} + Interceptors {info:arcContainer.interceptors.size} diff --git a/extensions/arc/deployment/src/main/resources/dev-templates/events.html b/extensions/arc/deployment/src/main/resources/dev-templates/events.html new file mode 100644 index 00000000000000..272d747b4d3455 --- /dev/null +++ b/extensions/arc/deployment/src/main/resources/dev-templates/events.html @@ -0,0 +1,65 @@ +{#include main fluid=true} + {#style} + .nav-item { + padding: .5rem .3rem; + } + {/style} + {#title}Fired Events{/title} + {#body} + + + + + + + + + + + + {#for event in info:eventsMonitor.lastEvents} + + + + + + + {/for} + +
#Event TypeQualifiersTimestamp
{count}. + {event.type} + + {#when event.qualifiers.size} + {#is 1} + {event.qualifiers.iterator.next} + {#is > 1} +
    + {#for q in event.qualifiers} +
  • {q}
  • + {/for} +
+ {/when} +
+ {event.timestamp} +
+ {/body} +{/include} diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/EventsMonitor.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/EventsMonitor.java new file mode 100644 index 00000000000000..e93bd1b33978b7 --- /dev/null +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/EventsMonitor.java @@ -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 events = Collections.synchronizedList(new ArrayList(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 getLastEvents() { + synchronized (events) { + List result = new ArrayList<>(events.size()); + for (ListIterator 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 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 qualifiers; + + EventInfo(Type type, List qualifiers) { + this.timestamp = LocalDateTime.now(); + this.type = type; + this.qualifiers = qualifiers; + } + + public LocalDateTime getTimestamp() { + return timestamp; + } + + public String getType() { + return type.getTypeName(); + } + + public List getQualifiers() { + return qualifiers; + } + + } + +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/ArcEndpointProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/ArcDevProcessor.java similarity index 84% rename from extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/ArcEndpointProcessor.java rename to extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/ArcDevProcessor.java index 1fdca99722c943..a8432a11a594ea 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/ArcEndpointProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/ArcDevProcessor.java @@ -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 routes, BuildProducer displayableEndpoints, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) { @@ -53,4 +54,12 @@ private Map 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()); + } + } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ArcEndpointRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ArcDevRecorder.java similarity index 91% rename from extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ArcEndpointRecorder.java rename to extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ArcDevRecorder.java index 6da5f0f6625ecd..b4ac8705e61007 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ArcEndpointRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ArcDevRecorder.java @@ -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 createSummaryHandler(Map configProperties) { return new Handler() { @@ -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 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(); + } + } + }; + } + }