From b13e790e0d1213ddd29b4931ed325ba52a27a3f5 Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 11 Jul 2022 21:16:07 +0200 Subject: [PATCH] Expose Prometheus metrics with Micrometer Signed-off-by: nscuro --- alpine-common/pom.xml | 4 ++ .../src/main/java/alpine/Config.java | 3 +- .../java/alpine/common/metrics/Metrics.java | 45 ++++++++++++++ .../event/framework/BaseEventService.java | 13 ++++- .../alpine/event/framework/EventService.java | 5 +- .../framework/SingleThreadedEventService.java | 5 +- .../notification/NotificationService.java | 41 ++++++++++--- .../server/metrics/MetricsInitializer.java | 58 +++++++++++++++++++ .../server/servlets/MetricsServlet.java | 51 ++++++++++++++++ example/src/main/webapp/WEB-INF/web.xml | 10 ++++ pom.xml | 7 +++ 11 files changed, 229 insertions(+), 13 deletions(-) create mode 100644 alpine-common/src/main/java/alpine/common/metrics/Metrics.java create mode 100644 alpine-server/src/main/java/alpine/server/metrics/MetricsInitializer.java create mode 100644 alpine-server/src/main/java/alpine/server/servlets/MetricsServlet.java diff --git a/alpine-common/pom.xml b/alpine-common/pom.xml index 58196322..7a702b51 100644 --- a/alpine-common/pom.xml +++ b/alpine-common/pom.xml @@ -39,6 +39,10 @@ com.fasterxml.jackson.core jackson-annotations + + io.micrometer + micrometer-registry-prometheus + junit diff --git a/alpine-common/src/main/java/alpine/Config.java b/alpine-common/src/main/java/alpine/Config.java index ee0b1628..d77080c4 100644 --- a/alpine-common/src/main/java/alpine/Config.java +++ b/alpine-common/src/main/java/alpine/Config.java @@ -129,7 +129,8 @@ public enum AlpineKey implements Key { LDAP_USERS_SEARCH_FILTER ("alpine.ldap.users.search.filter", null), LDAP_USER_PROVISIONING ("alpine.ldap.user.provisioning", false), LDAP_TEAM_SYNCHRONIZATION ("alpine.ldap.team.synchronization", false), - OIDC_ENABLED ("alpine.oidc.enabled", false), + METRICS_ENABLED ("alpine.metrics.enabled", false), + OIDC_ENABLED ("alpine.oidc.enabled", false), OIDC_ISSUER ("alpine.oidc.issuer", null), OIDC_CLIENT_ID ("alpine.oidc.client.id", null), OIDC_USERNAME_CLAIM ("alpine.oidc.username.claim", "sub"), diff --git a/alpine-common/src/main/java/alpine/common/metrics/Metrics.java b/alpine-common/src/main/java/alpine/common/metrics/Metrics.java new file mode 100644 index 00000000..22a225aa --- /dev/null +++ b/alpine-common/src/main/java/alpine/common/metrics/Metrics.java @@ -0,0 +1,45 @@ +/* + * This file is part of Alpine. + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) Steve Springett. All Rights Reserved. + */ +package alpine.common.metrics; + +import alpine.Config; +import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics; +import io.micrometer.prometheus.PrometheusConfig; +import io.micrometer.prometheus.PrometheusMeterRegistry; + +import java.util.concurrent.ExecutorService; + +/** + * @since 2.1.0 + */ +public final class Metrics { + + private static final PrometheusMeterRegistry registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); + + public static PrometheusMeterRegistry getRegistry() { + return registry; + } + + public static void registerExecutorService(final ExecutorService executorService, final String name) { + if (Config.getInstance().getPropertyAsBoolean(Config.AlpineKey.METRICS_ENABLED)) { + new ExecutorServiceMetrics(executorService, name, null).bindTo(registry); + } + } + +} diff --git a/alpine-infra/src/main/java/alpine/event/framework/BaseEventService.java b/alpine-infra/src/main/java/alpine/event/framework/BaseEventService.java index aa64fc00..2c1bd0da 100644 --- a/alpine-infra/src/main/java/alpine/event/framework/BaseEventService.java +++ b/alpine-infra/src/main/java/alpine/event/framework/BaseEventService.java @@ -19,8 +19,10 @@ package alpine.event.framework; import alpine.common.logging.Logger; +import alpine.common.metrics.Metrics; import alpine.model.EventServiceLog; import alpine.persistence.AlpineQueryManager; +import io.micrometer.core.instrument.Counter; import org.apache.commons.lang3.concurrent.BasicThreadFactory; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -86,7 +88,7 @@ public void publish(Event event) { if (event instanceof ChainableEvent) { if (! addTrackedEvent((ChainableEvent)event)) { return; - }; + } } // Check to see if the Event is Unblocked. If so, use a separate executor pool from normal events @@ -138,6 +140,7 @@ public void publish(Event event) { }); } + recordPublishedMetric(event); } /** @@ -189,6 +192,14 @@ private synchronized void removeTrackedEvent(ChainableEvent event) { } } + private void recordPublishedMetric(final Event event) { + Counter.builder("alpine_events_published_total") + .description("Total number of published events") + .tags("event", event.getClass().getName(), "publisher", this.getClass().getName()) + .register(Metrics.getRegistry()) + .increment(); + } + /** * {@inheritDoc} * @since 1.0.0 diff --git a/alpine-infra/src/main/java/alpine/event/framework/EventService.java b/alpine-infra/src/main/java/alpine/event/framework/EventService.java index 1a0fbfd9..d2793c32 100644 --- a/alpine-infra/src/main/java/alpine/event/framework/EventService.java +++ b/alpine-infra/src/main/java/alpine/event/framework/EventService.java @@ -20,6 +20,7 @@ import alpine.common.logging.Logger; import alpine.common.util.ThreadUtil; +import alpine.common.metrics.Metrics; import org.apache.commons.lang3.concurrent.BasicThreadFactory; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -42,15 +43,17 @@ public final class EventService extends BaseEventService { private static final EventService INSTANCE = new EventService(); private static final Logger LOGGER = Logger.getLogger(EventService.class); private static final ExecutorService EXECUTOR; + private static final String EXECUTOR_NAME = "Alpine-EventService"; static { BasicThreadFactory factory = new BasicThreadFactory.Builder() - .namingPattern("Alpine-EventService-%d") + .namingPattern(EXECUTOR_NAME + "-%d") .uncaughtExceptionHandler(new LoggableUncaughtExceptionHandler()) .build(); EXECUTOR = Executors.newFixedThreadPool(ThreadUtil.determineNumberOfWorkerThreads(), factory); INSTANCE.setExecutorService(EXECUTOR); INSTANCE.setLogger(LOGGER); + Metrics.registerExecutorService(EXECUTOR, EXECUTOR_NAME); } /** diff --git a/alpine-infra/src/main/java/alpine/event/framework/SingleThreadedEventService.java b/alpine-infra/src/main/java/alpine/event/framework/SingleThreadedEventService.java index 9550ba83..ebcb42c5 100644 --- a/alpine-infra/src/main/java/alpine/event/framework/SingleThreadedEventService.java +++ b/alpine-infra/src/main/java/alpine/event/framework/SingleThreadedEventService.java @@ -19,6 +19,7 @@ package alpine.event.framework; import alpine.common.logging.Logger; +import alpine.common.metrics.Metrics; import org.apache.commons.lang3.concurrent.BasicThreadFactory; import java.util.concurrent.ExecutorService; @@ -40,15 +41,17 @@ public final class SingleThreadedEventService extends BaseEventService { private static final SingleThreadedEventService INSTANCE = new SingleThreadedEventService(); private static final Logger LOGGER = Logger.getLogger(EventService.class); private static final ExecutorService EXECUTOR; + private static final String EXECUTOR_NAME = "Alpine-SingleThreadedEventService"; static { BasicThreadFactory factory = new BasicThreadFactory.Builder() - .namingPattern("Alpine-SingleThreadedEventService") + .namingPattern(EXECUTOR_NAME) .uncaughtExceptionHandler(new LoggableUncaughtExceptionHandler()) .build(); EXECUTOR = Executors.newSingleThreadExecutor(factory); INSTANCE.setExecutorService(EXECUTOR); INSTANCE.setLogger(LOGGER); + Metrics.registerExecutorService(EXECUTOR, EXECUTOR_NAME); } /** diff --git a/alpine-infra/src/main/java/alpine/notification/NotificationService.java b/alpine-infra/src/main/java/alpine/notification/NotificationService.java index 1164a1cb..993c92bd 100644 --- a/alpine-infra/src/main/java/alpine/notification/NotificationService.java +++ b/alpine-infra/src/main/java/alpine/notification/NotificationService.java @@ -18,9 +18,12 @@ */ package alpine.notification; -import alpine.event.framework.LoggableUncaughtExceptionHandler; import alpine.common.logging.Logger; +import alpine.common.metrics.Metrics; +import alpine.event.framework.LoggableUncaughtExceptionHandler; +import io.micrometer.core.instrument.Counter; import org.apache.commons.lang3.concurrent.BasicThreadFactory; + import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Map; @@ -42,12 +45,18 @@ public final class NotificationService implements INotificationService { private static final NotificationService INSTANCE = new NotificationService(); private static final Logger LOGGER = Logger.getLogger(NotificationService.class); private static final Map, ArrayList> SUBSCRIPTION_MAP = new ConcurrentHashMap<>(); - private static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(4, - new BasicThreadFactory.Builder() - .namingPattern("Alpine-NotificationService-%d") - .uncaughtExceptionHandler(new LoggableUncaughtExceptionHandler()) - .build() - ); + private static final ExecutorService EXECUTOR_SERVICE; + private static final String EXECUTOR_SERVICE_NAME = "Alpine-NotificationService"; + + static { + EXECUTOR_SERVICE = Executors.newFixedThreadPool(4, + new BasicThreadFactory.Builder() + .namingPattern(EXECUTOR_SERVICE_NAME + "-%d") + .uncaughtExceptionHandler(new LoggableUncaughtExceptionHandler()) + .build() + ); + Metrics.registerExecutorService(EXECUTOR_SERVICE, EXECUTOR_SERVICE_NAME); + } /** * Private constructor @@ -70,7 +79,7 @@ public void publish(final Notification notification) { LOGGER.debug("No subscribers to inform from notification: " + notification.getClass().getName()); return; } - for (final Subscription subscription: subscriptions) { + for (final Subscription subscription : subscriptions) { if (subscription.getScope() != null && subscription.getGroup() != null && subscription.getLevel() != null) { // subscription was the most specific if (subscription.getScope().equals(notification.getScope()) && subscription.getGroup().equals(notification.getGroup()) && subscription.getLevel() == notification.getLevel()) { alertSubscriber(notification, subscription.getSubscriber()); @@ -91,6 +100,7 @@ public void publish(final Notification notification) { alertSubscriber(notification, subscription.getSubscriber()); } } + recordPublishedMetric(notification); } private void alertSubscriber(final Notification notification, final Class subscriberClass) { @@ -98,12 +108,25 @@ private void alertSubscriber(final Notification notification, final Class { try { subscriberClass.getDeclaredConstructor().newInstance().inform(notification); - } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException | SecurityException e) { + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | + IllegalAccessException | SecurityException e) { LOGGER.error("An error occurred while informing subscriber: " + e); } }); } + private void recordPublishedMetric(final Notification notification) { + Counter.builder("alpine_notifications_published_total") + .description("Total number of published notifications") + .tags( + "group", notification.getGroup(), + "level", notification.getLevel().name(), + "scope", notification.getScope() + ) + .register(Metrics.getRegistry()) + .increment(); + } + /** * {@inheritDoc} * @since 1.3.0 diff --git a/alpine-server/src/main/java/alpine/server/metrics/MetricsInitializer.java b/alpine-server/src/main/java/alpine/server/metrics/MetricsInitializer.java new file mode 100644 index 00000000..7365b0e3 --- /dev/null +++ b/alpine-server/src/main/java/alpine/server/metrics/MetricsInitializer.java @@ -0,0 +1,58 @@ +/* + * This file is part of Alpine. + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) Steve Springett. All Rights Reserved. + */ +package alpine.server.metrics; + +import alpine.Config; +import alpine.common.logging.Logger; +import alpine.common.metrics.Metrics; +import io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics; +import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics; +import io.micrometer.core.instrument.binder.jvm.JvmInfoMetrics; +import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; +import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics; +import io.micrometer.core.instrument.binder.system.DiskSpaceMetrics; +import io.micrometer.core.instrument.binder.system.ProcessorMetrics; +import io.micrometer.core.instrument.binder.system.UptimeMetrics; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +/** + * @since 2.1.0 + */ +public class MetricsInitializer implements ServletContextListener { + + private static final Logger LOGGER = Logger.getLogger(MetricsInitializer.class); + + @Override + public void contextInitialized(final ServletContextEvent event) { + if (Config.getInstance().getPropertyAsBoolean(Config.AlpineKey.METRICS_ENABLED)) { + LOGGER.info("Registering system metrics"); + new ClassLoaderMetrics().bindTo(Metrics.getRegistry()); + new DiskSpaceMetrics(Config.getInstance().getDataDirectorty()).bindTo(Metrics.getRegistry()); + new JvmGcMetrics().bindTo(Metrics.getRegistry()); + new JvmInfoMetrics().bindTo(Metrics.getRegistry()); + new JvmMemoryMetrics().bindTo(Metrics.getRegistry()); + new JvmThreadMetrics().bindTo(Metrics.getRegistry()); + new ProcessorMetrics().bindTo(Metrics.getRegistry()); + new UptimeMetrics().bindTo(Metrics.getRegistry()); + } + } + +} diff --git a/alpine-server/src/main/java/alpine/server/servlets/MetricsServlet.java b/alpine-server/src/main/java/alpine/server/servlets/MetricsServlet.java new file mode 100644 index 00000000..376f99ca --- /dev/null +++ b/alpine-server/src/main/java/alpine/server/servlets/MetricsServlet.java @@ -0,0 +1,51 @@ +/* + * This file is part of Alpine. + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) Steve Springett. All Rights Reserved. + */ +package alpine.server.servlets; + +import alpine.Config; +import alpine.common.metrics.Metrics; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @since 2.1.0 + */ +public class MetricsServlet extends HttpServlet { + + private boolean metricsEnabled; + + @Override + public void init() throws ServletException { + metricsEnabled = Config.getInstance().getPropertyAsBoolean(Config.AlpineKey.METRICS_ENABLED); + } + + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws IOException { + if (metricsEnabled) { + Metrics.getRegistry().scrape(resp.getWriter()); + } else { + resp.setStatus(HttpServletResponse.SC_NO_CONTENT); + } + } + +} diff --git a/example/src/main/webapp/WEB-INF/web.xml b/example/src/main/webapp/WEB-INF/web.xml index f40b0ad8..e527a987 100644 --- a/example/src/main/webapp/WEB-INF/web.xml +++ b/example/src/main/webapp/WEB-INF/web.xml @@ -35,4 +35,14 @@ /api/* + + Metrics + alpine.server.servlets.MetricsServlet + 1 + + + Metrics + /metrics + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 2afb7869..a9d8d1bb 100644 --- a/pom.xml +++ b/pom.xml @@ -188,6 +188,7 @@ 1.1.4 1.2.5 1.2.11 + 1.9.1 9.37 1.2.3 1.1.7 @@ -380,6 +381,12 @@ jackson-jaxrs-json-provider ${lib.jackson.version} + + + io.micrometer + micrometer-registry-prometheus + ${lib.micrometer.version} +