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 17, 2021
1 parent c5b2520 commit 3570b99
Show file tree
Hide file tree
Showing 20 changed files with 772 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,49 @@ 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;
}
// 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()));
// TODO skip all fwk beans?
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 +157,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,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.orEmpty}
<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,88 @@
{#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 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="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">#</th>
<th scope="col">Start</th>
<th scope="col">Tree</th>
</tr>
</thead>
<tbody>
{#each info:invocationsMonitor.lastInvocations.orEmpty}
<tr>
<td>{count}.</td>
<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,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 useBadge=true /}
<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 /}
{/if}
</li>
Loading

0 comments on commit 3570b99

Please sign in to comment.