From 8cd042673bb77b40ee33848fce456420f0070415 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Fri, 12 Feb 2021 15:51:33 +0100 Subject: [PATCH] Dev UI - add basic monitoring of CDI components - by default, fired events and business method invocations are monitored - quarkus.arc.dev-mode-monitoring=false disables the feature --- .../io/quarkus/arc/deployment/ArcConfig.java | 6 + .../arc/deployment/ArcDevModeConfig.java | 15 ++ .../devconsole/DevConsoleProcessor.java | 71 ++++++- .../resources/dev-templates/embedded.html | 18 +- .../main/resources/dev-templates/events.html | 63 +++++++ .../resources/dev-templates/invocations.html | 85 +++++++++ .../dev-templates/tags/duration.html | 3 + .../resources/dev-templates/tags/kind.html | 8 + .../resources/dev-templates/tags/tree.html | 15 ++ .../arc/runtime/devconsole/EventsMonitor.java | 120 ++++++++++++ .../arc/runtime/devconsole/Invocation.java | 174 ++++++++++++++++++ .../devconsole/InvocationInterceptor.java | 96 ++++++++++ .../runtime/devconsole/InvocationTree.java | 44 +++++ .../devconsole/InvocationsMonitor.java | 58 ++++++ .../arc/runtime/devconsole/Monitored.java | 18 ++ ...intProcessor.java => ArcDevProcessor.java} | 23 ++- .../devmode/console/DevConsoleProcessor.java | 1 + .../main/resources/dev-templates/main.html | 3 +- ...pointRecorder.java => ArcDevRecorder.java} | 39 +++- .../io/quarkus/arc/processor/BuiltinBean.java | 4 +- .../io/quarkus/arc/processor/DotNames.java | 2 + 21 files changed, 854 insertions(+), 12 deletions(-) create mode 100644 extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcDevModeConfig.java create mode 100644 extensions/arc/deployment/src/main/resources/dev-templates/events.html create mode 100644 extensions/arc/deployment/src/main/resources/dev-templates/invocations.html create mode 100644 extensions/arc/deployment/src/main/resources/dev-templates/tags/duration.html create mode 100644 extensions/arc/deployment/src/main/resources/dev-templates/tags/kind.html create mode 100644 extensions/arc/deployment/src/main/resources/dev-templates/tags/tree.html create mode 100644 extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/EventsMonitor.java create mode 100644 extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/Invocation.java create mode 100644 extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/InvocationInterceptor.java create mode 100644 extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/InvocationTree.java create mode 100644 extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/InvocationsMonitor.java create mode 100644 extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/Monitored.java rename extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/{ArcEndpointProcessor.java => ArcDevProcessor.java} (76%) rename extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/{ArcEndpointRecorder.java => ArcDevRecorder.java} (85%) diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcConfig.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcConfig.java index f0ecb06bf0333..0d2cc81184ef3 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcConfig.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcConfig.java @@ -171,6 +171,12 @@ public class ArcConfig { @ConfigItem(defaultValue = "true") public boolean detectWrongAnnotations; + /** + * Dev mode configuration. + */ + @ConfigItem + public ArcDevModeConfig devMode; + public final boolean isRemoveUnusedBeansFieldValid() { return ALLOWED_REMOVE_UNUSED_BEANS_VALUES.contains(removeUnusedBeans.toLowerCase()); } diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcDevModeConfig.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcDevModeConfig.java new file mode 100644 index 0000000000000..18ac7917643ca --- /dev/null +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcDevModeConfig.java @@ -0,0 +1,15 @@ +package io.quarkus.arc.deployment; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class ArcDevModeConfig { + + /** + * If set to true then the container monitors business method invocations and fired events during the development mode. + */ + @ConfigItem(defaultValue = "true") + public boolean monitoringEnabled; + +} 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 8fa5826bd7dc4..c87ff32112c4c 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 @@ -1,24 +1,40 @@ package io.quarkus.arc.deployment.devconsole; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.AnnotationTarget.Kind; import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; import org.jboss.jandex.FieldInfo; import org.jboss.jandex.MethodInfo; +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; +import io.quarkus.arc.deployment.ArcConfig; +import io.quarkus.arc.deployment.BeanDefiningAnnotationBuildItem; +import io.quarkus.arc.deployment.CustomScopeAnnotationsBuildItem; import io.quarkus.arc.deployment.ValidationPhaseBuildItem; +import io.quarkus.arc.processor.AnnotationsTransformer; 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.arc.runtime.devconsole.InvocationInterceptor; +import io.quarkus.arc.runtime.devconsole.InvocationTree; +import io.quarkus.arc.runtime.devconsole.InvocationsMonitor; +import io.quarkus.arc.runtime.devconsole.Monitored; 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 +45,54 @@ 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 monitor(ArcConfig config, BuildProducer runtimeInfos, + BuildProducer beans, BuildProducer annotationTransformers, + CustomScopeAnnotationsBuildItem customScopes, + List beanDefiningAnnotations) { + if (!config.devMode.monitoringEnabled) { + return; + } + if (!config.transformUnproxyableClasses) { + throw new IllegalStateException( + "Dev UI problem: monitoring of CDI business method invocations not possible\n\t- quarkus.arc.transform-unproxyable-classes was set to false and therefore it would not be possible to apply interceptors to unproxyable bean classes\n\t- please disable the monitoring feature via quarkus.arc.dev-mode.monitoring-enabled=false or enable unproxyable classes transformation"); + } + // Events + runtimeInfos.produce( + new DevConsoleRuntimeTemplateInfoBuildItem("eventsMonitor", + new BeanLookupSupplier(EventsMonitor.class))); + beans.produce(AdditionalBeanBuildItem.unremovableOf(EventsMonitor.class)); + // Invocations + beans.produce(AdditionalBeanBuildItem.builder().setUnremovable() + .addBeanClasses(InvocationTree.class, InvocationsMonitor.class, InvocationInterceptor.class, + Monitored.class) + .build()); + Set skipNames = new HashSet<>(); + skipNames.add(DotName.createSimple(InvocationTree.class.getName())); + skipNames.add(DotName.createSimple(InvocationsMonitor.class.getName())); + skipNames.add(DotName.createSimple(EventsMonitor.class.getName())); + annotationTransformers.produce(new AnnotationsTransformerBuildItem(new AnnotationsTransformer() { + @Override + public void transform(TransformationContext transformationContext) { + if (transformationContext.isClass()) { + ClassInfo beanClass = transformationContext.getTarget().asClass(); + if ((customScopes.isScopeDeclaredOn(beanClass) + || isAdditionalBeanDefiningAnnotationOn(beanClass, beanDefiningAnnotations)) + && !skipNames.contains(beanClass.name())) { + transformationContext.transform().add(Monitored.class).done(); + } + } + } + })); + runtimeInfos.produce(new DevConsoleRuntimeTemplateInfoBuildItem("invocationsMonitor", + new BeanLookupSupplier(InvocationsMonitor.class))); + } + @BuildStep(onlyIf = IsDevelopment.class) public DevConsoleTemplateInfoBuildItem collectBeanInfo(ValidationPhaseBuildItem validationPhaseBuildItem) { BeanDeploymentValidator.ValidationContext validationContext = validationPhaseBuildItem.getContext(); @@ -103,4 +162,14 @@ private DevObserverInfo createDevInfo(ObserverInfo observer) { observer.isAsync(), observer.getReception(), observer.getTransactionPhase()); } + private boolean isAdditionalBeanDefiningAnnotationOn(ClassInfo beanClass, + List beanDefiningAnnotations) { + for (BeanDefiningAnnotationBuildItem beanDefiningAnnotation : beanDefiningAnnotations) { + if (beanClass.classAnnotation(beanDefiningAnnotation.getName()) != null) { + return true; + } + } + return false; + } + } 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 5053fe5bbaae8..bef53c49c182b 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,25 @@ - Beans {info:devBeanInfos.beans.size()} + Beans {info:devBeanInfos.beans.size}
- Observers {info:arcContainer.observers.size()} + Observers {info:arcContainer.observers.size}
+{#if config:property('quarkus.arc.dev-mode.monitoring-enabled') is "true"} + + + Fired Events +
+ + + Invocation Trees +
+{/if} - 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 0000000000000..d6f299f31f1e7 --- /dev/null +++ b/extensions/arc/deployment/src/main/resources/dev-templates/events.html @@ -0,0 +1,63 @@ +{#include main fluid=true} + {#style} + .nav-item { + padding: .5rem .3rem; + } + {/style} + {#title}Fired Events{/title} + {#body} + + + + + + + + + + + {#for event in info:eventsMonitor.lastEvents.orEmpty} + + + + + + {/for} + +
Timestamp Event TypeQualifiers
+ {event.timestamp} + + {event.type} + + {#when event.qualifiers.size} + {#is 1} + {event.qualifiers.iterator.next} + {#is > 1} +
    + {#for q in event.qualifiers} +
  • {q}
  • + {/for} +
+ {/when} +
+ {/body} +{/include} diff --git a/extensions/arc/deployment/src/main/resources/dev-templates/invocations.html b/extensions/arc/deployment/src/main/resources/dev-templates/invocations.html new file mode 100644 index 0000000000000..0d5eff42e7e27 --- /dev/null +++ b/extensions/arc/deployment/src/main/resources/dev-templates/invocations.html @@ -0,0 +1,85 @@ +{#include main fluid=true} + {#style} + .nav-item { + padding: .5rem .3rem; + } + ul#tree, ul.nested { + list-style-type: none; + } + #tree { + margin: 0; + padding: 0; + } + span.caret { + cursor: pointer; + user-select: none; + } + span.caret::before { + content: "\229E"; + color: black; + display: inline-block; + margin-right: 6px; + } + span.caret-down::before { + content: "\229F" + /*transform: rotate(90deg);*/ + } + ul.nested { + display: none; + } + ul.active { + display: block; + } + ul code { + color: #343a40; + } + {/style} + {#script} + var carets = document.getElementsByClassName("caret"); + for (i = 0; i < carets.length; i++) { + carets[i].addEventListener("click", function() { + this.parentElement.querySelector(".nested").classList.toggle("active"); + this.classList.toggle("caret-down"); + }); + } + {/script} + {#title}Invocation Trees{/title} + {#body} + + + + + + + + + + {#each info:invocationsMonitor.lastInvocations.orEmpty} + + + + + {/each} + +
Start Invocations
{it.startFormatted}
    {#tree it root=true /}
+ {/body} +{/include} diff --git a/extensions/arc/deployment/src/main/resources/dev-templates/tags/duration.html b/extensions/arc/deployment/src/main/resources/dev-templates/tags/duration.html new file mode 100644 index 0000000000000..adb73e72277ab --- /dev/null +++ b/extensions/arc/deployment/src/main/resources/dev-templates/tags/duration.html @@ -0,0 +1,3 @@ + + {#if it.durationMillis > 0}{it.durationMillis} ms{#else}< 1 ms{/if} + \ No newline at end of file diff --git a/extensions/arc/deployment/src/main/resources/dev-templates/tags/kind.html b/extensions/arc/deployment/src/main/resources/dev-templates/tags/kind.html new file mode 100644 index 0000000000000..078a39546f5be --- /dev/null +++ b/extensions/arc/deployment/src/main/resources/dev-templates/tags/kind.html @@ -0,0 +1,8 @@ +{#when it.kind} + {#is PRODUCER} +Producer + {#is DISPOSER} +Disposer + {#is OBSERVER} +Observer +{/when} \ No newline at end of file diff --git a/extensions/arc/deployment/src/main/resources/dev-templates/tags/tree.html b/extensions/arc/deployment/src/main/resources/dev-templates/tags/tree.html new file mode 100644 index 0000000000000..1e645c938abef --- /dev/null +++ b/extensions/arc/deployment/src/main/resources/dev-templates/tags/tree.html @@ -0,0 +1,15 @@ +
  • +{#if !root} +{#if next}├─{#else}└─{/if} +{/if} +{#if it.children} +{it.methodInfo} {#duration it /}{#kind it /}{#if it.message != null} Error{/if} +
      +{#each it.children} +{#tree it root=false next=hasNext /} +{/each} +
    +{#else} +{it.methodInfo} {#duration it /}{#kind it /}{#if it.message != null} Error{/if} +{/if} +
  • \ No newline at end of file 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 0000000000000..72c0cb997ead1 --- /dev/null +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/EventsMonitor.java @@ -0,0 +1,120 @@ +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 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; + +@Singleton +public class EventsMonitor { + + private static final int DEFAULT_LIMIT = 500; + + private volatile boolean skipContextEvents = true; + private final List events = Collections.synchronizedList(new ArrayList(DEFAULT_LIMIT)); + + void notify(@Observes Object payload, EventMetadata eventMetadata) { + if (skipContextEvents && isContextEvent(eventMetadata)) { + return; + } + if (events.size() > DEFAULT_LIMIT) { + // Remove some old data if the limit is exceeded + synchronized (events) { + if (events.size() > DEFAULT_LIMIT) { + events.subList(0, DEFAULT_LIMIT / 2).clear(); + } + } + } + events.add(EventInfo.from(eventMetadata)); + } + + public void clear() { + events.clear(); + } + + public List getLastEvents() { + List result = new ArrayList<>(events); + Collections.reverse(result); + 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/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/Invocation.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/Invocation.java new file mode 100644 index 0000000000000..e39534089db59 --- /dev/null +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/Invocation.java @@ -0,0 +1,174 @@ +package io.quarkus.arc.runtime.devconsole; + +import java.lang.reflect.Method; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import io.quarkus.arc.InjectableBean; + +/** + * Business method invocation. + */ +public class Invocation { + + private final InjectableBean interceptedBean; + /** + * Start time in ms + */ + private final long start; + /** + * Duration in ns + */ + private final long duration; + private final Method method; + private final Kind kind; + private final String message; + private final List children; + + Invocation(InjectableBean interceptedBean, long start, long duration, + Method method, Kind kind, String message, List children) { + this.interceptedBean = interceptedBean; + this.start = start; + this.duration = duration; + this.method = method; + this.children = children; + this.kind = kind; + this.message = message; + } + + public InjectableBean getInterceptedBean() { + return interceptedBean; + } + + public long getStart() { + return start; + } + + public String getStartFormatted() { + return LocalDateTime.ofInstant(Instant.ofEpochMilli(start), ZoneId.systemDefault()).toString(); + } + + public long getDuration() { + return duration; + } + + public long getDurationMillis() { + return TimeUnit.NANOSECONDS.toMillis(duration); + } + + public Method getMethod() { + return method; + } + + public String getMethodInfo() { + return method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()"; + } + + public List getChildren() { + return children; + } + + public Kind getKind() { + return kind; + } + + public String getMessage() { + return message; + } + + @Override + public String toString() { + return kind + " invocation of " + method; + } + + public enum Kind { + + BUSINESS, + PRODUCER, + DISPOSER, + OBSERVER + + } + + static class Builder { + + private InjectableBean interceptedBean; + private long start; + private long duration; + private Method method; + private List children; + private Builder parent; + private Kind kind; + private String message; + + Builder newChild() { + Invocation.Builder child = new Builder(); + addChild(child); + return child; + } + + Builder setInterceptedBean(InjectableBean bean) { + this.interceptedBean = bean; + return this; + } + + Builder setStart(long start) { + this.start = start; + return this; + } + + Builder setDuration(long duration) { + this.duration = duration; + return this; + } + + Builder setMethod(Method method) { + this.method = method; + return this; + } + + Builder setKind(Kind kind) { + this.kind = kind; + return this; + } + + Builder getParent() { + return parent; + } + + Builder setParent(Builder parent) { + this.parent = parent; + return this; + } + + Builder setMessage(String message) { + this.message = message; + return this; + } + + boolean addChild(Builder child) { + if (children == null) { + children = new ArrayList<>(); + } + child.setParent(this); + return children.add(child); + } + + Invocation build() { + List invocations = null; + if (children != null) { + invocations = new ArrayList<>(children.size()); + for (Builder builder : children) { + invocations.add(builder.build()); + } + } + return new Invocation(interceptedBean, start, duration, method, kind, message, invocations); + } + + } + +} diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/InvocationInterceptor.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/InvocationInterceptor.java new file mode 100644 index 0000000000000..74c96b01fbf2d --- /dev/null +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/InvocationInterceptor.java @@ -0,0 +1,96 @@ +package io.quarkus.arc.runtime.devconsole; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import javax.annotation.Priority; +import javax.enterprise.event.Observes; +import javax.enterprise.event.ObservesAsync; +import javax.enterprise.inject.Disposes; +import javax.enterprise.inject.Instance; +import javax.enterprise.inject.Intercepted; +import javax.enterprise.inject.Produces; +import javax.inject.Inject; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.InjectableBean; +import io.quarkus.arc.ManagedContext; +import io.quarkus.arc.runtime.devconsole.Invocation.Builder; +import io.quarkus.arc.runtime.devconsole.Invocation.Kind; + +@Priority(Interceptor.Priority.LIBRARY_BEFORE) +@Monitored +@Interceptor +public class InvocationInterceptor { + + @Inject + InvocationsMonitor invocationMonitor; + + @Intercepted + InjectableBean bean; + + @Inject + Instance invocationTree; + + @AroundInvoke + public Object monitor(InvocationContext context) throws Exception { + ManagedContext requestContext = Arc.container().requestContext(); + if (requestContext.isActive()) { + InvocationTree tree = invocationTree.get(); + return proceed(tree.invocationStarted(bean, context.getMethod(), getKind(context)), context, requestContext, tree); + } else { + return proceed( + new Builder().setInterceptedBean(bean).setMethod(context.getMethod()).setKind(getKind(context)) + .setStart(System.currentTimeMillis()), + context, requestContext, null); + } + } + + Object proceed(Invocation.Builder builder, InvocationContext context, ManagedContext requestContext, InvocationTree tree) + throws Exception { + long nanoTime = System.nanoTime(); + // Object result; + try { + return context.proceed(); + } catch (Exception e) { + builder.setMessage(e.getMessage()); + throw e; + } finally { + builder.setDuration(System.nanoTime() - nanoTime); + if (builder.getParent() == null) { + // Flush the data about a top-level invocation + invocationMonitor.addInvocation(builder.build()); + } + if (tree != null && requestContext.isActive()) { + tree.invocationCompleted(); + } + } + } + + Invocation.Kind getKind(InvocationContext ctx) { + // This will only work for "unmodified" discovered types + Method method = ctx.getMethod(); + if (!method.getReturnType().equals(Void.TYPE) && method.isAnnotationPresent(Produces.class)) { + return Kind.PRODUCER; + } else if (method.getParameterCount() > 0) { + Annotation[][] parameterAnnotations = method.getParameterAnnotations(); + if (parameterAnnotations.length > 0) { + for (Annotation[] annotations : parameterAnnotations) { + for (Annotation annotation : annotations) { + Class type = annotation.annotationType(); + if (Observes.class.equals(type) || ObservesAsync.class.equals(type)) { + return Kind.OBSERVER; + } else if (Disposes.class.equals(type)) { + return Kind.DISPOSER; + } + } + } + } + } + return Kind.BUSINESS; + } + +} diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/InvocationTree.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/InvocationTree.java new file mode 100644 index 0000000000000..6c1afff940609 --- /dev/null +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/InvocationTree.java @@ -0,0 +1,44 @@ +package io.quarkus.arc.runtime.devconsole; + +import java.lang.reflect.Method; + +import javax.enterprise.context.RequestScoped; + +import io.quarkus.arc.InjectableBean; + +@RequestScoped +public class InvocationTree { + + // The current invocation builder + // It is volatile because a request scoped bean should not be invoked concurrently, however it can be invoked on a different thread + private volatile Invocation.Builder current; + + Invocation.Builder invocationStarted(InjectableBean bean, Method method, Invocation.Kind kind) { + Invocation.Builder builder = this.current; + if (builder == null) { + // Entry point + builder = new Invocation.Builder(); + } else { + // Nested invocation + builder = builder.newChild(); + } + builder.setStart(System.currentTimeMillis()).setInterceptedBean(bean) + .setMethod(method).setKind(kind); + this.current = builder; + return builder; + } + + void invocationCompleted() { + Invocation.Builder current = this.current; + if (current == null) { + // Something went wrong, for example the request context was terminated unexpectedly + return; + } + if (current.getParent() != null) { + this.current = current.getParent(); + } else { + this.current = null; + } + } + +} diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/InvocationsMonitor.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/InvocationsMonitor.java new file mode 100644 index 0000000000000..0a65ff3bee254 --- /dev/null +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/InvocationsMonitor.java @@ -0,0 +1,58 @@ +package io.quarkus.arc.runtime.devconsole; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import javax.inject.Singleton; + +@Singleton +public class InvocationsMonitor { + + private static final int DEFAULT_LIMIT = 1000; + + private final List invocations = Collections.synchronizedList(new ArrayList<>()); + + private volatile boolean filterOutQuarkusBeans = true; + + void addInvocation(Invocation invocation) { + if (invocations.size() > DEFAULT_LIMIT) { + // Remove some old data if the limit is exceeded + synchronized (invocations) { + if (invocations.size() > DEFAULT_LIMIT) { + invocations.subList(0, DEFAULT_LIMIT / 2).clear(); + } + } + } + invocations.add(invocation); + } + + public List getLastInvocations() { + List result = new ArrayList<>(invocations); + if (filterOutQuarkusBeans) { + for (Iterator it = result.iterator(); it.hasNext();) { + Invocation invocation = it.next(); + if (invocation.getInterceptedBean().getBeanClass().getName().startsWith("io.quarkus")) { + it.remove(); + } + } + } + Collections.reverse(result); + return result; + } + + public void clear() { + invocations.clear(); + } + + public boolean isFilterOutQuarkusBeans() { + return filterOutQuarkusBeans; + } + + public void toggleFilterOutQuarkusBeans() { + // This is not thread-safe but we don't expect concurrent actions from dev ui + filterOutQuarkusBeans = !filterOutQuarkusBeans; + } + +} diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/Monitored.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/Monitored.java new file mode 100644 index 0000000000000..bd8a9e1c15d24 --- /dev/null +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/Monitored.java @@ -0,0 +1,18 @@ +package io.quarkus.arc.runtime.devconsole; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.interceptor.InterceptorBinding; + +@Inherited +@InterceptorBinding +@Target({ TYPE, METHOD }) +@Retention(RUNTIME) +public @interface Monitored { +} 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 76% 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 1fdca99722c94..1de137cccc47e 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,20 @@ 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 eventsEndpoint(ArcDevRecorder recorder) { + return new DevConsoleRouteBuildItem("io.quarkus", "quarkus-arc", "events", "POST", + recorder.events()); + } + + // 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 invocationsEndpoint(ArcDevRecorder recorder) { + return new DevConsoleRouteBuildItem("io.quarkus", "quarkus-arc", "invocations", "POST", + recorder.invocations()); + } + } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java index 730587d618e96..ba8f7787d573c 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java @@ -371,6 +371,7 @@ private Engine buildEngine(List devTemplatePaths) { }).build()); // {config:property('quarkus.lambda.handler')} + // Note that the output value is always string! builder.addNamespaceResolver(NamespaceResolver.builder("config").resolveAsync(ctx -> { List params = ctx.getParams(); if (params.size() != 1 || !ctx.getName().equals("property")) { diff --git a/extensions/vertx-http/deployment/src/main/resources/dev-templates/main.html b/extensions/vertx-http/deployment/src/main/resources/dev-templates/main.html index 51b7af2cd384b..b629347d64080 100644 --- a/extensions/vertx-http/deployment/src/main/resources/dev-templates/main.html +++ b/extensions/vertx-http/deployment/src/main/resources/dev-templates/main.html @@ -87,7 +87,8 @@ }, jQuery(".alert").data("timer")); }); } - |} + {#insert script /} + \ No newline at end of file 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 85% 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 6da5f0f6625ec..6b4722063f99c 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,23 @@ 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.arc.runtime.devconsole.InvocationsMonitor; +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 +205,36 @@ 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(); + } + } + }; + } + + // NOTE: we can't add this recorder to the ArC extension as it would cause a cyclic dependency + public Handler invocations() { + return new DevConsolePostHandler() { + @Override + protected void handlePost(RoutingContext event, MultiMap form) + throws Exception { + String action = form.get("action"); + if ("filterOutQuarkusBeans".equals(action)) { + Arc.container().instance(InvocationsMonitor.class).get().toggleFilterOutQuarkusBeans(); + } else { + Arc.container().instance(InvocationsMonitor.class).get().clear(); + } + } + }; + } + } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuiltinBean.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuiltinBean.java index df7309522bee4..0a056df71f367 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuiltinBean.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuiltinBean.java @@ -95,7 +95,7 @@ enum BuiltinBean { ctx.constructor.getThis(), beanProviderSupplier); }, ip -> { - return isCdiAndRawTypeMatches(ip, DotNames.BEAN) && ip.hasDefaultedQualifier(); + return isCdiAndRawTypeMatches(ip, DotNames.BEAN, DotNames.INJECTABLE_BEAN) && ip.hasDefaultedQualifier(); }, DotNames.BEAN), INTERCEPTED_BEAN(ctx -> { if (!(ctx.targetInfo instanceof InterceptorInfo)) { @@ -112,7 +112,7 @@ enum BuiltinBean { ctx.constructor.getThis(), interceptedBeanMetadataProviderSupplier); }, ip -> { - return isCdiAndRawTypeMatches(ip, DotNames.BEAN) && !ip.hasDefaultedQualifier() + return isCdiAndRawTypeMatches(ip, DotNames.BEAN, DotNames.INJECTABLE_BEAN) && !ip.hasDefaultedQualifier() && ip.getRequiredQualifiers().size() == 1 && ip.getRequiredQualifiers().iterator().next().name().equals(DotNames.INTERCEPTED); }, DotNames.BEAN), diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DotNames.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DotNames.java index e4fdac49b2ce3..4abb701ae574f 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DotNames.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DotNames.java @@ -2,6 +2,7 @@ import io.quarkus.arc.AlternativePriority; import io.quarkus.arc.DefaultBean; +import io.quarkus.arc.InjectableBean; import io.quarkus.arc.InjectableInstance; import io.quarkus.arc.impl.ComputingCache; import java.lang.annotation.Repeatable; @@ -76,6 +77,7 @@ public final class DotNames { public static final DotName DEFAULT = create(Default.class); public static final DotName ANY = create(Any.class); public static final DotName BEAN = create(Bean.class); + public static final DotName INJECTABLE_BEAN = create(InjectableBean.class); public static final DotName BEAN_MANAGER = create(BeanManager.class); public static final DotName EVENT = create(Event.class); public static final DotName EVENT_METADATA = create(EventMetadata.class);