diff --git a/dubbo-demo/dubbo-demo-spring-boot-idl/dubbo-demo-spring-boot-idl-consumer/src/main/resources/application.yml b/dubbo-demo/dubbo-demo-spring-boot-idl/dubbo-demo-spring-boot-idl-consumer/src/main/resources/application.yml index 9cbc17d4d882..86bb7cf40097 100644 --- a/dubbo-demo/dubbo-demo-spring-boot-idl/dubbo-demo-spring-boot-idl-consumer/src/main/resources/application.yml +++ b/dubbo-demo/dubbo-demo-spring-boot-idl/dubbo-demo-spring-boot-idl-consumer/src/main/resources/application.yml @@ -32,4 +32,3 @@ dubbo: metadata-report: address: zookeeper://127.0.0.1:2181 - diff --git a/dubbo-plugin/dubbo-qos/src/main/java/org/apache/dubbo/qos/command/impl/DiscoveryTimelineCommand.java b/dubbo-plugin/dubbo-qos/src/main/java/org/apache/dubbo/qos/command/impl/DiscoveryTimelineCommand.java new file mode 100644 index 000000000000..2c388be2063d --- /dev/null +++ b/dubbo-plugin/dubbo-qos/src/main/java/org/apache/dubbo/qos/command/impl/DiscoveryTimelineCommand.java @@ -0,0 +1,373 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.dubbo.qos.command.impl; + +import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.common.utils.StringUtils; +import org.apache.dubbo.qos.api.BaseCommand; +import org.apache.dubbo.qos.api.Cmd; +import org.apache.dubbo.qos.api.CommandContext; +import org.apache.dubbo.registry.client.ServiceDiscovery; +import org.apache.dubbo.registry.client.ServiceInstance; +import org.apache.dubbo.registry.client.event.ServiceInstancesChangedEvent; +import org.apache.dubbo.registry.client.event.listener.ServiceInstancesChangedListener; +import org.apache.dubbo.registry.support.RegistryManager; +import org.apache.dubbo.rpc.model.ApplicationModel; +import org.apache.dubbo.rpc.model.FrameworkModel; +import org.apache.dubbo.rpc.model.FrameworkServiceRepository; +import org.apache.dubbo.rpc.model.ProviderModel; + +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.apache.dubbo.common.constants.LoggerCodeConstants.CONFIG_PARAMETER_FORMAT_ERROR; +import static org.apache.dubbo.common.constants.LoggerCodeConstants.INTERNAL_ERROR; +import static org.apache.dubbo.common.constants.LoggerCodeConstants.REGISTRY_FAILED_FETCH_INSTANCE; +import static org.apache.dubbo.common.constants.LoggerCodeConstants.REGISTRY_FAILED_LOAD_METADATA; + +@Cmd( + name = "discovery-timeline", + summary = "Show service discovery timeline", + example = { + "discovery-timeline", + "discovery-timeline service=com.example.Service", + "discovery-timeline registry=zookeeper://localhost:2181", + "discovery-timeline page=2", + "discovery-timeline limit=5" + }) +public class DiscoveryTimelineCommand implements BaseCommand { + + private static final ErrorTypeAwareLogger logger = + LoggerFactory.getErrorTypeAwareLogger(DiscoveryTimelineCommand.class); + private final FrameworkModel frameworkModel; + private static final int DEFAULT_LIMIT = 10; + private static final Map globalProviderRegistrationTimes = new HashMap<>(); + private static final String TIMESTAMP_KEY = "timestamp"; + + public DiscoveryTimelineCommand(FrameworkModel frameworkModel) { + this.frameworkModel = frameworkModel; + } + + @Override + public String execute(CommandContext commandContext, String[] args) { + logger.debug("DiscoveryTimelineCommand started"); + + try { + ApplicationModel applicationModel = frameworkModel.defaultApplication(); + if (applicationModel == null) { + throw new IllegalStateException("No ApplicationModel available"); + } + + RegistryManager registryManager = applicationModel.getBeanFactory().getBean(RegistryManager.class); + if (registryManager == null) { + logger.warn(REGISTRY_FAILED_FETCH_INSTANCE, "", "", "RegistryManager not available"); + return "Error: RegistryManager not available. Check configuration."; + } + + List serviceDiscoveries = registryManager.getServiceDiscoveries(); + if (serviceDiscoveries == null || serviceDiscoveries.isEmpty()) { + logger.warn(REGISTRY_FAILED_LOAD_METADATA, "", "", "No ServiceDiscovery found"); + return "Error: No ServiceDiscovery instances found."; + } + + String filterServiceName = null; + String filterRegistry = null; + int page = 1; + int limit = DEFAULT_LIMIT; + if (args != null && args.length > 0) { + for (String arg : args) { + if (arg == null) continue; + if (arg.startsWith("service=")) { + filterServiceName = arg.substring("service=".length()); + } else if (arg.startsWith("registry=")) { + filterRegistry = arg.substring("registry=".length()); + } else if (arg.startsWith("page=")) { + try { + page = Math.max(1, Integer.parseInt(arg.substring("page=".length()))); + } catch (NumberFormatException e) { + logger.warn(CONFIG_PARAMETER_FORMAT_ERROR, "", "", "Invalid page number: " + arg, e); + } + } else if (arg.startsWith("limit=")) { + try { + limit = Math.max(1, Integer.parseInt(arg.substring("limit=".length()))); + } catch (NumberFormatException e) { + logger.warn(CONFIG_PARAMETER_FORMAT_ERROR, "", "", "Invalid limit number: " + arg, e); + } + } + } + } + + final String finalFilterServiceName = filterServiceName; + + Map listeners = new HashMap<>(); + Map providerRegistrationTimes = new HashMap<>(globalProviderRegistrationTimes); + Map> registryServices = new HashMap<>(); + + FrameworkServiceRepository serviceRepository = frameworkModel.getServiceRepository(); + Set uniqueProviderModels = new HashSet<>(serviceRepository.allProviderModels()); + List providerModels = uniqueProviderModels.stream() + .filter(model -> { + String serviceName = model.getServiceKey(); + return serviceName != null + && (finalFilterServiceName == null || serviceName.contains(finalFilterServiceName)); + }) + .sorted((m1, m2) -> { + String key1 = m1.getServiceKey(); + String key2 = m2.getServiceKey(); + String numStr1 = key1.replaceAll("[^0-9]", ""); + String numStr2 = key2.replaceAll("[^0-9]", ""); + if (StringUtils.isEmpty(numStr1) || StringUtils.isEmpty(numStr2)) { + return key1.compareTo(key2); + } + try { + int num1 = Integer.parseInt(numStr1); + int num2 = Integer.parseInt(numStr2); + return Integer.compare(num1, num2); + } catch (NumberFormatException e) { + logger.warn( + CONFIG_PARAMETER_FORMAT_ERROR, + "", + "", + "Failed to parse numbers for sorting: " + numStr1 + " vs " + numStr2, + e); + return key1.compareTo(key2); + } + }) + .collect(Collectors.toList()); + logger.debug( + "Filtered and sorted provider models: {}", + providerModels.stream().map(ProviderModel::getServiceKey).collect(Collectors.toList())); + + boolean hasServices = false; + for (ServiceDiscovery serviceDiscovery : serviceDiscoveries) { + if (filterRegistry != null + && (serviceDiscovery.getUrl() == null + || !serviceDiscovery.getUrl().getAddress().contains(filterRegistry))) { + continue; + } + + Set serviceNames = new HashSet<>(); + ServiceInstance instance = serviceDiscovery.getLocalInstance(); + if (instance == null) { + logger.warn( + REGISTRY_FAILED_FETCH_INSTANCE, + "", + "", + "No local instance found for registry: " + serviceDiscovery.getUrl()); + continue; + } + + Map metadata = instance.getMetadata(); + if (metadata == null || metadata.isEmpty()) { + logger.warn( + REGISTRY_FAILED_LOAD_METADATA, + "", + "", + "No metadata found for instance in registry: " + serviceDiscovery.getUrl()); + metadata = new HashMap<>(); + } + + for (ProviderModel providerModel : providerModels) { + String serviceName = providerModel.getServiceKey(); + if (serviceName != null) { + serviceNames.add(serviceName); + if (!providerRegistrationTimes.containsKey(serviceName)) { + String timestampStr = + metadata.getOrDefault(TIMESTAMP_KEY, String.valueOf(System.currentTimeMillis())); + try { + providerRegistrationTimes.put(serviceName, Long.parseLong(timestampStr)); + logger.debug("Set timestamp for {}: {}", serviceName, timestampStr); + } catch (NumberFormatException e) { + logger.warn( + CONFIG_PARAMETER_FORMAT_ERROR, + "", + "", + "Invalid timestamp format for service: " + serviceName + ", timestamp: " + + timestampStr, + e); + providerRegistrationTimes.put(serviceName, System.currentTimeMillis()); + } + } + } + } + registryServices.put(serviceDiscovery, serviceNames); + CustomServiceInstancesChangedListener listener = new CustomServiceInstancesChangedListener( + serviceNames, serviceDiscovery, providerRegistrationTimes); + serviceDiscovery.addServiceInstancesChangedListener(listener); + listeners.put(serviceDiscovery, listener); + hasServices = true; + logger.debug("Registered services for discovery {}: {}", serviceDiscovery.getUrl(), serviceNames); + } + + for (Map.Entry entry : providerRegistrationTimes.entrySet()) { + globalProviderRegistrationTimes.putIfAbsent(entry.getKey(), entry.getValue()); + } + + if (!hasServices) { + return "Error: No services discovered."; + } + + StringBuilder timeline = new StringBuilder("Discovery Timeline\n"); + timeline.append("------------------------------------------------------------\n"); + timeline.append(String.format("%-30s|%-30s%n", "Registry", "Last Refresh")); + timeline.append("------------------------------------------------------------\n"); + + for (ServiceDiscovery sd : serviceDiscoveries) { + if (filterRegistry != null + && (sd.getUrl() == null || !sd.getUrl().getAddress().contains(filterRegistry))) { + continue; + } + String refreshTimeStr = "Unknown"; + ServiceInstance instance = sd.getLocalInstance(); + if (instance != null + && instance.getMetadata() != null + && instance.getMetadata().containsKey(TIMESTAMP_KEY)) { + String timestampStr = instance.getMetadata() + .getOrDefault(TIMESTAMP_KEY, String.valueOf(System.currentTimeMillis())); + try { + long registryTimestamp = Long.parseLong(timestampStr); + refreshTimeStr = new Date(registryTimestamp).toString(); + } catch (NumberFormatException e) { + logger.warn( + CONFIG_PARAMETER_FORMAT_ERROR, + "", + "", + "Invalid timestamp format for registry: " + + sd.getUrl().getAddress() + ", timestamp: " + timestampStr, + e); + refreshTimeStr = new Date(System.currentTimeMillis()).toString(); + } + } + timeline.append(String.format("%-30s|%-30s%n", sd.getUrl().getAddress(), refreshTimeStr)); + } + + timeline.append("------------------------------------------------------------\n"); + timeline.append("Provider Services\n"); + + int providerStart = (page - 1) * limit; + int providerEnd = Math.min(providerStart + limit, providerModels.size()); + logger.debug( + "Pagination: page={}, limit={}, start={}, end={}, total providers={}", + page, + limit, + providerStart, + providerEnd, + providerModels.size()); + List paginatedProviders = providerModels.subList( + Math.min(providerStart, providerModels.size()), Math.min(providerEnd, providerModels.size())); + logger.debug( + "Paginated providers: {}", + paginatedProviders.stream() + .map(ProviderModel::getServiceKey) + .collect(Collectors.toList())); + + for (ProviderModel providerModel : paginatedProviders) { + String serviceName = providerModel.getServiceKey(); + Long lastRegistrationTime = providerRegistrationTimes.get(serviceName); + String refreshTimeStr = + lastRegistrationTime != null ? new Date(lastRegistrationTime).toString() : "Unknown"; + timeline.append(String.format("%-30s|%-30s%n", "Discovered: " + serviceName, refreshTimeStr)); + } + + timeline.append("------------------------------------------------------------\n"); + String result = timeline.toString(); + logger.debug("Final output:\n{}", result); + return result; + + } catch (Exception e) { + logger.error(INTERNAL_ERROR, "", "", "Failed to generate discovery timeline", e); + return "Error: " + e.getMessage(); + } + } + + private static class CustomServiceInstancesChangedListener extends ServiceInstancesChangedListener { + private final Set serviceNames; + private final Map serviceRegistrationTimes; + private Set previousInstances = new HashSet<>(); + + public CustomServiceInstancesChangedListener( + Set serviceNames, + ServiceDiscovery serviceDiscovery, + Map serviceRegistrationTimes) { + super(serviceNames, serviceDiscovery); + this.serviceNames = serviceNames; + this.serviceRegistrationTimes = serviceRegistrationTimes; + initializeRegistrationTimes(); + } + + private void initializeRegistrationTimes() { + long currentTime = System.currentTimeMillis(); + for (String serviceName : serviceNames) { + if (!serviceRegistrationTimes.containsKey(serviceName)) { + serviceRegistrationTimes.put(serviceName, currentTime); + logger.debug( + "Initialized registration time for {}: {}", serviceName, new Date(currentTime).toString()); + } + } + } + + @Override + public void onEvent(ServiceInstancesChangedEvent event) { + super.onEvent(event); + String serviceName = event.getServiceName(); + Set currentInstances = event.getServiceInstances().stream() + .map(ServiceInstance::getServiceName) + .filter(name -> name != null) + .collect(Collectors.toSet()); + + if (previousInstances.isEmpty() || !previousInstances.equals(currentInstances)) { + serviceNames.add(serviceName); + for (ServiceInstance instance : event.getServiceInstances()) { + String timestampStr = instance.getMetadata() != null + ? instance.getMetadata() + .getOrDefault(TIMESTAMP_KEY, String.valueOf(System.currentTimeMillis())) + : String.valueOf(System.currentTimeMillis()); + if (!serviceRegistrationTimes.containsKey(serviceName)) { + try { + serviceRegistrationTimes.put(serviceName, Long.parseLong(timestampStr)); + logger.debug( + "Updated timestamp for {}: {}", + serviceName, + new Date(Long.parseLong(timestampStr)).toString()); + } catch (NumberFormatException e) { + logger.warn( + CONFIG_PARAMETER_FORMAT_ERROR, + "", + "", + "Invalid timestamp format for service: " + serviceName + ", timestamp: " + + timestampStr, + e); + serviceRegistrationTimes.put(serviceName, System.currentTimeMillis()); + } + } + } + previousInstances = new HashSet<>(currentInstances); + } + logger.debug( + "Event received for service: {}, current instances: {}, registration times: {}", + serviceName, + currentInstances, + serviceRegistrationTimes); + } + } +} diff --git a/dubbo-plugin/dubbo-qos/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.qos.api.BaseCommand b/dubbo-plugin/dubbo-qos/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.qos.api.BaseCommand index 9a336915e1ba..27ccb8adfe2f 100644 --- a/dubbo-plugin/dubbo-qos/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.qos.api.BaseCommand +++ b/dubbo-plugin/dubbo-qos/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.qos.api.BaseCommand @@ -39,3 +39,4 @@ getAddress=org.apache.dubbo.qos.command.impl.GetAddress gracefulShutdown=org.apache.dubbo.qos.command.impl.GracefulShutdown metrics_default=org.apache.dubbo.qos.command.impl.DefaultMetricsReporterCmd getOpenAPI=org.apache.dubbo.qos.command.impl.GetOpenAPI +discovery-timeline=org.apache.dubbo.qos.command.impl.DiscoveryTimelineCommand diff --git a/dubbo-plugin/dubbo-qos/src/test/java/org/apache/dubbo/qos/command/impl/DiscoveryTimelineCommandTest.java b/dubbo-plugin/dubbo-qos/src/test/java/org/apache/dubbo/qos/command/impl/DiscoveryTimelineCommandTest.java new file mode 100644 index 000000000000..810021c30316 --- /dev/null +++ b/dubbo-plugin/dubbo-qos/src/test/java/org/apache/dubbo/qos/command/impl/DiscoveryTimelineCommandTest.java @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.dubbo.qos.command.impl; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.beans.factory.ScopeBeanFactory; +import org.apache.dubbo.common.logger.Logger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.qos.api.CommandContext; +import org.apache.dubbo.registry.client.DefaultServiceInstance; +import org.apache.dubbo.registry.client.ServiceDiscovery; +import org.apache.dubbo.registry.support.RegistryManager; +import org.apache.dubbo.rpc.model.ApplicationModel; +import org.apache.dubbo.rpc.model.FrameworkModel; +import org.apache.dubbo.rpc.model.FrameworkServiceRepository; +import org.apache.dubbo.rpc.model.ProviderModel; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.*; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DiscoveryTimelineCommandTest { + + private static final Logger logger = LoggerFactory.getLogger(DiscoveryTimelineCommandTest.class); + private DiscoveryTimelineCommand command; + private FrameworkModel frameworkModel; + private ApplicationModel applicationModel; + private RegistryManager registryManager; + private ServiceDiscovery serviceDiscovery; + private DefaultServiceInstance serviceInstance; + private FrameworkServiceRepository serviceRepository; + private ScopeBeanFactory beanFactory; + + @BeforeEach + public void setUp() { + frameworkModel = Mockito.mock(FrameworkModel.class); + applicationModel = Mockito.mock(ApplicationModel.class); + registryManager = Mockito.mock(RegistryManager.class); + serviceDiscovery = Mockito.mock(ServiceDiscovery.class); + serviceInstance = Mockito.mock(DefaultServiceInstance.class); + serviceRepository = Mockito.mock(FrameworkServiceRepository.class); + beanFactory = Mockito.mock(ScopeBeanFactory.class); + + command = new DiscoveryTimelineCommand(frameworkModel); + + Mockito.when(frameworkModel.getServiceRepository()).thenReturn(serviceRepository); + Mockito.when(frameworkModel.defaultApplication()).thenReturn(applicationModel); + Mockito.when(applicationModel.getBeanFactory()).thenReturn(beanFactory); + Mockito.when(beanFactory.getBean(RegistryManager.class)).thenReturn(registryManager); + Mockito.when(registryManager.getServiceDiscoveries()).thenReturn(Collections.singletonList(serviceDiscovery)); + Mockito.when(serviceDiscovery.getLocalInstance()).thenReturn(serviceInstance); + Mockito.when(serviceDiscovery.getUrl()).thenReturn(URL.valueOf("zookeeper://127.0.0.1:2181")); + } + + @Test + void testExecuteWithTimestamps() { + ProviderModel providerModel = Mockito.mock(ProviderModel.class); + Mockito.when(providerModel.getServiceKey()).thenReturn("org.apache.dubbo.demo.DemoService"); + Mockito.when(serviceRepository.allProviderModels()).thenReturn(Collections.singletonList(providerModel)); + + long ts = LocalDateTime.of(2025, 7, 27, 17, 54, 0).toEpochSecond(ZoneOffset.ofHours(5)) * 1000L; + Map metadata = new HashMap<>(); + metadata.put("timestamp", String.valueOf(ts)); + Mockito.when(serviceInstance.getMetadata()).thenReturn(metadata); + + String output = command.execute(new CommandContext("discovery-timeline"), null); + logger.info("testExecuteWithTimestamps Output:\n{}", output); + + assertTrue(output.contains("Discovery Timeline")); + assertTrue(output.contains("127.0.0.1:2181")); + assertTrue(output.contains("Discovered: org.apache.dubbo.demo.DemoService")); + } + + @Test + void testExecuteWithoutMetadata() { + ProviderModel providerModel = Mockito.mock(ProviderModel.class); + Mockito.when(providerModel.getServiceKey()).thenReturn("org.apache.dubbo.demo.DemoService"); + Mockito.when(serviceRepository.allProviderModels()).thenReturn(Collections.singletonList(providerModel)); + + Mockito.when(serviceInstance.getMetadata()).thenReturn(null); + + String output = command.execute(new CommandContext("discovery-timeline"), null); + logger.info("testExecuteWithoutMetadata Output:\n{}", output); + + assertTrue(output.contains("Unknown")); + assertTrue(output.contains("Discovered: org.apache.dubbo.demo.DemoService")); + } + + @Test + void testExecuteWithPagination() { + List providers = new ArrayList<>(); + long ts = LocalDateTime.of(2025, 7, 27, 17, 54, 0).toEpochSecond(ZoneOffset.ofHours(5)) * 1000L; + for (int i = 1; i <= 15; i++) { + ProviderModel model = Mockito.mock(ProviderModel.class); + String serviceName = "Service" + i; + Mockito.when(model.getServiceKey()).thenReturn(serviceName); + providers.add(model); + } + Mockito.when(serviceRepository.allProviderModels()).thenReturn(providers); + Map metadata = new HashMap<>(); + metadata.put("timestamp", String.valueOf(ts)); + Mockito.when(serviceInstance.getMetadata()).thenReturn(metadata); + + String output = command.execute(new CommandContext("discovery-timeline"), new String[] {"limit=5", "page=2"}); + logger.info("testExecuteWithPagination Output:\n{}", output); + logger.info( + "Providers mocked: {}", + providers.stream().map(ProviderModel::getServiceKey).collect(Collectors.toList())); + StringBuilder charLog = new StringBuilder("Output characters:\n"); + for (int i = 0; i < output.length(); i++) { + charLog.append(String.format("Index %d: %c (ASCII %d)%n", i, output.charAt(i), (int) output.charAt(i))); + } + logger.info("{}", charLog.toString()); + + assertTrue(output.contains("Discovery Timeline")); + assertTrue(output.contains("Discovered: Service6")); + assertTrue(output.contains("Discovered: Service10")); + } + + @Test + void testExecuteWithServiceFilter() { + ProviderModel providerModel = Mockito.mock(ProviderModel.class); + Mockito.when(providerModel.getServiceKey()).thenReturn("org.apache.dubbo.MyService"); + Mockito.when(serviceRepository.allProviderModels()).thenReturn(Collections.singletonList(providerModel)); + Mockito.when(serviceInstance.getMetadata()).thenReturn(new HashMap<>()); + + String output = command.execute(new CommandContext("discovery-timeline"), new String[] {"service=MyService"}); + logger.info("testExecuteWithServiceFilter Output:\n{}", output); + + assertTrue(output.contains("Discovered: org.apache.dubbo.MyService")); + } + + @Test + void testExecuteWithRegistryFilterMismatch() { + ProviderModel providerModel = Mockito.mock(ProviderModel.class); + Mockito.when(providerModel.getServiceKey()).thenReturn("org.apache.dubbo.MyService"); + Mockito.when(serviceRepository.allProviderModels()).thenReturn(Collections.singletonList(providerModel)); + Mockito.when(serviceInstance.getMetadata()).thenReturn(new HashMap<>()); + + String output = command.execute(new CommandContext("discovery-timeline"), new String[] {"registry=nacos"}); + logger.info("testExecuteWithRegistryFilterMismatch Output:\n{}", output); + + assertTrue(output.contains("Error: No services discovered.")); + } +} diff --git a/dubbo-plugin/dubbo-qos/src/test/java/org/apache/dubbo/qos/command/util/CommandHelperTest.java b/dubbo-plugin/dubbo-qos/src/test/java/org/apache/dubbo/qos/command/util/CommandHelperTest.java index 238b1421dd07..26a232c9363f 100644 --- a/dubbo-plugin/dubbo-qos/src/test/java/org/apache/dubbo/qos/command/util/CommandHelperTest.java +++ b/dubbo-plugin/dubbo-qos/src/test/java/org/apache/dubbo/qos/command/util/CommandHelperTest.java @@ -23,6 +23,7 @@ import org.apache.dubbo.qos.command.impl.DisableDetailProfiler; import org.apache.dubbo.qos.command.impl.DisableRouterSnapshot; import org.apache.dubbo.qos.command.impl.DisableSimpleProfiler; +import org.apache.dubbo.qos.command.impl.DiscoveryTimelineCommand; import org.apache.dubbo.qos.command.impl.EnableDetailProfiler; import org.apache.dubbo.qos.command.impl.EnableRouterSnapshot; import org.apache.dubbo.qos.command.impl.EnableSimpleProfiler; @@ -129,6 +130,7 @@ void testGetAllCommandClass() { expectedClasses.add(GracefulShutdown.class); expectedClasses.add(DefaultMetricsReporterCmd.class); expectedClasses.add(GetOpenAPI.class); + expectedClasses.add(DiscoveryTimelineCommand.class); assertThat(classes, containsInAnyOrder(expectedClasses.toArray(new Class[0]))); }