From 2cebe32f748b45233c70465f30059829c64bcbf3 Mon Sep 17 00:00:00 2001 From: Dev Hingu Date: Mon, 22 Jun 2026 16:04:44 +0530 Subject: [PATCH] HBASE-29289: Make hbase master ui show cluster client connections info (#8058) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Dávid Paksy Signed-off-by: Wellington Chevreuil --- .../org/apache/hadoop/hbase/UserMetrics.java | 8 ++ .../hadoop/hbase/UserMetricsBuilder.java | 40 +++++++++- .../hadoop/hbase/TestUserMetricsBuilder.java | 54 ++++++++++++++ .../hbase/regionserver/MetricsUserSource.java | 11 ++- .../regionserver/MetricsUserSourceImpl.java | 36 ++++++++- .../hadoop/hbase/hbtop/field/Field.java | 6 +- .../hbase/hbtop/mode/ClientModeStrategy.java | 8 +- .../apache/hadoop/hbase/hbtop/TestUtils.java | 63 +++++++++++----- .../src/main/protobuf/ClusterStatus.proto | 12 +++ .../apache/hadoop/hbase/ipc/RpcServer.java | 18 +++++ .../apache/hadoop/hbase/ipc/ServerCall.java | 8 ++ .../hadoop/hbase/ipc/ServerRpcConnection.java | 6 +- .../hadoop/hbase/ipc/SimpleServerCall.java | 3 +- .../hbase/regionserver/HRegionServer.java | 6 +- .../MetricsUserAggregateImpl.java | 62 +++++++++++++++- .../hbase-webapps/master/regionServerList.jsp | 6 ++ ...regionServerListClientConnectionsStats.jsp | 74 +++++++++++++++++++ .../master/http/TestMasterStatusPage.java | 9 +++ 18 files changed, 395 insertions(+), 35 deletions(-) create mode 100644 hbase-client/src/test/java/org/apache/hadoop/hbase/TestUserMetricsBuilder.java create mode 100644 hbase-server/src/main/resources/hbase-webapps/master/regionServerListClientConnectionsStats.jsp diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/UserMetrics.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/UserMetrics.java index 681b1f416c78..69095a898dcb 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/UserMetrics.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/UserMetrics.java @@ -38,6 +38,14 @@ interface ClientMetrics { long getWriteRequestsCount(); long getFilteredReadRequestsCount(); + + String getHostAddress(); + + String getUserName(); + + String getClientVersion(); + + String getServiceName(); } /** Returns the user name */ diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/UserMetricsBuilder.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/UserMetricsBuilder.java index 4a66283146d9..9d2b54c6f78c 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/UserMetricsBuilder.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/UserMetricsBuilder.java @@ -36,7 +36,9 @@ public static UserMetrics toUserMetrics(ClusterStatusProtos.UserLoad userLoad) { userLoad.getClientMetricsList().stream() .map(clientMetrics -> new ClientMetricsImpl(clientMetrics.getHostName(), clientMetrics.getReadRequestsCount(), clientMetrics.getWriteRequestsCount(), - clientMetrics.getFilteredRequestsCount())) + clientMetrics.getFilteredRequestsCount(), clientMetrics.getHostAddress(), + clientMetrics.getUserName(), clientMetrics.getServiceName(), + clientMetrics.getClientVersion())) .forEach(builder::addClientMetris); return builder.build(); } @@ -49,7 +51,10 @@ public static ClusterStatusProtos.UserLoad toUserMetrics(UserMetrics userMetrics .setHostName(clientMetrics.getHostName()) .setWriteRequestsCount(clientMetrics.getWriteRequestsCount()) .setReadRequestsCount(clientMetrics.getReadRequestsCount()) - .setFilteredRequestsCount(clientMetrics.getFilteredReadRequestsCount()).build()) + .setFilteredRequestsCount(clientMetrics.getFilteredReadRequestsCount()) + .setHostAddress(clientMetrics.getHostAddress()).setUserName(clientMetrics.getUserName()) + .setServiceName(clientMetrics.getServiceName()) + .setClientVersion(clientMetrics.getClientVersion()).build()) .forEach(builder::addClientMetrics); return builder.build(); } @@ -79,13 +84,22 @@ public static class ClientMetricsImpl implements UserMetrics.ClientMetrics { private final String hostName; private final long readRequestCount; private final long writeRequestCount; + private final String hostAddress; + private final String userName; + private final String serviceName; + private final String clientVersion; public ClientMetricsImpl(String hostName, long readRequest, long writeRequest, - long filteredReadRequestsCount) { + long filteredReadRequestsCount, String hostAddress, String userName, String serviceName, + String clientVersion) { this.hostName = hostName; this.readRequestCount = readRequest; this.writeRequestCount = writeRequest; this.filteredReadRequestsCount = filteredReadRequestsCount; + this.hostAddress = hostAddress != null ? hostAddress : "Unknown"; + this.userName = userName != null ? userName : "Unknown"; + this.serviceName = serviceName != null ? serviceName : "Unknown"; + this.clientVersion = clientVersion != null ? clientVersion : "Unknown"; } @Override @@ -107,6 +121,26 @@ public long getWriteRequestsCount() { public long getFilteredReadRequestsCount() { return filteredReadRequestsCount; } + + @Override + public String getHostAddress() { + return hostAddress; + } + + @Override + public String getUserName() { + return userName; + } + + @Override + public String getServiceName() { + return serviceName; + } + + @Override + public String getClientVersion() { + return clientVersion; + } } private static class UserMetricsImpl implements UserMetrics { diff --git a/hbase-client/src/test/java/org/apache/hadoop/hbase/TestUserMetricsBuilder.java b/hbase-client/src/test/java/org/apache/hadoop/hbase/TestUserMetricsBuilder.java new file mode 100644 index 000000000000..eb56f9a75687 --- /dev/null +++ b/hbase-client/src/test/java/org/apache/hadoop/hbase/TestUserMetricsBuilder.java @@ -0,0 +1,54 @@ +/* + * 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; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.apache.hadoop.hbase.testclassification.ClientTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.apache.hadoop.hbase.util.Bytes; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import org.apache.hadoop.hbase.shaded.protobuf.generated.ClusterStatusProtos; + +@Tag(ClientTests.TAG) +@Tag(SmallTests.TAG) +public class TestUserMetricsBuilder { + + @Test + public void testRoundTripPreservesClientConnectionFields() { + // Build a user metrics snapshot with all client-connection identity fields populated. + UserMetrics userMetrics = UserMetricsBuilder.newBuilder(Bytes.toBytes("alice")) + .addClientMetris(new UserMetricsBuilder.ClientMetricsImpl("clientHost", 11L, 22L, 3L, + "10.1.1.1", "alice", "ClientService", "1.2.3")) + .build(); + + // Convert to protobuf and back; UI/server paths rely on this round-trip preserving fields. + ClusterStatusProtos.UserLoad userLoad = UserMetricsBuilder.toUserMetrics(userMetrics); + UserMetrics converted = UserMetricsBuilder.toUserMetrics(userLoad); + + UserMetrics.ClientMetrics clientMetrics = converted.getClientMetrics().get("clientHost"); + assertNotNull(clientMetrics); + assertEquals("10.1.1.1", clientMetrics.getHostAddress()); + assertEquals("alice", clientMetrics.getUserName()); + assertEquals("ClientService", clientMetrics.getServiceName()); + assertEquals("1.2.3", clientMetrics.getClientVersion()); + } +} diff --git a/hbase-hadoop-compat/src/main/java/org/apache/hadoop/hbase/regionserver/MetricsUserSource.java b/hbase-hadoop-compat/src/main/java/org/apache/hadoop/hbase/regionserver/MetricsUserSource.java index feb173a94afd..da8ee8c39722 100644 --- a/hbase-hadoop-compat/src/main/java/org/apache/hadoop/hbase/regionserver/MetricsUserSource.java +++ b/hbase-hadoop-compat/src/main/java/org/apache/hadoop/hbase/regionserver/MetricsUserSource.java @@ -39,6 +39,14 @@ interface ClientMetrics { void incrementFilteredReadRequests(); long getFilteredReadRequests(); + + String getHostAddress(); + + String getUserName(); + + String getClientVersion(); + + String getServiceName(); } String getUser(); @@ -77,5 +85,6 @@ interface ClientMetrics { * @param hostName hostname of the client * @return Instance of ClientMetrics */ - ClientMetrics getOrCreateMetricsClient(String hostName); + ClientMetrics getOrCreateMetricsClient(String hostName, String hostAddress, String userName, + String clientVersion, String serviceName); } diff --git a/hbase-hadoop2-compat/src/main/java/org/apache/hadoop/hbase/regionserver/MetricsUserSourceImpl.java b/hbase-hadoop2-compat/src/main/java/org/apache/hadoop/hbase/regionserver/MetricsUserSourceImpl.java index 65dfaafe522c..41527c01f99b 100644 --- a/hbase-hadoop2-compat/src/main/java/org/apache/hadoop/hbase/regionserver/MetricsUserSourceImpl.java +++ b/hbase-hadoop2-compat/src/main/java/org/apache/hadoop/hbase/regionserver/MetricsUserSourceImpl.java @@ -69,9 +69,18 @@ static class ClientMetricsImpl implements ClientMetrics { final LongAdder readRequestsCount = new LongAdder(); final LongAdder writeRequestsCount = new LongAdder(); final LongAdder filteredRequestsCount = new LongAdder(); + private final String hostAddress; + private final String userName; + private final String clientVersion; + private final String serviceName; - public ClientMetricsImpl(String hostName) { + public ClientMetricsImpl(String hostName, String hostAddress, String userName, + String clientVersion, String serviceName) { this.hostName = hostName; + this.hostAddress = hostAddress != null ? hostAddress : "Unknown"; + this.userName = userName != null ? userName : "Unknown"; + this.clientVersion = clientVersion != null ? clientVersion : "Unknown"; + this.serviceName = serviceName != null ? serviceName : "Unknown"; } @Override @@ -109,6 +118,26 @@ public void incrementFilteredReadRequests() { public long getFilteredReadRequests() { return filteredRequestsCount.sum(); } + + @Override + public String getHostAddress() { + return hostAddress; + } + + @Override + public String getUserName() { + return userName; + } + + @Override + public String getClientVersion() { + return clientVersion; + } + + @Override + public String getServiceName() { + return serviceName; + } } public MetricsUserSourceImpl(String user, MetricsUserAggregateSourceImpl agg) { @@ -278,12 +307,13 @@ public Map getClientMetrics() { } @Override - public ClientMetrics getOrCreateMetricsClient(String client) { + public ClientMetrics getOrCreateMetricsClient(String client, String hostAddress, String userName, + String clientVersion, String serviceName) { ClientMetrics source = clientMetricsMap.get(client); if (source != null) { return source; } - source = new ClientMetricsImpl(client); + source = new ClientMetricsImpl(client, hostAddress, userName, clientVersion, serviceName); ClientMetrics prev = clientMetricsMap.putIfAbsent(client, source); if (prev != null) { return prev; diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/Field.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/Field.java index ab776cf03368..cf36da8dfc9c 100644 --- a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/Field.java +++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/Field.java @@ -58,7 +58,11 @@ public enum Field { MAX_HEAP_SIZE("MHEAP", "Max Heap Size", false, false, FieldValueType.SIZE), CLIENT_COUNT("#CLIENT", "Client Count", false, false, FieldValueType.INTEGER), USER_COUNT("#USER", "User Count", false, false, FieldValueType.INTEGER), - CLIENT("CLIENT", "Client Hostname", true, true, FieldValueType.STRING); + CLIENT("CLIENT", "Client Hostname", true, true, FieldValueType.STRING), + HOST_ADDRESS("HADDR", "Client Host Address", true, true, FieldValueType.STRING), + CLIENT_VERSION("CVER", "Client Version", true, true, FieldValueType.STRING), + USER_NAME("USER", "User Name", true, true, FieldValueType.STRING), + SERVICE_NAME("SVC", "Service Name", true, true, FieldValueType.STRING); private final String header; private final String description; diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/ClientModeStrategy.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/ClientModeStrategy.java index 8327b1425cfb..075cede17f9b 100644 --- a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/ClientModeStrategy.java +++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/ClientModeStrategy.java @@ -48,7 +48,9 @@ public final class ClientModeStrategy implements ModeStrategy { new FieldInfo(Field.REQUEST_COUNT_PER_SECOND, 10, true), new FieldInfo(Field.READ_REQUEST_COUNT_PER_SECOND, 10, true), new FieldInfo(Field.WRITE_REQUEST_COUNT_PER_SECOND, 10, true), - new FieldInfo(Field.FILTERED_READ_REQUEST_COUNT_PER_SECOND, 10, true)); + new FieldInfo(Field.FILTERED_READ_REQUEST_COUNT_PER_SECOND, 10, true), + new FieldInfo(Field.HOST_ADDRESS, 0, true), new FieldInfo(Field.USER_NAME, 0, true), + new FieldInfo(Field.CLIENT_VERSION, 0, true), new FieldInfo(Field.SERVICE_NAME, 0, true)); private final Map requestCountPerSecondMap = new HashMap<>(); ClientModeStrategy() { @@ -145,6 +147,10 @@ Record createRecord(String user, UserMetrics.ClientMetrics clientMetrics, requestCountPerSecond.getWriteRequestCountPerSecond()); builder.put(Field.FILTERED_READ_REQUEST_COUNT_PER_SECOND, requestCountPerSecond.getFilteredReadRequestCountPerSecond()); + builder.put(Field.HOST_ADDRESS, clientMetrics.getHostAddress()); + builder.put(Field.USER_NAME, clientMetrics.getUserName()); + builder.put(Field.CLIENT_VERSION, clientMetrics.getClientVersion()); + builder.put(Field.SERVICE_NAME, clientMetrics.getServiceName()); builder.put(Field.USER, user); return builder.build(); } diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestUtils.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestUtils.java index 6866337082e2..d9f8ecab5e9f 100644 --- a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestUtils.java +++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestUtils.java @@ -50,14 +50,23 @@ public final class TestUtils { private TestUtils() { } + private static final String TEST_USER_FOO = "FOO"; + private static final String TEST_USER_BAR = "BAR"; + private static final String TEST_HOST_ADDRESS_A = "10.0.0.1"; + private static final String TEST_HOST_ADDRESS_B = "10.0.0.2"; + private static final String TEST_SERVICE_NAME = "ClientService"; + private static final String TEST_CLIENT_VERSION = "test-client-version"; + public static ClusterMetrics createDummyClusterMetrics() { Map serverMetricsMap = new HashMap<>(); // host1 List regionMetricsList = new ArrayList<>(); List userMetricsList = new ArrayList<>(); - userMetricsList.add(createUserMetrics("FOO", 1, 2, 4)); - userMetricsList.add(createUserMetrics("BAR", 2, 3, 3)); + userMetricsList.add(createUserMetrics(TEST_USER_FOO, 1, 2, 4, TEST_HOST_ADDRESS_A, + TEST_HOST_ADDRESS_B, TEST_CLIENT_VERSION, TEST_SERVICE_NAME)); + userMetricsList.add(createUserMetrics(TEST_USER_BAR, 2, 3, 3, TEST_HOST_ADDRESS_A, + TEST_HOST_ADDRESS_B, TEST_CLIENT_VERSION, TEST_SERVICE_NAME)); regionMetricsList.add(createRegionMetrics("table1,,1.00000000000000000000000000000000.", 100, 50, 100, new Size(100, Size.Unit.MEGABYTE), new Size(200, Size.Unit.MEGABYTE), 1, new Size(100, Size.Unit.MEGABYTE), 0.1f, 100, 100, "2019-07-22 00:00:00")); @@ -76,8 +85,10 @@ public static ClusterMetrics createDummyClusterMetrics() { // host2 regionMetricsList.clear(); userMetricsList.clear(); - userMetricsList.add(createUserMetrics("FOO", 5, 7, 3)); - userMetricsList.add(createUserMetrics("BAR", 4, 8, 4)); + userMetricsList.add(createUserMetrics(TEST_USER_FOO, 5, 7, 3, TEST_HOST_ADDRESS_A, + TEST_HOST_ADDRESS_B, TEST_CLIENT_VERSION, TEST_SERVICE_NAME)); + userMetricsList.add(createUserMetrics(TEST_USER_BAR, 4, 8, 4, TEST_HOST_ADDRESS_A, + TEST_HOST_ADDRESS_B, TEST_CLIENT_VERSION, TEST_SERVICE_NAME)); regionMetricsList.add(createRegionMetrics("table1,1,4.00000000000000000000000000000003.", 100, 50, 100, new Size(100, Size.Unit.MEGABYTE), new Size(200, Size.Unit.MEGABYTE), 1, new Size(100, Size.Unit.MEGABYTE), 0.4f, 50, 100, "2019-07-22 00:00:03")); @@ -105,12 +116,15 @@ public static ClusterMetrics createDummyClusterMetrics() { } private static UserMetrics createUserMetrics(String user, long readRequestCount, - long writeRequestCount, long filteredReadRequestsCount) { + long writeRequestCount, long filteredReadRequestsCount, String hostAddressA, + String hostAddressB, String clientVersion, String serviceName) { return UserMetricsBuilder.newBuilder(Bytes.toBytes(user)) .addClientMetris(new UserMetricsBuilder.ClientMetricsImpl("CLIENT_A_" + user, - readRequestCount, writeRequestCount, filteredReadRequestsCount)) + readRequestCount, writeRequestCount, filteredReadRequestsCount, hostAddressA, user, + serviceName, clientVersion)) .addClientMetris(new UserMetricsBuilder.ClientMetricsImpl("CLIENT_B_" + user, - readRequestCount, writeRequestCount, filteredReadRequestsCount)) + readRequestCount, writeRequestCount, filteredReadRequestsCount, hostAddressB, user, + serviceName, clientVersion)) .build(); } @@ -312,10 +326,10 @@ public static void assertRecordsInUserMode(List records) { switch (user) { // readRequestPerSecond and writeRequestPerSecond will be zero // because there is no change or new metrics during refresh - case "FOO": + case TEST_USER_FOO: assertRecordInUserMode(record, 0L, 0L, 0L); break; - case "BAR": + case TEST_USER_BAR: assertRecordInUserMode(record, 0L, 0L, 0L); break; default: @@ -331,17 +345,21 @@ public static void assertRecordsInClientMode(List records) { switch (client) { // readRequestPerSecond and writeRequestPerSecond will be zero // because there is no change or new metrics during refresh - case "CLIENT_A_FOO": - assertRecordInClientMode(record, 0L, 0L, 0L); + case "CLIENT_A_" + TEST_USER_FOO: + assertRecordInClientMode(record, 0L, 0L, 0L, TEST_HOST_ADDRESS_A, TEST_USER_FOO, + TEST_CLIENT_VERSION, TEST_SERVICE_NAME); break; - case "CLIENT_A_BAR": - assertRecordInClientMode(record, 0L, 0L, 0L); + case "CLIENT_A_" + TEST_USER_BAR: + assertRecordInClientMode(record, 0L, 0L, 0L, TEST_HOST_ADDRESS_A, TEST_USER_BAR, + TEST_CLIENT_VERSION, TEST_SERVICE_NAME); break; - case "CLIENT_B_FOO": - assertRecordInClientMode(record, 0L, 0L, 0L); + case "CLIENT_B_" + TEST_USER_FOO: + assertRecordInClientMode(record, 0L, 0L, 0L, TEST_HOST_ADDRESS_B, TEST_USER_FOO, + TEST_CLIENT_VERSION, TEST_SERVICE_NAME); break; - case "CLIENT_B_BAR": - assertRecordInClientMode(record, 0L, 0L, 0L); + case "CLIENT_B_" + TEST_USER_BAR: + assertRecordInClientMode(record, 0L, 0L, 0L, TEST_HOST_ADDRESS_B, TEST_USER_BAR, + TEST_CLIENT_VERSION, TEST_SERVICE_NAME); break; default: fail(); @@ -351,7 +369,7 @@ public static void assertRecordsInClientMode(List records) { private static void assertRecordInUserMode(Record record, long readRequestCountPerSecond, long writeCountRequestPerSecond, long filteredReadRequestsCount) { - assertThat(record.size(), is(6)); + assertThat(record.size(), is(10)); assertThat(record.get(Field.READ_REQUEST_COUNT_PER_SECOND).asLong(), is(readRequestCountPerSecond)); assertThat(record.get(Field.WRITE_REQUEST_COUNT_PER_SECOND).asLong(), @@ -362,14 +380,19 @@ private static void assertRecordInUserMode(Record record, long readRequestCountP } private static void assertRecordInClientMode(Record record, long readRequestCountPerSecond, - long writeCountRequestPerSecond, long filteredReadRequestsCount) { - assertThat(record.size(), is(6)); + long writeCountRequestPerSecond, long filteredReadRequestsCount, String hostAddress, + String userName, String clientVersion, String serviceName) { + assertThat(record.size(), is(10)); assertThat(record.get(Field.READ_REQUEST_COUNT_PER_SECOND).asLong(), is(readRequestCountPerSecond)); assertThat(record.get(Field.WRITE_REQUEST_COUNT_PER_SECOND).asLong(), is(writeCountRequestPerSecond)); assertThat(record.get(Field.FILTERED_READ_REQUEST_COUNT_PER_SECOND).asLong(), is(filteredReadRequestsCount)); + assertThat(record.get(Field.HOST_ADDRESS).asString(), is(hostAddress)); + assertThat(record.get(Field.USER_NAME).asString(), is(userName)); + assertThat(record.get(Field.CLIENT_VERSION).asString(), is(clientVersion)); + assertThat(record.get(Field.SERVICE_NAME).asString(), is(serviceName)); assertThat(record.get(Field.USER_COUNT).asInt(), is(1)); } diff --git a/hbase-protocol-shaded/src/main/protobuf/ClusterStatus.proto b/hbase-protocol-shaded/src/main/protobuf/ClusterStatus.proto index 154bd5c5d458..c54b092aa7a2 100644 --- a/hbase-protocol-shaded/src/main/protobuf/ClusterStatus.proto +++ b/hbase-protocol-shaded/src/main/protobuf/ClusterStatus.proto @@ -212,6 +212,18 @@ message ClientMetrics { /** the current total filtered requests made from a client */ optional uint64 filtered_requests_count = 4; + + /** the IP address of the client */ + optional string host_address = 5; + + /** the name of the user making requests from this client */ + optional string user_name = 6; + + /** the version of the client */ + optional string client_version = 7; + + /** the protobuf RPC service name from the client connection header */ + optional string service_name = 8; } /* Server-level protobufs */ diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/RpcServer.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/RpcServer.java index c00717c69c8c..ab41b503f717 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/RpcServer.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/RpcServer.java @@ -799,6 +799,24 @@ public static Optional getRemoteAddress() { return getCurrentCall().map(RpcCall::getRemoteAddress); } + /** + * Returns the ServerRpcConnection for the current RPC request or not present if no connection is + * available. This allows access to connection-level information such as the client's IP address, + * authentication details, codec configuration, etc. + *

+ * This method should only be called from within an RPC handler thread context. + * @return the current ServerRpcConnection, or Optional.empty() if called outside of an RPC + * context + */ + public static Optional getCurrentServerRpcConnection() { + Optional call = getCurrentCall(); + if (call.isPresent() && call.get() instanceof ServerCall) { + ServerCall serverCall = (ServerCall) call.get(); + return Optional.ofNullable(serverCall.getConnection()); + } + return Optional.empty(); + } + /** * @param serviceName Some arbitrary string that represents a 'service'. * @param services Available service instances diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/ServerCall.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/ServerCall.java index 8702980a10d4..af4041d5380f 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/ServerCall.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/ServerCall.java @@ -256,6 +256,14 @@ public int getPriority() { return this.header.getPriority(); } + /** + * Get the ServerRpcConnection associated with this call. + * @return the connection object + */ + public T getConnection() { + return this.connection; + } + /* * Short string representation without param info because param itself could be huge depends on * the payload of a command diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/ServerRpcConnection.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/ServerRpcConnection.java index 104c5486951a..c01345acfa45 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/ServerRpcConnection.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/ServerRpcConnection.java @@ -96,7 +96,7 @@ @edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "VO_VOLATILE_INCREMENT", justification = "False positive according to http://sourceforge.net/p/findbugs/bugs/1032/") @InterfaceAudience.Private -abstract class ServerRpcConnection implements Closeable { +public abstract class ServerRpcConnection implements Closeable { private static final TextMapGetter getter = new RPCTInfoGetter(); @@ -484,6 +484,10 @@ private void processConnectionHeader(ByteBuff buf) throws IOException { this.hostAddress, this.remotePort, version, this.useSasl, this.ugi, serviceName); } + public ConnectionHeader getConnectionHeader() { + return connectionHeader; + } + /** * Send the response for connection header */ diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/SimpleServerCall.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/SimpleServerCall.java index 5c5e9102115c..ff0ab8067f7b 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/SimpleServerCall.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/SimpleServerCall.java @@ -70,7 +70,8 @@ public synchronized void sendResponseIfReady() throws IOException { this.responder.doRespond(getConnection(), this); } - SimpleServerRpcConnection getConnection() { + @Override + public SimpleServerRpcConnection getConnection() { return this.connection; } } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java index 4bd6e9f67f84..65ac9e333d4e 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java @@ -1981,7 +1981,11 @@ private UserLoad createUserLoad(String user, MetricsUserSource userSource) { .setHostName(clientMetrics.getHostName()) .setWriteRequestsCount(clientMetrics.getWriteRequestsCount()) .setFilteredRequestsCount(clientMetrics.getFilteredReadRequests()) - .setReadRequestsCount(clientMetrics.getReadRequestsCount()).build()) + .setReadRequestsCount(clientMetrics.getReadRequestsCount()) + .setHostAddress(clientMetrics.getHostAddress()).setUserName(clientMetrics.getUserName()) + .setClientVersion(clientMetrics.getClientVersion()) + .setServiceName(clientMetrics.getServiceName()) + .setClientVersion(clientMetrics.getClientVersion()).build()) .forEach(userLoadBldr::addClientMetrics); return userLoadBldr.build(); } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/MetricsUserAggregateImpl.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/MetricsUserAggregateImpl.java index 4856105f4e8f..dc981706710a 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/MetricsUserAggregateImpl.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/MetricsUserAggregateImpl.java @@ -23,11 +23,14 @@ import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.CompatibilitySingletonFactory; import org.apache.hadoop.hbase.ipc.RpcServer; +import org.apache.hadoop.hbase.ipc.ServerRpcConnection; import org.apache.hadoop.hbase.security.User; import org.apache.hadoop.hbase.security.UserProvider; import org.apache.hadoop.hbase.util.LossyCounting; import org.apache.yetus.audience.InterfaceAudience; +import org.apache.hadoop.hbase.shaded.protobuf.generated.RPCProtos; + @InterfaceAudience.Private public class MetricsUserAggregateImpl implements MetricsUserAggregate { @@ -83,23 +86,29 @@ private String getClient() { } private void incrementClientReadMetrics(MetricsUserSource userSource) { + ClientConnectionContext ctx = getClientConnectionContext(); String client = getClient(); if (client != null && userSource != null) { - userSource.getOrCreateMetricsClient(client).incrementReadRequest(); + userSource.getOrCreateMetricsClient(client, ctx.hostAddress, ctx.userName, ctx.clientVersion, + ctx.serviceName).incrementReadRequest(); } } private void incrementFilteredReadRequests(MetricsUserSource userSource) { + ClientConnectionContext ctx = getClientConnectionContext(); String client = getClient(); if (client != null && userSource != null) { - userSource.getOrCreateMetricsClient(client).incrementFilteredReadRequests(); + userSource.getOrCreateMetricsClient(client, ctx.hostAddress, ctx.userName, ctx.clientVersion, + ctx.serviceName).incrementFilteredReadRequests(); } } private void incrementClientWriteMetrics(MetricsUserSource userSource) { + ClientConnectionContext ctx = getClientConnectionContext(); String client = getClient(); if (client != null && userSource != null) { - userSource.getOrCreateMetricsClient(client).incrementWriteRequest(); + userSource.getOrCreateMetricsClient(client, ctx.hostAddress, ctx.userName, ctx.clientVersion, + ctx.serviceName).incrementWriteRequest(); } } @@ -193,4 +202,51 @@ private MetricsUserSource getOrCreateMetricsUser(String user) { userMetricLossyCounting.add(userSource); return userSource; } + + private ClientConnectionContext getClientConnectionContext() { + String hostAddress = null; + String userName = "Unknown"; + String clientVersion = "Unknown"; + String serviceName = "Unknown"; + + Optional rpcConnectionOptional = RpcServer.getCurrentServerRpcConnection(); + if (rpcConnectionOptional.isPresent()) { + ServerRpcConnection rpcConnection = rpcConnectionOptional.get(); + hostAddress = rpcConnection.getHostAddress(); + RPCProtos.ConnectionHeader connectionHeader = rpcConnection.getConnectionHeader(); + if (connectionHeader.hasUserInfo()) { + RPCProtos.UserInformation userInfoProto = connectionHeader.getUserInfo(); + if (userInfoProto.hasEffectiveUser()) { + userName = userInfoProto.getEffectiveUser(); + } + } + if (connectionHeader.hasVersionInfo()) { + clientVersion = connectionHeader.getVersionInfo().getVersion(); + } + if (connectionHeader.hasServiceName()) { + serviceName = connectionHeader.getServiceName(); + } + } + return new ClientConnectionContext(hostAddress, userName, clientVersion, serviceName); + } + + private static class ClientConnectionContext { + final String hostAddress; + final String userName; + final String clientVersion; + final String serviceName; + + ClientConnectionContext(String hostAddress, String userName, String clientVersion, + String serviceName) { + this.hostAddress = hostAddress; + this.userName = userName; + this.clientVersion = clientVersion; + this.serviceName = serviceName; + } + + public String toString() { + return "ClientConnectionContext{hostAddress=" + hostAddress + ", userName=" + userName + + ", clientVersion=" + clientVersion + ", serviceName=" + serviceName + "}"; + } + } } diff --git a/hbase-server/src/main/resources/hbase-webapps/master/regionServerList.jsp b/hbase-server/src/main/resources/hbase-webapps/master/regionServerList.jsp index 9fc0adda00cc..87f61115b054 100644 --- a/hbase-server/src/main/resources/hbase-webapps/master/regionServerList.jsp +++ b/hbase-server/src/main/resources/hbase-webapps/master/regionServerList.jsp @@ -63,6 +63,9 @@

+
<% request.setAttribute("serverNames", serverNames); %> @@ -84,6 +87,9 @@
+
+ +
diff --git a/hbase-server/src/main/resources/hbase-webapps/master/regionServerListClientConnectionsStats.jsp b/hbase-server/src/main/resources/hbase-webapps/master/regionServerListClientConnectionsStats.jsp new file mode 100644 index 000000000000..d559e29e3c2f --- /dev/null +++ b/hbase-server/src/main/resources/hbase-webapps/master/regionServerListClientConnectionsStats.jsp @@ -0,0 +1,74 @@ +<%-- +/** + * 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. + */ +--%> +<%@ page contentType="text/html;charset=UTF-8" + import="org.apache.hadoop.hbase.ServerName" + import="org.apache.hadoop.hbase.master.HMaster" + import="org.apache.hadoop.hbase.ServerMetrics" + import="org.apache.hadoop.hbase.master.ServerManager" + import="org.apache.hadoop.hbase.UserMetrics" + import="java.util.Map" + import="org.apache.commons.lang3.StringEscapeUtils" %> + +<% + HMaster master = (HMaster) getServletContext().getAttribute(HMaster.MASTER); + ServerName[] serverNames = (ServerName[]) request.getAttribute("serverNames"); + ServerManager serverManager = master.getServerManager(); +%> + + + + + + + + + + + + + <% + for (ServerName serverName: serverNames) { + ServerMetrics serverMetrics = serverManager.getLoad(serverName); + if(serverMetrics != null) { + + Map userMetricsMap = serverMetrics.getUserMetrics(); + for(Map.Entry entry : userMetricsMap.entrySet()) { + UserMetrics userMetrics = entry.getValue(); + Map clientMetricsMap = userMetrics.getClientMetrics(); + + for(Map.Entry clientEntry : clientMetricsMap.entrySet()) { + UserMetrics.ClientMetrics clientConnection = clientEntry.getValue(); + + %> + + + + + + + + <% + } + } + } + } + %> + +
ClientIPUserNameClientVersionServiceNameServerInfo
<%= StringEscapeUtils.escapeHtml4(clientConnection.getHostAddress()) %><%= StringEscapeUtils.escapeHtml4(clientConnection.getUserName()) %><%= StringEscapeUtils.escapeHtml4(clientConnection.getClientVersion()) %><%= StringEscapeUtils.escapeHtml4(clientConnection.getServiceName()) %><%= StringEscapeUtils.escapeHtml4(serverName.getServerName()) %>
diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/http/TestMasterStatusPage.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/http/TestMasterStatusPage.java index cc8a4fb448e7..5fe5a29e0536 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/http/TestMasterStatusPage.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/http/TestMasterStatusPage.java @@ -114,6 +114,15 @@ public void testMasterStatusPage() throws Exception { assertTrue(page.contains("

Tasks

")); assertTrue(page.contains("

Software Attributes

")); + assertTrue(page.contains("Client Connections")); + assertTrue(page.contains("tab_clientConnectionsStats")); + assertTrue(page.contains("clientConnectionsStatsTable")); + assertTrue(page.contains("ClientIP")); + assertTrue(page.contains("UserName")); + assertTrue(page.contains("ClientVersion")); + assertTrue(page.contains("ServiceName")); + assertTrue(page.contains("ServerInfo")); + assertTrue(page.contains(VersionInfo.getVersion())); }