This is an automated email from the ASF dual-hosted git repository.

frankchen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new d2f5f697db3 Add labels to sys.server (#18547)
d2f5f697db3 is described below

commit d2f5f697db34cb48dbb484d9599f7bd445e2773b
Author: Gabriel Chang <[email protected]>
AuthorDate: Sat Oct 11 10:21:14 2025 +0800

    Add labels to sys.server (#18547)
---
 docs/configuration/index.md                        |  7 +++
 docs/querying/sql-metadata-tables.md               |  1 +
 .../apache/druid/testsEx/config/Initializer.java   |  2 +-
 .../druid/testing/guice/DruidTestModule.java       |  2 +-
 .../results/auth_test_sys_schema_servers.json      |  6 ++-
 .../java/util/common/jackson/JacksonUtils.java     | 12 +++++
 .../java/org/apache/druid/server/DruidNode.java    | 32 +++++++++---
 .../druid/discovery/DiscoveryDruidNodeTest.java    | 12 +++--
 .../org/apache/druid/server/DruidNodeTest.java     | 40 ++++++++++-----
 .../druid/sql/calcite/schema/SystemSchema.java     | 25 ++++++---
 .../druid/sql/calcite/schema/SystemSchemaTest.java | 60 ++++++++++++++--------
 web-console/src/react-table/react-table-extra.scss |  6 +++
 .../__snapshots__/services-view.spec.tsx.snap      | 13 ++++-
 .../src/views/services-view/services-view.scss     | 10 ++++
 .../src/views/services-view/services-view.tsx      | 31 ++++++++++-
 15 files changed, 200 insertions(+), 59 deletions(-)

diff --git a/docs/configuration/index.md b/docs/configuration/index.md
index 55be9754b0d..8aa5e818468 100644
--- a/docs/configuration/index.md
+++ b/docs/configuration/index.md
@@ -741,6 +741,7 @@ These Coordinator static configurations can be defined in 
the `coordinator/runti
 |`druid.plaintextPort`|This is the port to actually listen on; unless port 
mapping is used, this will be the same port as is on `druid.host`|8081|
 |`druid.tlsPort`|TLS port for HTTPS connector, if 
[druid.enableTlsPort](../operations/tls-support.md) is set then this config 
will be used. If `druid.host` contains port then that port will be ignored. 
This should be a non-negative integer.|8281|
 |`druid.service`|The name of the service. This is used as a dimension when 
emitting metrics and alerts to differentiate between the various 
services.|`druid/coordinator`|
+|`druid.labels`|Optional JSON object of key-value pairs that define custom 
labels for the server. These labels are displayed in the web console under the 
"Services" tab. Example: `druid.labels={"location":"Airtrunk"}` or 
`druid.labels.location=Airtrunk`|`null`|
 
 ##### Coordinator operation
 
@@ -984,6 +985,7 @@ These Overlord static configurations can be defined in the 
`overlord/runtime.pro
 |`druid.plaintextPort`|This is the port to actually listen on; unless port 
mapping is used, this will be the same port as is on `druid.host`.|8090|
 |`druid.tlsPort`|TLS port for HTTPS connector, if 
[druid.enableTlsPort](../operations/tls-support.md) is set then this config 
will be used. If `druid.host` contains port then that port will be ignored. 
This should be a non-negative Integer.|8290|
 |`druid.service`|The name of the service. This is used as a dimension when 
emitting metrics and alerts to differentiate between the various 
services.|`druid/overlord`|
+|`druid.labels`|Optional JSON object of key-value pairs that define custom 
labels for the server. These labels are displayed in the web console under the 
"Services" tab. Example: `druid.labels={"location":"Airtrunk"}` or 
`druid.labels.location=Airtrunk`|`null`|
 
 ##### Overlord operations
 
@@ -1335,6 +1337,7 @@ These Middle Manager and Peon configurations can be 
defined in the `middleManage
 |`druid.plaintextPort`|This is the port to actually listen on; unless port 
mapping is used, this will be the same port as is on `druid.host`|8091|
 |`druid.tlsPort`|TLS port for HTTPS connector, if 
[druid.enableTlsPort](../operations/tls-support.md) is set then this config 
will be used. If `druid.host` contains port then that port will be ignored. 
This should be a non-negative Integer.|8291|
 |`druid.service`|The name of the service. This is used as a dimension when 
emitting metrics and alerts to differentiate between the various 
services|`druid/middlemanager`|
+|`druid.labels`|Optional JSON object of key-value pairs that define custom 
labels for the server. These labels are displayed in the web console under the 
"Services" tab. Example: `druid.labels={"location":"Airtrunk"}` or 
`druid.labels.location=Airtrunk`|`null`|
 
 #### Middle Manager configuration
 
@@ -1463,6 +1466,7 @@ For most types of tasks, `SegmentWriteOutMediumFactory` 
can be configured per-ta
 |`druid.plaintextPort`|This is the port to actually listen on; unless port 
mapping is used, this will be the same port as is on `druid.host`|8091|
 |`druid.tlsPort`|TLS port for HTTPS connector, if 
[druid.enableTlsPort](../operations/tls-support.md) is set then this config 
will be used. If `druid.host` contains port then that port will be ignored. 
This should be a non-negative Integer.|8283|
 |`druid.service`|The name of the service. This is used as a dimension when 
emitting metrics and alerts to differentiate between the various 
services|`druid/indexer`|
+|`druid.labels`|Optional JSON object of key-value pairs that define custom 
labels for the server. These labels are displayed in the web console under the 
"Services" tab. Example: `druid.labels={"location":"Airtrunk"}` or 
`druid.labels.location=Airtrunk`|`null`|
 
 #### Indexer general configuration
 
@@ -1559,6 +1563,7 @@ These Historical configurations can be defined in the 
`historical/runtime.proper
 |`druid.plaintextPort`|This is the port to actually listen on; unless port 
mapping is used, this will be the same port as is on `druid.host`|8083|
 |`druid.tlsPort`|TLS port for HTTPS connector, if 
[druid.enableTlsPort](../operations/tls-support.md) is set then this config 
will be used. If `druid.host` contains port then that port will be ignored. 
This should be a non-negative Integer.|8283|
 |`druid.service`|The name of the service. This is used as a dimension when 
emitting metrics and alerts to differentiate between the various 
services|`druid/historical`|
+|`druid.labels`|Optional JSON object of key-value pairs that define custom 
labels for the server. These labels are displayed in the web console under the 
"Services" tab. Example: `druid.labels={"location":"Airtrunk"}` or 
`druid.labels.location=Airtrunk`|`null`|
 
 #### Historical general configuration
 
@@ -1672,6 +1677,7 @@ These Broker configurations can be defined in the 
`broker/runtime.properties` fi
 |`druid.plaintextPort`|This is the port to actually listen on; unless port 
mapping is used, this will be the same port as is on `druid.host`|8082|
 |`druid.tlsPort`|TLS port for HTTPS connector, if 
[druid.enableTlsPort](../operations/tls-support.md) is set then this config 
will be used. If `druid.host` contains port then that port will be ignored. 
This should be a non-negative Integer.|8282|
 |`druid.service`|The name of the service. This is used as a dimension when 
emitting metrics and alerts to differentiate between the various 
services|`druid/broker`|
+|`druid.labels`|Optional JSON object of key-value pairs that define custom 
labels for the server. These labels are displayed in the web console under the 
"Services" tab. Example: `druid.labels={"location":"Airtrunk"}` or 
`druid.labels.location=Airtrunk`|`null`|
 
 #### Query configuration
 
@@ -2291,6 +2297,7 @@ Supported query contexts:
 |`druid.plaintextPort`|This is the port to actually listen on; unless port 
mapping is used, this will be the same port as is on `druid.host`|8888|
 |`druid.tlsPort`|TLS port for HTTPS connector, if 
[druid.enableTlsPort](../operations/tls-support.md) is set then this config 
will be used. If `druid.host` contains port then that port will be ignored. 
This should be a non-negative Integer.|9088|
 |`druid.service`|The name of the service. This is used as a dimension when 
emitting metrics and alerts to differentiate between the various 
services|`druid/router`|
+|`druid.labels`|Optional JSON object of key-value pairs that define custom 
labels for the server. These labels are displayed in the web console under the 
"Services" tab. Example: `druid.labels={"location":"Airtrunk"}` or 
`druid.labels.location=Airtrunk`|`null`|
 
 #### Runtime configuration
 
diff --git a/docs/querying/sql-metadata-tables.md 
b/docs/querying/sql-metadata-tables.md
index aa1804e29d5..6d59462fd1e 100644
--- a/docs/querying/sql-metadata-tables.md
+++ b/docs/querying/sql-metadata-tables.md
@@ -237,6 +237,7 @@ Servers table lists all discovered servers in the cluster.
 |is_leader|BIGINT|1 if the server is currently the 'leader' (for services 
which have the concept of leadership), otherwise 0 if the server is not the 
leader, or null if the server type does not have the concept of leadership|
 |start_time|STRING|Timestamp in ISO8601 format when the server was announced 
in the cluster|
 |version|VARCHAR|Druid version running on the server|
+|labels|VARCHAR|Labels for the server configured using the property 
[`druid.labels`](../configuration/index.md)|
 To retrieve information about all servers, use the query:
 
 ```sql
diff --git 
a/integration-tests-ex/cases/src/test/java/org/apache/druid/testsEx/config/Initializer.java
 
b/integration-tests-ex/cases/src/test/java/org/apache/druid/testsEx/config/Initializer.java
index b09c18e7264..d350e5175f4 100644
--- 
a/integration-tests-ex/cases/src/test/java/org/apache/druid/testsEx/config/Initializer.java
+++ 
b/integration-tests-ex/cases/src/test/java/org/apache/druid/testsEx/config/Initializer.java
@@ -148,7 +148,7 @@ public class Initializer
           .in(LazySingleton.class);
 
       // Dummy DruidNode instance to make Guice happy. This instance is unused.
-      DruidNode dummy = new DruidNode("integration-tests", "localhost", false, 
9191, null, null, true, false);
+      DruidNode dummy = new DruidNode("integration-tests", "localhost", false, 
9191, null, null, true, false, null);
       binder
           .bind(DruidNode.class)
           .annotatedWith(Self.class)
diff --git 
a/integration-tests/src/main/java/org/apache/druid/testing/guice/DruidTestModule.java
 
b/integration-tests/src/main/java/org/apache/druid/testing/guice/DruidTestModule.java
index ff381439d1e..3ac78bb9c35 100644
--- 
a/integration-tests/src/main/java/org/apache/druid/testing/guice/DruidTestModule.java
+++ 
b/integration-tests/src/main/java/org/apache/druid/testing/guice/DruidTestModule.java
@@ -56,7 +56,7 @@ public class DruidTestModule implements Module
 
     // Bind DruidNode instance to make Guice happy. This instance is currently 
unused.
     binder.bind(DruidNode.class).annotatedWith(Self.class).toInstance(
-        new DruidNode("integration-tests", "localhost", false, 9191, null, 
null, true, false)
+        new DruidNode("integration-tests", "localhost", false, 9191, null, 
null, true, false, null)
     );
 
     // Required for MSQIndexingModule
diff --git 
a/integration-tests/src/test/resources/results/auth_test_sys_schema_servers.json
 
b/integration-tests/src/test/resources/results/auth_test_sys_schema_servers.json
index 520fde8ff56..ddbd92e3111 100644
--- 
a/integration-tests/src/test/resources/results/auth_test_sys_schema_servers.json
+++ 
b/integration-tests/src/test/resources/results/auth_test_sys_schema_servers.json
@@ -10,7 +10,8 @@
     "max_size": 5000000000,
     "is_leader": null,
     "start_time": "0",
-    "version": "0.0.0"
+    "version": "0.0.0",
+    "labels": null
   },
   {
     "server": "%%BROKER%%:8282",
@@ -23,6 +24,7 @@
     "max_size": 1000000000,
     "is_leader": null,
     "start_time": "0",
-    "version": "0.0.0"
+    "version": "0.0.0",
+    "labels": null
   }
 ]
diff --git 
a/processing/src/main/java/org/apache/druid/java/util/common/jackson/JacksonUtils.java
 
b/processing/src/main/java/org/apache/druid/java/util/common/jackson/JacksonUtils.java
index ed6567bf931..3aebd02a9bb 100644
--- 
a/processing/src/main/java/org/apache/druid/java/util/common/jackson/JacksonUtils.java
+++ 
b/processing/src/main/java/org/apache/druid/java/util/common/jackson/JacksonUtils.java
@@ -30,6 +30,8 @@ import com.fasterxml.jackson.databind.JsonMappingException;
 import com.fasterxml.jackson.databind.JsonSerializer;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.SerializerProvider;
+import org.apache.druid.error.DruidException;
+import org.apache.druid.error.InternalServerError;
 import org.apache.druid.java.util.common.ISE;
 
 import javax.annotation.Nullable;
@@ -119,6 +121,16 @@ public final class JacksonUtils
     }
   }
 
+  public static String writeValueAsString(ObjectMapper jsonMapper, Object 
value) throws DruidException
+  {
+    try {
+      return jsonMapper.writeValueAsString(value);
+    }
+    catch (JsonProcessingException e) {
+      throw InternalServerError.exception(e, "Failed to serialize object as 
JSON");
+    }
+  }
+
   /**
    * Reads an object using the {@link JsonParser}. It reuses the provided 
{@link DeserializationContext} which offers
    * better performance that calling {@link JsonParser#readValueAs(Class)} 
because it avoids re-creating the {@link DeserializationContext}
diff --git a/server/src/main/java/org/apache/druid/server/DruidNode.java 
b/server/src/main/java/org/apache/druid/server/DruidNode.java
index 0e3b3e4b4d0..b6032bd0a1d 100644
--- a/server/src/main/java/org/apache/druid/server/DruidNode.java
+++ b/server/src/main/java/org/apache/druid/server/DruidNode.java
@@ -30,13 +30,14 @@ import org.apache.druid.common.utils.SocketUtil;
 import org.apache.druid.java.util.common.IAE;
 import org.apache.druid.java.util.common.ISE;
 
+import javax.annotation.Nullable;
 import javax.validation.constraints.Max;
 import javax.validation.constraints.NotNull;
-
 import java.net.InetAddress;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.UnknownHostException;
+import java.util.Map;
 import java.util.Objects;
 
 /**
@@ -91,6 +92,9 @@ public class DruidNode
       UNKNOWN_VERSION
   );
 
+  @JsonProperty
+  private Map<String, String> labels;
+
   public DruidNode(
       String serviceName,
       String host,
@@ -101,7 +105,7 @@ public class DruidNode
       boolean enableTlsPort
   )
   {
-    this(serviceName, host, bindOnHost, plaintextPort, null, tlsPort, 
enablePlaintextPort, enableTlsPort);
+    this(serviceName, host, bindOnHost, plaintextPort, null, tlsPort, 
enablePlaintextPort, enableTlsPort, null);
   }
 
   /**
@@ -129,7 +133,8 @@ public class DruidNode
       @JacksonInject @Named("servicePort") @JsonProperty("port") Integer port,
       @JacksonInject @Named("tlsServicePort") @JsonProperty("tlsPort") Integer 
tlsPort,
       @JsonProperty("enablePlaintextPort") Boolean enablePlaintextPort,
-      @JsonProperty("enableTlsPort") boolean enableTlsPort
+      @JsonProperty("enableTlsPort") boolean enableTlsPort,
+      @JsonProperty("labels") @Nullable Map<String, String> labels
   )
   {
     init(
@@ -138,8 +143,9 @@ public class DruidNode
         bindOnHost,
         plaintextPort != null ? plaintextPort : port,
         tlsPort,
-        enablePlaintextPort == null ? true : 
enablePlaintextPort.booleanValue(),
-        enableTlsPort
+        enablePlaintextPort == null || enablePlaintextPort.booleanValue(),
+        enableTlsPort,
+        labels
     );
   }
 
@@ -150,7 +156,8 @@ public class DruidNode
       Integer plainTextPort,
       Integer tlsPort,
       boolean enablePlaintextPort,
-      boolean enableTlsPort
+      boolean enableTlsPort,
+      Map<String, String> labels
   )
   {
     Preconditions.checkNotNull(serviceName);
@@ -210,6 +217,13 @@ public class DruidNode
     this.serviceName = serviceName;
     this.host = host;
     this.bindOnHost = bindOnHost;
+    this.labels = labels;
+  }
+
+  @Nullable
+  public Map<String, String> getLabels()
+  {
+    return labels;
   }
 
   public String getServiceName()
@@ -336,13 +350,14 @@ public class DruidNode
            tlsPort == druidNode.tlsPort &&
            enableTlsPort == druidNode.enableTlsPort &&
            Objects.equals(serviceName, druidNode.serviceName) &&
-           Objects.equals(host, druidNode.host);
+           Objects.equals(host, druidNode.host) &&
+           Objects.equals(labels, druidNode.labels);
   }
 
   @Override
   public int hashCode()
   {
-    return Objects.hash(serviceName, host, port, plaintextPort, 
enablePlaintextPort, tlsPort, enableTlsPort);
+    return Objects.hash(serviceName, host, port, plaintextPort, 
enablePlaintextPort, tlsPort, enableTlsPort, labels);
   }
 
   @Override
@@ -357,6 +372,7 @@ public class DruidNode
            ", enablePlaintextPort=" + enablePlaintextPort +
            ", tlsPort=" + tlsPort +
            ", enableTlsPort=" + enableTlsPort +
+           ", labels=" + labels +
            '}';
   }
 }
diff --git 
a/server/src/test/java/org/apache/druid/discovery/DiscoveryDruidNodeTest.java 
b/server/src/test/java/org/apache/druid/discovery/DiscoveryDruidNodeTest.java
index 0e55e5b68eb..b9c99a4fcb7 100644
--- 
a/server/src/test/java/org/apache/druid/discovery/DiscoveryDruidNodeTest.java
+++ 
b/server/src/test/java/org/apache/druid/discovery/DiscoveryDruidNodeTest.java
@@ -112,7 +112,8 @@ public class DiscoveryDruidNodeTest
             -1,
             8282,
             true,
-            true
+            true,
+            null
         ),
         NodeRole.BROKER,
         ImmutableMap.of(
@@ -167,7 +168,8 @@ public class DiscoveryDruidNodeTest
                 -1,
                 8282,
                 true,
-                true
+                true,
+                null
             ),
             NodeRole.BROKER,
             ImmutableMap.of(
@@ -216,7 +218,8 @@ public class DiscoveryDruidNodeTest
                 -1,
                 8282,
                 true,
-                true
+                true,
+                null
             ),
             NodeRole.BROKER,
             ImmutableMap.of(
@@ -266,7 +269,8 @@ public class DiscoveryDruidNodeTest
                 -1,
                 8282,
                 true,
-                true
+                true,
+                null
             ),
             NodeRole.BROKER,
             ImmutableMap.of()
diff --git a/server/src/test/java/org/apache/druid/server/DruidNodeTest.java 
b/server/src/test/java/org/apache/druid/server/DruidNodeTest.java
index 77edaaad137..9b7762904d1 100644
--- a/server/src/test/java/org/apache/druid/server/DruidNodeTest.java
+++ b/server/src/test/java/org/apache/druid/server/DruidNodeTest.java
@@ -21,11 +21,14 @@ package org.apache.druid.server;
 
 import com.fasterxml.jackson.databind.InjectableValues;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.net.HostAndPort;
 import org.apache.druid.jackson.DefaultObjectMapper;
 import org.junit.Assert;
 import org.junit.Test;
 
+import java.util.Map;
+
 public class DruidNodeTest
 {
   private final ObjectMapper mapper;
@@ -161,20 +164,23 @@ public class DruidNodeTest
     Assert.assertEquals(-1, node.getPlaintextPort());
     Assert.assertEquals(123, node.getTlsPort());
 
-    node = new DruidNode("test", "host", false, -1, 123, false, true);
+    node = new DruidNode("test", "host", false, -1, null, 123, false, true, 
ImmutableMap.of("labelKey1", "labelValue1"));
     Assert.assertEquals("host", node.getHost());
     Assert.assertEquals(-1, node.getPlaintextPort());
     Assert.assertEquals(123, node.getTlsPort());
+    Assert.assertEquals(ImmutableMap.of("labelKey1", "labelValue1"), 
node.getLabels());
 
-    node = new DruidNode("test", "host", false, -1, 123, true, false);
+    node = new DruidNode("test", "host", false, -1, null, 123, true, false, 
ImmutableMap.of("labelKey1", "labelValue1", "labelKey2", "labelValue2"));
     Assert.assertEquals("host", node.getHost());
     Assert.assertEquals(-1, node.getPlaintextPort());
     Assert.assertEquals(-1, node.getTlsPort());
+    Assert.assertEquals(ImmutableMap.of("labelKey1", "labelValue1", 
"labelKey2", "labelValue2"), node.getLabels());
 
     node = new DruidNode("test", "host:123", false, 123, null, true, false);
     Assert.assertEquals("host", node.getHost());
     Assert.assertEquals(123, node.getPlaintextPort());
     Assert.assertEquals(-1, node.getTlsPort());
+    Assert.assertNull(node.getLabels());
 
     node = new DruidNode("test", "host:123", false, null, 123, true, false);
     Assert.assertEquals("host", node.getHost());
@@ -260,7 +266,9 @@ public class DruidNodeTest
     final String serviceName = "serviceName";
     final String host = "some.host";
     final int port = 9898;
-    Assert.assertEquals(new DruidNode(serviceName, host, false, port, null, 
true, false), new DruidNode(serviceName, host, false, port, null, true, false));
+    final Map<String, String> labels = ImmutableMap.of("key1", "value1");
+    Assert.assertEquals(new DruidNode(serviceName, host, false, port, null, 
null, true, false, labels), new DruidNode(serviceName, host, false, port, null, 
null, true, false, labels));
+    Assert.assertEquals(new DruidNode(serviceName, host, false, port, null, 
null, true, false, labels), new DruidNode(serviceName, host, false, port, null, 
null, true, false, ImmutableMap.of("key1", "value1")));
     Assert.assertNotEquals(new DruidNode(serviceName, host, false, port, null, 
true, false), new DruidNode(serviceName, host, false, -1, null, true, false));
     Assert.assertNotEquals(new DruidNode(serviceName, host, false, port, null, 
true, false), new DruidNode(serviceName, "other.host", false, port, null, true, 
false));
     Assert.assertNotEquals(new DruidNode(serviceName, host, false, port, null, 
true, false), new DruidNode("otherServiceName", host, false, port, null, true, 
false));
@@ -273,7 +281,11 @@ public class DruidNodeTest
     final String serviceName = "serviceName";
     final String host = "some.host";
     final int port = 9898;
-    Assert.assertEquals(new DruidNode(serviceName, host, false, port, null, 
true, false).hashCode(), new DruidNode(serviceName, host, false, port, null, 
true, false).hashCode());
+    final Map<String, String> labels = ImmutableMap.of("key1", "value1");
+    Assert.assertEquals(
+        new DruidNode(serviceName, host, false, port, null, null, true, false, 
labels).hashCode(),
+        new DruidNode(serviceName, host, false, port, null, null, true, false, 
labels).hashCode()
+    );
     // Potential hash collision if hashCode method ever changes
     Assert.assertNotEquals(new DruidNode(serviceName, host, false, port, null, 
true, false).hashCode(), new DruidNode(serviceName, host, false, -1, null, 
true, false).hashCode());
     Assert.assertNotEquals(new DruidNode(serviceName, host, false, port, null, 
true, false).hashCode(), new DruidNode(serviceName, "other.host", false, port, 
null, true, false).hashCode());
@@ -285,7 +297,7 @@ public class DruidNodeTest
   public void testSerde1() throws Exception
   {
     DruidNode actual = mapper.readValue(
-        mapper.writeValueAsString(new DruidNode("service", "host", true, 1234, 
null, 5678, true, true)),
+        mapper.writeValueAsString(new DruidNode("service", "host", true, 1234, 
null, 5678, true, true, ImmutableMap.of("key1", "value1"))),
         DruidNode.class
     );
     Assert.assertEquals("service", actual.getServiceName());
@@ -295,13 +307,14 @@ public class DruidNodeTest
     Assert.assertTrue(actual.isEnableTlsPort());
     Assert.assertEquals(1234, actual.getPlaintextPort());
     Assert.assertEquals(5678, actual.getTlsPort());
+    Assert.assertEquals(ImmutableMap.of("key1", "value1"), actual.getLabels());
   }
 
   @Test
   public void testSerde2() throws Exception
   {
     DruidNode actual = mapper.readValue(
-        mapper.writeValueAsString(new DruidNode("service", "host", false, 
1234, null, 5678, null, false)),
+        mapper.writeValueAsString(new DruidNode("service", "host", false, 
1234, null, 5678, null, false, null)),
         DruidNode.class
     );
     Assert.assertEquals("service", actual.getServiceName());
@@ -311,13 +324,14 @@ public class DruidNodeTest
     Assert.assertFalse(actual.isEnableTlsPort());
     Assert.assertEquals(1234, actual.getPlaintextPort());
     Assert.assertEquals(-1, actual.getTlsPort());
+    Assert.assertNull(actual.getLabels());
   }
 
   @Test
   public void testSerde3() throws Exception
   {
     DruidNode actual = mapper.readValue(
-        mapper.writeValueAsString(new DruidNode("service", "host", true, 1234, 
null, 5678, false, true)),
+        mapper.writeValueAsString(new DruidNode("service", "host", true, 1234, 
null, 5678, false, true, ImmutableMap.of("key1", "value1", "key2", "value2"))),
         DruidNode.class
     );
     Assert.assertEquals("service", actual.getServiceName());
@@ -327,6 +341,7 @@ public class DruidNodeTest
     Assert.assertTrue(actual.isEnableTlsPort());
     Assert.assertEquals(-1, actual.getPlaintextPort());
     Assert.assertEquals(5678, actual.getTlsPort());
+    Assert.assertEquals(ImmutableMap.of("key1", "value1", "key2", "value2"), 
actual.getLabels());
   }
 
   @Test
@@ -339,12 +354,13 @@ public class DruidNodeTest
                   + "  \"plaintextPort\":1234,\n"
                   + "  \"tlsPort\":5678,\n"
                   + "  \"enablePlaintextPort\":true,\n"
-                  + "  \"enableTlsPort\":true\n"
+                  + "  \"enableTlsPort\":true,\n"
+                  + "  \"labels\":{\"key1\":\"value1\"}"
                   + "}\n";
 
 
     DruidNode actual = mapper.readValue(json, DruidNode.class);
-    Assert.assertEquals(new DruidNode("service", "host", true, 1234, null, 
5678, true, true), actual);
+    Assert.assertEquals(new DruidNode("service", "host", true, 1234, null, 
5678, true, true, ImmutableMap.of("key1", "value1")), actual);
 
     Assert.assertEquals("https", actual.getServiceScheme());
     Assert.assertEquals("host:1234", actual.getHostAndPort());
@@ -365,7 +381,7 @@ public class DruidNodeTest
 
 
     DruidNode actual = mapper.readValue(json, DruidNode.class);
-    Assert.assertEquals(new DruidNode("service", "host", false, 1234, null, 
5678, true, false), actual);
+    Assert.assertEquals(new DruidNode("service", "host", false, 1234, null, 
5678, true, false, null), actual);
 
     Assert.assertEquals("http", actual.getServiceScheme());
     Assert.assertEquals("host:1234", actual.getHostAndPort());
@@ -385,7 +401,7 @@ public class DruidNodeTest
 
 
     DruidNode actual = mapper.readValue(json, DruidNode.class);
-    Assert.assertEquals(new DruidNode("service", "host", false, 1234, null, 
5678, null, false), actual);
+    Assert.assertEquals(new DruidNode("service", "host", false, 1234, null, 
5678, null, false, null), actual);
 
     Assert.assertEquals("http", actual.getServiceScheme());
     Assert.assertEquals("host:1234", actual.getHostAndPort());
@@ -405,7 +421,7 @@ public class DruidNodeTest
 
 
     DruidNode actual = mapper.readValue(json, DruidNode.class);
-    Assert.assertEquals(new DruidNode("service", "host", false, null, 1234, 
5678, null, false), actual);
+    Assert.assertEquals(new DruidNode("service", "host", false, null, 1234, 
5678, null, false, null), actual);
 
     Assert.assertEquals("http", actual.getServiceScheme());
     Assert.assertEquals("host:1234", actual.getHostAndPort());
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java 
b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java
index 932404f21e7..1343ace28fe 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java
@@ -57,6 +57,7 @@ import org.apache.druid.indexer.TaskStatusPlus;
 import org.apache.druid.indexing.overlord.supervisor.SupervisorStatus;
 import org.apache.druid.java.util.common.ISE;
 import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.java.util.common.jackson.JacksonUtils;
 import org.apache.druid.java.util.common.parsers.CloseableIterator;
 import org.apache.druid.rpc.indexing.OverlordClient;
 import org.apache.druid.segment.column.ColumnType;
@@ -181,6 +182,7 @@ public class SystemSchema extends AbstractSchema
       .add("is_leader", ColumnType.LONG)
       .add("start_time", ColumnType.STRING)
       .add("version", ColumnType.STRING)
+      .add("labels", ColumnType.STRING)
       .build();
 
   static final RowSignature SERVER_SEGMENTS_SIGNATURE = RowSignature
@@ -245,7 +247,8 @@ public class SystemSchema extends AbstractSchema
             serverInventoryView,
             authorizerMapper,
             overlordClient,
-            coordinatorClient
+            coordinatorClient,
+            jsonMapper
         ),
         SERVER_SEGMENTS_TABLE,
         new ServerSegmentsTable(serverView, authorizerMapper),
@@ -526,13 +529,15 @@ public class SystemSchema extends AbstractSchema
     private final FilteredServerInventoryView serverInventoryView;
     private final OverlordClient overlordClient;
     private final CoordinatorClient coordinatorClient;
+    private final ObjectMapper jsonMapper;
 
     public ServersTable(
         DruidNodeDiscoveryProvider druidNodeDiscoveryProvider,
         FilteredServerInventoryView serverInventoryView,
         AuthorizerMapper authorizerMapper,
         OverlordClient overlordClient,
-        CoordinatorClient coordinatorClient
+        CoordinatorClient coordinatorClient,
+        ObjectMapper jsonMapper
     )
     {
       this.authorizerMapper = authorizerMapper;
@@ -540,6 +545,7 @@ public class SystemSchema extends AbstractSchema
       this.serverInventoryView = serverInventoryView;
       this.overlordClient = overlordClient;
       this.coordinatorClient = coordinatorClient;
+      this.jsonMapper = jsonMapper;
     }
 
     @Override
@@ -627,7 +633,7 @@ public class SystemSchema extends AbstractSchema
     /**
      * Returns a row for all node types which don't serve data. The returned 
row contains only static information.
      */
-    private static Object[] buildRowForNonDataServer(DiscoveryDruidNode 
discoveryDruidNode)
+    private Object[] buildRowForNonDataServer(DiscoveryDruidNode 
discoveryDruidNode)
     {
       final DruidNode node = discoveryDruidNode.getDruidNode();
       return new Object[]{
@@ -641,14 +647,15 @@ public class SystemSchema extends AbstractSchema
           UNKNOWN_SIZE,
           null,
           toStringOrNull(discoveryDruidNode.getStartTime()),
-          node.getVersion()
+          node.getVersion(),
+          node.getLabels() == null ? null : 
JacksonUtils.writeValueAsString(jsonMapper, node.getLabels())
       };
     }
 
     /**
      * Returns a row for all node types which don't serve data. The returned 
row contains only static information.
      */
-    private static Object[] buildRowForNonDataServerWithLeadership(
+    private Object[] buildRowForNonDataServerWithLeadership(
         DiscoveryDruidNode discoveryDruidNode,
         boolean isLeader
     )
@@ -665,7 +672,8 @@ public class SystemSchema extends AbstractSchema
           UNKNOWN_SIZE,
           isLeader ? 1L : 0L,
           toStringOrNull(discoveryDruidNode.getStartTime()),
-          node.getVersion()
+          node.getVersion(),
+          node.getLabels() == null ? null : 
JacksonUtils.writeValueAsString(jsonMapper, node.getLabels())
       };
     }
 
@@ -674,7 +682,7 @@ public class SystemSchema extends AbstractSchema
      * {@code serverFromInventoryView} if available which is the current state 
of the server. Otherwise, it
      * will get the information from {@code discoveryDruidNode} which has only 
static configurations.
      */
-    private static Object[] buildRowForDiscoverableDataServer(
+    private Object[] buildRowForDiscoverableDataServer(
         DiscoveryDruidNode discoveryDruidNode,
         @Nullable DruidServer serverFromInventoryView
     )
@@ -701,7 +709,8 @@ public class SystemSchema extends AbstractSchema
           druidServerToUse.getMaxSize(),
           null,
           toStringOrNull(discoveryDruidNode.getStartTime()),
-          node.getVersion()
+          node.getVersion(),
+          node.getLabels() == null ? null : 
JacksonUtils.writeValueAsString(jsonMapper, node.getLabels())
       };
     }
 
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/schema/SystemSchemaTest.java 
b/sql/src/test/java/org/apache/druid/sql/calcite/schema/SystemSchemaTest.java
index d523d970fcc..dd5af77e92f 100644
--- 
a/sql/src/test/java/org/apache/druid/sql/calcite/schema/SystemSchemaTest.java
+++ 
b/sql/src/test/java/org/apache/druid/sql/calcite/schema/SystemSchemaTest.java
@@ -402,7 +402,7 @@ public class SystemSchemaTest extends CalciteTestBase
   );
 
   private final DiscoveryDruidNode overlord = new DiscoveryDruidNode(
-      new DruidNode("s2", "localhost", false, 8090, null, true, false),
+      new DruidNode("s2", "localhost", false, 8090, null, null, true, false, 
ImmutableMap.of("overlordKey", "overlordValue")),
       NodeRole.OVERLORD,
       ImmutableMap.of(),
       startTime
@@ -416,7 +416,7 @@ public class SystemSchemaTest extends CalciteTestBase
   );
 
   private final DiscoveryDruidNode broker1 = new DiscoveryDruidNode(
-      new DruidNode("s3", "localhost", false, 8082, null, true, false),
+      new DruidNode("s3", "localhost", false, 8082, null, null, true, false, 
ImmutableMap.of("brokerKey", "brokerValue", "brokerKey2", "brokerValue2")),
       NodeRole.BROKER,
       ImmutableMap.of(),
       startTime
@@ -558,7 +558,7 @@ public class SystemSchemaTest extends CalciteTestBase
     final SystemSchema.ServersTable serversTable = (SystemSchema.ServersTable) 
schema.getTableMap().get("servers");
     final RelDataType serverRowType = serversTable.getRowType(new 
JavaTypeFactoryImpl());
     final List<RelDataTypeField> serverFields = serverRowType.getFieldList();
-    Assert.assertEquals(11, serverFields.size());
+    Assert.assertEquals(12, serverFields.size());
     Assert.assertEquals("server", serverFields.get(0).getName());
     Assert.assertEquals(SqlTypeName.VARCHAR, 
serverFields.get(0).getType().getSqlTypeName());
   }
@@ -761,7 +761,8 @@ public class SystemSchemaTest extends CalciteTestBase
                                                          serverInventoryView,
                                                          authMapper,
                                                          overlordClient,
-                                                         coordinatorClient
+                                                         coordinatorClient,
+                                                         MAPPER
                                                      )
                                                      .createMock();
     EasyMock.replay(serversTable);
@@ -858,7 +859,8 @@ public class SystemSchemaTest extends CalciteTestBase
             0L,
             nonLeader,
             startTimeStr,
-            version
+            version,
+            null
         )
     );
     expectedRows.add(
@@ -873,7 +875,8 @@ public class SystemSchemaTest extends CalciteTestBase
             1000L,
             nonLeader,
             startTimeStr,
-            version
+            version,
+            null
         )
     );
     expectedRows.add(
@@ -888,7 +891,8 @@ public class SystemSchemaTest extends CalciteTestBase
             1000L,
             nonLeader,
             startTimeStr,
-            version
+            version,
+            null
         )
     );
     expectedRows.add(
@@ -903,7 +907,8 @@ public class SystemSchemaTest extends CalciteTestBase
             1000L,
             nonLeader,
             startTimeStr,
-            version
+            version,
+            null
         )
     );
     expectedRows.add(
@@ -918,7 +923,8 @@ public class SystemSchemaTest extends CalciteTestBase
             1000L,
             nonLeader,
             startTimeStr,
-            version
+            version,
+            null
         )
     );
     expectedRows.add(createExpectedRow(
@@ -932,7 +938,8 @@ public class SystemSchemaTest extends CalciteTestBase
         1000L,
         nonLeader,
         startTimeStr,
-        version
+        version,
+        null
     ));
     expectedRows.add(
         createExpectedRow(
@@ -946,7 +953,8 @@ public class SystemSchemaTest extends CalciteTestBase
             0L,
             1L,
             startTimeStr,
-            version
+            version,
+            null
         )
     );
     expectedRows.add(
@@ -961,7 +969,8 @@ public class SystemSchemaTest extends CalciteTestBase
             0L,
             nonLeader,
             startTimeStr,
-            version
+            version,
+            "{\"brokerKey\":\"brokerValue\",\"brokerKey2\":\"brokerValue2\"}"
         )
     );
     expectedRows.add(
@@ -976,7 +985,8 @@ public class SystemSchemaTest extends CalciteTestBase
             1000L,
             nonLeader,
             startTimeStr,
-            version
+            version,
+            null
         )
     );
     expectedRows.add(
@@ -991,7 +1001,8 @@ public class SystemSchemaTest extends CalciteTestBase
             0L,
             1L,
             startTimeStr,
-            version
+            version,
+            "{\"overlordKey\":\"overlordValue\"}"
         )
     );
     expectedRows.add(
@@ -1006,7 +1017,8 @@ public class SystemSchemaTest extends CalciteTestBase
             0L,
             0L,
             startTimeStr,
-            version
+            version,
+            null
         )
     );
     expectedRows.add(
@@ -1021,7 +1033,8 @@ public class SystemSchemaTest extends CalciteTestBase
             0L,
             0L,
             startTimeStr,
-            version
+            version,
+            null
         )
     );
     expectedRows.add(
@@ -1036,7 +1049,8 @@ public class SystemSchemaTest extends CalciteTestBase
             0L,
             nonLeader,
             startTimeStr,
-            version
+            version,
+            null
         )
     );
     expectedRows.add(
@@ -1051,7 +1065,8 @@ public class SystemSchemaTest extends CalciteTestBase
             0L,
             nonLeader,
             startTimeStr,
-            version
+            version,
+            null
         )
     );
     expectedRows.add(createExpectedRow(
@@ -1065,7 +1080,8 @@ public class SystemSchemaTest extends CalciteTestBase
         1000L,
         nonLeader,
         startTimeStr,
-        version
+        version,
+        null
     ));
     Assert.assertEquals(expectedRows.size(), rows.size());
     for (int i = 0; i < rows.size(); i++) {
@@ -1099,7 +1115,8 @@ public class SystemSchemaTest extends CalciteTestBase
       @Nullable Long maxSize,
       @Nullable Long isLeader,
       String startTime,
-      String version
+      String version,
+      String labels
   )
   {
     return new Object[]{
@@ -1113,7 +1130,8 @@ public class SystemSchemaTest extends CalciteTestBase
         maxSize,
         isLeader,
         startTime,
-        version
+        version,
+        labels
     };
   }
 
diff --git a/web-console/src/react-table/react-table-extra.scss 
b/web-console/src/react-table/react-table-extra.scss
index 534b417298f..ecb9953330f 100644
--- a/web-console/src/react-table/react-table-extra.scss
+++ b/web-console/src/react-table/react-table-extra.scss
@@ -19,6 +19,12 @@
 @import '../variables';
 
 .ReactTable {
+  &.centered-table {
+    .rt-th,
+    .rt-td {
+      align-content: center;
+    }
+  }
   .rt-tr {
     min-height: 38px;
 
diff --git 
a/web-console/src/views/services-view/__snapshots__/services-view.spec.tsx.snap 
b/web-console/src/views/services-view/__snapshots__/services-view.spec.tsx.snap
index f17d35a1438..38e99faa1d2 100644
--- 
a/web-console/src/views/services-view/__snapshots__/services-view.spec.tsx.snap
+++ 
b/web-console/src/views/services-view/__snapshots__/services-view.spec.tsx.snap
@@ -59,6 +59,7 @@ exports[`ServicesView renders data 1`] = `
           "Usage",
           "Start time",
           "Version",
+          "Labels",
           "Detail",
         ]
       }
@@ -89,7 +90,7 @@ exports[`ServicesView renders data 1`] = `
       TrComponent={[Function]}
       TrGroupComponent={[Function]}
       aggregatedKey="_aggregated"
-      className=""
+      className="centered-table -striped -highlight padded-header"
       collapseOnDataChange={true}
       collapseOnPageChange={true}
       collapseOnSortingChange={true}
@@ -219,6 +220,16 @@ exports[`ServicesView renders data 1`] = `
             "show": true,
             "width": 200,
           },
+          {
+            "Aggregated": [Function],
+            "Cell": [Function],
+            "Header": "Labels",
+            "accessor": "labels",
+            "className": "padded",
+            "filterable": false,
+            "show": true,
+            "width": 200,
+          },
           {
             "Aggregated": [Function],
             "Cell": [Function],
diff --git a/web-console/src/views/services-view/services-view.scss 
b/web-console/src/views/services-view/services-view.scss
index 642b65c6683..53bcac2d48c 100644
--- a/web-console/src/views/services-view/services-view.scss
+++ b/web-console/src/views/services-view/services-view.scss
@@ -36,4 +36,14 @@
   ul {
     line-height: 20px;
   }
+  .labels-list {
+    list-style-type: none;
+    padding-left: 0;
+    margin: 0;
+
+    li {
+      margin: 0;
+      padding: 0;
+    }
+  }
 }
diff --git a/web-console/src/views/services-view/services-view.tsx 
b/web-console/src/views/services-view/services-view.tsx
index 6fe1fe0236e..f0d97f35612 100644
--- a/web-console/src/views/services-view/services-view.tsx
+++ b/web-console/src/views/services-view/services-view.tsx
@@ -43,6 +43,7 @@ import type { Capabilities, CapabilitiesMode } from 
'../../helpers';
 import {
   booleanCustomTableFilter,
   combineModeAndNeedle,
+  DEFAULT_TABLE_CLASS_NAME,
   parseFilterModeAndNeedle,
   STANDARD_TABLE_PAGE_SIZE,
   STANDARD_TABLE_PAGE_SIZE_OPTIONS,
@@ -88,6 +89,7 @@ const TABLE_COLUMNS_BY_MODE: Record<CapabilitiesMode, 
TableColumnSelectorColumn[
     'Usage',
     'Start time',
     'Version',
+    'Labels',
     'Detail',
   ],
   'no-sql': [
@@ -149,6 +151,7 @@ interface ServiceResultRow {
   readonly tls_port: number;
   readonly start_time: string;
   readonly version: string;
+  readonly labels: string | null;
 }
 
 interface ServicesWithAuxiliaryInfo {
@@ -250,7 +253,8 @@ export class ServicesView extends 
React.PureComponent<ServicesViewProps, Service
   "max_size",
   "is_leader",
   "start_time",
-  "version"
+  "version",
+  "labels"
 FROM sys.servers
 ORDER BY
   (
@@ -300,6 +304,7 @@ ORDER BY
                 start_time: '1970:01:01T00:00:00Z',
                 is_leader: 0,
                 version: '',
+                labels: null,
               };
             },
           );
@@ -434,6 +439,7 @@ ORDER BY
           }
           filterable
           filtered={filters}
+          className={`centered-table ${DEFAULT_TABLE_CLASS_NAME}`}
           onFilteredChange={onFiltersChange}
           pivotBy={groupServicesBy ? [groupServicesBy] : []}
           defaultPageSize={STANDARD_TABLE_PAGE_SIZE}
@@ -656,6 +662,29 @@ ORDER BY
           Cell: this.renderFilterableCell('version'),
           Aggregated: () => '',
         },
+        {
+          Header: 'Labels',
+          show: visibleColumns.shown('Labels'),
+          accessor: 'labels',
+          className: 'padded',
+          filterable: false,
+          width: 200,
+          Cell: ({ value }: { value: string | null }) => {
+            if (!value) return '';
+            return (
+              <ul className="labels-list">
+                {Object.entries(JSON.parse(value)).map(([key, val]) => {
+                  return (
+                    <li key={key}>
+                      {key}: {String(val)}
+                    </li>
+                  );
+                })}
+              </ul>
+            );
+          },
+          Aggregated: () => '',
+        },
         {
           Header: 'Detail',
           show: visibleColumns.shown('Detail'),


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to