001/*
002 * Copyright 2021 DuraSpace, Inc.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.fcrepo.migration.metrics;
017
018import static java.net.HttpURLConnection.HTTP_OK;
019
020import java.io.IOException;
021import java.net.InetSocketAddress;
022import java.time.Duration;
023import java.util.concurrent.ExecutorService;
024import java.util.concurrent.Executors;
025
026import com.sun.net.httpserver.HttpServer;
027import io.micrometer.core.instrument.Meter;
028import io.micrometer.core.instrument.Metrics;
029import io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics;
030import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics;
031import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics;
032import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics;
033import io.micrometer.core.instrument.binder.system.ProcessorMetrics;
034import io.micrometer.core.instrument.config.MeterFilter;
035import io.micrometer.core.instrument.distribution.DistributionStatisticConfig;
036import io.micrometer.prometheus.PrometheusConfig;
037import io.micrometer.prometheus.PrometheusMeterRegistry;
038
039/**
040 * Simple actuator for publishing metrics for Prometheus
041 *
042 * @author mikejritter
043 */
044public class PrometheusActuator {
045
046    private final ExecutorService executor;
047
048    private HttpServer server;
049    private PrometheusMeterRegistry registry;
050
051    public PrometheusActuator(final boolean enableMetrics) {
052        if (enableMetrics) {
053            configureRegistry();
054            configureServer();
055        }
056        this.executor = Executors.newSingleThreadExecutor();
057    }
058
059    private void configureRegistry() {
060        registry = new PrometheusMeterRegistry(new PrometheusConfig() {
061            @Override
062            public Duration step() {
063                return Duration.ofSeconds(10);
064            }
065
066            @Override
067            public String get(final String key) {
068                return null;
069            }
070        });
071
072        registry.config().meterFilter(new MeterFilter() {
073            @Override
074            public DistributionStatisticConfig configure(final Meter.Id id,
075                                                         final DistributionStatisticConfig config) {
076                if (id.getType() == Meter.Type.TIMER) {
077                    return new DistributionStatisticConfig.Builder()
078                        .percentilesHistogram(true)
079                        .percentiles(0.90, 0.95, 0.99)
080                        .build().merge(config);
081                }
082                return config;
083            }
084        });
085
086        new JvmGcMetrics().bindTo(registry);
087        new JvmMemoryMetrics().bindTo(registry);
088        new JvmThreadMetrics().bindTo(registry);
089        new ProcessorMetrics().bindTo(registry);
090        new ClassLoaderMetrics().bindTo(registry);
091
092        Metrics.addRegistry(registry);
093    }
094
095    private void configureServer() {
096        try {
097            server = HttpServer.create(new InetSocketAddress(8080), 0);
098        } catch (IOException e) {
099            throw new RuntimeException("Unable to start http server for publishing metrics!", e);
100        }
101
102        server.setExecutor(executor);
103        server.createContext("/prometheus", handler -> {
104            final var response = registry.scrape();
105            handler.sendResponseHeaders(HTTP_OK, response.getBytes().length);
106            try (final var os = handler.getResponseBody()) {
107                os.write(response.getBytes());
108            }
109        });
110    }
111
112    /**
113     * Start the HTTP server for metric publishing
114     *
115     * @throws RuntimeException if the server cannot be started
116     */
117    public void start() {
118        if (server != null) {
119            server.start();
120        }
121    }
122
123    /**
124     * Stop the HTTP server
125     */
126    public void stop() {
127        if (server != null) {
128            server.stop(0);
129        }
130        executor.shutdown();
131    }
132
133}