reporter for health status
diff --git a/geronimo-microprofile-reporter/pom.xml b/geronimo-microprofile-reporter/pom.xml
index b5b4ae6..7db4665 100644
--- a/geronimo-microprofile-reporter/pom.xml
+++ b/geronimo-microprofile-reporter/pom.xml
@@ -35,6 +35,7 @@
<properties>
<geronimo-microprofile.Automatic-Module-Name>${project.groupId}.microprofile.reporter</geronimo-microprofile.Automatic-Module-Name>
+ <chart.js.version>2.7.3</chart.js.version>
</properties>
<dependencies>
@@ -53,6 +54,13 @@
</dependency>
<dependency>
+ <groupId>org.webjars.bower</groupId>
+ <artifactId>chart.js</artifactId>
+ <version>${chart.js.version}</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
@@ -76,6 +84,11 @@
</configuration>
<dependencies>
<dependency>
+ <groupId>org.webjars.bower</groupId>
+ <artifactId>chart.js</artifactId>
+ <version>${chart.js.version}</version>
+ </dependency>
+ <dependency>
<groupId>org.apache.geronimo</groupId>
<artifactId>geronimo-microprofile-aggregator</artifactId>
<version>${project.version}</version>
diff --git a/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/FakeCheck1.java b/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/FakeCheck1.java
new file mode 100644
index 0000000..9c12b18
--- /dev/null
+++ b/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/FakeCheck1.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright (C) 2006-2019 Talend Inc. - www.talend.com
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.geronimo.microprofile.reporter.storage;
+
+import javax.enterprise.context.Dependent;
+
+import org.eclipse.microprofile.health.Health;
+import org.eclipse.microprofile.health.HealthCheck;
+import org.eclipse.microprofile.health.HealthCheckResponse;
+
+@Health
+@Dependent
+public class FakeCheck1 implements HealthCheck {
+ @Override
+ public HealthCheckResponse call() {
+ return HealthCheckResponse.named("check1").up().build();
+ }
+}
diff --git a/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/FakeCheck2.java b/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/FakeCheck2.java
new file mode 100644
index 0000000..2bab8bb
--- /dev/null
+++ b/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/FakeCheck2.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright (C) 2006-2019 Talend Inc. - www.talend.com
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.geronimo.microprofile.reporter.storage;
+
+import javax.enterprise.context.Dependent;
+
+import org.eclipse.microprofile.health.Health;
+import org.eclipse.microprofile.health.HealthCheck;
+import org.eclipse.microprofile.health.HealthCheckResponse;
+import org.eclipse.microprofile.health.HealthCheckResponseBuilder;
+
+@Health
+@Dependent
+public class FakeCheck2 implements HealthCheck {
+ @Override
+ public HealthCheckResponse call() {
+ final HealthCheckResponseBuilder named = HealthCheckResponse.named("check_2");
+ if (System.currentTimeMillis() % 2 == 0) {
+ return named.up().withData("foo", "bar").withData("another", "dummy").build();
+ }
+ return named.down().withData("foo", "bar").withData("another", "dummy").build();
+ }
+}
diff --git a/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/HealthRegistry.java b/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/HealthRegistry.java
new file mode 100644
index 0000000..0b97764
--- /dev/null
+++ b/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/HealthRegistry.java
@@ -0,0 +1,96 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.geronimo.microprofile.reporter.storage;
+
+import static java.util.stream.Collectors.toList;
+
+import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Stream;
+
+import javax.enterprise.context.spi.CreationalContext;
+import javax.enterprise.event.Observes;
+import javax.enterprise.inject.spi.AfterDeploymentValidation;
+import javax.enterprise.inject.spi.Bean;
+import javax.enterprise.inject.spi.BeanManager;
+import javax.enterprise.inject.spi.BeforeShutdown;
+import javax.enterprise.inject.spi.Extension;
+import javax.enterprise.inject.spi.ProcessBean;
+
+import org.eclipse.microprofile.health.Health;
+import org.eclipse.microprofile.health.HealthCheck;
+import org.eclipse.microprofile.health.HealthCheckResponse;
+
+public class HealthRegistry implements Extension {
+ private static final Annotation[] NO_ANNOTATION = new Annotation[0];
+
+ private final Collection<Bean<?>> beans = new ArrayList<>();
+ private final Collection<CreationalContext<?>> contexts = new ArrayList<>();
+ private final List<HealthCheck> checks = new ArrayList<>();
+
+ public Stream<HealthCheckResponse> doCheck() {
+ return checks.stream().map(check -> invoke(check));
+ }
+
+ private HealthCheckResponse invoke(final HealthCheck check) {
+ try {
+ return check.call();
+ } catch (final RuntimeException re) {
+ return HealthCheckResponse.named(check.getClass().getName())
+ .down()
+ .withData("exceptionMessage", re.getMessage())
+ .build();
+ }
+ }
+
+ void findChecks(@Observes final ProcessBean<?> bean) {
+ if (bean.getAnnotated().isAnnotationPresent(Health.class) && bean.getBean().getTypes().contains(HealthCheck.class)) {
+ beans.add(bean.getBean());
+ }
+ }
+
+ void start(@Observes final AfterDeploymentValidation afterDeploymentValidation, final BeanManager beanManager) {
+ checks.addAll(beans.stream().map(it -> lookup(it, beanManager)).collect(toList()));
+ }
+
+ void stop(@Observes final BeforeShutdown beforeShutdown) {
+ final IllegalStateException ise = new IllegalStateException("Something went wrong releasing health checks");
+ contexts.forEach(c -> {
+ try {
+ c.release();
+ } catch (final RuntimeException re) {
+ ise.addSuppressed(re);
+ }
+ });
+ if (ise.getSuppressed().length > 0) {
+ throw ise;
+ }
+ }
+
+ private HealthCheck lookup(final Bean<?> bean, final BeanManager manager) {
+ final Class<?> beanClass = bean.getBeanClass();
+ final Bean<?> resolvedBean = manager.resolve(manager.getBeans(
+ beanClass != null ? beanClass : HealthCheck.class, bean.getQualifiers().toArray(NO_ANNOTATION)));
+ final CreationalContext<Object> creationalContext = manager.createCreationalContext(null);
+ if (!manager.isNormalScope(resolvedBean.getScope())) {
+ contexts.add(creationalContext);
+ }
+ return HealthCheck.class.cast(manager.getReference(resolvedBean, HealthCheck.class, creationalContext));
+ }
+}
diff --git a/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/InMemoryDatabase.java b/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/InMemoryDatabase.java
index a2eef7d..7b3d86c 100644
--- a/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/InMemoryDatabase.java
+++ b/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/InMemoryDatabase.java
@@ -20,6 +20,7 @@
import java.util.ArrayList;
import java.util.Collection;
+import java.util.LinkedList;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
@@ -55,12 +56,12 @@
return unit;
}
- public Collection<Value<T>> snapshot() {
+ public LinkedList<Value<T>> snapshot() {
ensureUpToDate();
final Lock lock = this.lock.readLock();
lock.lock();
try {
- return new ArrayList<>(bucket.values());
+ return new LinkedList<>(bucket.values());
} finally {
lock.unlock();
}
diff --git a/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/MicroprofileDatabase.java b/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/MicroprofileDatabase.java
index e600543..cf67677 100644
--- a/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/MicroprofileDatabase.java
+++ b/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/MicroprofileDatabase.java
@@ -39,6 +39,7 @@
import org.apache.geronimo.microprofile.opentracing.common.impl.FinishedSpan;
import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.metrics.Metered;
import org.eclipse.microprofile.metrics.MetricRegistry;
import org.eclipse.microprofile.metrics.Snapshot;
@@ -65,6 +66,9 @@
@Inject
private MetricRegistry applicationRegistry;
+ @Inject
+ private HealthRegistry healthRegistry;
+
private ScheduledExecutorService scheduler;
private ScheduledFuture<?> pollFuture;
@@ -77,6 +81,7 @@
private final Map<String, InMemoryDatabase<Snapshot>> histograms = new HashMap<>();
private final Map<String, InMemoryDatabase<MeterSnapshot>> meters = new HashMap<>();
private final Map<String, InMemoryDatabase<TimerSnapshot>> timers = new HashMap<>();
+ private final Map<String, InMemoryDatabase<CheckSnapshot>> checks = new HashMap<>();
public InMemoryDatabase<Span> getSpans() {
return spanDatabase;
@@ -102,43 +107,67 @@
return timers;
}
+ public Map<String, InMemoryDatabase<CheckSnapshot>> getChecks() {
+ return checks;
+ }
+
private void poll() {
- metrics.forEach((type, registry) -> {
- registry.getCounters().forEach((name, counter) -> {
- final String virtualName = getMetricStorageName(type, name);
- final long count = counter.getCount();
- getDb(counters, virtualName, registry, name).add(count);
- });
+ metrics.forEach(this::updateMetrics);
- registry.getGauges().forEach((name, gauge) -> {
- final String virtualName = getMetricStorageName(type, name);
- final Object value = gauge.getValue();
- if (Number.class.isInstance(value)) {
- try {
- getDb(gauges, virtualName, registry, name).add(Number.class.cast(value).doubleValue());
- } catch (final NullPointerException | NumberFormatException nfe) {
- // ignore, we can't do much if the value is not a double
- }
- } // else ignore, will not be able to do anything of it anyway
- });
+ healthRegistry.doCheck().forEach(this::updateHealthCheck);
+ }
- registry.getHistograms().forEach((name, histogram) -> {
- final String virtualName = getMetricStorageName(type, name);
- final Snapshot snapshot = histogram.getSnapshot();
- getDb(histograms, virtualName, registry, name).add(snapshot);
- });
+ private void updateHealthCheck(final HealthCheckResponse healthCheckResponse) {
+ final String name = healthCheckResponse.getName();
+ InMemoryDatabase<CheckSnapshot> db = checks.get(name);
+ if (db == null) {
+ db = new InMemoryDatabase<>("check");
+ final InMemoryDatabase<CheckSnapshot> existing = checks.putIfAbsent(name, db);
+ if (existing != null) {
+ db = existing;
+ }
+ }
+ db.add(new CheckSnapshot(
+ healthCheckResponse.getName(),
+ ofNullable(healthCheckResponse.getState()).orElse(HealthCheckResponse.State.DOWN).name(),
+ healthCheckResponse.getData().map(HashMap::new).orElseGet(HashMap::new)));
+ }
- registry.getMeters().forEach((name, meter) -> {
- final String virtualName = getMetricStorageName(type, name);
- final MeterSnapshot snapshot = new MeterSnapshot(meter);
- getDb(meters, virtualName, registry, name).add(snapshot);
- });
+ private void updateMetrics(final String type, final MetricRegistry registry) {
+ registry.getCounters().forEach((name, counter) -> {
+ final String virtualName = getMetricStorageName(type, name);
+ final long count = counter.getCount();
+ getDb(counters, virtualName, registry, name).add(count);
+ });
- registry.getTimers().forEach((name, timer) -> {
- final String virtualName = getMetricStorageName(type, name);
- final TimerSnapshot snapshot = new TimerSnapshot(new MeterSnapshot(timer), timer.getSnapshot());
- getDb(timers, virtualName, registry, name).add(snapshot);
- });
+ registry.getGauges().forEach((name, gauge) -> {
+ final String virtualName = getMetricStorageName(type, name);
+ final Object value = gauge.getValue();
+ if (Number.class.isInstance(value)) {
+ try {
+ getDb(gauges, virtualName, registry, name).add(Number.class.cast(value).doubleValue());
+ } catch (final NullPointerException | NumberFormatException nfe) {
+ // ignore, we can't do much if the value is not a double
+ }
+ } // else ignore, will not be able to do anything of it anyway
+ });
+
+ registry.getHistograms().forEach((name, histogram) -> {
+ final String virtualName = getMetricStorageName(type, name);
+ final Snapshot snapshot = histogram.getSnapshot();
+ getDb(histograms, virtualName, registry, name).add(snapshot);
+ });
+
+ registry.getMeters().forEach((name, meter) -> {
+ final String virtualName = getMetricStorageName(type, name);
+ final MeterSnapshot snapshot = new MeterSnapshot(meter);
+ getDb(meters, virtualName, registry, name).add(snapshot);
+ });
+
+ registry.getTimers().forEach((name, timer) -> {
+ final String virtualName = getMetricStorageName(type, name);
+ final TimerSnapshot snapshot = new TimerSnapshot(new MeterSnapshot(timer), timer.getSnapshot());
+ getDb(timers, virtualName, registry, name).add(snapshot);
});
}
@@ -227,14 +256,6 @@
this.meter = meter;
this.histogram = histogram;
}
-
- public MeterSnapshot getMeter() {
- return meter;
- }
-
- public Snapshot getHistogram() {
- return histogram;
- }
}
public static class MeterSnapshot {
@@ -256,25 +277,17 @@
this.rate5 = rate5;
this.rate15 = rate15;
}
+ }
- public long getCount() {
- return count;
- }
+ public static class CheckSnapshot {
+ private final String name;
+ private final String state;
+ private final Map<String, Object> data;
- public double getRateMean() {
- return rateMean;
- }
-
- public double getRate1() {
- return rate1;
- }
-
- public double getRate5() {
- return rate5;
- }
-
- public double getRate15() {
- return rate15;
+ private CheckSnapshot(final String name, final String state, final Map<String, Object> data) {
+ this.name = name;
+ this.state = state;
+ this.data = data;
}
}
}
diff --git a/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/ReporterEndpoints.java b/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/ReporterEndpoints.java
index 28230fb..984adba 100644
--- a/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/ReporterEndpoints.java
+++ b/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/ReporterEndpoints.java
@@ -17,19 +17,34 @@
package org.apache.geronimo.microprofile.reporter.storage;
import static java.util.Arrays.asList;
+import static java.util.Comparator.comparing;
+import static java.util.Objects.requireNonNull;
+import static java.util.Optional.ofNullable;
+import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static javax.ws.rs.core.MediaType.TEXT_HTML;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
import java.util.TreeSet;
+import java.util.stream.Stream;
+import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
+import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.metrics.Snapshot;
import io.opentracing.Span;
@@ -46,13 +61,46 @@
@Inject
private SpanMapper spanMapper;
+ @Inject
+ private HealthRegistry healthRegistry;
+
+ private String chartJs;
+
+ @PostConstruct
+ private void init() {
+ // we load chart.js like that to enable to override it easily and respect our relative path properly
+ final String chartJsResource = System.getProperty( // don't use mp-config, it is optional
+ "geronimo.microprofile.reporter.chartjs.resources",
+ "/META-INF/resources/webjars/chart.js/2.7.3/dist/Chart.bundle.min.js");
+ final ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ chartJs = (chartJsResource.startsWith("/") ?
+ Stream.of(chartJsResource, chartJsResource.substring(1)) : Stream.of(chartJsResource, '/' + chartJsResource))
+ .map(it -> {
+ final InputStream stream = loader.getResourceAsStream(it);
+ if (stream == null) {
+ return null;
+ }
+ try (final BufferedReader reader = new BufferedReader(new InputStreamReader(
+ requireNonNull(stream,
+ "Chart.js bundle not found")))) {
+ return reader.lines().collect(joining("\n"));
+ } catch (final IOException e) {
+ throw new IllegalStateException("Didn't find chart.js bundle");
+ }
+ })
+ .filter(Objects::nonNull)
+ .findFirst()
+ .orElseThrow(() -> new IllegalStateException("No " + chartJsResource + " found, did you add org.webjars.bower:chart.js:2.7.3 to your classpath?"));
+
+ }
+
@GET
public Html get() {
return new Html("main.html")
.with("view", "index.html")
.with("colors", COLORS)
.with("title", "Home")
- .with("tiles", asList("Spans", "Counters", "Gauges", "Histograms", "Meters", "Timers"));
+ .with("tiles", asList("Spans", "Counters", "Gauges", "Histograms", "Meters", "Timers", "Health Checks"));
}
@GET
@@ -62,6 +110,13 @@
}
@GET
+ @Path("Chart.bundle.min.js")
+ @Produces("application/javascript")
+ public String getChartJsBundle() {
+ return chartJs;
+ }
+
+ @GET
@Path("counters")
public Html getCounters() {
return new Html("main.html")
@@ -211,6 +266,63 @@
.with("span", value);
}
+ @GET
+ @Path("health-checks")
+ public Html getHealths() {
+ return new Html("main.html")
+ .with("view", "health-checks.html")
+ .with("colors", COLORS)
+ .with("title", "Health Checks")
+ .with("checks", new TreeSet<>(database.getChecks().keySet()));
+ }
+
+ @GET
+ @Path("check")
+ public Html getHealth(@QueryParam("check") final String name) {
+ final InMemoryDatabase<MicroprofileDatabase.CheckSnapshot> db = database.getChecks().get(name);
+ return new Html("main.html")
+ .with("view", "health.html")
+ .with("colors", COLORS)
+ .with("title", "Health Check")
+ .with("name", name)
+ .with("message", db == null ? "No matching check for name '" + name + "'" : null)
+ .with("points", db == null ? null : db.snapshot().stream().map(Point::new).collect(toList()));
+ }
+
+ @GET
+ @Path("health-check-detail")
+ public Html getHealthCheckDetail(@QueryParam("check") final String name) {
+ final InMemoryDatabase.Value<MicroprofileDatabase.CheckSnapshot> last = ofNullable(database.getChecks().get(name))
+ .map(InMemoryDatabase::snapshot)
+ .map(it -> it.isEmpty() ? null : it.getLast())
+ .orElse(null); // todo: orElseGet -> call them all and filter per name?
+ return new Html("main.html")
+ .with("view", "health-check-detail.html")
+ .with("colors", COLORS)
+ .with("title", "Health Check")
+ .with("name", name)
+ .with("message", last == null ? "No matching check yet for name '" + name + "'" : null)
+ .with("lastCheckTimestamp", last == null ? null : new Date(last.getTimestamp()))
+ .with("lastCheck", last == null ? null : last.getValue());
+ }
+
+ @GET
+ @Path("health-application")
+ public Html getApplicationHealth() {
+ final List<HealthCheckResponse> checks = healthRegistry.doCheck()
+ .sorted(comparing(HealthCheckResponse::getName))
+ .collect(toList());
+ final boolean stateOk = checks.stream()
+ .noneMatch(it -> it.getState().equals(HealthCheckResponse.State.DOWN));
+ return new Html("main.html")
+ .with("view", "health-application.html")
+ .with("colors", COLORS)
+ .with("title", "Application Health")
+ .with("globalStateOk", stateOk)
+ .with("globalStateKo", !stateOk)
+ .with("checks", checks);
+ }
+
public static class Point<T> {
private final long timestamp;
private final T value;
diff --git a/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/TemplatingEngine.java b/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/TemplatingEngine.java
index e98af03..142e6ee 100644
--- a/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/TemplatingEngine.java
+++ b/geronimo-microprofile-reporter/src/main/java/org/apache/geronimo/microprofile/reporter/storage/TemplatingEngine.java
@@ -19,10 +19,12 @@
import static java.util.Locale.ROOT;
import static java.util.stream.Collectors.joining;
+import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -139,41 +141,20 @@
return compileIfNeeded(templateLoader.apply(templatePath), templateLoader).apply(includeData);
});
} else if (substring.startsWith("@escape(")) {
- final String value = builder.toString();
- segments.add(data -> value);
- builder.setLength(0);
-
- final int end = findEndingParenthesis(chars, i + "@escape(".length() + 1);
- if (end < 0) {
- throw new IllegalArgumentException("Missing ')' token for @escape at position " + i + " for:\n" + template);
- }
- final String toEscape = template.substring(i + "@escape(".length(), end);
- i = end;
- segments.add(data -> {
- final String escapableValue = compileIfNeeded(toEscape, templateLoader).apply(data);
- if (escapableValue == null) {
- return "";
+ i = handleFn("escape", template, templateLoader, segments, builder, chars, i, templateHelper::escape);
+ } else if (substring.startsWith("@attributify(")) {
+ i = handleFn("attributify", template, templateLoader, segments, builder, chars, i,
+ v -> v.toLowerCase(ROOT).replace(' ', '-'));
+ } else if (substring.startsWith("@url(")) {
+ i = handleFn("url", template, templateLoader, segments, builder, chars, i, v -> {
+ try {
+ return URLEncoder.encode(v, "UTF-8");
+ } catch (final UnsupportedEncodingException e) {
+ throw new IllegalStateException(e);
}
- return templateHelper.escape(escapableValue);
});
} else if (substring.startsWith("@lowercase(")) {
- final String value = builder.toString();
- segments.add(data -> value);
- builder.setLength(0);
-
- final int end = findEndingParenthesis(chars, i + "@lowercase(".length() + 1);
- if (end < 0) {
- throw new IllegalArgumentException("Missing ')' token for @lowercase at position " + i + " for:\n" + template);
- }
- final String toEscape = template.substring(i + "@lowercase(".length(), end);
- i = end;
- segments.add(data -> {
- final String escapableValue = compileIfNeeded(toEscape, templateLoader).apply(data);
- if (escapableValue == null) {
- return "";
- }
- return escapableValue.toLowerCase(ROOT);
- });
+ i = handleFn("lowercase", template, templateLoader, segments, builder, chars, i, v -> v.toLowerCase(ROOT));
} else if (substring.startsWith("@each(")) {
final String value = builder.toString();
segments.add(ctx -> value);
@@ -281,6 +262,28 @@
return segments;
}
+ private int handleFn(final String name, final String template, final Function<String, String> templateLoader,
+ final Collection<Function<Object, String>> segments, final StringBuilder builder,
+ final char[] chars, final int currentIndex, final Function<String, String> impl) {
+ final String value = builder.toString();
+ segments.add(data -> value);
+ builder.setLength(0);
+
+ final int end = findEndingParenthesis(chars, currentIndex + name .length() + 2 /*@ and (*/ + 1);
+ if (end < 0) {
+ throw new IllegalArgumentException("Missing ')' token for @" + name + " at position " + currentIndex + " for:\n" + template);
+ }
+ final String toEscape = template.substring(currentIndex + name.length() + 2, end);
+ segments.add(data -> {
+ final String escapableValue = compileIfNeeded(toEscape, templateLoader).apply(data);
+ if (escapableValue == null) {
+ return "";
+ }
+ return impl.apply(escapableValue);
+ });
+ return end;
+ }
+
private int findEndingParenthesis(final char[] chars, final int from) {
int remaining = 1;
for (int i = from; i < chars.length; i++) {
diff --git a/geronimo-microprofile-reporter/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension b/geronimo-microprofile-reporter/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension
new file mode 100644
index 0000000..d0ef3e1
--- /dev/null
+++ b/geronimo-microprofile-reporter/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension
@@ -0,0 +1 @@
+org.apache.geronimo.microprofile.reporter.storage.HealthRegistry
diff --git a/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/chartsjs.html b/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/chartsjs.html
index 3c0c38a..61704a9 100644
--- a/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/chartsjs.html
+++ b/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/chartsjs.html
@@ -14,5 +14,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.bundle.min.js"
- integrity="sha256-MZo5XY1Ah7Z2Aui4/alkfeiq3CopMdV/bbkc/Sh41+s=" crossorigin="anonymous"></script>
\ No newline at end of file
+<script src="Chart.bundle.min.js"></script>
diff --git a/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/health-application.html b/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/health-application.html
new file mode 100644
index 0000000..cc93320
--- /dev/null
+++ b/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/health-application.html
@@ -0,0 +1,32 @@
+/**
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You under the Apache License, Version 2.0
+* (the "License"); you may not use this file except in compliance with
+* the License. You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+<h1>Application Health</h1>
+
+<div>
+ <h2>Overall State</h2>
+ <div class="health-check-state @if($globalStateOk,inline:green)@if($globalStateKo,inline:red)"></div>
+
+ <h2>Checks</h2>
+ <table>
+ <thead>
+ <tr><th>Name</th><th>State</th><th>Data</th></tr>
+ </thead>
+ <tbody>
+ @each($checks,incline:<tr><td>$$value.name</td><td>$$value.state</td><td>$$value.data</td></tr>)
+ </tbody>
+ </table>
+</div>
diff --git a/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/health-check-detail.html b/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/health-check-detail.html
new file mode 100644
index 0000000..eb65539
--- /dev/null
+++ b/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/health-check-detail.html
@@ -0,0 +1,36 @@
+/**
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You under the Apache License, Version 2.0
+* (the "License"); you may not use this file except in compliance with
+* the License. You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+<h1>Health Check @escape($name) at @escape($lastCheckTimestamp)</h1>
+
+@if($message,inline:<div class="error">@escape($message)</div>)
+
+<div>
+ <h2>State</h2>
+ <div class="health-check-state health-check-@lowercase($lastCheck.state)">
+ <span>$lastCheck.state</span>
+ </div>
+
+ <h2>Data</h2>
+ <table>
+ <thead>
+ <tr><th>Name</th><th>Value</th></tr>
+ </thead>
+ <tbody>
+ @each($lastCheck.data,inline:<tr><td>$$value.key</td><td>$$value.value</td></tr>)
+ </tbody>
+ </table>
+</div>
diff --git a/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/health-checks.html b/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/health-checks.html
new file mode 100644
index 0000000..015f0ed
--- /dev/null
+++ b/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/health-checks.html
@@ -0,0 +1,29 @@
+/**
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You under the Apache License, Version 2.0
+* (the "License"); you may not use this file except in compliance with
+* the License. You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+<h1>Health Checks</h1>
+
+<div>
+ See the application global instantaneous state <a href="health-application">here</a>.
+</div>
+<div>
+ <span>Select a particular check to visualize:</span>
+ <form action="check" method="get">
+ <input list="check" name="check">
+ <datalist id="check">@each($checks,datalist-option.html)</datalist>
+ <input type="submit">
+ </form>
+</div>
diff --git a/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/health.html b/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/health.html
new file mode 100644
index 0000000..25e910d
--- /dev/null
+++ b/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/health.html
@@ -0,0 +1,67 @@
+/**
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You under the Apache License, Version 2.0
+* (the "License"); you may not use this file except in compliance with
+* the License. You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+<h1>Health Check @escape($name)</h1>
+
+@if($message,inline:<div class="error">@escape($message)</div>)
+
+<div>
+ <div>See health check <a href="health-check-detail?check=@url($name)">last</a> status detail.</div>
+ <canvas id="check-chart" height="50"></canvas>
+</div>
+
+
+@include(chartsjs.html)
+<script>
+new Chart(document.getElementById('check-chart').getContext('2d'), {
+ type: 'line',
+ data: {
+ xLabels: [ @each($points,inline:new Date($$value.timestamp).toLocaleString()@if($hasNext,inline:,)) ],
+ yLabels: [ 'UP', 'DOWN' ],
+ datasets: [{
+ label: '$name',
+ data: [ @each($points,inline:'$$value.value.state'@if($hasNext,inline:,)) ],
+ steppedLine: true,
+ fill: true
+ }]
+ },
+ options: {
+ responsive: true,
+ title: {
+ display: true,
+ text: '$name'
+ },
+ scales: {
+ xAxes: [{
+ display: true,
+ scaleLabel: {
+ display: true,
+ labelString: 'Date'
+ }
+ }],
+ yAxes: [{
+ type: 'category',
+ position: 'left',
+ display: true,
+ scaleLabel: {
+ display: true,
+ labelString: 'State'
+ }
+ }]
+ }
+ }
+});
+</script>
diff --git a/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/main.html b/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/main.html
index bd4bfff..dca8f31 100644
--- a/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/main.html
+++ b/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/main.html
@@ -18,7 +18,7 @@
<html>
<head>
<meta charset="utf-8">
- <title>Geronimo Microprofile :: ${title}</title>
+ <title>Geronimo Microprofile :: $title</title>
<style>@include(style.css)</style>
</head>
<body>
diff --git a/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/style.css b/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/style.css
index c93c12d..f24d017 100644
--- a/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/style.css
+++ b/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/style.css
@@ -64,3 +64,20 @@
.tile > a:hover, .tile:hover > a {
color: $colors.hover;
}
+
+.health-check-state {
+ width: 100%;
+ height: 50px;
+ color: white;
+ font-weight: bolder;
+ display: flex;
+}
+.health-check-state > span {
+ margin: auto;
+}
+.health-check-up {
+ background-color: green;
+}
+.health-check-down {
+ background-color: red;
+}
diff --git a/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/tile.html b/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/tile.html
index bed4155..01cb3cb 100644
--- a/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/tile.html
+++ b/geronimo-microprofile-reporter/src/main/resources/geronimo/microprofile/reporter/tile.html
@@ -14,6 +14,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-<div class="tile @lowercase($$value)">
- <a href="@lowercase($$value)">$$value</a>
+<div class="tile @attributify($$value)">
+ <a href="@attributify($$value)">$$value</a>
</div>
\ No newline at end of file
diff --git a/geronimo-microprofile-site/src/main/jbake/content/reporter.adoc b/geronimo-microprofile-site/src/main/jbake/content/reporter.adoc
index 0ef149b..8d40572 100644
--- a/geronimo-microprofile-site/src/main/jbake/content/reporter.adoc
+++ b/geronimo-microprofile-site/src/main/jbake/content/reporter.adoc
@@ -16,6 +16,11 @@
<artifactId>geronimo-microprofile-reporter</artifactId>
<version>${geronimo-microprofile.version}</version>
</dependency>
+<dependency> <!-- to have chart pages -->
+ <groupId>org.webjars.bower</groupId>
+ <artifactId>chart.js</artifactId>
+ <version>2.7.3</version>
+</dependency>
----
== Usage