Skip to content

Commit

Permalink
Dev UI - add basic monitoring of CDI components
Browse files Browse the repository at this point in the history
- by default, fired events and business method invocations are monitored
- quarkus.arc.dev-mode-monitoring=false disables the feature
  • Loading branch information
mkouba committed Feb 23, 2021
1 parent 8f5fede commit 8cd0426
Show file tree
Hide file tree
Showing 21 changed files with 854 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<DevConsoleRuntimeTemplateInfoBuildItem> runtimeInfos,
BuildProducer<AdditionalBeanBuildItem> beans, BuildProducer<AnnotationsTransformerBuildItem> annotationTransformers,
CustomScopeAnnotationsBuildItem customScopes,
List<BeanDefiningAnnotationBuildItem> 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<DotName> 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();
Expand Down Expand Up @@ -103,4 +162,14 @@ private DevObserverInfo createDevInfo(ObserverInfo observer) {
observer.isAsync(), observer.getReception(), observer.getTransactionPhase());
}

private boolean isAdditionalBeanDefiningAnnotationOn(ClassInfo beanClass,
List<BeanDefiningAnnotationBuildItem> beanDefiningAnnotations) {
for (BeanDefiningAnnotationBuildItem beanDefiningAnnotation : beanDefiningAnnotations) {
if (beanClass.classAnnotation(beanDefiningAnnotation.getName()) != null) {
return true;
}
}
return false;
}

}
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
<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>
{#if config:property('quarkus.arc.dev-mode.monitoring-enabled') is "true"}
<a href="{urlbase}/events" class="badge badge-light">
<i class="far fa-eye"></i>
Fired Events</a>
<br>
<a href="{urlbase}/invocations" class="badge badge-light">
<i class="fas fa-project-diagram"></i>
Invocation Trees</a>
<br>
{/if}
<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,63 @@
{#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">Timestamp <i class="fas fa-caret-down"></i></th>
<th scope="col">Event Type</th>
<th scope="col">Qualifiers</th>
</tr>
</thead>
<tbody>
{#for event in info:eventsMonitor.lastEvents.orEmpty}
<tr>
<td>
{event.timestamp}
</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>
</tr>
{/for}
</tbody>
</table>
{/body}
{/include}
Original file line number Diff line number Diff line change
@@ -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}
<ul class="nav">
<li class="nav-item">
<input 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">
<button type="submit" class="btn btn-primary btn-sm">Clear</button>
</form>
</li>
<li class="nav-item">
<form method="post" enctype="application/x-www-form-urlencoded" class="form-inline">
<input id="filterOutQuarkusBeans" type="checkbox" class="form-check-input" disabled{#if info:invocationsMonitor.filterOutQuarkusBeans} checked{/if}>
<label for="filterOutQuarkusBeans">Filter out Quarkus beans</label>
<input type="hidden" name="action" value="filterOutQuarkusBeans">
&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">Start <i class="fas fa-caret-down"></i></th>
<th scope="col" style="width:80%;">Invocations</th>
</tr>
</thead>
<tbody>
{#each info:invocationsMonitor.lastInvocations.orEmpty}
<tr>
<td>{it.startFormatted}</td>
<td><ul id="tree">{#tree it root=true /}</ul></td>
</tr>
{/each}
</tbody>
</table>
{/body}
{/include}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<span class="badge badge-info"{#if it.durationMillis le 0} title="{it.duration} ns"{/if}>
{#if it.durationMillis > 0}{it.durationMillis} ms{#else}&lt; 1 ms{/if}
</span>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{#when it.kind}
{#is PRODUCER}
<span class="badge badge-success">Producer</span>
{#is DISPOSER}
<span class="badge badge-warning">Disposer</span>
{#is OBSERVER}
<span class="badge badge-secondary">Observer</span>
{/when}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<li>
{#if !root}
{#if next}├─{#else}└─{/if}
{/if}
{#if it.children}
<span class="caret" title="{it.method}"><code>{it.methodInfo}</code></span> {#duration it /}{#kind it /}{#if it.message != null} <span class="badge badge-danger" title="Exception message: {it.message}">Error</span>{/if}
<ul class="nested">
{#each it.children}
{#tree it root=false next=hasNext /}
{/each}
</ul>
{#else}
<span title="{it.method}"><code>{it.methodInfo}</code></span> {#duration it /}{#kind it /}{#if it.message != null} <span class="badge badge-danger" title="Exception message: {it.message}">Error</span>{/if}
{/if}
</li>
Loading

0 comments on commit 8cd0426

Please sign in to comment.