diff --git a/phoenix-queryserver/src/main/java/org/apache/hadoop/metrics2/impl/MetricsExportHelper.java b/phoenix-queryserver/src/main/java/org/apache/hadoop/metrics2/impl/MetricsExportHelper.java new file mode 100644 index 0000000..0dbab9c --- /dev/null +++ b/phoenix-queryserver/src/main/java/org/apache/hadoop/metrics2/impl/MetricsExportHelper.java @@ -0,0 +1,39 @@ +/* + * 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.hadoop.metrics2.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.apache.hadoop.metrics2.MetricsRecord; +import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem; + +public final class MetricsExportHelper { + private MetricsExportHelper() { + } + + public static Collection export() { + MetricsSystemImpl instance = (MetricsSystemImpl) DefaultMetricsSystem.instance(); + MetricsBuffer metricsBuffer = instance.sampleMetrics(); + List metrics = new ArrayList<>(); + for (MetricsBuffer.Entry entry : metricsBuffer) { + entry.records().forEach(metrics::add); + } + return metrics; + } +} diff --git a/phoenix-queryserver/src/main/java/org/apache/phoenix/queryserver/QueryServerOptions.java b/phoenix-queryserver/src/main/java/org/apache/phoenix/queryserver/QueryServerOptions.java index 95f11b3..c3f0e8e 100644 --- a/phoenix-queryserver/src/main/java/org/apache/phoenix/queryserver/QueryServerOptions.java +++ b/phoenix-queryserver/src/main/java/org/apache/phoenix/queryserver/QueryServerOptions.java @@ -38,6 +38,7 @@ public class QueryServerOptions { public static final String DEFAULT_QUERY_SERVER_REMOTEUSEREXTRACTOR_PARAM = "doAs"; public static final boolean DEFAULT_QUERY_SERVER_DISABLE_KERBEROS_LOGIN = false; public static final boolean DEFAULT_QUERY_SERVER_JMXJSONENDPOINT_DISABLED = false; + public static final boolean DEFAULT_QUERY_SERVER_PROMETHEUS_ENDPOINT_ENABLED = true; public static final boolean DEFAULT_QUERY_SERVER_TLS_ENABLED = false; //We default to empty *store password diff --git a/phoenix-queryserver/src/main/java/org/apache/phoenix/queryserver/QueryServerProperties.java b/phoenix-queryserver/src/main/java/org/apache/phoenix/queryserver/QueryServerProperties.java index dda88cf..6afe8d6 100644 --- a/phoenix-queryserver/src/main/java/org/apache/phoenix/queryserver/QueryServerProperties.java +++ b/phoenix-queryserver/src/main/java/org/apache/phoenix/queryserver/QueryServerProperties.java @@ -75,6 +75,9 @@ public class QueryServerProperties { public static final String QUERY_SERVER_JMX_JSON_ENDPOINT_DISABLED = "phoenix.queryserver.jmxjsonendpoint.disabled"; + public static final String QUERY_SERVER_PROMETHEUS_ENDPOINT_ENABLED = + "phoenix.queryserver.prometheusendpoint.enabled"; + // keys for load balancer public static final String PHOENIX_QUERY_SERVER_LOADBALANCER_ENABLED = "phoenix.queryserver.loadbalancer.enabled"; diff --git a/phoenix-queryserver/src/main/java/org/apache/phoenix/queryserver/server/ServerCustomizersFactory.java b/phoenix-queryserver/src/main/java/org/apache/phoenix/queryserver/server/ServerCustomizersFactory.java index 7f4105d..5641168 100644 --- a/phoenix-queryserver/src/main/java/org/apache/phoenix/queryserver/server/ServerCustomizersFactory.java +++ b/phoenix-queryserver/src/main/java/org/apache/phoenix/queryserver/server/ServerCustomizersFactory.java @@ -29,6 +29,7 @@ import org.apache.phoenix.queryserver.QueryServerProperties; import org.apache.phoenix.queryserver.server.customizers.HostedClientJarsServerCustomizer; import org.apache.phoenix.queryserver.server.customizers.JMXJsonEndpointServerCustomizer; +import org.apache.phoenix.queryserver.server.customizers.prometheus.PrometheusEndpointServerCustomizer; import org.eclipse.jetty.server.Server; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -76,6 +77,12 @@ public List> createServerCustomizers(Configuration conf QueryServerOptions.DEFAULT_QUERY_SERVER_JMXJSONENDPOINT_DISABLED)) { customizers.add(new JMXJsonEndpointServerCustomizer()); } + + if (conf.getBoolean(QueryServerProperties.QUERY_SERVER_PROMETHEUS_ENDPOINT_ENABLED, + QueryServerOptions.DEFAULT_QUERY_SERVER_PROMETHEUS_ENDPOINT_ENABLED)) { + customizers.add(new PrometheusEndpointServerCustomizer()); + } + return Collections.unmodifiableList(customizers); } } diff --git a/phoenix-queryserver/src/main/java/org/apache/phoenix/queryserver/server/customizers/prometheus/PrometheusEndpointServerCustomizer.java b/phoenix-queryserver/src/main/java/org/apache/phoenix/queryserver/server/customizers/prometheus/PrometheusEndpointServerCustomizer.java new file mode 100644 index 0000000..0f574c4 --- /dev/null +++ b/phoenix-queryserver/src/main/java/org/apache/phoenix/queryserver/server/customizers/prometheus/PrometheusEndpointServerCustomizer.java @@ -0,0 +1,57 @@ +/* + * 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.phoenix.queryserver.server.customizers.prometheus; + +import org.apache.calcite.avatica.server.ServerCustomizer; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.HandlerList; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.Servlet; +import java.util.Arrays; + +public class PrometheusEndpointServerCustomizer implements ServerCustomizer { + private static final Logger LOG = LoggerFactory.getLogger(PrometheusEndpointServerCustomizer.class); + + @Override + public void customize(Server server) { + Handler[] handlers = server.getHandlers(); + if (handlers.length != 1) { + LOG.warn("Observed handlers on server {}", Arrays.toString(handlers)); + throw new IllegalStateException("Expected to find one handler"); + } + HandlerList list = (HandlerList) handlers[0]; + + ServletContextHandler ctx = new ServletContextHandler(); + ctx.setContextPath("/prometheus"); + + Servlet servlet = new PrometheusHadoopServlet(); + ServletHolder holder = new ServletHolder(servlet); + ctx.addServlet(holder, "/"); + + Handler[] realHandlers = list.getChildHandlers(); + Handler[] newHandlers = new Handler[realHandlers.length + 1]; + newHandlers[0] = ctx; + System.arraycopy(realHandlers, 0, newHandlers, 1, realHandlers.length); + server.setHandler(new HandlerList(newHandlers)); + } +} \ No newline at end of file diff --git a/phoenix-queryserver/src/main/java/org/apache/phoenix/queryserver/server/customizers/prometheus/PrometheusHadoopServlet.java b/phoenix-queryserver/src/main/java/org/apache/phoenix/queryserver/server/customizers/prometheus/PrometheusHadoopServlet.java new file mode 100644 index 0000000..c456c39 --- /dev/null +++ b/phoenix-queryserver/src/main/java/org/apache/phoenix/queryserver/server/customizers/prometheus/PrometheusHadoopServlet.java @@ -0,0 +1,83 @@ +/* + * 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.phoenix.queryserver.server.customizers.prometheus; + +import java.io.IOException; +import java.io.Writer; +import java.util.Collection; +import java.util.regex.Pattern; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.hadoop.metrics2.AbstractMetric; +import org.apache.hadoop.metrics2.MetricType; +import org.apache.hadoop.metrics2.MetricsRecord; +import org.apache.hadoop.metrics2.MetricsTag; +import org.apache.hadoop.metrics2.impl.MetricsExportHelper; + +public class PrometheusHadoopServlet extends HttpServlet { + private static final Pattern SPLIT_PATTERN = + Pattern.compile("(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=([A-Z][a-z]))|\\W|(_)+"); + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + writeMetrics(resp.getWriter(), "true".equals(req.getParameter("description")), + req.getParameter("qry")); + } + + String toPrometheusName(String metricRecordName, String metricName) { + String baseName = metricRecordName + metricName.substring(0,1).toUpperCase() + metricName.substring(1); + String[] parts = SPLIT_PATTERN.split(baseName); + return String.join("_", parts).toLowerCase(); + } + + void writeMetrics(Writer writer, boolean descriptionEnabled, String queryParam) + throws IOException { + Collection metricRecords = MetricsExportHelper.export(); + for (MetricsRecord metricsRecord : metricRecords) { + for (AbstractMetric metrics : metricsRecord.metrics()) { + if (metrics.type() == MetricType.COUNTER || metrics.type() == MetricType.GAUGE) { + + String key = toPrometheusName(metricsRecord.name(), metrics.name()); + + if (queryParam == null || key.contains(queryParam)) { + + if (descriptionEnabled) { + String description = metrics.description(); + if (!description.isEmpty()) writer.append("# HELP ").append(description).append('\n'); + } + + writer.append("# TYPE ").append(key).append(" ") + .append(metrics.type().toString().toLowerCase()).append('\n').append(key).append("{"); + + /* add tags */ + String sep = ""; + for (MetricsTag tag : metricsRecord.tags()) { + String tagName = tag.name().toLowerCase(); + writer.append(sep).append(tagName).append("=\"").append(tag.value()).append("\""); + sep = ","; + } + writer.append("} "); + writer.append(metrics.value().toString()).append('\n'); + } + } + } + } + writer.flush(); + } +} diff --git a/phoenix-queryserver/src/test/java/org/apache/phoenix/queryserver/server/ServerCustomizersTest.java b/phoenix-queryserver/src/test/java/org/apache/phoenix/queryserver/server/ServerCustomizersTest.java index e593bd1..b2d9ab8 100644 --- a/phoenix-queryserver/src/test/java/org/apache/phoenix/queryserver/server/ServerCustomizersTest.java +++ b/phoenix-queryserver/src/test/java/org/apache/phoenix/queryserver/server/ServerCustomizersTest.java @@ -26,6 +26,7 @@ import org.apache.hadoop.conf.Configuration; import org.apache.phoenix.query.QueryServices; import org.apache.phoenix.queryserver.QueryServerProperties; +import org.apache.phoenix.queryserver.server.customizers.prometheus.PrometheusEndpointServerCustomizer; import org.apache.phoenix.util.InstanceResolver; import org.eclipse.jetty.server.Server; import org.junit.After; @@ -51,7 +52,7 @@ public void testDefaultFactory() { // the default factory creates an empty list of server customizers List> customizers = queryServer.createServerCustomizers(new Configuration(), avaticaServerConfiguration); - Assert.assertEquals(1, customizers.size()); + Assert.assertEquals(2, customizers.size()); } @Test @@ -77,4 +78,14 @@ public List> createServerCustomizers(Configuration conf List> actual = queryServer.createServerCustomizers(conf, avaticaServerConfiguration); Assert.assertEquals("Customizers are different", expected, actual); } + + @Test + public void testPrometheusServletCustomizer() { + QueryServer queryServer = new QueryServer(); + + List> customizers = + queryServer.createServerCustomizers(new Configuration(), null); + + Assert.assertTrue(customizers.get(1) instanceof PrometheusEndpointServerCustomizer); + } } diff --git a/phoenix-queryserver/src/test/java/org/apache/phoenix/queryserver/server/customizers/prometheus/PrometheusHadoopServletTest.java b/phoenix-queryserver/src/test/java/org/apache/phoenix/queryserver/server/customizers/prometheus/PrometheusHadoopServletTest.java new file mode 100644 index 0000000..c62e49a --- /dev/null +++ b/phoenix-queryserver/src/test/java/org/apache/phoenix/queryserver/server/customizers/prometheus/PrometheusHadoopServletTest.java @@ -0,0 +1,53 @@ +/* + * 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.phoenix.queryserver.server.customizers.prometheus; + +import java.io.IOException; +import java.io.PrintWriter; + +import org.junit.Assert; +import org.junit.Test; + +import javax.servlet.http.HttpServletResponse; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class PrometheusHadoopServletTest { + private final PrometheusHadoopServlet servlet = new PrometheusHadoopServlet(); + + //NullPointerException is expected because the MetricExportHelper doesn't have any metrics to export + @Test(expected = NullPointerException.class) + public void testPrometheusServletMetricsWriter() throws IOException { + HttpServletResponse response = mock(HttpServletResponse.class); + + PrintWriter writer = new PrintWriter(System.out); + when(response.getWriter()).thenReturn(writer); + + servlet.writeMetrics(response.getWriter(),false,""); + } + + @Test + public void testPrometheusNameConversion() { + String metricRecordName = "TestMetricRecord"; + String metricName = "TestMetricName"; + + Assert.assertEquals("test_metric_record_test_metric_name",servlet.toPrometheusName(metricRecordName,metricName)); + } +} +