This is an automated email from the ASF dual-hosted git repository. imbajin pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/hugegraph-toolchain.git
commit e0a9547339869b653cf4d2b86a6be3a86668b9cb Author: dark <[email protected]> AuthorDate: Sat Aug 29 23:30:16 2026 +0800 fix(hubble): align distributed health signals - derive PD health from PD nodes only - reconcile Store direct probes with topology - keep source summaries, facts, and cards consistent --- .../hugegraph/service/HugeClientPoolService.java | 10 + .../service/op/LiveOperationsCollector.java | 315 +++++++++++++++++---- .../service/op/LiveOperationsCollectorTest.java | 124 +++++++- .../src/i18n/resources/en-US/modules/pages.json | 4 +- .../src/i18n/resources/zh-CN/modules/pages.json | 4 +- .../src/pages/Operations/Overview.test.js | 3 +- .../hubble-fe/src/pages/Operations/components.js | 12 +- .../src/pages/Operations/components.test.js | 11 +- 8 files changed, 403 insertions(+), 80 deletions(-) diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/HugeClientPoolService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/HugeClientPoolService.java index bb92abc6e..b627b2d32 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/HugeClientPoolService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/HugeClientPoolService.java @@ -117,6 +117,16 @@ public final class HugeClientPoolService { return getOrCreate(null, graphSpace, graph, token); } + public List<String> discoveredServerURLs() { + return new ArrayList<>(this.allAvailableURLs(null, null)); + } + + public HugeClient createDiscoveredServerClient(String url, + String authContext, + int timeout) { + return this.create(url, null, null, authContext, null, null, timeout); + } + public HugeClient getOrCreate(String url, String graphSpace, String graph, String token) { // 去掉缓存,固定每个 request 分配一个 client diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/LiveOperationsCollector.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/LiveOperationsCollector.java index c21f1be62..5d2dd9dc4 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/LiveOperationsCollector.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/LiveOperationsCollector.java @@ -48,6 +48,7 @@ import org.springframework.stereotype.Service; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.service.HugeClientPoolService; import org.apache.hugegraph.service.op.OperationsModels.Node; import org.apache.hugegraph.service.op.OperationsModels.MetricStatus; import org.apache.hugegraph.service.op.OperationsModels.Snapshot; @@ -78,9 +79,11 @@ public class LiveOperationsCollector implements OperationsCollector { private final ExecutorService storeExecutor; private final int storeDeadlineMillis; private final Set<String> storeAllowedTargets; + private final ServerClientProvider serverClients; @Autowired - public LiveOperationsCollector(HugeConfig config, ObjectMapper mapper) { + public LiveOperationsCollector(HugeConfig config, ObjectMapper mapper, + HugeClientPoolService clientPool) { this(config.get(HubbleOptions.PD_ENABLED), pdBase(config.get(HubbleOptions.SERVER_PROTOCOL), config.get(HubbleOptions.PD_SERVER)), @@ -98,7 +101,8 @@ public class LiveOperationsCollector implements OperationsCollector { config.get(HubbleOptions.OPERATIONS_STORE_THREADS), config.get(HubbleOptions.OPERATIONS_STORE_DEADLINE), new java.util.LinkedHashSet<>(config.get( - HubbleOptions.OPERATIONS_STORE_ALLOWED_TARGETS))); + HubbleOptions.OPERATIONS_STORE_ALLOWED_TARGETS)), + serverClients(clientPool)); } LiveOperationsCollector(boolean pdEnabled, String pdBase, @@ -108,7 +112,7 @@ public class LiveOperationsCollector implements OperationsCollector { OperationsPayloadParser parser, Clock clock) { this(pdEnabled, pdBase, pdUsername, pdPassword, storeUsername, storePassword, serverIdentity, http, parser, clock, 16, 5000, - defaultStoreAllowedTargets()); + defaultStoreAllowedTargets(), null); } LiveOperationsCollector(boolean pdEnabled, String pdBase, @@ -119,7 +123,7 @@ public class LiveOperationsCollector implements OperationsCollector { int storeThreads, int storeDeadlineMillis) { this(pdEnabled, pdBase, pdUsername, pdPassword, storeUsername, storePassword, serverIdentity, http, parser, clock, storeThreads, - storeDeadlineMillis, defaultStoreAllowedTargets()); + storeDeadlineMillis, defaultStoreAllowedTargets(), null); } LiveOperationsCollector(boolean pdEnabled, String pdBase, @@ -129,6 +133,19 @@ public class LiveOperationsCollector implements OperationsCollector { OperationsPayloadParser parser, Clock clock, int storeThreads, int storeDeadlineMillis, Set<String> storeAllowedTargets) { + this(pdEnabled, pdBase, pdUsername, pdPassword, storeUsername, + storePassword, serverIdentity, http, parser, clock, storeThreads, + storeDeadlineMillis, storeAllowedTargets, null); + } + + LiveOperationsCollector(boolean pdEnabled, String pdBase, + String pdUsername, String pdPassword, + String storeUsername, String storePassword, + String serverIdentity, OperationsHttpClient http, + OperationsPayloadParser parser, Clock clock, + int storeThreads, int storeDeadlineMillis, + Set<String> storeAllowedTargets, + ServerClientProvider serverClients) { if (storeThreads <= 0 || storeDeadlineMillis <= 0) { throw new IllegalArgumentException( "Store metric collection limits must be positive"); @@ -140,6 +157,7 @@ public class LiveOperationsCollector implements OperationsCollector { this.storeUsername = storeUsername; this.storePassword = storePassword; this.serverIdentity = serverIdentity; + this.serverClients = serverClients; this.http = http; this.parser = parser; this.clock = clock; @@ -192,45 +210,178 @@ public class LiveOperationsCollector implements OperationsCollector { private void collectServer(HugeClient client, boolean includeMetrics, long now, Map<String, SourceStatus> sources, List<Node> nodes) { + List<String> urls = this.discoveredServerURLs(); + if (urls.isEmpty()) { + this.collectSingleServer(client, this.serverIdentity, + "HugeGraph Server", includeMetrics, now, + sources, nodes); + return; + } + int up = 0; + boolean partial = false; + String reason = null; + String authContext = client.getAuthContext(); + List<Future<ServerResult>> futures; try { - String version = client.versionManager().getCoreVersion(); - Map<String, Object> metrics = Collections.emptyMap(); - Map<String, MetricStatus> metricStatuses = Collections.emptyMap(); - String availability = "AVAILABLE"; - String reason = null; - if (includeMetrics) { - metrics = new LinkedHashMap<>(); - metricStatuses = new LinkedHashMap<>(); - try { - Map<String, Object> system = this.safeSystemMetrics( - client.metrics().system()); - metrics.put("system", system); - metricStatuses.put("system", availableMetric(now)); - } catch (RuntimeException e) { - availability = "PARTIAL"; - reason = metricReason(e); - metricStatuses.put("system", metricStatus(e, now, false)); - } - try { - metrics.put("backend", this.safeBackendMetrics( - client.metrics().backend())); - metricStatuses.put("backend", availableMetric(now)); - } catch (RuntimeException e) { - availability = "PARTIAL"; - reason = mergeReason(reason, metricReason(e)); - metricStatuses.put("backend", metricStatus(e, now, false)); - } + futures = this.storeExecutor.invokeAll( + urls.stream().map(url -> + (java.util.concurrent.Callable<ServerResult>) () -> + this.collectDiscoveredServer( + url, authContext, includeMetrics, now)) + .collect(Collectors.toList()), + this.storeDeadlineMillis, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + sources.put("server", unavailable("upstream_interrupted", now)); + return; + } + for (int i = 0; i < urls.size(); i++) { + ServerResult result; + String url = urls.get(i); + try { + result = futures.get(i).get(); + } catch (CancellationException e) { + result = ServerResult.failure(url, now, "upstream_deadline"); + } catch (ExecutionException e) { + result = ServerResult.failure(url, now, + "upstream_unavailable"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + result = ServerResult.failure(url, now, + "upstream_interrupted"); + } + nodes.add(result.getNode()); + if ("UP".equals(result.getNode().getStatus())) { + up++; + } + if (result.isPartial()) { + partial = true; + reason = mergeServerReason(reason, result.getReason()); } - String id = stableId("server", this.serverIdentity); - nodes.add(new Node(id, "SERVER", "HugeGraph Server", null, - version, "UP", now, metrics, metricStatuses)); - sources.put("server", new SourceStatus(availability, "UP", now, - now, true, false, reason)); + } + String status = up == urls.size() ? "UP" : + up == 0 ? "DOWN" : "DEGRADED"; + String availability = up == 0 ? "UNAVAILABLE" : + partial ? "PARTIAL" : "AVAILABLE"; + sources.put("server", new SourceStatus( + availability, status, now, up > 0 ? now : null, + up > 0 && !partial, false, reason)); + } + + private ServerResult collectDiscoveredServer(String url, + String authContext, + boolean includeMetrics, + long now) { + HugeClient server = null; + try { + server = this.serverClients.create( + url, authContext, + Math.max(1, this.storeDeadlineMillis / 1000)); + return this.serverResult( + server, url, serverName(url), includeMetrics, now); + } catch (RuntimeException e) { + return ServerResult.failure(url, now, metricReason(e)); + } finally { + if (server != null) { + server.close(); + } + } + } + + private List<String> discoveredServerURLs() { + if (!this.pdEnabled || this.serverClients == null) { + return Collections.emptyList(); + } + return this.serverClients.urls().stream() + .filter(url -> url != null && !url.trim().isEmpty()) + .distinct() + .collect(Collectors.toList()); + } + + private static ServerClientProvider serverClients( + HugeClientPoolService clientPool) { + return new ServerClientProvider() { + @Override + public List<String> urls() { + return clientPool.discoveredServerURLs(); + } + + @Override + public HugeClient create(String url, String authContext, + int timeout) { + return clientPool.createDiscoveredServerClient( + url, authContext, timeout); + } + }; + } + + private void collectSingleServer(HugeClient client, String identity, + String name, boolean includeMetrics, + long now, + Map<String, SourceStatus> sources, + List<Node> nodes) { + try { + ServerResult result = this.serverResult(client, identity, name, + includeMetrics, now); + nodes.add(result.getNode()); + sources.put("server", new SourceStatus( + result.isPartial() ? "PARTIAL" : "AVAILABLE", + "UP", now, now, !result.isPartial(), false, + result.getReason())); } catch (RuntimeException e) { sources.put("server", unavailable("upstream_unavailable", now)); } } + private ServerResult serverResult(HugeClient client, String identity, + String name, boolean includeMetrics, + long now) { + String version = client.versionManager().getCoreVersion(); + Map<String, Object> metrics = Collections.emptyMap(); + Map<String, MetricStatus> metricStatuses = Collections.emptyMap(); + boolean partial = false; + String reason = null; + if (includeMetrics) { + metrics = new LinkedHashMap<>(); + metricStatuses = new LinkedHashMap<>(); + try { + Map<String, Object> system = this.safeSystemMetrics( + client.metrics().system()); + metrics.put("system", system); + metricStatuses.put("system", availableMetric(now)); + } catch (RuntimeException e) { + partial = true; + reason = metricReason(e); + metricStatuses.put("system", metricStatus(e, now, false)); + } + try { + metrics.put("backend", this.safeBackendMetrics( + client.metrics().backend())); + metricStatuses.put("backend", availableMetric(now)); + } catch (RuntimeException e) { + partial = true; + reason = mergeServerReason(reason, metricReason(e)); + metricStatuses.put("backend", metricStatus(e, now, false)); + } + } + Node node = new Node(stableId("server", identity), "SERVER", name, + null, version, "UP", now, metrics, + metricStatuses); + return new ServerResult(node, partial, reason); + } + + private static String mergeServerReason(String current, String addition) { + if (current == null || current.equals(addition)) { + return addition; + } + return "server_metrics_partial"; + } + + private static String serverName(String url) { + return "HugeGraph Server " + + stableId("server", url).substring("server-".length()); + } + private void collectPd(boolean includeMetrics, long now, Map<String, SourceStatus> sources, List<Node> nodes, Map<String, Long> facts) { @@ -259,11 +410,8 @@ public class LiveOperationsCollector implements OperationsCollector { now); this.mergeNodes(nodes, topology.getNodes()); facts.putAll(topology.getFacts()); - pdStatus = available(this.moreSevereStatus( - topology.getStatus(), - this.nodeStatus(topology.getNodes(), "PD")), - now, - topology.getReason()); + pdStatus = available(this.nodeStatus(topology.getNodes(), "PD"), + now); clusterParsed = true; } catch (MalformedUpstreamException e) { pdStatus = malformed(now); @@ -289,6 +437,9 @@ public class LiveOperationsCollector implements OperationsCollector { if (includeMetrics && storesParsed) { storesStatus = this.collectStoreMetrics(stores, now, storesStatus, nodes); + storesStatus = withStatus(storesStatus, + this.nodeStatus(nodes, "STORE")); + this.reconcileStoreFacts(nodes, facts); } else if (includeMetrics) { this.applyStoreMetricStatus(nodes, this.metricStatus(storesStatus, now)); @@ -321,19 +472,6 @@ public class LiveOperationsCollector implements OperationsCollector { return "DEGRADED"; } - private String moreSevereStatus(String first, String second) { - if ("DOWN".equals(first) || "DOWN".equals(second)) { - return "DOWN"; - } - if ("DEGRADED".equals(first) || "DEGRADED".equals(second)) { - return "DEGRADED"; - } - if ("UNKNOWN".equals(first) || "UNKNOWN".equals(second)) { - return "UNKNOWN"; - } - return "UP"; - } - private void reconcileStoreFacts(List<Node> nodes, Map<String, Long> facts) { long stores = nodes.stream() @@ -576,8 +714,17 @@ public class LiveOperationsCollector implements OperationsCollector { statuses.put(group, metricStatus(e, now, true)); } } - return new StoreMetricResult(copyNode(job.getNode(), metrics, statuses), - successfulGroups, failureReason); + Node node = copyNode(job.getNode(), metrics, statuses); + if (successfulGroups == 0 && directStoreFailure(failureReason)) { + node = copyNodeWithStatus(node, "DOWN"); + } + return new StoreMetricResult(node, successfulGroups, failureReason); + } + + private static boolean directStoreFailure(String reason) { + return "upstream_unavailable".equals(reason) || + "upstream_timeout".equals(reason) || + "upstream_deadline".equals(reason); } private static StoreTarget storeTarget(String nodeId, @@ -695,6 +842,21 @@ public class LiveOperationsCollector implements OperationsCollector { node.getObservedAt(), metrics, statuses); } + private static Node copyNodeWithStatus(Node node, String status) { + return new Node(node.getId(), node.getType(), node.getName(), + node.getRole(), node.getVersion(), status, + node.getObservedAt(), node.getMetrics(), + node.getMetricStatuses()); + } + + private static SourceStatus withStatus(SourceStatus source, + String status) { + return new SourceStatus(source.getAvailability(), status, + source.getObservedAt(), + source.getLastSuccessAt(), source.isFresh(), + source.isStale(), source.getReason()); + } + private String get(String path) { URI target = URI.create(this.pdBase + path); return this.http.get(target, this.pdUsername, this.pdPassword); @@ -835,6 +997,47 @@ public class LiveOperationsCollector implements OperationsCollector { return "upstream_unavailable"; } + private static final class ServerResult { + + private final Node node; + private final boolean partial; + private final String reason; + + private ServerResult(Node node, boolean partial, String reason) { + this.node = node; + this.partial = partial; + this.reason = reason; + } + + private Node getNode() { + return this.node; + } + + private boolean isPartial() { + return this.partial; + } + + private String getReason() { + return this.reason; + } + + private static ServerResult failure(String url, long now, + String reason) { + Node node = new Node(stableId("server", url), "SERVER", + serverName(url), null, null, "DOWN", now, + Collections.emptyMap(), + Collections.emptyMap()); + return new ServerResult(node, true, reason); + } + } + + interface ServerClientProvider { + + List<String> urls(); + + HugeClient create(String url, String authContext, int timeout); + } + private static final class StoreTarget { private final URI uri; diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/LiveOperationsCollectorTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/LiveOperationsCollectorTest.java index 91160cbc9..9869b963e 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/LiveOperationsCollectorTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/LiveOperationsCollectorTest.java @@ -24,6 +24,7 @@ import java.nio.charset.StandardCharsets; import java.time.Clock; import java.time.Instant; import java.time.ZoneOffset; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; @@ -81,6 +82,46 @@ public class LiveOperationsCollectorTest { Mockito.verify(client, Mockito.never()).metrics(); } + @Test + public void testPdModeCollectsEveryDiscoveredServer() + throws IOException { + HttpServer pd = pdServer(200, cluster(), 200, stores()); + LiveOperationsCollector.ServerClientProvider servers = + new LiveOperationsCollector.ServerClientProvider() { + @Override + public java.util.List<String> urls() { + return Arrays.asList("http://server-a:8080", + "http://server-b:8080"); + } + + @Override + public HugeClient create(String url, String authContext, + int timeout) { + return serverClient(); + } + }; + HugeClient requestClient = serverClient(); + Mockito.when(requestClient.getAuthContext()).thenReturn("Bearer token"); + LiveOperationsCollector collector = collector(true, pd, servers); + + Snapshot snapshot; + try { + snapshot = collector.collect(requestClient, false); + } finally { + collector.close(); + pd.stop(0); + } + + Assert.assertEquals(2L, snapshot.getNodes().stream() + .filter(node -> "SERVER".equals(node.getType())) + .count()); + Assert.assertEquals("UP", snapshot.getSources().get("server") + .getStatus()); + Assert.assertEquals("AVAILABLE", + snapshot.getSources().get("server") + .getAvailability()); + } + @Test public void testNonPdModeIsDownWhenOnlySupportedSourceFails() { Snapshot snapshot = collector(false, null).collect( @@ -162,7 +203,8 @@ public class LiveOperationsCollectorTest { } @Test - public void testPdDegradedStatusMakesOverallSnapshotDegraded() throws IOException { + public void testClusterWarningDoesNotOverrideHealthyPdNodes() + throws IOException { String degraded = cluster().replace("Cluster_OK", "Cluster_Warn"); HttpServer pd = pdServer(200, degraded, 200, stores()); Snapshot snapshot; @@ -172,12 +214,13 @@ public class LiveOperationsCollectorTest { pd.stop(0); } - Assert.assertEquals("DEGRADED", snapshot.getStatus()); - Assert.assertEquals("DEGRADED", snapshot.getSources().get("pd").getStatus()); + Assert.assertEquals("UP", snapshot.getStatus()); + Assert.assertEquals("UP", snapshot.getSources().get("pd").getStatus()); } @Test - public void testPdNotReadyKeepsPdNodesUpAndOverallDegraded() throws IOException { + public void testPdNotReadyKeepsPdSourceAlignedWithNodes() + throws IOException { String notReady = cluster().replace("Cluster_OK", "Cluster_Not_Ready"); HttpServer pd = pdServer(200, notReady, 200, stores()); Snapshot snapshot; @@ -190,9 +233,9 @@ public class LiveOperationsCollectorTest { OperationsModels.SourceStatus pdSource = snapshot.getSources().get("pd"); Assert.assertEquals("AVAILABLE", pdSource.getAvailability()); Assert.assertTrue(pdSource.isFresh()); - Assert.assertEquals("DEGRADED", pdSource.getStatus()); - Assert.assertEquals("cluster_not_ready", pdSource.getReason()); - Assert.assertEquals("DEGRADED", snapshot.getStatus()); + Assert.assertEquals("UP", pdSource.getStatus()); + Assert.assertNull(pdSource.getReason()); + Assert.assertEquals("UP", snapshot.getStatus()); long pdCount = snapshot.getNodes().stream() .filter(node -> "PD".equals(node.getType())) .count(); @@ -260,7 +303,8 @@ public class LiveOperationsCollectorTest { } @Test - public void testPdNotReadyReasonSurvivesMetricsFailure() throws IOException { + public void testPdMetricsFailureDoesNotChangeHealthyNodeStatus() + throws IOException { String notReady = cluster().replace("Cluster_OK", "Cluster_Not_Ready"); HttpServer pd = HttpServer.create(new InetSocketAddress(0), 0); String storePayload = stores().replace( @@ -279,12 +323,13 @@ public class LiveOperationsCollectorTest { OperationsModels.SourceStatus source = snapshot.getSources().get("pd"); Assert.assertEquals("PARTIAL", source.getAvailability()); - Assert.assertEquals("DEGRADED", source.getStatus()); - Assert.assertEquals("cluster_not_ready", source.getReason()); + Assert.assertEquals("UP", source.getStatus()); + Assert.assertEquals("upstream_rejected", source.getReason()); } @Test - public void testPdUnknownStatusMakesOverallSnapshotDegraded() throws IOException { + public void testUnknownClusterStateDoesNotOverrideHealthyPdNodes() + throws IOException { String unknown = cluster().replace("Cluster_OK", "Cluster_Starting"); HttpServer pd = pdServer(200, unknown, 200, stores()); Snapshot snapshot; @@ -294,8 +339,8 @@ public class LiveOperationsCollectorTest { pd.stop(0); } - Assert.assertEquals("DEGRADED", snapshot.getStatus()); - Assert.assertEquals("UNKNOWN", snapshot.getSources().get("pd").getStatus()); + Assert.assertEquals("UP", snapshot.getStatus()); + Assert.assertEquals("UP", snapshot.getSources().get("pd").getStatus()); } @Test @@ -305,7 +350,7 @@ public class LiveOperationsCollectorTest { Snapshot snapshot = collector.collect(serverClient(), false); - Assert.assertEquals("DEGRADED", snapshot.getSources().get("pd").getStatus()); + Assert.assertEquals("UP", snapshot.getSources().get("pd").getStatus()); Assert.assertEquals(0, http.leaderRequests()); } @@ -354,6 +399,34 @@ public class LiveOperationsCollectorTest { Assert.assertEquals(Long.valueOf(2000L), pdNode.getObservedAt()); } + @Test + public void testDirectMetricFailureOverridesStaleStoreRegistration() { + RecordingHttpClient http = new RecordingHttpClient( + storesWithDifferentRestAddresses(), + targets("127.0.0.1:8520", "127.0.0.1:9520")); + http.unavailableAuthority("127.0.0.1:9520"); + LiveOperationsCollector collector = collector(http, 4, 1000); + Snapshot snapshot; + try { + snapshot = collector.collect(serverClient(), true); + } finally { + collector.close(); + } + + Assert.assertEquals("UP", + snapshot.getSources().get("pd").getStatus()); + Assert.assertEquals("DEGRADED", + snapshot.getSources().get("stores").getStatus()); + Assert.assertEquals(Long.valueOf(2L), + snapshot.getFacts().get("stores")); + Assert.assertEquals(Long.valueOf(1L), + snapshot.getFacts().get("stores_up")); + Assert.assertEquals(1L, snapshot.getNodes().stream() + .filter(node -> "STORE".equals(node.getType())) + .filter(node -> "DOWN".equals(node.getStatus())) + .count()); + } + @Test public void testMalformedStoresKeepValidPdTopologyAndFacts() throws IOException { @@ -818,6 +891,18 @@ public class LiveOperationsCollectorTest { "http://127.0.0.1:" + pd.getAddress().getPort())); } + private static LiveOperationsCollector collector( + boolean pdEnabled, HttpServer pd, + LiveOperationsCollector.ServerClientProvider servers) { + String pdBase = "http://127.0.0.1:" + pd.getAddress().getPort(); + return new LiveOperationsCollector( + pdEnabled, pdBase, "hubble", "secret", "store-hubble", + "store-secret", "server-under-test", + new OperationsHttpClient(1000, 1000, 8192), + new OperationsPayloadParser(new ObjectMapper()), CLOCK, + 16, 5000, Collections.singleton(pdBase), servers); + } + private static LiveOperationsCollector collector(RecordingHttpClient http, int threads, int deadlineMillis) { @@ -992,6 +1077,7 @@ public class LiveOperationsCollectorTest { private final Set<String> metricAuthorities; private volatile long delayMillis; private volatile String delayAuthority; + private volatile String unavailableAuthority; private RecordingHttpClient(String stores, String targets) { super(1000, 1000, 8192); @@ -1036,7 +1122,11 @@ public class LiveOperationsCollectorTest { return "process_uptime_seconds{hg=\"pd\"} 12\n"; } this.metrics.incrementAndGet(); - this.metricAuthorities.add(OperationsHttpClient.authority(target)); + String authority = OperationsHttpClient.authority(target); + this.metricAuthorities.add(authority); + if (authority.equals(this.unavailableAuthority)) { + throw new UpstreamRequestException("upstream_unavailable"); + } int current = this.active.incrementAndGet(); this.maximum.accumulateAndGet(current, Math::max); try { @@ -1069,6 +1159,10 @@ public class LiveOperationsCollectorTest { this.delayMillis = delayMillis; } + private void unavailableAuthority(String authority) { + this.unavailableAuthority = authority; + } + private int metricRequests() { return this.metrics.get(); } diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json index 4fd812f84..bee87bfe0 100644 --- a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json @@ -1291,12 +1291,12 @@ "fresh": "Fresh", "observed_at": "Observed", "sources": "Source freshness", - "topology": "Service topology", + "topology": "HugeGraph Cluster Topology", "topology_view": "Topology", "node_list_view": "Node list", "cluster_nodes": "Cluster nodes", "logical_relationship": "Logical service relationship, not network traffic", - "topology_label": "Server, PD, and Store service topology", + "topology_label": "HugeGraph cluster topology", "attention_nodes": "Nodes needing attention", "attention_items": "Items needing attention", "all_nodes_healthy": "All discovered nodes are healthy", diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/pages.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/pages.json index e6ea0732b..855b6adfd 100644 --- a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/pages.json +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/pages.json @@ -1291,12 +1291,12 @@ "fresh": "数据新鲜", "observed_at": "观测时间", "sources": "来源新鲜度", - "topology": "服务拓扑图", + "topology": "HugeGraph 集群拓扑", "topology_view": "拓扑图", "node_list_view": "节点列表", "cluster_nodes": "集群节点", "logical_relationship": "表示逻辑服务关系,不表示网络流量", - "topology_label": "Server、PD 与 Store 服务拓扑", + "topology_label": "HugeGraph 集群拓扑", "attention_nodes": "需关注的节点", "attention_items": "需关注项", "all_nodes_healthy": "已发现的节点均健康", diff --git a/hugegraph-hubble/hubble-fe/src/pages/Operations/Overview.test.js b/hugegraph-hubble/hubble-fe/src/pages/Operations/Overview.test.js index c3fe5fa9b..54f777df9 100644 --- a/hugegraph-hubble/hubble-fe/src/pages/Operations/Overview.test.js +++ b/hugegraph-hubble/hubble-fe/src/pages/Operations/Overview.test.js @@ -355,7 +355,8 @@ test('uses explicit Chinese topology labels and a compact monitoring tool status renderOverview(); expect(await screen.findByRole('radio', {name: '拓扑图'})).toBeInTheDocument(); - expect(screen.getByRole('heading', {name: '服务拓扑图'})).toBeInTheDocument(); + expect(screen.getByRole('heading', {name: 'HugeGraph 集群拓扑'})) + .toBeInTheDocument(); expect(screen.getByRole('link', {name: 'Server 层'})).toBeInTheDocument(); const tools = document.querySelector('.operations-header-tools'); expect(tools).not.toHaveTextContent('Dashboard 不可用'); diff --git a/hugegraph-hubble/hubble-fe/src/pages/Operations/components.js b/hugegraph-hubble/hubble-fe/src/pages/Operations/components.js index 036d78954..2bd2c70b7 100644 --- a/hugegraph-hubble/hubble-fe/src/pages/Operations/components.js +++ b/hugegraph-hubble/hubble-fe/src/pages/Operations/components.js @@ -61,6 +61,13 @@ const NODE_TYPE_LABELS = { const displayNodeType = type => NODE_TYPE_LABELS[type] ?? type ?? '—'; +const displayTopologyNodeName = node => { + if (node?.type !== 'SERVER') { + return node?.name; + } + return node.name?.replace(/^HugeGraph Server\b/, 'Server'); +}; + const displayHealthStatus = (status, t) => ( status === 'DEGRADED' ? t('operations.status_degraded') : status ); @@ -269,6 +276,7 @@ const nodeRoleLabel = (node, t) => { const TierNode = ({node, returnState}) => { const {t} = useTranslation(); + const name = displayTopologyNodeName(node); return ( <Link className={[ @@ -279,12 +287,12 @@ const TierNode = ({node, returnState}) => { ].filter(Boolean).join(' ')} to={`/operations/nodes/${node.id}`} state={returnState} - aria-label={`${node.type} ${node.name} ${nodeRoleLabel(node, t)} ${ + aria-label={`${node.type} ${name} ${nodeRoleLabel(node, t)} ${ displayHealthStatus(node.status, t)}`} > <TierIcon type={node.type} /> <span className='operations-node-copy'> - <strong>{node.name}</strong> + <strong>{name}</strong> <span> {node.type === 'PD' && node.role === 'LEADER' && ( <CrownOutlined diff --git a/hugegraph-hubble/hubble-fe/src/pages/Operations/components.test.js b/hugegraph-hubble/hubble-fe/src/pages/Operations/components.test.js index e3152cf89..562647b91 100644 --- a/hugegraph-hubble/hubble-fe/src/pages/Operations/components.test.js +++ b/hugegraph-hubble/hubble-fe/src/pages/Operations/components.test.js @@ -32,7 +32,7 @@ test('localizes the topology accessible name', async () => { </MemoryRouter> ); - expect(screen.getByLabelText('Server、PD 与 Store 服务拓扑')).toBeInTheDocument(); + expect(screen.getByLabelText('HugeGraph 集群拓扑')).toBeInTheDocument(); expect(screen.queryByLabelText('Server PD Store topology')).not.toBeInTheDocument(); }); @@ -73,7 +73,12 @@ test('uses semantic tier icons and keeps the PD leader on the visual axis', () = render( <MemoryRouter future={{v7_startTransition: true, v7_relativeSplatPath: true}}> <ClusterTopology nodes={[ - {id: 'server-1', name: 'server-1', type: 'SERVER', status: 'UP'}, + { + id: 'server-1', + name: 'HugeGraph Server 123abc', + type: 'SERVER', + status: 'UP', + }, {id: 'pd-2', name: 'pd-2', type: 'PD', status: 'UP', role: 'FOLLOWER'}, {id: 'pd-1', name: 'pd-1', type: 'PD', status: 'UP', role: 'LEADER'}, {id: 'store-1', name: 'store-1', type: 'STORE', status: 'UP'}, @@ -83,6 +88,8 @@ test('uses semantic tier icons and keeps the PD leader on the visual axis', () = ); expect(screen.getByLabelText('SERVER icon')).toBeInTheDocument(); + expect(screen.getByText('Server 123abc')).toBeInTheDocument(); + expect(screen.queryByText('HugeGraph Server 123abc')).not.toBeInTheDocument(); expect(screen.getAllByLabelText('PD icon')).toHaveLength(2); expect(screen.getByLabelText('STORE icon')).toBeInTheDocument(); expect(screen.getByText('pd-1').closest('a')).toHaveClass('is-axis-node');
