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