blob: cf676776b7ffdb2c3af1715f33d2e95a27d45c67 [file] [log] [blame]
/**
* 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.lang.Thread.NORM_PRIORITY;
import static java.util.Optional.ofNullable;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.eclipse.microprofile.metrics.MetricRegistry.Type.BASE;
import static org.eclipse.microprofile.metrics.MetricRegistry.Type.VENDOR;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.stream.Stream;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.Destroyed;
import javax.enterprise.context.Initialized;
import javax.enterprise.event.Observes;
import javax.inject.Inject;
import javax.servlet.ServletContext;
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;
import org.eclipse.microprofile.metrics.annotation.RegistryType;
import io.opentracing.Span;
// TODO: make span dependency optional and composable (events?)
@ApplicationScoped
public class MicroprofileDatabase {
@Inject
@ConfigProperty(name = "geronimo.microprofile.reporter.metrics.pollingInterval", defaultValue = "5000")
private Long pollingInterval;
@Inject
@RegistryType(type = BASE)
private MetricRegistry baseRegistry;
@Inject
@RegistryType(type = VENDOR)
private MetricRegistry vendorRegistry;
@Inject
private MetricRegistry applicationRegistry;
@Inject
private HealthRegistry healthRegistry;
private ScheduledExecutorService scheduler;
private ScheduledFuture<?> pollFuture;
private Map<String, MetricRegistry> metrics;
private final InMemoryDatabase<Span> spanDatabase = new InMemoryDatabase<>("none");
private final Map<String, InMemoryDatabase<Long>> counters = new HashMap<>();
private final Map<String, InMemoryDatabase<Double>> gauges = new HashMap<>();
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;
}
public Map<String, InMemoryDatabase<Long>> getCounters() {
return counters;
}
public Map<String, InMemoryDatabase<Double>> getGauges() {
return gauges;
}
public Map<String, InMemoryDatabase<Snapshot>> getHistograms() {
return histograms;
}
public Map<String, InMemoryDatabase<MeterSnapshot>> getMeters() {
return meters;
}
public Map<String, InMemoryDatabase<TimerSnapshot>> getTimers() {
return timers;
}
public Map<String, InMemoryDatabase<CheckSnapshot>> getChecks() {
return checks;
}
private void poll() {
metrics.forEach(this::updateMetrics);
healthRegistry.doCheck().forEach(this::updateHealthCheck);
}
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)));
}
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.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);
});
}
// alternatively we can decorate the registries and register/unregister following the registry lifecycle
// shouldnt be worth it for now
private <T> InMemoryDatabase<T> getDb(final Map<String, InMemoryDatabase<T>> registry,
final String virtualName, final MetricRegistry source,
final String key) {
InMemoryDatabase<T> db = registry.get(virtualName);
if (db == null) {
db = new InMemoryDatabase<>(ofNullable(source.getMetadata().get(key).getUnit()).orElse(""));
final InMemoryDatabase<T> existing = registry.putIfAbsent(virtualName, db);
if (existing != null) {
db = existing;
}
}
return db;
}
private String getMetricStorageName(final String type, final String name) {
return type + "#" + name;
}
private String name(final Object start) {
if (ServletContext.class.isInstance(start)) {
final ServletContext context = ServletContext.class.cast(start);
try {
return "[web=" + context.getVirtualServerName() + '/' + context.getContextPath() + "]";
} catch (final Error | Exception e) { // no getVirtualServerName() for this context
return "[web=" + context.getContextPath() + "]";
}
}
return start.toString();
}
void onSpan(@Observes final FinishedSpan span) {
final Span value = span.getSpan();
if (value.getClass().getName().equals("org.apache.geronimo.microprofile.opentracing.common.impl.SpanImpl")) {
spanDatabase.add(value);
} // else we will not be able to read the metadata
}
void onStart(@Observes @Initialized(ApplicationScoped.class) final Object start) {
metrics = new HashMap<>(3);
metrics.put("vendor", vendorRegistry);
metrics.put("base", baseRegistry);
metrics.put("application", applicationRegistry);
final ClassLoader appLoader = Thread.currentThread().getContextClassLoader();
scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
final Thread thread = new Thread(r, "geronimo-microprofile-reporter-poller-" + name(start));
thread.setContextClassLoader(appLoader);
if (thread.isDaemon()) {
thread.setDaemon(false);
}
if (thread.getPriority() != NORM_PRIORITY) {
thread.setPriority(NORM_PRIORITY);
}
return thread;
});
pollFuture = scheduler.scheduleAtFixedRate(this::poll, pollingInterval, pollingInterval, MILLISECONDS);
}
void onStop(@Observes @Destroyed(ApplicationScoped.class) final Object stop) {
if (pollFuture != null) {
pollFuture.cancel(true);
pollFuture = null;
}
if (scheduler != null) {
scheduler.shutdownNow();
try {
scheduler.awaitTermination(10, SECONDS);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
scheduler = null;
}
Stream.of(counters, gauges, histograms, meters, timers).forEach(Map::clear);
}
public static class TimerSnapshot {
private final MeterSnapshot meter;
private final Snapshot histogram;
private TimerSnapshot(final MeterSnapshot meter, final Snapshot histogram) {
this.meter = meter;
this.histogram = histogram;
}
}
public static class MeterSnapshot {
private final long count;
private final double rateMean;
private final double rate1;
private final double rate5;
private final double rate15;
private MeterSnapshot(final Metered meter) {
this(meter.getCount(), meter.getMeanRate(), meter.getOneMinuteRate(), meter.getFiveMinuteRate(), meter.getFifteenMinuteRate());
}
private MeterSnapshot(final long count, final double rateMean,
final double rate1, final double rate5, final double rate15) {
this.count = count;
this.rateMean = rateMean;
this.rate1 = rate1;
this.rate5 = rate5;
this.rate15 = rate15;
}
}
public static class CheckSnapshot {
private final String name;
private final String state;
private final Map<String, Object> data;
private CheckSnapshot(final String name, final String state, final Map<String, Object> data) {
this.name = name;
this.state = state;
this.data = data;
}
}
}