| /** |
| * 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.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; |
| |
| @Path("geronimo/microprofile/reporter") |
| @ApplicationScoped |
| @Produces(TEXT_HTML) |
| public class ReporterEndpoints { |
| private static final Colors COLORS = new Colors("#007bff", "#0000CD"); |
| |
| @Inject |
| private MicroprofileDatabase database; |
| |
| @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", "Health Checks")); |
| } |
| |
| @GET |
| @Path("index.html") // we are too used to that to not provide it |
| public Html getIndex() { |
| return get(); |
| } |
| |
| @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") |
| .with("view", "counters.html") |
| .with("colors", COLORS) |
| .with("title", "Counters") |
| .with("counters", new TreeSet<>(database.getCounters().keySet())); |
| } |
| |
| @GET |
| @Path("counter") |
| public Html getCounter(@QueryParam("counter") final String name) { |
| final InMemoryDatabase<Long> db = database.getCounters().get(name); |
| return new Html("main.html") |
| .with("view", "counter.html") |
| .with("colors", COLORS) |
| .with("title", "Counters") |
| .with("name", name) |
| .with("unit", db == null ? null : db.getUnit()) |
| .with("message", db == null ? "No matching counter for name '" + name + "'" : null) |
| .with("points", db == null ? null : db.snapshot().stream().map(Point::new).collect(toList())); |
| } |
| |
| @GET |
| @Path("gauges") |
| public Html getGauges() { |
| return new Html("main.html") |
| .with("view", "gauges.html") |
| .with("colors", COLORS) |
| .with("title", "Gauges") |
| .with("gauges", new TreeSet<>(database.getGauges().keySet())); |
| } |
| |
| @GET |
| @Path("gauge") |
| public Html getGauge(@QueryParam("gauge") final String name) { |
| final InMemoryDatabase<Double> db = database.getGauges().get(name); |
| return new Html("main.html") |
| .with("view", "gauge.html") |
| .with("colors", COLORS) |
| .with("title", "Gauges") |
| .with("name", name) |
| .with("unit", db == null ? null : db.getUnit()) |
| .with("message", db == null ? "No matching gauge for name '" + name + "'" : null) |
| .with("points", db == null ? null : db.snapshot().stream().map(Point::new).collect(toList())); |
| } |
| |
| @GET |
| @Path("histograms") |
| public Html getHistograms() { |
| return new Html("main.html") |
| .with("view", "histograms.html") |
| .with("colors", COLORS) |
| .with("title", "Histograms") |
| .with("histograms", new TreeSet<>(database.getHistograms().keySet())); |
| } |
| |
| @GET |
| @Path("histogram") |
| public Html getHistogram(@QueryParam("histogram") final String name) { |
| final InMemoryDatabase<Snapshot> db = database.getHistograms().get(name); |
| return new Html("main.html") |
| .with("view", "histogram.html") |
| .with("colors", COLORS) |
| .with("title", "Histogram") |
| .with("name", name) |
| .with("unit", db == null ? null : db.getUnit()) |
| .with("message", db == null ? "No matching histogram for name '" + name + "'" : null) |
| .with("points", db == null ? null : db.snapshot().stream().map(Point::new).collect(toList())); |
| } |
| |
| @GET |
| @Path("meters") |
| public Html getMeters() { |
| return new Html("main.html") |
| .with("view", "meters.html") |
| .with("colors", COLORS) |
| .with("title", "Meters") |
| .with("meters", new TreeSet<>(database.getMeters().keySet())); |
| } |
| |
| @GET |
| @Path("meter") |
| public Html getMeter(@QueryParam("meter") final String name) { |
| final InMemoryDatabase<MicroprofileDatabase.MeterSnapshot> db = database.getMeters().get(name); |
| return new Html("main.html") |
| .with("view", "meter.html") |
| .with("colors", COLORS) |
| .with("title", "Meter") |
| .with("name", name) |
| .with("unit", db == null ? null : db.getUnit()) |
| .with("message", db == null ? "No matching meter for name '" + name + "'" : null) |
| .with("points", db == null ? null : db.snapshot().stream().map(Point::new).collect(toList())); |
| } |
| |
| @GET |
| @Path("timers") |
| public Html getTimers() { |
| return new Html("main.html") |
| .with("view", "timers.html") |
| .with("colors", COLORS) |
| .with("title", "Timers") |
| .with("timers", new TreeSet<>(database.getTimers().keySet())); |
| } |
| |
| @GET |
| @Path("timer") |
| public Html getTimer(@QueryParam("timer") final String name) { |
| final InMemoryDatabase<MicroprofileDatabase.TimerSnapshot> db = database.getTimers().get(name); |
| return new Html("main.html") |
| .with("view", "timer.html") |
| .with("colors", COLORS) |
| .with("title", "Timer") |
| .with("name", name) |
| .with("unit", db == null ? null : db.getUnit()) |
| .with("message", db == null ? "No matching timer for name '" + name + "'" : null) |
| .with("points", db == null ? null : db.snapshot().stream().map(Point::new).collect(toList())); |
| } |
| |
| @GET |
| @Path("spans") |
| public Html getSpans() { |
| final InMemoryDatabase<Span> db = database.getSpans(); |
| return new Html("main.html") |
| .with("view", "spans.html") |
| .with("colors", COLORS) |
| .with("title", "Spans") |
| .with("spans", db == null ? |
| null : |
| db.snapshot().stream() |
| .map(it -> new Point<>(it.getTimestamp(), spanMapper.map(it.getValue()))) |
| .collect(toList())); |
| } |
| |
| @GET |
| @Path("span") |
| public Html getSpan(@QueryParam("spanId") final String id) { |
| final SpanMapper.SpanEntry value = database.getSpans().snapshot().stream() |
| .map(it -> spanMapper.map(it.getValue())) |
| .filter(it -> it.getSpanId().equals(id)) |
| .findFirst() |
| .orElseThrow(() -> new BadRequestException("No matching span")); |
| return new Html("main.html") |
| .with("view", "span.html") |
| .with("colors", COLORS) |
| .with("title", "Span") |
| .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; |
| |
| private Point(final InMemoryDatabase.Value<T> value) { |
| this(value.getTimestamp(), value.getValue()); |
| } |
| |
| private Point(final long timestamp, final T value) { |
| this.timestamp = timestamp; |
| this.value = value; |
| } |
| } |
| |
| private static class Colors { |
| private final String main; |
| private final String hover; |
| |
| private Colors(final String main, final String hover) { |
| this.main = main; |
| this.hover = hover; |
| } |
| } |
| } |