diff --git a/pom.xml b/pom.xml index 1e27b19..43ca518 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,20 @@ docker-java 3.0.0 + + org.springframework.boot + spring-boot-starter-actuator + + + io.prometheus + simpleclient + RELEASE + + + io.prometheus + simpleclient_common + RELEASE + org.springframework.boot spring-boot-starter-test diff --git a/src/main/java/works/weave/socks/queuemaster/configuration/PrometheusEndpointContextConfiguration.java b/src/main/java/works/weave/socks/queuemaster/configuration/PrometheusEndpointContextConfiguration.java new file mode 100644 index 0000000..1992520 --- /dev/null +++ b/src/main/java/works/weave/socks/queuemaster/configuration/PrometheusEndpointContextConfiguration.java @@ -0,0 +1,40 @@ +package works.weave.socks.queuemaster.configuration; + +import io.prometheus.client.CollectorRegistry; +import org.springframework.boot.actuate.autoconfigure.ExportMetricWriter; +import org.springframework.boot.actuate.autoconfigure.ManagementContextConfiguration; +import org.springframework.boot.actuate.condition.ConditionalOnEnabledEndpoint; +import org.springframework.boot.actuate.metrics.writer.MetricWriter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.context.annotation.Bean; +import works.weave.socks.queuemaster.controllers.PrometheusEndpoint; +import works.weave.socks.queuemaster.controllers.PrometheusMvcEndpoint; +import works.weave.socks.queuemaster.monitoring.PrometheusMetricWriter; + +@ManagementContextConfiguration +public class PrometheusEndpointContextConfiguration { + + @Bean + public PrometheusEndpoint prometheusEndpoint(CollectorRegistry registry) { + return new PrometheusEndpoint(registry); + } + + @Bean + @ConditionalOnBean(PrometheusEndpoint.class) + @ConditionalOnEnabledEndpoint("prometheus") + PrometheusMvcEndpoint prometheusMvcEndpoint(PrometheusEndpoint prometheusEndpoint) { + return new PrometheusMvcEndpoint(prometheusEndpoint); + } + + @Bean + CollectorRegistry collectorRegistry() { + return new CollectorRegistry(); + } + + @Bean + @ExportMetricWriter + MetricWriter prometheusMetricWriter(CollectorRegistry registry) { + return new PrometheusMetricWriter(registry); + } + +} diff --git a/src/main/java/works/weave/socks/queuemaster/controllers/PrometheusEndpoint.java b/src/main/java/works/weave/socks/queuemaster/controllers/PrometheusEndpoint.java new file mode 100644 index 0000000..916a6c4 --- /dev/null +++ b/src/main/java/works/weave/socks/queuemaster/controllers/PrometheusEndpoint.java @@ -0,0 +1,30 @@ +package works.weave.socks.queuemaster.controllers; + +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.exporter.common.TextFormat; +import org.springframework.boot.actuate.endpoint.AbstractEndpoint; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; + +public class PrometheusEndpoint extends AbstractEndpoint { + + private CollectorRegistry registry; + + public PrometheusEndpoint(CollectorRegistry registry) { + super("prometheus", false, true); + this.registry = registry; + } + + @Override + public String invoke() { + Writer writer = new StringWriter(); + try { + TextFormat.write004(writer, registry.metricFamilySamples()); + } catch (IOException e) { + e.printStackTrace(); + } + return writer.toString(); + } +} diff --git a/src/main/java/works/weave/socks/queuemaster/controllers/PrometheusMvcEndpoint.java b/src/main/java/works/weave/socks/queuemaster/controllers/PrometheusMvcEndpoint.java new file mode 100644 index 0000000..820517e --- /dev/null +++ b/src/main/java/works/weave/socks/queuemaster/controllers/PrometheusMvcEndpoint.java @@ -0,0 +1,31 @@ +package works.weave.socks.queuemaster.controllers; + +import io.prometheus.client.exporter.common.TextFormat; +import org.springframework.boot.actuate.endpoint.mvc.AbstractEndpointMvcAdapter; +import org.springframework.boot.actuate.endpoint.mvc.HypermediaDisabled; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.util.Collections; + +public class PrometheusMvcEndpoint extends AbstractEndpointMvcAdapter { + + public PrometheusMvcEndpoint(PrometheusEndpoint delegate) { + super(delegate); + } + + @RequestMapping(method = RequestMethod.GET, produces = TextFormat.CONTENT_TYPE_004) + @ResponseBody + @HypermediaDisabled + protected Object invoke() { + if (!getDelegate().isEnabled()) { + return new ResponseEntity<>( + Collections.singletonMap("message", "This endpoint is disabled"), + HttpStatus.NOT_FOUND); + } + return super.invoke(); + } +} diff --git a/src/main/java/works/weave/socks/queuemaster/monitoring/PrometheusMetricWriter.java b/src/main/java/works/weave/socks/queuemaster/monitoring/PrometheusMetricWriter.java new file mode 100644 index 0000000..5245171 --- /dev/null +++ b/src/main/java/works/weave/socks/queuemaster/monitoring/PrometheusMetricWriter.java @@ -0,0 +1,54 @@ +package works.weave.socks.queuemaster.monitoring; + +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.Counter; +import io.prometheus.client.Gauge; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.writer.Delta; +import org.springframework.boot.actuate.metrics.writer.MetricWriter; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public class PrometheusMetricWriter implements MetricWriter { + + private final ConcurrentMap counters = new ConcurrentHashMap<>(); + private final ConcurrentMap gauges = new ConcurrentHashMap<>(); + private CollectorRegistry registry; + + @Autowired + public PrometheusMetricWriter(CollectorRegistry registry) { + this.registry = registry; + } + + @Override + public void increment(Delta delta) { + counter(delta.getName()).inc(delta.getValue().doubleValue()); + } + + @Override + public void reset(String metricName) { + counter(metricName).clear(); + } + + @Override + public void set(Metric value) { + gauge(value.getName()).set(value.getValue().doubleValue()); + } + + private Counter counter(String name) { + String key = sanitizeName(name); + return counters.computeIfAbsent(key, k -> Counter.build().name(k).help(k).register(registry)); + } + + private Gauge gauge(String name) { + String key = sanitizeName(name); + return gauges.computeIfAbsent(key, k -> Gauge.build().name(k).help(k).register(registry)); + } + + private String sanitizeName(String name) { + return name.replaceAll("[^a-zA-Z0-9_]", "_"); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..44dad51 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ +server.port=${port:8080} \ No newline at end of file