GEODE-7390: Add Micrometer metrics example (#90)
Add example to demonstrate publishing metrics from Geode to a monitoring
system comprised of Prometheus and Grafana.
Co-authored-by: Aaron Lindsey <alindsey@pivotal.io>
Co-authored-by: Dale Emery <demery@pivotal.io>
Co-authored-by: Mark Hanson <mhanson@pivotal.io>
Co-authored-by: Nick Vallely <nvallely@pivotal.io>
Authored-by: Aaron Lindsey <alindsey@pivotal.io>
diff --git a/README.md b/README.md
index 00fade2..0c8a667 100644
--- a/README.md
+++ b/README.md
@@ -89,6 +89,7 @@
* [Lucene Spatial Indexing](luceneSpatial/README.md)
* [WAN Gateway](wan/README.md)
* [Durable Messaging for Subscriptions](durableMessaging/README.md)
+* [Micrometer Metrics](micrometerMetrics/README.md)
* Delta propagation
* Network partition detection
* D-lock
diff --git a/gradle.properties b/gradle.properties
index 4bbd66b..4328334 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -29,3 +29,4 @@
mockitocoreVersion = 2.19.1
log4jVersion = 2.11.0
systemrulesVersion = 1.16.1
+micrometerVersion = 1.2.0
diff --git a/gradle/rat.gradle b/gradle/rat.gradle
index a37298a..2aedfe0 100644
--- a/gradle/rat.gradle
+++ b/gradle/rat.gradle
@@ -74,7 +74,9 @@
'**/server-ln-2/**',
'**/locator-ny/**',
'**/server-ny-1/**',
- '**/server-ny-2/**'
+ '**/server-ny-2/**',
+
+ '**/META-INF/**'
]
}
diff --git a/micrometerMetrics/README.md b/micrometerMetrics/README.md
new file mode 100644
index 0000000..225ca4e
--- /dev/null
+++ b/micrometerMetrics/README.md
@@ -0,0 +1,87 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+-->
+
+# Geode Micrometer Metrics Example
+
+This example demonstrates publishing metrics from Geode to a monitoring system comprised of
+Prometheus and Grafana. For more details about how this works, see [Publishing Geode Metrics to
+External Monitoring
+Systems](https://cwiki.apache.org/confluence/display/GEODE/Publishing+Geode+Metrics+to+External+Monitoring+Systems).
+
+## Steps to Run and Validate the Example
+
+1. From the `geode-examples/micrometerMetrics` directory, run the `start` task to build the example
+ and start a cluster. The cluster will have one locator, one server, and a single region. The
+ locator and server will expose HTTP endpoints for Prometheus to scrape.
+
+ $ ../gradlew start
+
+1. Run the example to put entries into the region and verify that the HTTP endpoints are working.
+
+ $ ../gradlew run
+
+ The previous command should produce output like the following:
+
+ The entry count for region example-region on the server is 10.
+ A Prometheus endpoint is running at http://localhost:9914.
+ A Prometheus endpoint is running at http://localhost:9915.
+
+1. To validate the example, navigate to the endpoints above in a browser. You should see text
+ resembling the [Prometheus exposition
+ format](https://github.com/prometheus/docs/blob/master/content/docs/instrumenting/exposition_formats.md).
+
+## (Optional) Viewing Metrics Using Prometheus and Grafana
+
+1. Download and run Prometheus. Go to <https://prometheus.io/download> to download the
+ latest release. Then, run the commands below to extract and run it:
+
+ $ tar xvfz prometheus-*.tar.gz
+ $ cd prometheus-*
+ $ ./prometheus --config.file=../prometheus.yml
+
+ The provided `prometheus.yml` file configures Prometheus to scrape Geode's Prometheus endpoints
+ at two-second intervals.
+
+1. In a browser, navigate to <http://localhost:9090/targets>. This page shows Geode's Prometheus
+ endpoints, and their state should be "UP" as shown below:
+
+ ![Prometheus targets](prometheus-targets.png "Prometheus targets")
+
+1. Navigate to
+ <http://localhost:9090/graph?g0.range_input=30m&g0.expr=geode_cache_entries&g0.tab=0>. This page
+ shows the result of the PromQL query `geode_cache_entries` over the last 30 minutes. It should
+ show a value of "10" as the entry count for region `example-region` on the server:
+
+ ![Prometheus graph](prometheus-graph.png "Prometheus graph")
+
+1. Download and run Grafana. Add a new Prometheus data source with the address of the Prometheus
+ server from the previous step. For detailed instructions, see Grafana's [Getting Started
+ Guide](https://grafana.com/docs/guides/getting_started/).
+
+1. Once you have added the Prometheus data source, you can import [this
+ dashboard](https://grafana.com/grafana/dashboards/11060) from Grafana.com.
+
+ To import, select the "plus" icon on the left-hand side and choose the "Import" option. Use the
+ ID from the link above for the dashboard and specify the Prometheus data source you created in
+ the last step.
+
+## Clean Up
+1. Shut down the cluster.
+
+ $ cd ..
+ $ ../gradlew stop
+
diff --git a/micrometerMetrics/build.gradle b/micrometerMetrics/build.gradle
new file mode 100644
index 0000000..45ebcd4
--- /dev/null
+++ b/micrometerMetrics/build.gradle
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+configurations {
+ dependenciesToIncludeInEndpointJar
+}
+
+dependencies {
+ dependenciesToIncludeInEndpointJar "io.micrometer:micrometer-registry-prometheus:$micrometerVersion"
+ configurations.compile.extendsFrom(configurations.dependenciesToIncludeInEndpointJar)
+}
+
+jar {
+ from {
+ configurations.dependenciesToIncludeInEndpointJar.collect { it.isDirectory() ? it : zipTree(it) }
+ }
+}
diff --git a/micrometerMetrics/prometheus-graph.png b/micrometerMetrics/prometheus-graph.png
new file mode 100644
index 0000000..ac3122c
--- /dev/null
+++ b/micrometerMetrics/prometheus-graph.png
Binary files differ
diff --git a/micrometerMetrics/prometheus-targets.png b/micrometerMetrics/prometheus-targets.png
new file mode 100644
index 0000000..5eb7dbc
--- /dev/null
+++ b/micrometerMetrics/prometheus-targets.png
Binary files differ
diff --git a/micrometerMetrics/prometheus.yml b/micrometerMetrics/prometheus.yml
new file mode 100644
index 0000000..100ad44
--- /dev/null
+++ b/micrometerMetrics/prometheus.yml
@@ -0,0 +1,21 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+# agreements. See the NOTICE file distributed with this work for additional information regarding
+# copyright ownership. The ASF licenses this file to You 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.
+
+global:
+ scrape_interval: 2s
+scrape_configs:
+ - job_name: 'geode-examples'
+ metrics_path: /
+ static_configs:
+ - targets: ['localhost:9914']
+ - targets: ['localhost:9915']
diff --git a/micrometerMetrics/scripts/start.gfsh b/micrometerMetrics/scripts/start.gfsh
new file mode 100644
index 0000000..1bf3a2e
--- /dev/null
+++ b/micrometerMetrics/scripts/start.gfsh
@@ -0,0 +1,24 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+
+# ports 9914 and 9915 chosen at random
+
+start locator --name=locator --bind-address=127.0.0.1 --classpath=../build/libs/micrometerMetrics.jar --J=-Dprometheus.metrics.port=9914
+
+start server --name=server --locators=127.0.0.1[10334] --server-port=0 --classpath=../build/libs/micrometerMetrics.jar --J=-Dprometheus.metrics.port=9915
+
+create region --name=example-region --type=REPLICATE
diff --git a/micrometerMetrics/scripts/stop.gfsh b/micrometerMetrics/scripts/stop.gfsh
new file mode 100644
index 0000000..15cd93c
--- /dev/null
+++ b/micrometerMetrics/scripts/stop.gfsh
@@ -0,0 +1,19 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+
+connect --locator=127.0.0.1[10334]
+shutdown --include-locators=true
diff --git a/micrometerMetrics/src/main/java/org/apache/geode_examples/micrometerMetrics/Example.java b/micrometerMetrics/src/main/java/org/apache/geode_examples/micrometerMetrics/Example.java
new file mode 100644
index 0000000..079ff81
--- /dev/null
+++ b/micrometerMetrics/src/main/java/org/apache/geode_examples/micrometerMetrics/Example.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You 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.
+ */
+package org.apache.geode_examples.micrometerMetrics;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.stream.IntStream;
+
+import org.apache.geode.cache.Region;
+import org.apache.geode.cache.client.ClientCache;
+import org.apache.geode.cache.client.ClientCacheFactory;
+import org.apache.geode.cache.client.ClientRegionShortcut;
+
+public class Example {
+ public static void main(String[] args) {
+ addCacheEntries();
+ verifyPrometheusEndpointsAreRunning();
+ }
+
+ private static void addCacheEntries() {
+ // connect to the locator using default port
+ ClientCache cache = new ClientCacheFactory().addPoolLocator("localhost", 10334)
+ .set("log-level", "WARN").create();
+
+ // create a local region that matches the server region
+ Region<Integer, String> region =
+ cache.<Integer, String>createClientRegionFactory(ClientRegionShortcut.PROXY)
+ .create("example-region");
+
+ // add entries to the region
+ IntStream.rangeClosed(1, 10).forEach(i -> region.put(i, "value" + i));
+
+ System.out.println(String.format("The entry count for region %s on the server is %d.",
+ region.getName(), region.sizeOnServer()));
+
+ cache.close();
+ }
+
+ private static void verifyPrometheusEndpointsAreRunning() {
+ String[] endpoints = {"http://localhost:9914", "http://localhost:9915"};
+
+ for (String endpoint : endpoints) {
+ try {
+ URL url = new URL(endpoint);
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.connect();
+
+ if (HttpURLConnection.HTTP_OK != connection.getResponseCode()) {
+ throw new IllegalStateException(
+ "Prometheus endpoint returned status code " + connection.getResponseCode());
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to connect to Prometheus endpoint", e);
+ }
+
+ System.out.println("A Prometheus endpoint is running at " + endpoint + ".");
+ }
+ }
+}
diff --git a/micrometerMetrics/src/main/java/org/apache/geode_examples/micrometerMetrics/SimpleMetricsPublishingService.java b/micrometerMetrics/src/main/java/org/apache/geode_examples/micrometerMetrics/SimpleMetricsPublishingService.java
new file mode 100644
index 0000000..1880ce2
--- /dev/null
+++ b/micrometerMetrics/src/main/java/org/apache/geode_examples/micrometerMetrics/SimpleMetricsPublishingService.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You 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.
+ */
+package org.apache.geode_examples.micrometerMetrics;
+
+import static io.micrometer.prometheus.PrometheusConfig.DEFAULT;
+import static java.lang.Integer.getInteger;
+import static org.slf4j.LoggerFactory.getLogger;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+
+import com.sun.net.httpserver.HttpContext;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+import io.micrometer.prometheus.PrometheusMeterRegistry;
+import org.slf4j.Logger;
+
+import org.apache.geode.metrics.MetricsPublishingService;
+import org.apache.geode.metrics.MetricsSession;
+
+public class SimpleMetricsPublishingService implements MetricsPublishingService {
+ private static final String PORT_PROPERTY = "prometheus.metrics.port";
+ private static final int DEFAULT_PORT = 0; // If no port specified, use any port
+ private static final String HOSTNAME = "localhost";
+ private static final int PORT = getInteger(PORT_PROPERTY, DEFAULT_PORT);
+
+ private static Logger LOG = getLogger(SimpleMetricsPublishingService.class);
+
+ private final int port;
+ private PrometheusMeterRegistry registry;
+ private HttpServer server;
+
+ public SimpleMetricsPublishingService() {
+ this(PORT);
+ }
+
+ public SimpleMetricsPublishingService(int port) {
+ this.port = port;
+ }
+
+ @Override
+ public void start(MetricsSession session) {
+ registry = new PrometheusMeterRegistry(DEFAULT);
+ session.addSubregistry(registry);
+
+ InetSocketAddress address = new InetSocketAddress(HOSTNAME, port);
+ server = null;
+ try {
+ server = HttpServer.create(address, 0);
+
+ HttpContext context = server.createContext("/");
+ context.setHandler(this::requestHandler);
+ server.start();
+
+ int boundPort = server.getAddress().getPort();
+ LOG.info("Started {} http://{}:{}/", getClass().getSimpleName(), HOSTNAME, boundPort);
+ } catch (IOException thrown) {
+ LOG.error("Exception while starting " + getClass().getSimpleName(), thrown);
+ }
+ }
+
+ private void requestHandler(HttpExchange httpExchange) throws IOException {
+ final byte[] scrapeBytes = registry.scrape().getBytes();
+ httpExchange.sendResponseHeaders(200, scrapeBytes.length);
+ final OutputStream responseBody = httpExchange.getResponseBody();
+ responseBody.write(scrapeBytes);
+ responseBody.close();
+ }
+
+ @Override
+ public void stop(MetricsSession session) {
+ session.removeSubregistry(registry);
+ registry = null;
+ server.stop(0);
+ }
+}
diff --git a/micrometerMetrics/src/main/resources/META-INF/services/org.apache.geode.metrics.MetricsPublishingService b/micrometerMetrics/src/main/resources/META-INF/services/org.apache.geode.metrics.MetricsPublishingService
new file mode 100644
index 0000000..0bfa3ce
--- /dev/null
+++ b/micrometerMetrics/src/main/resources/META-INF/services/org.apache.geode.metrics.MetricsPublishingService
@@ -0,0 +1 @@
+org.apache.geode_examples.micrometerMetrics.SimpleMetricsPublishingService
\ No newline at end of file
diff --git a/micrometerMetrics/src/test/java/org/apache/geode_examples/micrometerMetrics/SimpleMetricsPublishingServiceTest.java b/micrometerMetrics/src/test/java/org/apache/geode_examples/micrometerMetrics/SimpleMetricsPublishingServiceTest.java
new file mode 100644
index 0000000..39acfac
--- /dev/null
+++ b/micrometerMetrics/src/test/java/org/apache/geode_examples/micrometerMetrics/SimpleMetricsPublishingServiceTest.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You 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.
+ */
+package org.apache.geode_examples.micrometerMetrics;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.catchThrowable;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.quality.Strictness.STRICT_STUBS;
+
+import java.io.IOException;
+
+import io.micrometer.prometheus.PrometheusMeterRegistry;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.conn.HttpHostConnectException;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.util.EntityUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import org.apache.geode.metrics.MetricsPublishingService;
+import org.apache.geode.metrics.MetricsSession;
+
+public class SimpleMetricsPublishingServiceTest {
+ @Rule
+ public MockitoRule mockitoRule = MockitoJUnit.rule().strictness(STRICT_STUBS);
+
+ @Mock
+ public MetricsSession metricsSession;
+
+ private MetricsPublishingService subject;
+
+ @Before
+ public void setUp() {
+ subject = new SimpleMetricsPublishingService(9000);
+ }
+
+ @Test
+ public void start_addsRegistryToMetricsSession() {
+ subject.start(metricsSession);
+
+ verify(metricsSession).addSubregistry(any(PrometheusMeterRegistry.class));
+
+ subject.stop(metricsSession);
+ }
+
+ @Test
+ public void start_addsAnHttpEndpointThatReturnsStatusOK() throws IOException {
+ subject.start(metricsSession);
+
+ HttpGet request = new HttpGet("http://localhost:9000/");
+ HttpResponse response = HttpClientBuilder.create().build().execute(request);
+
+ assertThat(response.getStatusLine().getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+
+ subject.stop(metricsSession);
+ }
+
+ @Test
+ public void start_addsAnHttpEndpointThatContainsRegistryData() throws IOException {
+ subject.start(metricsSession);
+
+ HttpGet request = new HttpGet("http://localhost:9000/");
+ HttpResponse response = HttpClientBuilder.create().build().execute(request);
+
+ String responseBody = EntityUtils.toString(response.getEntity());
+ assertThat(responseBody).isEmpty();
+
+ subject.stop(metricsSession);
+ }
+
+ @Test
+ public void stop_removesRegistryFromMetricsSession() {
+ subject.start(metricsSession);
+ subject.stop(metricsSession);
+
+ verify(metricsSession).removeSubregistry(any(PrometheusMeterRegistry.class));
+ }
+
+ @Test
+ public void stop_hasNoHttpEndpointRunning() {
+ subject.start(metricsSession);
+ subject.stop(metricsSession);
+
+ HttpGet request = new HttpGet("http://localhost:9000/");
+
+ Throwable thrown = catchThrowable(() -> HttpClientBuilder.create().build().execute(request));
+
+ assertThat(thrown).isInstanceOf(HttpHostConnectException.class);
+ }
+}
diff --git a/settings.gradle b/settings.gradle
index 57d108d..19ac5c0 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -40,6 +40,7 @@
include 'jdbc'
include 'sessionState'
include 'colocation'
+include 'micrometerMetrics'
// Logic for defining a custom Geode clone for integration with this project
// Define `-PgeodeCompositeDirectory` to your geode root, default `../geode`