From b2d8bb8e53a09dabce25b9d2cba743cc551bd347 Mon Sep 17 00:00:00 2001 From: Gabriel Chang <77312579+GabrielCWT@users.noreply.github.com> Date: Fri, 19 Sep 2025 09:19:56 +0800 Subject: [PATCH 01/22] Add labels to servers --- docs/configuration/index.md | 7 ++ docs/querying/sql-metadata-tables.md | 1 + .../druid/testsEx/config/Initializer.java | 2 +- .../druid/testing/guice/DruidTestModule.java | 2 +- .../org/apache/druid/server/DruidNode.java | 23 +++- .../discovery/DiscoveryDruidNodeTest.java | 12 +- .../apache/druid/server/DruidNodeTest.java | 14 +-- .../sql/calcite/schema/SystemSchema.java | 105 +++++++++++------- .../sql/calcite/schema/SystemSchemaTest.java | 3 +- 9 files changed, 109 insertions(+), 60 deletions(-) diff --git a/docs/configuration/index.md b/docs/configuration/index.md index c925fe964c14..08537f20ff46 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -740,6 +740,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.|`null`| ##### Coordinator operation @@ -983,6 +984,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.|`null`| ##### Overlord operations @@ -1334,6 +1336,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.|`null`| #### Middle Manager configuration @@ -1461,6 +1464,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.|`null`| #### Indexer general configuration @@ -1554,6 +1558,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.|`null`| #### Historical general configuration @@ -1666,6 +1671,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.|`null`| #### Query configuration @@ -2285,6 +2291,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.|`null`| #### Runtime configuration diff --git a/docs/querying/sql-metadata-tables.md b/docs/querying/sql-metadata-tables.md index cd2db5cf14c2..28a035a1eceb 100644 --- a/docs/querying/sql-metadata-tables.md +++ b/docs/querying/sql-metadata-tables.md @@ -236,6 +236,7 @@ Servers table lists all discovered servers in the cluster. |max_size|BIGINT|Max size in bytes this server recommends to assign to segments see [druid.server.maxSize](../configuration/index.md#historical-general-configuration). Only valid for HISTORICAL type, for other types it's 0| |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| +|labels|VARCHAR|Labels for the server see [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/server/src/main/java/org/apache/druid/server/DruidNode.java b/server/src/main/java/org/apache/druid/server/DruidNode.java index 225f054c73ef..58b5f89ebd27 100644 --- a/server/src/main/java/org/apache/druid/server/DruidNode.java +++ b/server/src/main/java/org/apache/druid/server/DruidNode.java @@ -32,11 +32,12 @@ 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.HashMap; +import java.util.Map; import java.util.Objects; /** @@ -89,6 +90,9 @@ public class DruidNode "unknown" ); + @JsonProperty + private Map labels = new HashMap<>(); + public DruidNode( String serviceName, String host, @@ -99,7 +103,7 @@ public DruidNode( boolean enableTlsPort ) { - this(serviceName, host, bindOnHost, plaintextPort, null, tlsPort, enablePlaintextPort, enableTlsPort); + this(serviceName, host, bindOnHost, plaintextPort, null, tlsPort, enablePlaintextPort, enableTlsPort, null); } /** @@ -127,7 +131,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") Map labels ) { init( @@ -137,7 +142,8 @@ public DruidNode( plaintextPort != null ? plaintextPort : port, tlsPort, enablePlaintextPort == null ? true : enablePlaintextPort.booleanValue(), - enableTlsPort + enableTlsPort, + labels ); } @@ -148,7 +154,8 @@ private void init( Integer plainTextPort, Integer tlsPort, boolean enablePlaintextPort, - boolean enableTlsPort + boolean enableTlsPort, + Map labels ) { Preconditions.checkNotNull(serviceName); @@ -208,6 +215,12 @@ private void init( this.serviceName = serviceName; this.host = host; this.bindOnHost = bindOnHost; + this.labels = labels; + } + + public Map getLabels() + { + return labels; } public String getServiceName() 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 f0fde0117ac9..a9c675f66895 100644 --- a/server/src/test/java/org/apache/druid/server/DruidNodeTest.java +++ b/server/src/test/java/org/apache/druid/server/DruidNodeTest.java @@ -285,7 +285,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, null)), DruidNode.class ); Assert.assertEquals("service", actual.getServiceName()); @@ -301,7 +301,7 @@ public void testSerde1() throws Exception 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()); @@ -317,7 +317,7 @@ public void testSerde2() throws Exception 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, null)), DruidNode.class ); Assert.assertEquals("service", actual.getServiceName()); @@ -344,7 +344,7 @@ public void testDeserialization1() throws Exception 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, null), actual); Assert.assertEquals("https", actual.getServiceScheme()); Assert.assertEquals("host:1234", actual.getHostAndPort()); @@ -365,7 +365,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 +385,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 +405,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 76e6f37ab597..bc08b8f0afba 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 @@ -180,6 +180,7 @@ public class SystemSchema extends AbstractSchema .add("max_size", ColumnType.LONG) .add("is_leader", ColumnType.LONG) .add("start_time", ColumnType.STRING) + .add("labels", ColumnType.STRING) .build(); static final RowSignature SERVER_SEGMENTS_SIGNATURE = RowSignature @@ -244,7 +245,8 @@ public SystemSchema( serverInventoryView, authorizerMapper, overlordClient, - coordinatorClient + coordinatorClient, + jsonMapper ), SERVER_SEGMENTS_TABLE, new ServerSegmentsTable(serverView, authorizerMapper), @@ -525,13 +527,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; @@ -539,6 +543,7 @@ public ServersTable( this.serverInventoryView = serverInventoryView; this.overlordClient = overlordClient; this.coordinatorClient = coordinatorClient; + this.jsonMapper = jsonMapper; } @Override @@ -626,44 +631,56 @@ 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[]{ - node.getHostAndPortToUse(), - node.getHost(), - (long) node.getPlaintextPort(), - (long) node.getTlsPort(), - StringUtils.toLowerCase(discoveryDruidNode.getNodeRole().toString()), - null, - UNKNOWN_SIZE, - UNKNOWN_SIZE, - null, - toStringOrNull(discoveryDruidNode.getStartTime()) - }; + try { + return new Object[]{ + node.getHostAndPortToUse(), + node.getHost(), + (long) node.getPlaintextPort(), + (long) node.getTlsPort(), + StringUtils.toLowerCase(discoveryDruidNode.getNodeRole().toString()), + null, + UNKNOWN_SIZE, + UNKNOWN_SIZE, + null, + toStringOrNull(discoveryDruidNode.getStartTime()), + node.getLabels() == null ? null : jsonMapper.writeValueAsString(node.getLabels()) + }; + } + catch (JsonProcessingException e) { + throw new RuntimeException(e); + } } /** * 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 ) { final DruidNode node = discoveryDruidNode.getDruidNode(); - return new Object[]{ - node.getHostAndPortToUse(), - node.getHost(), - (long) node.getPlaintextPort(), - (long) node.getTlsPort(), - StringUtils.toLowerCase(discoveryDruidNode.getNodeRole().toString()), - null, - UNKNOWN_SIZE, - UNKNOWN_SIZE, - isLeader ? 1L : 0L, - toStringOrNull(discoveryDruidNode.getStartTime()) - }; + try { + return new Object[]{ + node.getHostAndPortToUse(), + node.getHost(), + (long) node.getPlaintextPort(), + (long) node.getTlsPort(), + StringUtils.toLowerCase(discoveryDruidNode.getNodeRole().toString()), + null, + UNKNOWN_SIZE, + UNKNOWN_SIZE, + isLeader ? 1L : 0L, + toStringOrNull(discoveryDruidNode.getStartTime()), + node.getLabels() == null ? null : jsonMapper.writeValueAsString(node.getLabels()) + }; + } + catch (JsonProcessingException e) { + throw new RuntimeException(e); + } } /** @@ -671,7 +688,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 ) @@ -687,18 +704,24 @@ private static Object[] buildRowForDiscoverableDataServer( } else { currentSize = serverFromInventoryView.getCurrSize(); } - return new Object[]{ - node.getHostAndPortToUse(), - node.getHost(), - (long) node.getPlaintextPort(), - (long) node.getTlsPort(), - StringUtils.toLowerCase(discoveryDruidNode.getNodeRole().toString()), - druidServerToUse.getTier(), - currentSize, - druidServerToUse.getMaxSize(), - null, - toStringOrNull(discoveryDruidNode.getStartTime()) - }; + try { + return new Object[]{ + node.getHostAndPortToUse(), + node.getHost(), + (long) node.getPlaintextPort(), + (long) node.getTlsPort(), + StringUtils.toLowerCase(discoveryDruidNode.getNodeRole().toString()), + druidServerToUse.getTier(), + currentSize, + druidServerToUse.getMaxSize(), + null, + toStringOrNull(discoveryDruidNode.getStartTime()), + node.getLabels() == null ? null : jsonMapper.writeValueAsString(node.getLabels()) + }; + } + catch (JsonProcessingException e) { + throw new RuntimeException(e); + } } private static boolean isDiscoverableDataServer(DataNodeService dataNodeService) 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 0fc01c1aa09c..822983c48f16 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 @@ -755,7 +755,8 @@ public void testServersTable() throws URISyntaxException serverInventoryView, authMapper, overlordClient, - coordinatorClient + coordinatorClient, + MAPPER ) .createMock(); EasyMock.replay(serversTable); From 71e549fec37eec2d3c58dd0ff716edd627379630 Mon Sep 17 00:00:00 2001 From: Gabriel Chang <77312579+GabrielCWT@users.noreply.github.com> Date: Fri, 19 Sep 2025 09:20:03 +0800 Subject: [PATCH 02/22] Add labels to FE --- .../src/react-table/react-table-extra.scss | 7 +++++ .../views/services-view/services-view.scss | 10 +++++++ .../src/views/services-view/services-view.tsx | 29 ++++++++++++++++++- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/web-console/src/react-table/react-table-extra.scss b/web-console/src/react-table/react-table-extra.scss index 534b417298f6..49436a067b24 100644 --- a/web-console/src/react-table/react-table-extra.scss +++ b/web-console/src/react-table/react-table-extra.scss @@ -19,6 +19,13 @@ @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/services-view.scss b/web-console/src/views/services-view/services-view.scss index 642b65c66834..a34f43242ddf 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 ea9c5653e9dc..cfc7e6e10c7c 100644 --- a/web-console/src/views/services-view/services-view.tsx +++ b/web-console/src/views/services-view/services-view.tsx @@ -41,6 +41,7 @@ import type { QueryWithContext } from '../../druid-models'; import { getConsoleViewIcon } from '../../druid-models'; import type { Capabilities, CapabilitiesMode } from '../../helpers'; import { + DEFAULT_TABLE_CLASS_NAME, STANDARD_TABLE_PAGE_SIZE, STANDARD_TABLE_PAGE_SIZE_OPTIONS, suggestibleFilterInput, @@ -84,6 +85,7 @@ const TABLE_COLUMNS_BY_MODE: Record '', }, + { + Header: 'Labels', + show: visibleColumns.shown('Labels'), + accessor: 'labels', + className: 'padded', + filterable: false, + width: 200, + Cell: ({ value }) => { + if (!value) return ''; + return ( +
    + {Object.entries(JSON.parse(value)).map(([key, val]) => { + return ( +
  • + {key}: {String(val)} +
  • + ); + })} +
+ ); + }, + }, { Header: 'Detail', show: visibleColumns.shown('Detail'), From 7dee2ca0947fcf5193adc06d99e15237509b651a Mon Sep 17 00:00:00 2001 From: Gabriel Chang <77312579+GabrielCWT@users.noreply.github.com> Date: Fri, 19 Sep 2025 09:39:03 +0800 Subject: [PATCH 03/22] Fix missing assignment to labels variable --- web-console/src/views/services-view/services-view.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web-console/src/views/services-view/services-view.tsx b/web-console/src/views/services-view/services-view.tsx index cfc7e6e10c7c..772e9c69916c 100644 --- a/web-console/src/views/services-view/services-view.tsx +++ b/web-console/src/views/services-view/services-view.tsx @@ -145,7 +145,7 @@ interface ServiceResultRow { readonly plaintext_port: number; readonly tls_port: number; readonly start_time: string; - readonly labels: string; + readonly labels: string | null; } interface ServicesWithAuxiliaryInfo { @@ -291,6 +291,7 @@ ORDER BY max_size: s.maxSize, start_time: '1970:01:01T00:00:00Z', is_leader: 0, + labels: null, }; }, ); From 1c24c13b4ea6be9627c53d6a2d39aa071d7b7a01 Mon Sep 17 00:00:00 2001 From: Gabriel Chang <77312579+GabrielCWT@users.noreply.github.com> Date: Fri, 19 Sep 2025 11:07:52 +0800 Subject: [PATCH 04/22] Update docs --- docs/configuration/index.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/configuration/index.md b/docs/configuration/index.md index 08537f20ff46..d3dc77f7cae3 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -740,7 +740,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.|`null`| +|`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,7 +984,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.|`null`| +|`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 @@ -1336,7 +1336,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.|`null`| +|`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 @@ -1464,7 +1464,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.|`null`| +|`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,7 +1558,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.|`null`| +|`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,7 +1671,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.|`null`| +|`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,7 +2291,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.|`null`| +|`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 From 22de456fc27496d24ef594f92a0942bef463c399 Mon Sep 17 00:00:00 2001 From: Gabriel Chang <77312579+GabrielCWT@users.noreply.github.com> Date: Tue, 23 Sep 2025 10:12:54 +0800 Subject: [PATCH 05/22] Remove labels default value --- server/src/main/java/org/apache/druid/server/DruidNode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 58b5f89ebd27..c2135d16e3ac 100644 --- a/server/src/main/java/org/apache/druid/server/DruidNode.java +++ b/server/src/main/java/org/apache/druid/server/DruidNode.java @@ -91,7 +91,7 @@ public class DruidNode ); @JsonProperty - private Map labels = new HashMap<>(); + private Map labels; public DruidNode( String serviceName, From da63bf9548f1a7f90c2ace27c7d98718beaa67af Mon Sep 17 00:00:00 2001 From: Gabriel Chang <77312579+GabrielCWT@users.noreply.github.com> Date: Tue, 23 Sep 2025 10:13:33 +0800 Subject: [PATCH 06/22] Return labels as JSON object instead of string --- .../sql/calcite/schema/SystemSchema.java | 109 ++++++++---------- .../src/views/services-view/services-view.tsx | 10 +- 2 files changed, 50 insertions(+), 69 deletions(-) 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 bc08b8f0afba..0ac57b8390b9 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 @@ -180,7 +180,7 @@ public class SystemSchema extends AbstractSchema .add("max_size", ColumnType.LONG) .add("is_leader", ColumnType.LONG) .add("start_time", ColumnType.STRING) - .add("labels", ColumnType.STRING) + .add("labels", ColumnType.NESTED_DATA) .build(); static final RowSignature SERVER_SEGMENTS_SIGNATURE = RowSignature @@ -245,8 +245,7 @@ public SystemSchema( serverInventoryView, authorizerMapper, overlordClient, - coordinatorClient, - jsonMapper + coordinatorClient ), SERVER_SEGMENTS_TABLE, new ServerSegmentsTable(serverView, authorizerMapper), @@ -527,15 +526,13 @@ 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, - ObjectMapper jsonMapper + CoordinatorClient coordinatorClient ) { this.authorizerMapper = authorizerMapper; @@ -543,7 +540,6 @@ public ServersTable( this.serverInventoryView = serverInventoryView; this.overlordClient = overlordClient; this.coordinatorClient = coordinatorClient; - this.jsonMapper = jsonMapper; } @Override @@ -631,56 +627,46 @@ 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 Object[] buildRowForNonDataServer(DiscoveryDruidNode discoveryDruidNode) + private static Object[] buildRowForNonDataServer(DiscoveryDruidNode discoveryDruidNode) { final DruidNode node = discoveryDruidNode.getDruidNode(); - try { - return new Object[]{ - node.getHostAndPortToUse(), - node.getHost(), - (long) node.getPlaintextPort(), - (long) node.getTlsPort(), - StringUtils.toLowerCase(discoveryDruidNode.getNodeRole().toString()), - null, - UNKNOWN_SIZE, - UNKNOWN_SIZE, - null, - toStringOrNull(discoveryDruidNode.getStartTime()), - node.getLabels() == null ? null : jsonMapper.writeValueAsString(node.getLabels()) - }; - } - catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + return new Object[] { + node.getHostAndPortToUse(), + node.getHost(), + (long) node.getPlaintextPort(), + (long) node.getTlsPort(), + StringUtils.toLowerCase(discoveryDruidNode.getNodeRole().toString()), + null, + UNKNOWN_SIZE, + UNKNOWN_SIZE, + null, + toStringOrNull(discoveryDruidNode.getStartTime()), + node.getLabels() + }; } /** * Returns a row for all node types which don't serve data. The returned row contains only static information. */ - private Object[] buildRowForNonDataServerWithLeadership( + private static Object[] buildRowForNonDataServerWithLeadership( DiscoveryDruidNode discoveryDruidNode, boolean isLeader ) { final DruidNode node = discoveryDruidNode.getDruidNode(); - try { - return new Object[]{ - node.getHostAndPortToUse(), - node.getHost(), - (long) node.getPlaintextPort(), - (long) node.getTlsPort(), - StringUtils.toLowerCase(discoveryDruidNode.getNodeRole().toString()), - null, - UNKNOWN_SIZE, - UNKNOWN_SIZE, - isLeader ? 1L : 0L, - toStringOrNull(discoveryDruidNode.getStartTime()), - node.getLabels() == null ? null : jsonMapper.writeValueAsString(node.getLabels()) - }; - } - catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + return new Object[] { + node.getHostAndPortToUse(), + node.getHost(), + (long) node.getPlaintextPort(), + (long) node.getTlsPort(), + StringUtils.toLowerCase(discoveryDruidNode.getNodeRole().toString()), + null, + UNKNOWN_SIZE, + UNKNOWN_SIZE, + isLeader ? 1L : 0L, + toStringOrNull(discoveryDruidNode.getStartTime()), + node.getLabels() + }; } /** @@ -688,7 +674,7 @@ private 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 Object[] buildRowForDiscoverableDataServer( + private static Object[] buildRowForDiscoverableDataServer( DiscoveryDruidNode discoveryDruidNode, @Nullable DruidServer serverFromInventoryView ) @@ -704,24 +690,19 @@ private Object[] buildRowForDiscoverableDataServer( } else { currentSize = serverFromInventoryView.getCurrSize(); } - try { - return new Object[]{ - node.getHostAndPortToUse(), - node.getHost(), - (long) node.getPlaintextPort(), - (long) node.getTlsPort(), - StringUtils.toLowerCase(discoveryDruidNode.getNodeRole().toString()), - druidServerToUse.getTier(), - currentSize, - druidServerToUse.getMaxSize(), - null, - toStringOrNull(discoveryDruidNode.getStartTime()), - node.getLabels() == null ? null : jsonMapper.writeValueAsString(node.getLabels()) - }; - } - catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + return new Object[] { + node.getHostAndPortToUse(), + node.getHost(), + (long) node.getPlaintextPort(), + (long) node.getTlsPort(), + StringUtils.toLowerCase(discoveryDruidNode.getNodeRole().toString()), + druidServerToUse.getTier(), + currentSize, + druidServerToUse.getMaxSize(), + null, + toStringOrNull(discoveryDruidNode.getStartTime()), + node.getLabels() + }; } private static boolean isDiscoverableDataServer(DataNodeService dataNodeService) diff --git a/web-console/src/views/services-view/services-view.tsx b/web-console/src/views/services-view/services-view.tsx index 772e9c69916c..13dd8e535726 100644 --- a/web-console/src/views/services-view/services-view.tsx +++ b/web-console/src/views/services-view/services-view.tsx @@ -145,7 +145,7 @@ interface ServiceResultRow { readonly plaintext_port: number; readonly tls_port: number; readonly start_time: string; - readonly labels: string | null; + readonly labels: Record | null; } interface ServicesWithAuxiliaryInfo { @@ -629,14 +629,14 @@ ORDER BY className: 'padded', filterable: false, width: 200, - Cell: ({ value }) => { + Cell: ({ value }: { value: Record | null }) => { if (!value) return ''; return (
    - {Object.entries(JSON.parse(value)).map(([key, val]) => { + {Object.entries(value).map(([key, val]) => { return ( -
  • - {key}: {String(val)} +
  • + {key}: {val}
  • ); })} From 69a602517f8434f2a2401a26c15fb63adbe7df37 Mon Sep 17 00:00:00 2001 From: Gabriel Chang <77312579+GabrielCWT@users.noreply.github.com> Date: Tue, 23 Sep 2025 14:06:23 +0800 Subject: [PATCH 07/22] Fix checkstyle --- server/src/main/java/org/apache/druid/server/DruidNode.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 c2135d16e3ac..1f4763521e65 100644 --- a/server/src/main/java/org/apache/druid/server/DruidNode.java +++ b/server/src/main/java/org/apache/druid/server/DruidNode.java @@ -36,7 +36,6 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; -import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -67,7 +66,7 @@ public class DruidNode @Deprecated @JsonProperty @Max(0xffff) - private int port = -1; + private final int port = -1; @JsonProperty @Max(0xffff) @@ -141,7 +140,7 @@ public DruidNode( bindOnHost, plaintextPort != null ? plaintextPort : port, tlsPort, - enablePlaintextPort == null ? true : enablePlaintextPort.booleanValue(), + enablePlaintextPort == null || enablePlaintextPort.booleanValue(), enableTlsPort, labels ); From 8113e173eed16903f4470dc5df91d3aeea78deb3 Mon Sep 17 00:00:00 2001 From: Gabriel Chang <77312579+GabrielCWT@users.noreply.github.com> Date: Tue, 23 Sep 2025 15:11:57 +0800 Subject: [PATCH 08/22] Update tests --- .../sql/calcite/schema/SystemSchema.java | 12 ++-- .../sql/calcite/schema/SystemSchemaTest.java | 65 ++++++++++++------- 2 files changed, 48 insertions(+), 29 deletions(-) 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 0ac57b8390b9..e3250fb7bad0 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 @@ -630,8 +630,8 @@ public Enumerable scan(DataContext root) private static Object[] buildRowForNonDataServer(DiscoveryDruidNode discoveryDruidNode) { final DruidNode node = discoveryDruidNode.getDruidNode(); - return new Object[] { - node.getHostAndPortToUse(), + return new Object[]{ + node.getHostAndPortToUse(), node.getHost(), (long) node.getPlaintextPort(), (long) node.getTlsPort(), @@ -654,8 +654,8 @@ private static Object[] buildRowForNonDataServerWithLeadership( ) { final DruidNode node = discoveryDruidNode.getDruidNode(); - return new Object[] { - node.getHostAndPortToUse(), + return new Object[]{ + node.getHostAndPortToUse(), node.getHost(), (long) node.getPlaintextPort(), (long) node.getTlsPort(), @@ -690,8 +690,8 @@ private static Object[] buildRowForDiscoverableDataServer( } else { currentSize = serverFromInventoryView.getCurrSize(); } - return new Object[] { - node.getHostAndPortToUse(), + return new Object[]{ + node.getHostAndPortToUse(), node.getHost(), (long) node.getPlaintextPort(), (long) node.getTlsPort(), 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 822983c48f16..35385b69f8af 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 @@ -396,7 +396,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 @@ -410,7 +410,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 @@ -552,7 +552,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(10, serverFields.size()); + Assert.assertEquals(11, serverFields.size()); Assert.assertEquals("server", serverFields.get(0).getName()); Assert.assertEquals(SqlTypeName.VARCHAR, serverFields.get(0).getType().getSqlTypeName()); } @@ -755,8 +755,7 @@ public void testServersTable() throws URISyntaxException serverInventoryView, authMapper, overlordClient, - coordinatorClient, - MAPPER + coordinatorClient ) .createMock(); EasyMock.replay(serversTable); @@ -852,7 +851,8 @@ public void testServersTable() throws URISyntaxException 0L, 0L, nonLeader, - startTimeStr + startTimeStr, + null ) ); expectedRows.add( @@ -866,7 +866,8 @@ public void testServersTable() throws URISyntaxException 0L, 1000L, nonLeader, - startTimeStr + startTimeStr, + null ) ); expectedRows.add( @@ -880,7 +881,8 @@ public void testServersTable() throws URISyntaxException 400L, 1000L, nonLeader, - startTimeStr + startTimeStr, + null ) ); expectedRows.add( @@ -894,7 +896,8 @@ public void testServersTable() throws URISyntaxException 0L, 1000L, nonLeader, - startTimeStr + startTimeStr, + null ) ); expectedRows.add( @@ -908,7 +911,8 @@ public void testServersTable() throws URISyntaxException 0L, 1000L, nonLeader, - startTimeStr + startTimeStr, + null ) ); expectedRows.add(createExpectedRow( @@ -921,7 +925,8 @@ public void testServersTable() throws URISyntaxException 0L, 1000L, nonLeader, - startTimeStr + startTimeStr, + null )); expectedRows.add( createExpectedRow( @@ -934,7 +939,8 @@ public void testServersTable() throws URISyntaxException 0L, 0L, 1L, - startTimeStr + startTimeStr, + null ) ); expectedRows.add( @@ -948,7 +954,8 @@ public void testServersTable() throws URISyntaxException 0L, 0L, nonLeader, - startTimeStr + startTimeStr, + ImmutableMap.of("brokerKey", "brokerValue", "brokerKey2", "brokerValue2") ) ); expectedRows.add( @@ -962,7 +969,8 @@ public void testServersTable() throws URISyntaxException 200L, 1000L, nonLeader, - startTimeStr + startTimeStr, + null ) ); expectedRows.add( @@ -976,7 +984,8 @@ public void testServersTable() throws URISyntaxException 0L, 0L, 1L, - startTimeStr + startTimeStr, + ImmutableMap.of("overlordKey", "overlordValue") ) ); expectedRows.add( @@ -990,7 +999,8 @@ public void testServersTable() throws URISyntaxException 0L, 0L, 0L, - startTimeStr + startTimeStr, + null ) ); expectedRows.add( @@ -1004,7 +1014,8 @@ public void testServersTable() throws URISyntaxException 0L, 0L, 0L, - startTimeStr + startTimeStr, + null ) ); expectedRows.add( @@ -1018,7 +1029,8 @@ public void testServersTable() throws URISyntaxException 0L, 0L, nonLeader, - startTimeStr + startTimeStr, + null ) ); expectedRows.add( @@ -1032,7 +1044,8 @@ public void testServersTable() throws URISyntaxException 0L, 0L, nonLeader, - startTimeStr + startTimeStr, + null ) ); expectedRows.add(createExpectedRow( @@ -1045,7 +1058,8 @@ public void testServersTable() throws URISyntaxException 0L, 1000L, nonLeader, - startTimeStr + startTimeStr, + null )); Assert.assertEquals(expectedRows.size(), rows.size()); for (int i = 0; i < rows.size(); i++) { @@ -1078,10 +1092,11 @@ private Object[] createExpectedRow( @Nullable Long currSize, @Nullable Long maxSize, @Nullable Long isLeader, - String startTime + String startTime, + Map labels ) { - return new Object[]{ + return new Object[] { server, host, (long) plaintextPort, @@ -1091,7 +1106,8 @@ private Object[] createExpectedRow( currSize, maxSize, isLeader, - startTime + startTime, + labels }; } @@ -1516,6 +1532,9 @@ private static void verifyTypes(final List rows, final RowSignature si case STRING: expectedClass = String.class; break; + case COMPLEX: + expectedClass = Map.class; + break; default: throw new IAE("Don't know what class to expect for valueType[%s]", columnType); } From adb0f924523228face2ab483a46d37696d51aac8 Mon Sep 17 00:00:00 2001 From: Gabriel Chang <77312579+GabrielCWT@users.noreply.github.com> Date: Tue, 23 Sep 2025 15:40:42 +0800 Subject: [PATCH 09/22] Revert Intellij change --- server/src/main/java/org/apache/druid/server/DruidNode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1f4763521e65..fb11ef30a880 100644 --- a/server/src/main/java/org/apache/druid/server/DruidNode.java +++ b/server/src/main/java/org/apache/druid/server/DruidNode.java @@ -66,7 +66,7 @@ public class DruidNode @Deprecated @JsonProperty @Max(0xffff) - private final int port = -1; + private int port = -1; @JsonProperty @Max(0xffff) From 34e565639b8d2a691e151cf46b70f3f1f7079d14 Mon Sep 17 00:00:00 2001 From: Gabriel Chang <77312579+GabrielCWT@users.noreply.github.com> Date: Tue, 23 Sep 2025 18:28:30 +0800 Subject: [PATCH 10/22] Update spelling --- website/.spelling | 1 + 1 file changed, 1 insertion(+) diff --git a/website/.spelling b/website/.spelling index 767ef4249aba..e412d8757598 100644 --- a/website/.spelling +++ b/website/.spelling @@ -2070,6 +2070,7 @@ druid.enableTlsPort druid.indexer.autoscale.workerVersion druid.service druid.storage.disableAcl +druid.labels druid_audit druid_config druid_dataSource From 71fddfb5507c45eed44715ab89a7e75c677c4e2f Mon Sep 17 00:00:00 2001 From: Gabriel Chang <77312579+GabrielCWT@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:23:35 +0800 Subject: [PATCH 11/22] Update IT json --- .../resources/results/auth_test_sys_schema_servers.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 8c4fb14c5206..b43f756a3930 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 @@ -9,7 +9,8 @@ "curr_size": 2208932412, "max_size": 5000000000, "is_leader": null, - "start_time": "0" + "start_time": "0", + "labels": null }, { "server": "%%BROKER%%:8282", @@ -21,6 +22,7 @@ "curr_size": 0, "max_size": 1000000000, "is_leader": null, - "start_time": "0" + "start_time": "0", + "labels": null } ] From 440be3b013d78f1c9b39fc0fce49f8bef7dc7165 Mon Sep 17 00:00:00 2001 From: Gabriel Chang <77312579+GabrielCWT@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:26:47 +0800 Subject: [PATCH 12/22] Fix format for scss --- .../src/react-table/react-table-extra.scss | 9 ++++----- .../src/views/services-view/services-view.scss | 16 ++++++++-------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/web-console/src/react-table/react-table-extra.scss b/web-console/src/react-table/react-table-extra.scss index 49436a067b24..ecb9953330f1 100644 --- a/web-console/src/react-table/react-table-extra.scss +++ b/web-console/src/react-table/react-table-extra.scss @@ -20,12 +20,11 @@ .ReactTable { &.centered-table { - - .rt-th, - .rt-td { - align-content: center; - } + .rt-th, + .rt-td { + align-content: center; } + } .rt-tr { min-height: 38px; diff --git a/web-console/src/views/services-view/services-view.scss b/web-console/src/views/services-view/services-view.scss index a34f43242ddf..53bcac2d48ca 100644 --- a/web-console/src/views/services-view/services-view.scss +++ b/web-console/src/views/services-view/services-view.scss @@ -36,14 +36,14 @@ ul { line-height: 20px; } -.labels-list { - list-style-type: none; - padding-left: 0; - margin: 0; - - li { + .labels-list { + list-style-type: none; + padding-left: 0; margin: 0; - padding: 0; + + li { + margin: 0; + padding: 0; + } } } -} From 0b8e2c21df447337e530d9b036b377fd38466043 Mon Sep 17 00:00:00 2001 From: Gabriel Chang <77312579+GabrielCWT@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:59:31 +0800 Subject: [PATCH 13/22] Update snapshot --- .../__snapshots__/services-view.spec.tsx.snap | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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 329a7fd3562a..2286aac93bc2 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 @@ -58,6 +58,7 @@ exports[`ServicesView renders data 1`] = ` "Max size", "Usage", "Start time", + "Labels", "Detail", ] } @@ -88,7 +89,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} @@ -208,6 +209,15 @@ exports[`ServicesView renders data 1`] = ` "show": true, "width": 200, }, + { + "Cell": [Function], + "Header": "Labels", + "accessor": "labels", + "className": "padded", + "filterable": false, + "show": true, + "width": 200, + }, { "Aggregated": [Function], "Cell": [Function], From fec3d612fb5a6aa96e192859030bcb605e44d488 Mon Sep 17 00:00:00 2001 From: Gabriel Chang <77312579+GabrielCWT@users.noreply.github.com> Date: Wed, 24 Sep 2025 14:59:39 +0800 Subject: [PATCH 14/22] Fix based on comments --- docs/querying/sql-metadata-tables.md | 2 +- .../org/apache/druid/server/DruidNode.java | 10 +- .../sql/calcite/schema/SystemSchema.java | 109 ++++++++++-------- .../sql/calcite/schema/SystemSchemaTest.java | 11 +- website/.spelling | 1 - 5 files changed, 78 insertions(+), 55 deletions(-) diff --git a/docs/querying/sql-metadata-tables.md b/docs/querying/sql-metadata-tables.md index 28a035a1eceb..938477a4caa1 100644 --- a/docs/querying/sql-metadata-tables.md +++ b/docs/querying/sql-metadata-tables.md @@ -236,7 +236,7 @@ Servers table lists all discovered servers in the cluster. |max_size|BIGINT|Max size in bytes this server recommends to assign to segments see [druid.server.maxSize](../configuration/index.md#historical-general-configuration). Only valid for HISTORICAL type, for other types it's 0| |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| -|labels|VARCHAR|Labels for the server see [druid.labels](../configuration/index.md)| +|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/server/src/main/java/org/apache/druid/server/DruidNode.java b/server/src/main/java/org/apache/druid/server/DruidNode.java index fb11ef30a880..c5f4f82132c7 100644 --- a/server/src/main/java/org/apache/druid/server/DruidNode.java +++ b/server/src/main/java/org/apache/druid/server/DruidNode.java @@ -30,6 +30,7 @@ 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; @@ -131,7 +132,7 @@ public DruidNode( @JacksonInject @Named("tlsServicePort") @JsonProperty("tlsPort") Integer tlsPort, @JsonProperty("enablePlaintextPort") Boolean enablePlaintextPort, @JsonProperty("enableTlsPort") boolean enableTlsPort, - @JsonProperty("labels") Map labels + @JsonProperty("labels") @Nullable Map labels ) { init( @@ -217,6 +218,7 @@ private void init( this.labels = labels; } + @Nullable public Map getLabels() { return labels; @@ -346,13 +348,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 @@ -367,6 +370,7 @@ public String toString() ", enablePlaintextPort=" + enablePlaintextPort + ", tlsPort=" + tlsPort + ", enableTlsPort=" + enableTlsPort + + ", labels=" + labels + '}'; } } 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 e3250fb7bad0..bc08b8f0afba 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 @@ -180,7 +180,7 @@ public class SystemSchema extends AbstractSchema .add("max_size", ColumnType.LONG) .add("is_leader", ColumnType.LONG) .add("start_time", ColumnType.STRING) - .add("labels", ColumnType.NESTED_DATA) + .add("labels", ColumnType.STRING) .build(); static final RowSignature SERVER_SEGMENTS_SIGNATURE = RowSignature @@ -245,7 +245,8 @@ public SystemSchema( serverInventoryView, authorizerMapper, overlordClient, - coordinatorClient + coordinatorClient, + jsonMapper ), SERVER_SEGMENTS_TABLE, new ServerSegmentsTable(serverView, authorizerMapper), @@ -526,13 +527,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 +543,7 @@ public ServersTable( this.serverInventoryView = serverInventoryView; this.overlordClient = overlordClient; this.coordinatorClient = coordinatorClient; + this.jsonMapper = jsonMapper; } @Override @@ -627,46 +631,56 @@ 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[]{ - node.getHostAndPortToUse(), - node.getHost(), - (long) node.getPlaintextPort(), - (long) node.getTlsPort(), - StringUtils.toLowerCase(discoveryDruidNode.getNodeRole().toString()), - null, - UNKNOWN_SIZE, - UNKNOWN_SIZE, - null, - toStringOrNull(discoveryDruidNode.getStartTime()), - node.getLabels() - }; + try { + return new Object[]{ + node.getHostAndPortToUse(), + node.getHost(), + (long) node.getPlaintextPort(), + (long) node.getTlsPort(), + StringUtils.toLowerCase(discoveryDruidNode.getNodeRole().toString()), + null, + UNKNOWN_SIZE, + UNKNOWN_SIZE, + null, + toStringOrNull(discoveryDruidNode.getStartTime()), + node.getLabels() == null ? null : jsonMapper.writeValueAsString(node.getLabels()) + }; + } + catch (JsonProcessingException e) { + throw new RuntimeException(e); + } } /** * 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 ) { final DruidNode node = discoveryDruidNode.getDruidNode(); - return new Object[]{ - node.getHostAndPortToUse(), - node.getHost(), - (long) node.getPlaintextPort(), - (long) node.getTlsPort(), - StringUtils.toLowerCase(discoveryDruidNode.getNodeRole().toString()), - null, - UNKNOWN_SIZE, - UNKNOWN_SIZE, - isLeader ? 1L : 0L, - toStringOrNull(discoveryDruidNode.getStartTime()), - node.getLabels() - }; + try { + return new Object[]{ + node.getHostAndPortToUse(), + node.getHost(), + (long) node.getPlaintextPort(), + (long) node.getTlsPort(), + StringUtils.toLowerCase(discoveryDruidNode.getNodeRole().toString()), + null, + UNKNOWN_SIZE, + UNKNOWN_SIZE, + isLeader ? 1L : 0L, + toStringOrNull(discoveryDruidNode.getStartTime()), + node.getLabels() == null ? null : jsonMapper.writeValueAsString(node.getLabels()) + }; + } + catch (JsonProcessingException e) { + throw new RuntimeException(e); + } } /** @@ -674,7 +688,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 ) @@ -690,19 +704,24 @@ private static Object[] buildRowForDiscoverableDataServer( } else { currentSize = serverFromInventoryView.getCurrSize(); } - return new Object[]{ - node.getHostAndPortToUse(), - node.getHost(), - (long) node.getPlaintextPort(), - (long) node.getTlsPort(), - StringUtils.toLowerCase(discoveryDruidNode.getNodeRole().toString()), - druidServerToUse.getTier(), - currentSize, - druidServerToUse.getMaxSize(), - null, - toStringOrNull(discoveryDruidNode.getStartTime()), - node.getLabels() - }; + try { + return new Object[]{ + node.getHostAndPortToUse(), + node.getHost(), + (long) node.getPlaintextPort(), + (long) node.getTlsPort(), + StringUtils.toLowerCase(discoveryDruidNode.getNodeRole().toString()), + druidServerToUse.getTier(), + currentSize, + druidServerToUse.getMaxSize(), + null, + toStringOrNull(discoveryDruidNode.getStartTime()), + node.getLabels() == null ? null : jsonMapper.writeValueAsString(node.getLabels()) + }; + } + catch (JsonProcessingException e) { + throw new RuntimeException(e); + } } private static boolean isDiscoverableDataServer(DataNodeService dataNodeService) 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 35385b69f8af..e18691c43b8c 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 @@ -755,7 +755,8 @@ public void testServersTable() throws URISyntaxException serverInventoryView, authMapper, overlordClient, - coordinatorClient + coordinatorClient, + MAPPER ) .createMock(); EasyMock.replay(serversTable); @@ -955,7 +956,7 @@ public void testServersTable() throws URISyntaxException 0L, nonLeader, startTimeStr, - ImmutableMap.of("brokerKey", "brokerValue", "brokerKey2", "brokerValue2") + "{\"brokerKey\":\"brokerValue\",\"brokerKey2\":\"brokerValue2\"}" ) ); expectedRows.add( @@ -985,7 +986,7 @@ public void testServersTable() throws URISyntaxException 0L, 1L, startTimeStr, - ImmutableMap.of("overlordKey", "overlordValue") + "{\"overlordKey\":\"overlordValue\"}" ) ); expectedRows.add( @@ -1093,10 +1094,10 @@ private Object[] createExpectedRow( @Nullable Long maxSize, @Nullable Long isLeader, String startTime, - Map labels + String labels ) { - return new Object[] { + return new Object[]{ server, host, (long) plaintextPort, diff --git a/website/.spelling b/website/.spelling index e412d8757598..767ef4249aba 100644 --- a/website/.spelling +++ b/website/.spelling @@ -2070,7 +2070,6 @@ druid.enableTlsPort druid.indexer.autoscale.workerVersion druid.service druid.storage.disableAcl -druid.labels druid_audit druid_config druid_dataSource From 466e00fbba714c6fa00f2d1067499e6465302ed0 Mon Sep 17 00:00:00 2001 From: Gabriel Chang <77312579+GabrielCWT@users.noreply.github.com> Date: Wed, 24 Sep 2025 15:01:24 +0800 Subject: [PATCH 15/22] Add DruidNode tests --- .../apache/druid/server/DruidNodeTest.java | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) 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 a9c675f66895..2563ef079903 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, null)), + mapper.writeValueAsString(new DruidNode("service", "host", true, 1234, null, 5678, true, true, ImmutableMap.of("key1", "value1"))), DruidNode.class ); Assert.assertEquals("service", actual.getServiceName()); @@ -295,6 +307,7 @@ 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 @@ -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, null)), + 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, null), 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()); From 01f49f041d6b2848a394a6e28c349e2d012e59e8 Mon Sep 17 00:00:00 2001 From: Gabriel Chang <77312579+GabrielCWT@users.noreply.github.com> Date: Wed, 24 Sep 2025 16:46:12 +0800 Subject: [PATCH 16/22] Refactor to use JacksonUtils.writeValueAsString --- .../util/common/jackson/JacksonUtils.java | 12 ++++ .../sql/calcite/schema/SystemSchema.java | 63 ++++++++----------- 2 files changed, 39 insertions(+), 36 deletions(-) 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..15d3c5a5c5d3 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.InvalidInput; 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 InvalidInput.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/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 bc08b8f0afba..7e46694e777b 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; @@ -663,24 +664,19 @@ private Object[] buildRowForNonDataServerWithLeadership( ) { final DruidNode node = discoveryDruidNode.getDruidNode(); - try { - return new Object[]{ - node.getHostAndPortToUse(), - node.getHost(), - (long) node.getPlaintextPort(), - (long) node.getTlsPort(), - StringUtils.toLowerCase(discoveryDruidNode.getNodeRole().toString()), - null, - UNKNOWN_SIZE, - UNKNOWN_SIZE, - isLeader ? 1L : 0L, - toStringOrNull(discoveryDruidNode.getStartTime()), - node.getLabels() == null ? null : jsonMapper.writeValueAsString(node.getLabels()) - }; - } - catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + return new Object[]{ + node.getHostAndPortToUse(), + node.getHost(), + (long) node.getPlaintextPort(), + (long) node.getTlsPort(), + StringUtils.toLowerCase(discoveryDruidNode.getNodeRole().toString()), + null, + UNKNOWN_SIZE, + UNKNOWN_SIZE, + isLeader ? 1L : 0L, + toStringOrNull(discoveryDruidNode.getStartTime()), + node.getLabels() == null ? null : JacksonUtils.writeValueAsString(jsonMapper, node.getLabels()) + }; } /** @@ -704,24 +700,19 @@ private Object[] buildRowForDiscoverableDataServer( } else { currentSize = serverFromInventoryView.getCurrSize(); } - try { - return new Object[]{ - node.getHostAndPortToUse(), - node.getHost(), - (long) node.getPlaintextPort(), - (long) node.getTlsPort(), - StringUtils.toLowerCase(discoveryDruidNode.getNodeRole().toString()), - druidServerToUse.getTier(), - currentSize, - druidServerToUse.getMaxSize(), - null, - toStringOrNull(discoveryDruidNode.getStartTime()), - node.getLabels() == null ? null : jsonMapper.writeValueAsString(node.getLabels()) - }; - } - catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + return new Object[]{ + node.getHostAndPortToUse(), + node.getHost(), + (long) node.getPlaintextPort(), + (long) node.getTlsPort(), + StringUtils.toLowerCase(discoveryDruidNode.getNodeRole().toString()), + druidServerToUse.getTier(), + currentSize, + druidServerToUse.getMaxSize(), + null, + toStringOrNull(discoveryDruidNode.getStartTime()), + node.getLabels() == null ? null : JacksonUtils.writeValueAsString(jsonMapper, node.getLabels()) + }; } private static boolean isDiscoverableDataServer(DataNodeService dataNodeService) From b722389675b2296ec4aedfcbf3af4ace71fbe6b2 Mon Sep 17 00:00:00 2001 From: Gabriel Chang <77312579+GabrielCWT@users.noreply.github.com> Date: Wed, 24 Sep 2025 16:46:38 +0800 Subject: [PATCH 17/22] Remove unused case for columnType --- .../org/apache/druid/sql/calcite/schema/SystemSchemaTest.java | 3 --- 1 file changed, 3 deletions(-) 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 e18691c43b8c..a1cac1a1593f 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 @@ -1533,9 +1533,6 @@ private static void verifyTypes(final List rows, final RowSignature si case STRING: expectedClass = String.class; break; - case COMPLEX: - expectedClass = Map.class; - break; default: throw new IAE("Don't know what class to expect for valueType[%s]", columnType); } From ff9bed7190e733957b22d6fdbeb16fa4bc811bcb Mon Sep 17 00:00:00 2001 From: Gabriel Chang <77312579+GabrielCWT@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:28:18 +0800 Subject: [PATCH 18/22] Update services-view to expect string return for labels --- web-console/src/views/services-view/services-view.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web-console/src/views/services-view/services-view.tsx b/web-console/src/views/services-view/services-view.tsx index 13dd8e535726..abbce95366dd 100644 --- a/web-console/src/views/services-view/services-view.tsx +++ b/web-console/src/views/services-view/services-view.tsx @@ -145,7 +145,7 @@ interface ServiceResultRow { readonly plaintext_port: number; readonly tls_port: number; readonly start_time: string; - readonly labels: Record | null; + readonly labels: string | null; } interface ServicesWithAuxiliaryInfo { @@ -629,14 +629,14 @@ ORDER BY className: 'padded', filterable: false, width: 200, - Cell: ({ value }: { value: Record | null }) => { + Cell: ({ value }: { value: string | null }) => { if (!value) return ''; return (
      - {Object.entries(value).map(([key, val]) => { + {Object.entries(JSON.parse(value)).map(([key, val]) => { return (
    • - {key}: {val} + {key}: {String(val)}
    • ); })} From 8ae4dfda6b9b9a6f4c2af02729242c53969b866f Mon Sep 17 00:00:00 2001 From: Gabriel Chang <77312579+GabrielCWT@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:57:00 +0800 Subject: [PATCH 19/22] Throw InternalServerError instead of InvalidInput --- .../apache/druid/java/util/common/jackson/JacksonUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 15d3c5a5c5d3..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 @@ -31,7 +31,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializerProvider; import org.apache.druid.error.DruidException; -import org.apache.druid.error.InvalidInput; +import org.apache.druid.error.InternalServerError; import org.apache.druid.java.util.common.ISE; import javax.annotation.Nullable; @@ -127,7 +127,7 @@ public static String writeValueAsString(ObjectMapper jsonMapper, Object value) t return jsonMapper.writeValueAsString(value); } catch (JsonProcessingException e) { - throw InvalidInput.exception(e, "Failed to serialize object as JSON"); + throw InternalServerError.exception(e, "Failed to serialize object as JSON"); } } From 89d4d191dfa4735b4fd0e036576cbffdbfc96777 Mon Sep 17 00:00:00 2001 From: Gabriel Chang <77312579+GabrielCWT@users.noreply.github.com> Date: Thu, 25 Sep 2025 10:35:23 +0800 Subject: [PATCH 20/22] Update snapshot and format --- .../__snapshots__/services-view.spec.tsx.snap | 14 +++++++------- .../src/views/services-view/services-view.tsx | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) 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 40d10b8520d0..47a96449358d 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 @@ -211,19 +211,19 @@ exports[`ServicesView renders data 1`] = ` "width": 200, }, { + "Aggregated": [Function], "Cell": [Function], - "Header": "Labels", - "accessor": "labels", - "className": "padded", - "filterable": false, + "Header": "Version", + "accessor": "version", "show": true, "width": 200, }, { - "Aggregated": [Function], "Cell": [Function], - "Header": "Version", - "accessor": "version", + "Header": "Labels", + "accessor": "labels", + "className": "padded", + "filterable": false, "show": true, "width": 200, }, diff --git a/web-console/src/views/services-view/services-view.tsx b/web-console/src/views/services-view/services-view.tsx index 664b7a7e6792..da69afb6c0b1 100644 --- a/web-console/src/views/services-view/services-view.tsx +++ b/web-console/src/views/services-view/services-view.tsx @@ -655,7 +655,7 @@ ORDER BY })}
    ); - } + }, }, { Header: 'Detail', From b03bc3d1702c5e35eeac04d4b0dbc9fe4dfb3261 Mon Sep 17 00:00:00 2001 From: Gabriel Chang <77312579+GabrielCWT@users.noreply.github.com> Date: Fri, 26 Sep 2025 14:44:54 +0800 Subject: [PATCH 21/22] Update test with new row size --- .../org/apache/druid/sql/calcite/schema/SystemSchemaTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 003a20f28fc1..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 @@ -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()); } From 5cda0527a5b59249dc3c9ab25c63d4b0a62c2e8e Mon Sep 17 00:00:00 2001 From: Gabriel Chang <77312579+GabrielCWT@users.noreply.github.com> Date: Wed, 8 Oct 2025 11:12:04 +0800 Subject: [PATCH 22/22] Hide labels during aggregation --- .../services-view/__snapshots__/services-view.spec.tsx.snap | 1 + web-console/src/views/services-view/services-view.tsx | 1 + 2 files changed, 2 insertions(+) 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 47a96449358d..e4fb6336b0f5 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 @@ -219,6 +219,7 @@ exports[`ServicesView renders data 1`] = ` "width": 200, }, { + "Aggregated": [Function], "Cell": [Function], "Header": "Labels", "accessor": "labels", diff --git a/web-console/src/views/services-view/services-view.tsx b/web-console/src/views/services-view/services-view.tsx index da69afb6c0b1..2c7e8a7db0d9 100644 --- a/web-console/src/views/services-view/services-view.tsx +++ b/web-console/src/views/services-view/services-view.tsx @@ -656,6 +656,7 @@ ORDER BY
); }, + Aggregated: () => '', }, { Header: 'Detail',