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 extends Subscriber> subscriberClass) {
@@ -98,12 +108,25 @@ private void alertSubscriber(final Notification notification, final Class exte
EXECUTOR_SERVICE.execute(() -> {
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..0c5bc3b4
--- /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_NOT_FOUND);
+ }
+ }
+
+}
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 888b0f3c..bfa5a9ce 100644
--- a/pom.xml
+++ b/pom.xml
@@ -188,6 +188,7 @@
1.1.4
1.2.5
1.2.11
+ 1.9.1
9.38
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}
+