From be0afbf23ac47f6207fa58a06af9dbdc770c38a9 Mon Sep 17 00:00:00 2001 From: Nick Dimiduk Date: Fri, 14 May 2021 18:07:03 -0700 Subject: [PATCH] HBASE-25895 Implement a Cluster Metrics JSON endpoint Publishes a set of JSON endpoints following a RESTful structure, which expose a subset of the `o.a.h.h.ClusterMetrics` object tree. The URI structure is as follows /api/v1/admin/cluster_metrics /api/v1/admin/cluster_metrics/live_servers /api/v1/admin/cluster_metrics/dead_servers Signed-off-by: Sean Busbey Signed-off-by: Andrew Purtell --- .../apache/hadoop/hbase/http/HttpServer.java | 58 ++++- .../apache/hadoop/hbase/http/InfoServer.java | 19 +- .../hbase/http/gson/ByteArraySerializer.java | 38 ++++ .../http/gson/GsonMessageBodyWriter.java | 99 +++++++++ .../http/jersey/ResponseEntityMapper.java | 53 +++++ .../http/jersey/SupplierFactoryAdapter.java | 42 ++++ .../apache/hadoop/hbase/master/HMaster.java | 13 +- .../http/api_v1/ResourceConfigFactory.java | 59 +++++ .../cluster_metrics/model/ClusterMetrics.java | 70 ++++++ .../api_v1/cluster_metrics/package-info.java | 27 +++ .../resource/ClusterMetricsResource.java | 87 ++++++++ .../hbase/master/http/gson/GsonFactory.java | 43 ++++ .../http/gson/GsonSerializationFeature.java | 67 ++++++ .../http/gson/SizeAsBytesSerializer.java | 38 ++++ .../hbase/master/http/gson/package-info.java | 24 +++ .../master/http/jersey/MasterFeature.java | 73 +++++++ .../apache/hadoop/hbase/MiniClusterRule.java | 5 + .../http/TestApiV1ClusterMetricsResource.java | 203 ++++++++++++++++++ .../master/http/gson/GsonFactoryTest.java | 105 +++++++++ 19 files changed, 1112 insertions(+), 11 deletions(-) create mode 100644 hbase-http/src/main/java/org/apache/hadoop/hbase/http/gson/ByteArraySerializer.java create mode 100644 hbase-http/src/main/java/org/apache/hadoop/hbase/http/gson/GsonMessageBodyWriter.java create mode 100644 hbase-http/src/main/java/org/apache/hadoop/hbase/http/jersey/ResponseEntityMapper.java create mode 100644 hbase-http/src/main/java/org/apache/hadoop/hbase/http/jersey/SupplierFactoryAdapter.java create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/master/http/api_v1/ResourceConfigFactory.java create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/master/http/api_v1/cluster_metrics/model/ClusterMetrics.java create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/master/http/api_v1/cluster_metrics/package-info.java create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/master/http/api_v1/cluster_metrics/resource/ClusterMetricsResource.java create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/master/http/gson/GsonFactory.java create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/master/http/gson/GsonSerializationFeature.java create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/master/http/gson/SizeAsBytesSerializer.java create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/master/http/gson/package-info.java create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/master/http/jersey/MasterFeature.java create mode 100644 hbase-server/src/test/java/org/apache/hadoop/hbase/master/http/TestApiV1ClusterMetricsResource.java create mode 100644 hbase-server/src/test/java/org/apache/hadoop/hbase/master/http/gson/GsonFactoryTest.java diff --git a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/HttpServer.java b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/HttpServer.java index 884780cecc30..f8c04bac9715 100644 --- a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/HttpServer.java +++ b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/HttpServer.java @@ -39,6 +39,7 @@ import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; +import javax.servlet.Servlet; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRequest; @@ -838,6 +839,17 @@ public void addUnprivilegedServlet(String name, String pathSpec, addServletWithAuth(name, pathSpec, clazz, false); } + /** + * Adds a servlet in the server that any user can access. This method differs from + * {@link #addPrivilegedServlet(String, ServletHolder)} in that any authenticated user + * can interact with the servlet added by this method. + * @param pathSpec The path spec for the servlet + * @param holder The servlet holder + */ + public void addUnprivilegedServlet(String pathSpec, ServletHolder holder) { + addServletWithAuth(pathSpec, holder, false); + } + /** * Adds a servlet in the server that only administrators can access. This method differs from * {@link #addUnprivilegedServlet(String, String, Class)} in that only those authenticated user @@ -848,6 +860,16 @@ public void addPrivilegedServlet(String name, String pathSpec, addServletWithAuth(name, pathSpec, clazz, true); } + /** + * Adds a servlet in the server that only administrators can access. This method differs from + * {@link #addUnprivilegedServlet(String, ServletHolder)} in that only those + * authenticated user who are identified as administrators can interact with the servlet added by + * this method. + */ + public void addPrivilegedServlet(String pathSpec, ServletHolder holder) { + addServletWithAuth(pathSpec, holder, true); + } + /** * Internal method to add a servlet to the HTTP server. Developers should not call this method * directly, but invoke it via {@link #addUnprivilegedServlet(String, String, Class)} or @@ -859,6 +881,16 @@ void addServletWithAuth(String name, String pathSpec, addFilterPathMapping(pathSpec, webAppContext); } + /** + * Internal method to add a servlet to the HTTP server. Developers should not call this method + * directly, but invoke it via {@link #addUnprivilegedServlet(String, ServletHolder)} or + * {@link #addPrivilegedServlet(String, ServletHolder)}. + */ + void addServletWithAuth(String pathSpec, ServletHolder holder, boolean requireAuthz) { + addInternalServlet(pathSpec, holder, requireAuthz); + addFilterPathMapping(pathSpec, webAppContext); + } + /** * Add an internal servlet in the server, specifying whether or not to * protect with Kerberos authentication. @@ -867,17 +899,33 @@ void addServletWithAuth(String name, String pathSpec, * servlets added using this method, filters (except internal Kerberos * filters) are not enabled. * - * @param name The name of the servlet (can be passed as null) - * @param pathSpec The path spec for the servlet - * @param clazz The servlet class - * @param requireAuth Require Kerberos authenticate to access servlet + * @param name The name of the {@link Servlet} (can be passed as null) + * @param pathSpec The path spec for the {@link Servlet} + * @param clazz The {@link Servlet} class + * @param requireAuthz Require Kerberos authenticate to access servlet */ void addInternalServlet(String name, String pathSpec, - Class clazz, boolean requireAuthz) { + Class clazz, boolean requireAuthz) { ServletHolder holder = new ServletHolder(clazz); if (name != null) { holder.setName(name); } + addInternalServlet(pathSpec, holder, requireAuthz); + } + + /** + * Add an internal servlet in the server, specifying whether or not to + * protect with Kerberos authentication. + * Note: This method is to be used for adding servlets that facilitate + * internal communication and not for user facing functionality. For + * servlets added using this method, filters (except internal Kerberos + * filters) are not enabled. + * + * @param pathSpec The path spec for the {@link Servlet} + * @param holder The object providing the {@link Servlet} instance + * @param requireAuthz Require Kerberos authenticate to access servlet + */ + void addInternalServlet(String pathSpec, ServletHolder holder, boolean requireAuthz) { if (authenticationEnabled && requireAuthz) { FilterHolder filter = new FilterHolder(AdminAuthorizedFilter.class); filter.setName(AdminAuthorizedFilter.class.getSimpleName()); diff --git a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/InfoServer.java b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/InfoServer.java index 5ed380e4c357..8b13e2b22053 100644 --- a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/InfoServer.java +++ b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/InfoServer.java @@ -1,4 +1,4 @@ -/** +/* * 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 @@ -19,18 +19,16 @@ import java.io.IOException; import java.net.URI; - import javax.servlet.ServletContext; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; - import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.CommonConfigurationKeys; import org.apache.hadoop.hbase.HBaseConfiguration; import org.apache.hadoop.security.authorize.AccessControlList; import org.apache.yetus.audience.InterfaceAudience; - import org.apache.hbase.thirdparty.com.google.common.net.HostAndPort; +import org.apache.hbase.thirdparty.org.eclipse.jetty.servlet.ServletHolder; /** * Create a Jetty embedded server to answer http requests. The primary goal @@ -128,6 +126,7 @@ public void addServlet(String name, String pathSpec, } /** + * Adds a servlet in the server that any user can access. * @see HttpServer#addUnprivilegedServlet(String, String, Class) */ public void addUnprivilegedServlet(String name, String pathSpec, @@ -136,6 +135,18 @@ public void addUnprivilegedServlet(String name, String pathSpec, } /** + * Adds a servlet in the server that any user can access. + * @see HttpServer#addUnprivilegedServlet(String, ServletHolder) + */ + public void addUnprivilegedServlet(String name, String pathSpec, ServletHolder holder) { + if (name != null) { + holder.setName(name); + } + this.httpServer.addUnprivilegedServlet(pathSpec, holder); + } + + /** + * Adds a servlet in the server that any user can access. * @see HttpServer#addPrivilegedServlet(String, String, Class) */ public void addPrivilegedServlet(String name, String pathSpec, diff --git a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/gson/ByteArraySerializer.java b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/gson/ByteArraySerializer.java new file mode 100644 index 000000000000..fdcd34783c04 --- /dev/null +++ b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/gson/ByteArraySerializer.java @@ -0,0 +1,38 @@ +/* + * 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.hbase.http.gson; + +import java.lang.reflect.Type; +import org.apache.hadoop.hbase.util.Bytes; +import org.apache.yetus.audience.InterfaceAudience; +import org.apache.hbase.thirdparty.com.google.gson.JsonElement; +import org.apache.hbase.thirdparty.com.google.gson.JsonPrimitive; +import org.apache.hbase.thirdparty.com.google.gson.JsonSerializationContext; +import org.apache.hbase.thirdparty.com.google.gson.JsonSerializer; + +/** + * Serialize a {@code byte[]} using {@link Bytes#toString()}. + */ +@InterfaceAudience.Private +public final class ByteArraySerializer implements JsonSerializer { + + @Override + public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(Bytes.toString(src)); + } +} diff --git a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/gson/GsonMessageBodyWriter.java b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/gson/GsonMessageBodyWriter.java new file mode 100644 index 000000000000..c75113ded730 --- /dev/null +++ b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/gson/GsonMessageBodyWriter.java @@ -0,0 +1,99 @@ +/* + * 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.hbase.http.gson; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.nio.charset.Charset; +import java.nio.charset.IllegalCharsetNameException; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnsupportedCharsetException; +import java.util.Optional; +import javax.inject.Inject; +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.hbase.thirdparty.com.google.gson.Gson; +import org.apache.hbase.thirdparty.javax.ws.rs.Produces; +import org.apache.hbase.thirdparty.javax.ws.rs.WebApplicationException; +import org.apache.hbase.thirdparty.javax.ws.rs.core.MediaType; +import org.apache.hbase.thirdparty.javax.ws.rs.core.MultivaluedMap; +import org.apache.hbase.thirdparty.javax.ws.rs.ext.MessageBodyWriter; + +/** + * Implements JSON serialization via {@link Gson} for JAX-RS. + */ +@InterfaceAudience.Private +@Produces(MediaType.APPLICATION_JSON) +public final class GsonMessageBodyWriter implements MessageBodyWriter { + private static final Logger logger = LoggerFactory.getLogger(GsonMessageBodyWriter.class); + + private final Gson gson; + + @Inject + public GsonMessageBodyWriter(Gson gson) { + this.gson = gson; + } + + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, + MediaType mediaType) { + return mediaType == null || MediaType.APPLICATION_JSON_TYPE.isCompatible(mediaType); + } + + @Override + public void writeTo( + T t, + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + OutputStream entityStream + ) throws IOException, WebApplicationException { + final Charset outputCharset = requestedCharset(mediaType); + try (Writer writer = new OutputStreamWriter(entityStream, outputCharset)) { + gson.toJson(t, writer); + } + } + + private static Charset requestedCharset(MediaType mediaType) { + return Optional.ofNullable(mediaType) + .map(MediaType::getParameters) + .map(params -> params.get("charset")) + .map(c -> { + try { + return Charset.forName(c); + } catch (IllegalCharsetNameException e) { + logger.debug("Client requested illegal Charset '{}'", c); + return null; + } catch (UnsupportedCharsetException e) { + logger.debug("Client requested unsupported Charset '{}'", c); + return null; + } catch (Exception e) { + logger.debug("Error while resolving Charset '{}'", c, e); + return null; + } + }) + .orElse(StandardCharsets.UTF_8); + } +} diff --git a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/jersey/ResponseEntityMapper.java b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/jersey/ResponseEntityMapper.java new file mode 100644 index 000000000000..dc3f8a7bf430 --- /dev/null +++ b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/jersey/ResponseEntityMapper.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.hadoop.hbase.http.jersey; + +import java.io.IOException; +import org.apache.yetus.audience.InterfaceAudience; +import org.apache.hbase.thirdparty.com.google.common.collect.ImmutableMap; +import org.apache.hbase.thirdparty.javax.ws.rs.container.ContainerRequestContext; +import org.apache.hbase.thirdparty.javax.ws.rs.container.ContainerResponseContext; +import org.apache.hbase.thirdparty.javax.ws.rs.container.ContainerResponseFilter; +import org.apache.hbase.thirdparty.javax.ws.rs.core.Response.Status; + +/** + * Generate a uniform response wrapper around the Entity returned from the resource. + * @see JSON API Document Structure + * @see JSON API Error Objects + */ +@InterfaceAudience.Private +public class ResponseEntityMapper implements ContainerResponseFilter { + + @Override + public void filter( + ContainerRequestContext requestContext, + ContainerResponseContext responseContext + ) throws IOException { + /* + * Follows very loosely the top-level document specification described in by JSON API. Only + * handles 200 response codes; leaves room for errors and other response types. + */ + + final int statusCode = responseContext.getStatus(); + if (Status.OK.getStatusCode() != statusCode) { + return; + } + + responseContext.setEntity(ImmutableMap.of("data", responseContext.getEntity())); + } +} diff --git a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/jersey/SupplierFactoryAdapter.java b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/jersey/SupplierFactoryAdapter.java new file mode 100644 index 000000000000..57a7e930905b --- /dev/null +++ b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/jersey/SupplierFactoryAdapter.java @@ -0,0 +1,42 @@ +/* + * 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.hbase.http.jersey; + +import java.util.function.Supplier; +import org.apache.yetus.audience.InterfaceAudience; +import org.apache.hbase.thirdparty.org.glassfish.hk2.api.Factory; + +/** + * Use a {@link Supplier} of type {@code T} as a {@link Factory} that provides instances of + * {@code T}. Modeled after Jersey's internal implementation. + */ +@InterfaceAudience.Private +public class SupplierFactoryAdapter implements Factory { + + private final Supplier supplier; + + public SupplierFactoryAdapter(Supplier supplier) { + this.supplier = supplier; + } + + @Override public T provide() { + return supplier.get(); + } + + @Override public void dispose(T instance) { } +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/HMaster.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/HMaster.java index f6382cb8f38c..3d9e149ac395 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/HMaster.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/HMaster.java @@ -131,6 +131,7 @@ import org.apache.hadoop.hbase.master.http.MasterDumpServlet; import org.apache.hadoop.hbase.master.http.MasterRedirectServlet; import org.apache.hadoop.hbase.master.http.MasterStatusServlet; +import org.apache.hadoop.hbase.master.http.api_v1.ResourceConfigFactory; import org.apache.hadoop.hbase.master.janitor.CatalogJanitor; import org.apache.hadoop.hbase.master.locking.LockManager; import org.apache.hadoop.hbase.master.migrate.RollingUpgradeChore; @@ -257,7 +258,8 @@ import org.apache.hbase.thirdparty.org.eclipse.jetty.server.ServerConnector; import org.apache.hbase.thirdparty.org.eclipse.jetty.servlet.ServletHolder; import org.apache.hbase.thirdparty.org.eclipse.jetty.webapp.WebAppContext; - +import org.apache.hbase.thirdparty.org.glassfish.jersey.server.ResourceConfig; +import org.apache.hbase.thirdparty.org.glassfish.jersey.servlet.ServletContainer; import org.apache.hadoop.hbase.shaded.protobuf.RequestConverter; import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.GetRegionInfoResponse; import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotDescription; @@ -281,7 +283,7 @@ public class HMaster extends HBaseServerBase implements Maste private static final Logger LOG = LoggerFactory.getLogger(HMaster.class); // MASTER is name of the webapp and the attribute name used stuffing this - //instance into web context. + // instance into a web context !! AND OTHER PLACES !! public static final String MASTER = "master"; // Manager and zk listener for master election @@ -689,9 +691,16 @@ protected MasterRpcServices createRpcServices() throws IOException { @Override protected void configureInfoServer(InfoServer infoServer) { infoServer.addUnprivilegedServlet("master-status", "/master-status", MasterStatusServlet.class); + infoServer.addUnprivilegedServlet("api_v1", "/api/v1/*", buildApiV1Servlet()); + infoServer.setAttribute(MASTER, this); } + private ServletHolder buildApiV1Servlet() { + final ResourceConfig config = ResourceConfigFactory.createResourceConfig(conf, this); + return new ServletHolder(new ServletContainer(config)); + } + @Override protected Class getDumpServlet() { return MasterDumpServlet.class; diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/http/api_v1/ResourceConfigFactory.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/http/api_v1/ResourceConfigFactory.java new file mode 100644 index 000000000000..97860d8d6d30 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/http/api_v1/ResourceConfigFactory.java @@ -0,0 +1,59 @@ +/* + * 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.hbase.master.http.api_v1; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.http.jersey.ResponseEntityMapper; +import org.apache.hadoop.hbase.master.HMaster; +import org.apache.hadoop.hbase.master.http.gson.GsonSerializationFeature; +import org.apache.hadoop.hbase.master.http.jersey.MasterFeature; +import org.apache.yetus.audience.InterfaceAudience; +import org.apache.hbase.thirdparty.org.glassfish.jersey.server.ResourceConfig; +import org.apache.hbase.thirdparty.org.glassfish.jersey.server.ServerProperties; +import org.apache.hbase.thirdparty.org.glassfish.jersey.server.TracingConfig; + +/** + * Encapsulates construction and configuration of the {@link ResourceConfig} that implements + * the {@code cluster-metrics} endpoints. + */ +@InterfaceAudience.Private +public final class ResourceConfigFactory { + + private ResourceConfigFactory() {} + + public static ResourceConfig createResourceConfig(Configuration conf, HMaster master) { + return new ResourceConfig() + .setApplicationName("api_v1") + .packages(ResourceConfigFactory.class.getPackage().getName()) + // TODO: anything registered here that does not have necessary bindings won't inject properly + // at annotation sites and will result in a WARN logged by o.a.h.t.o.g.j.i.inject.Providers. + // These warnings should be treated by the service as fatal errors, but I have not found a + // callback API for registering a failed binding handler. + .register(ResponseEntityMapper.class) + .register(GsonSerializationFeature.class) + .register(new MasterFeature(master)) + + // devs: enable TRACING to see how jersey is dispatching to resources. + // in hbase-site.xml, set 'hbase.http.jersey.tracing.type=ON_DEMAND` and + // to curl, add `-H X-Jersey-Tracing-Accept:true` + .property(ServerProperties.TRACING, conf.get( + "hbase.http.jersey.tracing.type", TracingConfig.OFF.name())) + .property(ServerProperties.TRACING_THRESHOLD, conf.get( + "hbase.http.jersey.tracing.threshold", "TRACE")); + } +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/http/api_v1/cluster_metrics/model/ClusterMetrics.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/http/api_v1/cluster_metrics/model/ClusterMetrics.java new file mode 100644 index 000000000000..5364194828e7 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/http/api_v1/cluster_metrics/model/ClusterMetrics.java @@ -0,0 +1,70 @@ +/* + * 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.hbase.master.http.api_v1.cluster_metrics.model; + +import java.util.List; +import org.apache.hadoop.hbase.ServerName; +import org.apache.yetus.audience.InterfaceAudience; + +/** + * Exposes a subset of fields from {@link org.apache.hadoop.hbase.ClusterMetrics}. + */ +@InterfaceAudience.Private +public final class ClusterMetrics { + + private final String hbaseVersion; + private final String clusterId; + private final ServerName masterName; + private final List backupMasterNames; + + public static ClusterMetrics from(org.apache.hadoop.hbase.ClusterMetrics clusterMetrics) { + return new ClusterMetrics( + clusterMetrics.getHBaseVersion(), + clusterMetrics.getClusterId(), + clusterMetrics.getMasterName(), + clusterMetrics.getBackupMasterNames()); + } + + private ClusterMetrics( + String hbaseVersion, + String clusterId, + ServerName masterName, + List backupMasterNames + ) { + this.hbaseVersion = hbaseVersion; + this.clusterId = clusterId; + this.masterName = masterName; + this.backupMasterNames = backupMasterNames; + } + + public String getHBaseVersion() { + return hbaseVersion; + } + + public String getClusterId() { + return clusterId; + } + + public ServerName getMasterName() { + return masterName; + } + + public List getBackupMasterNames() { + return backupMasterNames; + } +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/http/api_v1/cluster_metrics/package-info.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/http/api_v1/cluster_metrics/package-info.java new file mode 100644 index 000000000000..5a470d19b027 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/http/api_v1/cluster_metrics/package-info.java @@ -0,0 +1,27 @@ +/* + * 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. + */ + +/** + * Exposes the {@link org.apache.hadoop.hbase.ClusterMetrics} object over HTTP as a REST + * resource hierarchy. Intended for Master + * {@link org.apache.hadoop.hbase.http.InfoServer} consumption only. + */ +@InterfaceAudience.Private +package org.apache.hadoop.hbase.master.http.api_v1.cluster_metrics; + +import org.apache.yetus.audience.InterfaceAudience; diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/http/api_v1/cluster_metrics/resource/ClusterMetricsResource.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/http/api_v1/cluster_metrics/resource/ClusterMetricsResource.java new file mode 100644 index 000000000000..5c3129c22a7d --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/http/api_v1/cluster_metrics/resource/ClusterMetricsResource.java @@ -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. + */ +package org.apache.hadoop.hbase.master.http.api_v1.cluster_metrics.resource; + +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.concurrent.ExecutionException; +import javax.inject.Inject; +import org.apache.hadoop.hbase.ClusterMetrics.Option; +import org.apache.hadoop.hbase.ServerMetrics; +import org.apache.hadoop.hbase.ServerName; +import org.apache.hadoop.hbase.client.AsyncAdmin; +import org.apache.hadoop.hbase.master.MasterServices; +import org.apache.hadoop.hbase.master.http.api_v1.cluster_metrics.model.ClusterMetrics; +import org.apache.yetus.audience.InterfaceAudience; +import org.apache.hbase.thirdparty.javax.ws.rs.GET; +import org.apache.hbase.thirdparty.javax.ws.rs.Path; +import org.apache.hbase.thirdparty.javax.ws.rs.Produces; +import org.apache.hbase.thirdparty.javax.ws.rs.core.MediaType; + +/** + * The root object exposing a subset of {@link org.apache.hadoop.hbase.ClusterMetrics}. + */ +@InterfaceAudience.Private +@Path("cluster_metrics") +@Produces({ MediaType.APPLICATION_JSON }) +public class ClusterMetricsResource { + + // TODO: using the async client API lends itself well to using the JAX-RS 2.0 Spec's asynchronous + // server APIs. However, these are only available when Jersey is wired up using Servlet 3.x + // container and all of our existing InfoServer stuff is build on Servlet 2.x. + // See also https://blog.allegro.tech/2014/10/async-rest.html#mixing-with-completablefuture + + private final AsyncAdmin admin; + + @Inject + public ClusterMetricsResource(MasterServices master) { + this.admin = master.getAsyncConnection().getAdmin(); + } + + private org.apache.hadoop.hbase.ClusterMetrics get(EnumSet