package com.onthegomap.planetiler.stats;
import com.onthegomap.planetiler.util.FileUtils;
import com.onthegomap.planetiler.util.MemoryEstimator;
import io.prometheus.client.Collector;
import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.CounterMetricFamily;
import io.prometheus.client.GaugeMetricFamily;
import io.prometheus.client.Histogram;
import io.prometheus.client.exporter.BasicAuthHttpConnectionFactory;
import io.prometheus.client.exporter.PushGateway;
import io.prometheus.client.exporter.common.TextFormat;
import io.prometheus.client.hotspot.DefaultExports;
import java.io.IOException;
import java.io.StringWriter;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.net.URL;
import java.nio.file.FileStore;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A {@link Stats} implementation that pushes metrics to a prometheus instance
* through a push gateway.
*
* See {@code grafana.json} for an example grafana dashboard you can use to monitor progress.
*/
class PrometheusStats implements Stats {
private static final Logger LOGGER = LoggerFactory.getLogger(PrometheusStats.class);
private final CollectorRegistry registry = new CollectorRegistry();
private final Timers timers = new Timers();
private static final String BASE = "planetiler_";
private PushGateway pg;
private ScheduledExecutorService executor;
private final String job;
private final Map filesToMonitor = new ConcurrentSkipListMap<>();
private final Map heapObjectsToMonitor = new ConcurrentSkipListMap<>();
/** Constructs a new instance but does not start polling (for tests). */
PrometheusStats(String job) {
this.job = job;
DefaultExports.register(registry);
new ThreadDetailsExports().register(registry);
new InProgressTasks().register(registry);
new FileSizeCollector().register(registry);
new HeapObjectSizeCollector().register(registry);
new PostGcMemoryCollector().register(registry);
}
private PrometheusStats(String destination, String job, Duration interval) {
this(job);
try {
URL url = new URL(destination);
pg = new PushGateway(url);
if (url.getUserInfo() != null) {
String[] parts = url.getUserInfo().split(":");
if (parts.length == 2) {
pg.setConnectionFactory(new BasicAuthHttpConnectionFactory(parts[0], parts[1]));
}
}
this.push();
executor = Executors.newScheduledThreadPool(1, r -> {
Thread thread = new Thread(r);
thread.setDaemon(true);
thread.setName("prometheus-pusher");
return thread;
});
executor.scheduleAtFixedRate(this::push, 0, Math.max(interval.getSeconds(), 5), TimeUnit.SECONDS);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Returns a new {@code PrometheusStats} that and schedules it to push to {@code destination} every {@code interval}.
*/
static PrometheusStats createAndStartPushing(String destination, String job, Duration interval) {
return new PrometheusStats(destination, job, interval);
}
private void push() {
try {
pg.push(registry, job);
} catch (IOException e) {
LOGGER.error("Error pushing stats to prometheus", e);
}
}
@Override
public void gauge(String name, Supplier value) {
new Collector() {
@Override
public List collect() {
return List.of(new GaugeMetricFamily(BASE + sanitizeMetricName(name), "", value.get().doubleValue()));
}
}.register(registry);
}
private final io.prometheus.client.Counter processedElements = io.prometheus.client.Counter
.build(BASE + "renderer_elements_processed", "Number of source elements processed")
.labelNames("type", "layer")
.register(registry);
@Override
public void processedElement(String elemType, String layer) {
processedElements.labels(elemType, layer).inc();
}
private final io.prometheus.client.Counter dataErrors = io.prometheus.client.Counter
.build(BASE + "bad_input_data", "Number of data inconsistencies encountered in source data")
.labelNames("type")
.register(registry);
@Override
public void dataError(String errorCode) {
dataErrors.labels(errorCode).inc();
}
private final io.prometheus.client.Counter emittedFeatures = io.prometheus.client.Counter
.build(BASE + "renderer_features_emitted", "Features enqueued for writing to feature DB")
.labelNames("zoom", "layer")
.register(registry);
@Override
public void emittedFeatures(int z, String layer, int numFeatures) {
emittedFeatures.labels(Integer.toString(z), layer).inc(numFeatures);
}
/** Returns the full payload that we would send to push gateway for a poll right way. */
public String getMetricsAsString() {
try (StringWriter writer = new StringWriter()) {
TextFormat.write004(writer, registry.metricFamilySamples());
return writer.toString();
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
private final Histogram tilesWrittenBytes = Histogram
.build(BASE + "mbtiles_tile_written_bytes", "Written tile sizes by zoom level")
.buckets(1_000, 10_000, 100_000, 500_000)
.labelNames("zoom")
.register(registry);
@Override
public void wroteTile(int zoom, int bytes) {
tilesWrittenBytes.labels(Integer.toString(zoom)).observe(bytes);
}
@Override
public Timers timers() {
return timers;
}
@Override
public Map monitoredFiles() {
return filesToMonitor;
}
@Override
public void monitorInMemoryObject(String name, MemoryEstimator.HasEstimate object) {
heapObjectsToMonitor.put(name, object);
}
@Override
public void counter(String name, Supplier supplier) {
new Collector() {
@Override
public List collect() {
return List.of(new CounterMetricFamily(BASE + sanitizeMetricName(name), "", supplier.get().doubleValue()));
}
}.register(registry);
}
@Override
public void counter(String name, String label, Supplier