From 514a63a696d0cb1bb8bd21a4430c34541e5737fb Mon Sep 17 00:00:00 2001 From: Gabriel Chang <77312579+GabrielCWT@users.noreply.github.com> Date: Sat, 11 Oct 2025 10:21:14 +0800 Subject: [PATCH] Add labels to sys.server (#18547) --- docs/configuration/index.md | 7 +++ docs/querying/sql-metadata-tables.md | 1 + .../druid/testsEx/config/Initializer.java | 2 +- .../druid/testing/guice/DruidTestModule.java | 2 +- .../results/auth_test_sys_schema_servers.json | 6 +- .../util/common/jackson/JacksonUtils.java | 12 ++++ .../org/apache/druid/server/DruidNode.java | 32 +++++++--- .../discovery/DiscoveryDruidNodeTest.java | 12 ++-- .../apache/druid/server/DruidNodeTest.java | 40 +++++++++---- .../sql/calcite/schema/SystemSchema.java | 25 +++++--- .../sql/calcite/schema/SystemSchemaTest.java | 60 ++++++++++++------- .../src/react-table/react-table-extra.scss | 6 ++ .../__snapshots__/services-view.spec.tsx.snap | 13 +++- .../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 3198f6529f49..e86751048192 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 @@ -1558,6 +1562,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 @@ -1671,6 +1676,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 @@ -2290,6 +2296,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 aa1804e29d56..6d59462fd1e9 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 b09c18e72642..d350e5175f46 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 void configure(Binder binder) .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 ff381439d1e0..3ac78bb9c35d 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 void configure(Binder binder) // 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 520fde8ff56e..ddbd92e3111e 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 ed6567bf9313..3aebd02a9bba 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.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 static void writeObjectUsingSerializerProvider( } } + 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 0e3b3e4b4d02..b6032bd0a1d3 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.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 labels; + public DruidNode( String serviceName, String host, @@ -101,7 +105,7 @@ public 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 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 labels ) { init( @@ -138,8 +143,9 @@ public DruidNode( bindOnHost, plaintextPort != null ? plaintextPort : port, tlsPort, - enablePlaintextPort == null ? true : enablePlaintextPort.booleanValue(), - enableTlsPort + enablePlaintextPort == null || enablePlaintextPort.booleanValue(), + enableTlsPort, + labels ); } @@ -150,7 +156,8 @@ private void init( Integer plainTextPort, Integer tlsPort, boolean enablePlaintextPort, - boolean enableTlsPort + boolean enableTlsPort, + Map labels ) { Preconditions.checkNotNull(serviceName); @@ -210,6 +217,13 @@ private void init( this.serviceName = serviceName; this.host = host; this.bindOnHost = bindOnHost; + this.labels = labels; + } + + @Nullable + public Map getLabels() + { + return labels; } public String getServiceName() @@ -336,13 +350,14 @@ public boolean equals(Object o) 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 String toString() ", 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 0e55e5b68eb2..b9c99a4fcb7d 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 void testSerdeWithDataNodeAndLookupNodeServices() throws JsonProcessingEx -1, 8282, true, - true + true, + null ), NodeRole.BROKER, ImmutableMap.of( @@ -167,7 +168,8 @@ public void testDeserializeWithDataNodeServiceWithAWrongPropertyOrder() throws J -1, 8282, true, - true + true, + null ), NodeRole.BROKER, ImmutableMap.of( @@ -216,7 +218,8 @@ public void testDeserialize_duplicateProperties_shouldSucceedToDeserialize() thr -1, 8282, true, - true + true, + null ), NodeRole.BROKER, ImmutableMap.of( @@ -266,7 +269,8 @@ public void testDeserialize_duplicateKeysWithDifferentValus_shouldIgnoreDataNode -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 77edaaad1379..9b7762904d17 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 @@ 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 void testDefaultsAndSanity() 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 void testEquals() 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 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 void testHashCode() 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 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 void testHashCode() 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 void testSerde1() throws Exception 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 void testSerde2() throws Exception 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 void testSerde3() throws Exception 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 void testDeserialization1() throws Exception + " \"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 void testDeserialization2() throws Exception 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 void testDeserialization3() throws Exception 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 void testDeserialization4() throws Exception 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 932404f21e78..1343ace28fe4 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.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 SystemSchema( serverInventoryView, authorizerMapper, overlordClient, - coordinatorClient + coordinatorClient, + jsonMapper ), SERVER_SEGMENTS_TABLE, new ServerSegmentsTable(serverView, authorizerMapper), @@ -526,13 +529,15 @@ static class ServersTable extends AbstractTable implements ScannableTable 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 ServersTable( this.serverInventoryView = serverInventoryView; this.overlordClient = overlordClient; this.coordinatorClient = coordinatorClient; + this.jsonMapper = jsonMapper; } @Override @@ -627,7 +633,7 @@ public Enumerable scan(DataContext root) /** * 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 @@ private static Object[] buildRowForNonDataServer(DiscoveryDruidNode discoveryDru 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 @@ private static Object[] buildRowForNonDataServerWithLeadership( 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 @@ private static Object[] buildRowForNonDataServerWithLeadership( * {@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 @@ private static Object[] buildRowForDiscoverableDataServer( 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 d523d970fccf..dd5af77e92f9 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 void setUp(@TempDir File tmpDir) throws Exception ); 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 void setUp(@TempDir File tmpDir) throws Exception ); 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 void testGetTableMap() final SystemSchema.ServersTable serversTable = (SystemSchema.ServersTable) schema.getTableMap().get("servers"); final RelDataType serverRowType = serversTable.getRowType(new JavaTypeFactoryImpl()); final List 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 void testServersTable() throws URISyntaxException serverInventoryView, authMapper, overlordClient, - coordinatorClient + coordinatorClient, + MAPPER ) .createMock(); EasyMock.replay(serversTable); @@ -858,7 +859,8 @@ public void testServersTable() throws URISyntaxException 0L, nonLeader, startTimeStr, - version + version, + null ) ); expectedRows.add( @@ -873,7 +875,8 @@ public void testServersTable() throws URISyntaxException 1000L, nonLeader, startTimeStr, - version + version, + null ) ); expectedRows.add( @@ -888,7 +891,8 @@ public void testServersTable() throws URISyntaxException 1000L, nonLeader, startTimeStr, - version + version, + null ) ); expectedRows.add( @@ -903,7 +907,8 @@ public void testServersTable() throws URISyntaxException 1000L, nonLeader, startTimeStr, - version + version, + null ) ); expectedRows.add( @@ -918,7 +923,8 @@ public void testServersTable() throws URISyntaxException 1000L, nonLeader, startTimeStr, - version + version, + null ) ); expectedRows.add(createExpectedRow( @@ -932,7 +938,8 @@ public void testServersTable() throws URISyntaxException 1000L, nonLeader, startTimeStr, - version + version, + null )); expectedRows.add( createExpectedRow( @@ -946,7 +953,8 @@ public void testServersTable() throws URISyntaxException 0L, 1L, startTimeStr, - version + version, + null ) ); expectedRows.add( @@ -961,7 +969,8 @@ public void testServersTable() throws URISyntaxException 0L, nonLeader, startTimeStr, - version + version, + "{\"brokerKey\":\"brokerValue\",\"brokerKey2\":\"brokerValue2\"}" ) ); expectedRows.add( @@ -976,7 +985,8 @@ public void testServersTable() throws URISyntaxException 1000L, nonLeader, startTimeStr, - version + version, + null ) ); expectedRows.add( @@ -991,7 +1001,8 @@ public void testServersTable() throws URISyntaxException 0L, 1L, startTimeStr, - version + version, + "{\"overlordKey\":\"overlordValue\"}" ) ); expectedRows.add( @@ -1006,7 +1017,8 @@ public void testServersTable() throws URISyntaxException 0L, 0L, startTimeStr, - version + version, + null ) ); expectedRows.add( @@ -1021,7 +1033,8 @@ public void testServersTable() throws URISyntaxException 0L, 0L, startTimeStr, - version + version, + null ) ); expectedRows.add( @@ -1036,7 +1049,8 @@ public void testServersTable() throws URISyntaxException 0L, nonLeader, startTimeStr, - version + version, + null ) ); expectedRows.add( @@ -1051,7 +1065,8 @@ public void testServersTable() throws URISyntaxException 0L, nonLeader, startTimeStr, - version + version, + null ) ); expectedRows.add(createExpectedRow( @@ -1065,7 +1080,8 @@ public void testServersTable() throws URISyntaxException 1000L, nonLeader, startTimeStr, - version + version, + null )); Assert.assertEquals(expectedRows.size(), rows.size()); for (int i = 0; i < rows.size(); i++) { @@ -1099,7 +1115,8 @@ private Object[] createExpectedRow( @Nullable Long maxSize, @Nullable Long isLeader, String startTime, - String version + String version, + String labels ) { return new Object[]{ @@ -1113,7 +1130,8 @@ private Object[] createExpectedRow( 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 534b417298f6..ecb9953330f1 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 f17d35a1438f..38e99faa1d20 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 642b65c66834..53bcac2d48ca 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 6fe1fe0236e0..f0d97f356127 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 '', }, + { + Header: 'Labels', + show: visibleColumns.shown('Labels'), + accessor: 'labels', + className: 'padded', + filterable: false, + width: 200, + Cell: ({ value }: { value: string | null }) => { + if (!value) return ''; + return ( +
    + {Object.entries(JSON.parse(value)).map(([key, val]) => { + return ( +
  • + {key}: {String(val)} +
  • + ); + })} +
+ ); + }, + Aggregated: () => '', + }, { Header: 'Detail', show: visibleColumns.shown('Detail'),