You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2021/09/18 04:02:46 UTC
[skywalking] branch master updated: Support dynamic configurations
for openAPI endpoint name grouping rule (#7746)
This is an automated email from the ASF dual-hosted git repository.
wusheng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking.git
The following commit(s) were added to refs/heads/master by this push:
new fc7db29 Support dynamic configurations for openAPI endpoint name grouping rule (#7746)
fc7db29 is described below
commit fc7db29b2e28017e5c74a6c2c5e87cf9f521f428
Author: wankai123 <wa...@foxmail.com>
AuthorDate: Sat Sep 18 12:02:35 2021 +0800
Support dynamic configurations for openAPI endpoint name grouping rule (#7746)
---
CHANGES.md | 1 +
docs/en/setup/backend/dynamic-config.md | 6 +-
docs/en/setup/backend/endpoint-grouping-rules.md | 4 +
.../oap/server/core/CoreModuleProvider.java | 10 +-
.../EndpointGroupingRuleReader4Openapi.java | 96 ++++++--
.../EndpointNameGroupingRule4OpenapiWatcher.java | 62 +++++
...ndpointNameGroupingRule4OpenapiWatcherTest.java | 261 +++++++++++++++++++++
7 files changed, 411 insertions(+), 29 deletions(-)
diff --git a/CHANGES.md b/CHANGES.md
index d9c7e13..52b6d1a 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -68,6 +68,7 @@ Release Notes.
* Fix `LogHandler` of `kafka-fetcher-plugin` cannot recognize namespace.
* Improve the speed of writing TiDB by batching the SQL execution.
* Fix wrong service name when IP is node IP in `k8s-mesh`.
+* Support dynamic configurations for openAPI endpoint name grouping rule.
#### UI
diff --git a/docs/en/setup/backend/dynamic-config.md b/docs/en/setup/backend/dynamic-config.md
index 6cc46fa..b233bcf 100755
--- a/docs/en/setup/backend/dynamic-config.md
+++ b/docs/en/setup/backend/dynamic-config.md
@@ -40,7 +40,7 @@ Supported configurations are as follows:
|configuration-discovery.default.agentConfigurations| The ConfigurationDiscovery settings. | See [`configuration-discovery.md`](https://github.com/apache/skywalking-java/blob/20fb8c81b3da76ba6628d34c12d23d3d45c973ef/docs/en/setup/service-agent/java-agent/configuration-discovery.md). |
## Group Configuration
-Single Configuration is a config key that corresponds to a group sub config items. A sub config item is a key value pair. The logic structure is:
+Group Configuration is a config key that corresponds to a group sub config items. A sub config item is a key value pair. The logic structure is:
```
{configKey}: |{subItemkey1}:{subItemValue1}
|{subItemkey2}:{subItemValue2}
@@ -56,6 +56,10 @@ For example:
```
Supported configurations are as follows:
+| Config Key | SubItem Key Description | Value Description | Value Format Example |
+|:----:|:----:|:----:|:----:|
+|core.default.endpoint-name-grouping-openapi|The serviceName relevant to openAPI definition file. eg. `serviceA`. If the serviceName relevant to multiple files should add subItems for each files, and each subItem key should split serviceName and fileName with `.` eg. `serviceA.API-file1`,`serviceA.API-file2` |The openAPI definitions file contents(yaml format) for create endpoint name grouping rules.|Same as [`productAPI-v2.yaml`](endpoint-grouping-rules.md)|
+
## Dynamic Configuration Implementations
- [Dynamic Configuration Service, DCS](./dynamic-config-service.md)
- [Zookeeper Implementation](./dynamic-config-zookeeper.md)
diff --git a/docs/en/setup/backend/endpoint-grouping-rules.md b/docs/en/setup/backend/endpoint-grouping-rules.md
index 681ec9e..1802b33 100644
--- a/docs/en/setup/backend/endpoint-grouping-rules.md
+++ b/docs/en/setup/backend/endpoint-grouping-rules.md
@@ -285,6 +285,10 @@ Here are some use cases:
| `GET:/products/123` | serviceB | default | default | `${PATH}:<${METHOD}>` | true | `/products/{id}:<GET>` |
| `/products/123:<GET>` | serviceB | default | `${PATH}:<${METHOD}>` | default | true | `GET:/products/{id}` |
+### Initialize and update the OpenAPI definitions dynamically
+Use [Dynamic Configuration](dynamic-config) to initialize and update OpenAPI definitions, the endpoint grouping rules from OpenAPI
+will re-create by new config.
+
## Endpoint name grouping by custom configuration
Currently, a user could set up grouping rules through the static YAML file named `endpoint-name-grouping.yml`,
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/CoreModuleProvider.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/CoreModuleProvider.java
index edd7084..87fd5a3 100755
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/CoreModuleProvider.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/CoreModuleProvider.java
@@ -48,7 +48,7 @@ import org.apache.skywalking.oap.server.core.config.IComponentLibraryCatalogServ
import org.apache.skywalking.oap.server.core.config.NamingControl;
import org.apache.skywalking.oap.server.core.config.group.EndpointNameGrouping;
import org.apache.skywalking.oap.server.core.config.group.EndpointNameGroupingRuleWatcher;
-import org.apache.skywalking.oap.server.core.config.group.openapi.EndpointGroupingRuleReader4Openapi;
+import org.apache.skywalking.oap.server.core.config.group.openapi.EndpointNameGroupingRule4OpenapiWatcher;
import org.apache.skywalking.oap.server.core.logging.LoggingConfigWatcher;
import org.apache.skywalking.oap.server.core.management.ui.template.UITemplateInitializer;
import org.apache.skywalking.oap.server.core.management.ui.template.UITemplateManagementService;
@@ -123,6 +123,7 @@ public class CoreModuleProvider extends ModuleProvider {
private EndpointNameGroupingRuleWatcher endpointNameGroupingRuleWatcher;
private OALEngineLoaderService oalEngineLoaderService;
private LoggingConfigWatcher loggingConfigWatcher;
+ private EndpointNameGroupingRule4OpenapiWatcher endpointNameGroupingRule4OpenapiWatcher;
public CoreModuleProvider() {
super();
@@ -164,8 +165,8 @@ public class CoreModuleProvider extends ModuleProvider {
this, endpointNameGrouping);
if (moduleConfig.isEnableEndpointNameGroupingByOpenapi()) {
- endpointNameGrouping.setEndpointGroupingRule4Openapi(
- new EndpointGroupingRuleReader4Openapi("openapi-definitions").read());
+ endpointNameGroupingRule4OpenapiWatcher = new EndpointNameGroupingRule4OpenapiWatcher(
+ this, endpointNameGrouping);
}
} catch (FileNotFoundException e) {
throw new ModuleStartException(e.getMessage(), e);
@@ -354,6 +355,9 @@ public class CoreModuleProvider extends ModuleProvider {
dynamicConfigurationService.registerConfigChangeWatcher(apdexThresholdConfig);
dynamicConfigurationService.registerConfigChangeWatcher(endpointNameGroupingRuleWatcher);
dynamicConfigurationService.registerConfigChangeWatcher(loggingConfigWatcher);
+ if (moduleConfig.isEnableEndpointNameGroupingByOpenapi()) {
+ dynamicConfigurationService.registerConfigChangeWatcher(endpointNameGroupingRule4OpenapiWatcher);
+ }
}
@Override
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/config/group/openapi/EndpointGroupingRuleReader4Openapi.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/config/group/openapi/EndpointGroupingRuleReader4Openapi.java
index ab945e7..366f3fe 100644
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/config/group/openapi/EndpointGroupingRuleReader4Openapi.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/config/group/openapi/EndpointGroupingRuleReader4Openapi.java
@@ -22,6 +22,8 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.Reader;
+import java.io.StringReader;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
@@ -32,8 +34,7 @@ import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
public class EndpointGroupingRuleReader4Openapi {
-
- private final String openapiDefPath;
+ private final Map<String, /*serviceName*/ List<Map>/*openapiData*/> serviceOpenapiDefMap;
private final static String DEFAULT_ENDPOINT_NAME_FORMAT = "${METHOD}:${PATH}";
private final static String DEFAULT_ENDPOINT_NAME_MATCH_RULE = "${METHOD}:${PATH}";
private final Map<String, String> requestMethodsMap = new HashMap<String, String>() {
@@ -49,51 +50,87 @@ public class EndpointGroupingRuleReader4Openapi {
}
};
- public EndpointGroupingRuleReader4Openapi(final String openapiDefPath) {
+ public EndpointGroupingRuleReader4Openapi(final String openapiDefPath) throws FileNotFoundException {
+ this.serviceOpenapiDefMap = this.parseFromDir(openapiDefPath);
+ }
- this.openapiDefPath = openapiDefPath;
+ public EndpointGroupingRuleReader4Openapi(final Map<String, String> openapiDefsConf) {
+ this.serviceOpenapiDefMap = this.parseFromDynamicConf(openapiDefsConf);
}
- public EndpointGroupingRule4Openapi read() throws FileNotFoundException {
+ public EndpointGroupingRule4Openapi read() {
EndpointGroupingRule4Openapi endpointGroupingRule = new EndpointGroupingRule4Openapi();
-
- List<File> fileList = ResourceUtils.getDirectoryFilesRecursive(openapiDefPath, 1);
- for (File file : fileList) {
- if (!file.getName().endsWith(".yaml")) {
- continue;
- }
- Reader reader = new FileReader(file);
- Yaml yaml = new Yaml(new SafeConstructor());
- Map openapiData = yaml.load(reader);
- if (openapiData != null) {
- String serviceName = getServiceName(openapiData, file);
+ serviceOpenapiDefMap.forEach((serviceName, openapiDefs) -> {
+ openapiDefs.forEach(openapiData -> {
LinkedHashMap<String, LinkedHashMap<String, LinkedHashMap>> paths =
- (LinkedHashMap<String, LinkedHashMap<String, LinkedHashMap>>) openapiData.get("paths");
-
+ (LinkedHashMap<String, LinkedHashMap<String, LinkedHashMap>>) openapiData.get(
+ "paths");
if (paths != null) {
paths.forEach((pathString, pathItem) -> {
pathItem.keySet().forEach(key -> {
String requestMethod = requestMethodsMap.get(key);
if (!StringUtil.isEmpty(requestMethod)) {
- String endpointGroupName = formatEndPointName(pathString, requestMethod, openapiData);
- String groupRegex = getGroupRegex(pathString, requestMethod, openapiData);
+ String endpointGroupName = formatEndPointName(
+ pathString, requestMethod, openapiData);
+ String groupRegex = getGroupRegex(
+ pathString, requestMethod, openapiData);
if (isTemplatePath(pathString)) {
- endpointGroupingRule.addGroupedRule(serviceName, endpointGroupName, groupRegex);
+ endpointGroupingRule.addGroupedRule(
+ serviceName, endpointGroupName, groupRegex);
} else {
- endpointGroupingRule.addDirectLookup(serviceName, groupRegex, endpointGroupName);
+ endpointGroupingRule.addDirectLookup(
+ serviceName, groupRegex, endpointGroupName);
}
}
});
});
}
- }
- }
+ });
+ });
endpointGroupingRule.sortRulesAll();
return endpointGroupingRule;
}
- private String getServiceName(Map openapiData, File file) {
+ private Map<String, List<Map>> parseFromDir(String openapiDefPath) throws FileNotFoundException {
+ Map<String, List<Map>> serviceOpenapiDefMap = new HashMap<>();
+ List<File> fileList = ResourceUtils.getDirectoryFilesRecursive(openapiDefPath, 1);
+ for (File file : fileList) {
+ if (!file.getName().endsWith(".yaml")) {
+ continue;
+ }
+ Reader reader = new FileReader(file);
+ Yaml yaml = new Yaml(new SafeConstructor());
+ Map openapiData = yaml.load(reader);
+ if (openapiData != null) {
+ serviceOpenapiDefMap.computeIfAbsent(getServiceName(openapiDefPath, file, openapiData), k -> new ArrayList<>()).add(openapiData);
+ }
+ }
+
+ return serviceOpenapiDefMap;
+ }
+ private Map<String, List<Map>> parseFromDynamicConf(final Map<String, String> openapiDefsConf) {
+ Map<String, List<Map>> serviceOpenapiDefMap = new HashMap<>();
+ openapiDefsConf.forEach((itemName, openapiDefs) -> {
+ String serviceName = itemName;
+ //service map to multiple openapiDefs
+ String[] itemNameInfo = itemName.split("\\.");
+ if (itemNameInfo.length > 1) {
+ serviceName = itemNameInfo[0];
+ }
+ Reader reader = new StringReader(openapiDefs);
+ Yaml yaml = new Yaml(new SafeConstructor());
+ Map openapiData = yaml.load(reader);
+ if (openapiData != null) {
+ serviceOpenapiDefMap.computeIfAbsent(getServiceName(serviceName, openapiData), k -> new ArrayList<>())
+ .add(openapiData);
+ }
+ });
+
+ return serviceOpenapiDefMap;
+ }
+
+ private String getServiceName(String openapiDefPath, File file, Map openapiData) {
String serviceName = (String) openapiData.get("x-sw-service-name");
if (StringUtil.isEmpty(serviceName)) {
File directory = new File(file.getParent());
@@ -107,6 +144,15 @@ public class EndpointGroupingRuleReader4Openapi {
return serviceName;
}
+ private String getServiceName(String defaultServiceName, Map openapiData) {
+ String serviceName = (String) openapiData.get("x-sw-service-name");
+ if (StringUtil.isEmpty(serviceName)) {
+ serviceName = defaultServiceName;
+ }
+
+ return serviceName;
+ }
+
private boolean isTemplatePath(String pathString) {
return pathString.matches("(.*)\\{(.+?)}(.*)");
}
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/config/group/openapi/EndpointNameGroupingRule4OpenapiWatcher.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/config/group/openapi/EndpointNameGroupingRule4OpenapiWatcher.java
new file mode 100644
index 0000000..f562b53
--- /dev/null
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/config/group/openapi/EndpointNameGroupingRule4OpenapiWatcher.java
@@ -0,0 +1,62 @@
+/*
+ * 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.skywalking.oap.server.core.config.group.openapi;
+
+import java.io.FileNotFoundException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.skywalking.oap.server.configuration.api.GroupConfigChangeWatcher;
+import org.apache.skywalking.oap.server.core.CoreModule;
+import org.apache.skywalking.oap.server.core.config.group.EndpointNameGrouping;
+import org.apache.skywalking.oap.server.library.module.ModuleProvider;
+
+@Slf4j
+public class EndpointNameGroupingRule4OpenapiWatcher extends GroupConfigChangeWatcher {
+ private final EndpointNameGrouping grouping;
+ private final Map<String, String> openapiDefs;
+
+ public EndpointNameGroupingRule4OpenapiWatcher(ModuleProvider provider,
+ EndpointNameGrouping grouping) throws FileNotFoundException {
+ super(CoreModule.NAME, provider, "endpoint-name-grouping-openapi");
+ this.grouping = grouping;
+ this.openapiDefs = new ConcurrentHashMap<>();
+ this.grouping.setEndpointGroupingRule4Openapi(
+ new EndpointGroupingRuleReader4Openapi("openapi-definitions").read());
+ }
+
+ @Override
+ public Map<String, String> groupItems() {
+ return openapiDefs;
+ }
+
+ @Override
+ public void notifyGroup(final Map<String, ConfigChangeEvent> groupItems) {
+ groupItems.forEach((groupItemName, event) -> {
+ if (EventType.DELETE.equals(event.getEventType())) {
+ this.openapiDefs.remove(groupItemName);
+ log.info("EndpointNameGroupingRule4OpenapiWatcher removed groupItem: {}", groupItemName);
+ } else {
+ this.openapiDefs.put(groupItemName, event.getNewValue());
+ log.info("EndpointNameGroupingRule4OpenapiWatcher modified groupItem: {}", groupItemName);
+ }
+ });
+ this.grouping.setEndpointGroupingRule4Openapi(new EndpointGroupingRuleReader4Openapi(openapiDefs).read());
+ }
+}
diff --git a/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/config/group/openapi/EndpointNameGroupingRule4OpenapiWatcherTest.java b/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/config/group/openapi/EndpointNameGroupingRule4OpenapiWatcherTest.java
new file mode 100644
index 0000000..605f9b3
--- /dev/null
+++ b/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/config/group/openapi/EndpointNameGroupingRule4OpenapiWatcherTest.java
@@ -0,0 +1,261 @@
+/*
+ * 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.skywalking.oap.server.core.config.group.openapi;
+
+import java.io.FileNotFoundException;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.skywalking.oap.server.configuration.api.ConfigChangeWatcher;
+import org.apache.skywalking.oap.server.core.CoreModule;
+import org.apache.skywalking.oap.server.core.config.group.EndpointNameGrouping;
+import org.apache.skywalking.oap.server.library.module.ModuleConfig;
+import org.apache.skywalking.oap.server.library.module.ModuleDefine;
+import org.apache.skywalking.oap.server.library.module.ModuleProvider;
+import org.apache.skywalking.oap.server.library.module.ServiceNotProvidedException;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class EndpointNameGroupingRule4OpenapiWatcherTest {
+ @Test
+ public void testWatcher() throws FileNotFoundException {
+ EndpointNameGrouping endpointNameGrouping = new EndpointNameGrouping();
+
+ EndpointNameGroupingRule4OpenapiWatcher watcher = new EndpointNameGroupingRule4OpenapiWatcher(
+ new ModuleProvider() {
+ @Override
+ public String name() {
+ return "test";
+ }
+
+ @Override
+ public Class<? extends ModuleDefine> module() {
+ return CoreModule.class;
+ }
+
+ @Override
+ public ModuleConfig createConfigBeanIfAbsent() {
+ return null;
+ }
+
+ @Override
+ public void prepare() throws ServiceNotProvidedException {
+
+ }
+
+ @Override
+ public void start() throws ServiceNotProvidedException {
+
+ }
+
+ @Override
+ public void notifyAfterCompleted() throws ServiceNotProvidedException {
+
+ }
+
+ @Override
+ public String[] requiredModules() {
+ return new String[0];
+ }
+ }, endpointNameGrouping);
+ Assert.assertEquals("GET:/products/{id}", endpointNameGrouping.format("serviceA", "GET:/products/123"));
+
+ Map<String, ConfigChangeWatcher.ConfigChangeEvent> groupItems = new HashMap<>();
+ groupItems.put(
+ "serviceA.productAPI-v1",
+ new ConfigChangeWatcher
+ .ConfigChangeEvent(
+ "openapi: 3.0.0\n" +
+ "\n" +
+ "info:\n" +
+ " description: OpenAPI definition for SkyWalking test.\n" +
+ " version: v1\n" +
+ " title: Product API\n" +
+ "\n" +
+ "tags:\n" +
+ " - name: product\n" +
+ " description: product\n" +
+ " - name: relatedProducts\n" +
+ " description: Related Products\n" +
+ "\n" +
+ "paths:\n" +
+ " /products:\n" +
+ " get:\n" +
+ " tags:\n" +
+ " - product\n" +
+ " summary: Get all products list\n" +
+ " description: Get all products list.\n" +
+ " operationId: getProducts\n" +
+ " responses:\n" +
+ " \"200\":\n" +
+ " description: Success\n" +
+ " content:\n" +
+ " application/json:\n" +
+ " schema:\n" +
+ " type: array\n" +
+ " items:\n" +
+ " $ref: \"#/components/schemas/Product\"\n" +
+ " /products/{order-id}:\n" + //modified from /products/{id}
+ " get:\n" +
+ " tags:\n" +
+ " - product\n" +
+ " summary: Get product details\n" +
+ " description: Get product details with the given id.\n" +
+ " operationId: getProduct\n" +
+ " parameters:\n" +
+ " - name: id\n" +
+ " in: path\n" +
+ " description: Product id\n" +
+ " required: true\n" +
+ " schema:\n" +
+ " type: integer\n" +
+ " format: int64\n" +
+ " responses:\n" +
+ " \"200\":\n" +
+ " description: successful operation\n" +
+ " content:\n" +
+ " application/json:\n" +
+ " schema:\n" +
+ " $ref: \"#/components/schemas/ProductDetails\"\n" +
+ " \"400\":\n" +
+ " description: Invalid product id\n" +
+ " post:\n" +
+ " tags:\n" +
+ " - product\n" +
+ " summary: Update product details\n" +
+ " description: Update product details with the given id.\n" +
+ " operationId: updateProduct\n" +
+ " parameters:\n" +
+ " - name: id\n" +
+ " in: path\n" +
+ " description: Product id\n" +
+ " required: true\n" +
+ " schema:\n" +
+ " type: integer\n" +
+ " format: int64\n" +
+ " - name: name\n" +
+ " in: query\n" +
+ " description: Product name\n" +
+ " required: true\n" +
+ " schema:\n" +
+ " type: string\n" +
+ " responses:\n" +
+ " \"200\":\n" +
+ " description: successful operation\n" +
+ " delete:\n" +
+ " tags:\n" +
+ " - product\n" +
+ " summary: Delete product details\n" +
+ " description: Delete product details with the given id.\n" +
+ " operationId: deleteProduct\n" +
+ " parameters:\n" +
+ " - name: id\n" +
+ " in: path\n" +
+ " description: Product id\n" +
+ " required: true\n" +
+ " schema:\n" +
+ " type: integer\n" +
+ " format: int64\n" +
+ " responses:\n" +
+ " \"200\":\n" +
+ " description: successful operation\n" +
+ " /products/{id}/relatedProducts:\n" +
+ " get:\n" +
+ " tags:\n" +
+ " - relatedProducts\n" +
+ " summary: Get related products\n" +
+ " description: Get related products with the given product id.\n" +
+ " operationId: getRelatedProducts\n" +
+ " parameters:\n" +
+ " - name: id\n" +
+ " in: path\n" +
+ " description: Product id\n" +
+ " required: true\n" +
+ " schema:\n" +
+ " type: integer\n" +
+ " format: int64\n" +
+ " responses:\n" +
+ " \"200\":\n" +
+ " description: successful operation\n" +
+ " content:\n" +
+ " application/json:\n" +
+ " schema:\n" +
+ " $ref: \"#/components/schemas/RelatedProducts\"\n" +
+ " \"400\":\n" +
+ " description: Invalid product id\n" +
+ "\n" +
+ "components:\n" +
+ " schemas:\n" +
+ " Product:\n" +
+ " type: object\n" +
+ " description: Product id and name\n" +
+ " properties:\n" +
+ " id:\n" +
+ " type: integer\n" +
+ " format: int64\n" +
+ " description: Product id\n" +
+ " name:\n" +
+ " type: string\n" +
+ " description: Product name\n" +
+ " required:\n" +
+ " - id\n" +
+ " - name\n" +
+ " ProductDetails:\n" +
+ " type: object\n" +
+ " description: Product details\n" +
+ " properties:\n" +
+ " id:\n" +
+ " type: integer\n" +
+ " format: int64\n" +
+ " description: Product id\n" +
+ " name:\n" +
+ " type: string\n" +
+ " description: Product name\n" +
+ " description:\n" +
+ " type: string\n" +
+ " description: Product description\n" +
+ " required:\n" +
+ " - id\n" +
+ " - name\n" +
+ " RelatedProducts:\n" +
+ " type: object\n" +
+ " description: Related Products\n" +
+ " properties:\n" +
+ " id:\n" +
+ " type: integer\n" +
+ " format: int32\n" +
+ " description: Product id\n" +
+ " relatedProducts:\n" +
+ " type: array\n" +
+ " description: List of related products\n" +
+ " items:\n" +
+ " $ref: \"#/components/schemas/Product\"",
+ ConfigChangeWatcher.EventType.MODIFY
+ )
+ );
+
+ watcher.notifyGroup(groupItems);
+ Assert.assertEquals("GET:/products/{order-id}", endpointNameGrouping.format("serviceA", "GET:/products/123"));
+
+ groupItems.put("serviceA.productAPI-v1", new ConfigChangeWatcher.ConfigChangeEvent("", ConfigChangeWatcher.EventType.DELETE));
+ watcher.notifyGroup(groupItems);
+
+ Assert.assertEquals("GET:/products/123", endpointNameGrouping.format("serviceA", "GET:/products/123"));
+
+ }
+}