diff --git a/sentinel-extension/pom.xml b/sentinel-extension/pom.xml index a02832d2e6..b62a50bf87 100755 --- a/sentinel-extension/pom.xml +++ b/sentinel-extension/pom.xml @@ -22,6 +22,7 @@ sentinel-datasource-spring-cloud-config sentinel-datasource-consul sentinel-datasource-etcd + sentinel-datasource-eureka sentinel-annotation-cdi-interceptor diff --git a/sentinel-extension/sentinel-datasource-eureka/README.md b/sentinel-extension/sentinel-datasource-eureka/README.md new file mode 100644 index 0000000000..927b4cf446 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-eureka/README.md @@ -0,0 +1,64 @@ +# Sentinel DataSource Eureka + +Sentinel DataSource Eureka provides integration with [Eureka](https://github.com/Netflix/eureka) so that Eureka +can be the dynamic rule data source of Sentinel. + +To use Sentinel DataSource Eureka, you should add the following dependency: + +```xml + + com.alibaba.csp + sentinel-datasource-eureka + x.y.z + +``` + +Then you can create an `EurekaDataSource` and register to rule managers. + +SDK usage: + +```java +EurekaDataSource> eurekaDataSource = new EurekaDataSource("app-id", "instance-id", + Arrays.asList("http://localhost:8761/eureka", "http://localhost:8762/eureka", "http://localhost:8763/eureka"), + "rule-key", new Converter>() { + @Override + public List convert(String o) { + return JSON.parseObject(o, new TypeReference>() { + }); + } +}); +FlowRuleManager.register2Property(eurekaDataSource.getProperty()); +``` + +Example for Spring Cloud Application: + +```java +@Bean +public EurekaDataSource> eurekaDataSource(EurekaInstanceConfig eurekaInstanceConfig, EurekaClientConfig eurekaClientConfig) { + + List serviceUrls = EndpointUtils.getServiceUrlsFromConfig(eurekaClientConfig, + eurekaInstanceConfig.getMetadataMap().get("zone"), eurekaClientConfig.shouldPreferSameZoneEureka()); + + EurekaDataSource> eurekaDataSource = new EurekaDataSource(eurekaInstanceConfig.getAppname(), + eurekaInstanceConfig.getInstanceId(), serviceUrls, "flowrules", new Converter>() { + @Override + public List convert(String o) { + return JSON.parseObject(o, new TypeReference>() { + }); + } + }); + + FlowRuleManager.register2Property(eurekaDataSource.getProperty()); + return eurekaDataSource; +} + +``` + +To refresh the rule dynamically,you need to call [Eureka-REST-operations](https://github.com/Netflix/eureka/wiki/Eureka-REST-operations) +to update instance metadata: + +``` +PUT /eureka/apps/{appID}/{instanceID}/metadata?{ruleKey}={json of the rules} +``` + +Note: don't forget to encode your json string in the url. \ No newline at end of file diff --git a/sentinel-extension/sentinel-datasource-eureka/pom.xml b/sentinel-extension/sentinel-datasource-eureka/pom.xml new file mode 100644 index 0000000000..666f10e172 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-eureka/pom.xml @@ -0,0 +1,66 @@ + + + + sentinel-extension + com.alibaba.csp + 1.8.0-SNAPSHOT + + 4.0.0 + + sentinel-datasource-eureka + + + 2.1.2.RELEASE + + + + + + com.alibaba.csp + sentinel-datasource-extension + + + + com.alibaba + fastjson + + + + junit + junit + test + + + + org.awaitility + awaitility + test + + + + org.springframework.boot + spring-boot-starter-test + ${spring.cloud.version} + test + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-server + ${spring.cloud.version} + test + + + com.google.code.gson + gson + + + + + + + + + \ No newline at end of file diff --git a/sentinel-extension/sentinel-datasource-eureka/src/main/java/com/alibaba/csp/sentinel/datasource/eureka/EurekaDataSource.java b/sentinel-extension/sentinel-datasource-eureka/src/main/java/com/alibaba/csp/sentinel/datasource/eureka/EurekaDataSource.java new file mode 100644 index 0000000000..8029e8e978 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-eureka/src/main/java/com/alibaba/csp/sentinel/datasource/eureka/EurekaDataSource.java @@ -0,0 +1,213 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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 com.alibaba.csp.sentinel.datasource.eureka; + +import com.alibaba.csp.sentinel.datasource.AutoRefreshDataSource; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.datasource.ReadableDataSource; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.fastjson.JSON; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + *

+ * A {@link ReadableDataSource} based on Eureka. This class will automatically + * fetches the metadata of the instance every period. + *

+ *

+ * Limitations: Default refresh interval is 10s. Because there is synchronization between eureka servers, + * it may take longer to take effect. + *

+ * + * @author: liyang + * @create: 2020-05-23 12:01 + */ +public class EurekaDataSource extends AutoRefreshDataSource { + + private static final long DEFAULT_REFRESH_MS = 10000; + + /** + * connect timeout: 3s + */ + private static final int DEFAULT_CONNECT_TIMEOUT_MS = 3000; + + /** + * read timeout: 30s + */ + private static final int DEFAULT_READ_TIMEOUT_MS = 30000; + + + private int connectTimeoutMills; + + + private int readTimeoutMills; + + /** + * eureka instance appid + */ + private String appId; + /** + * eureka instance id + */ + private String instanceId; + + /** + * collect of eureka server urls + */ + private List serviceUrls; + + /** + * metadata key of the rule source + */ + private String ruleKey; + + + public EurekaDataSource(String appId, String instanceId, List serviceUrls, String ruleKey, + Converter configParser) { + this(appId, instanceId, serviceUrls, ruleKey, configParser, DEFAULT_REFRESH_MS, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_READ_TIMEOUT_MS); + } + + + public EurekaDataSource(String appId, String instanceId, List serviceUrls, String ruleKey, + Converter configParser, long refreshMs, int connectTimeoutMills, + int readTimeoutMills) { + super(configParser, refreshMs); + AssertUtil.notNull(appId, "appId can't be null"); + AssertUtil.notNull(instanceId, "instanceId can't be null"); + AssertUtil.assertNotEmpty(serviceUrls, "serviceUrls can't be empty"); + AssertUtil.notNull(ruleKey, "ruleKey can't be null"); + AssertUtil.assertState(connectTimeoutMills > 0, "connectTimeoutMills must be greater than 0"); + AssertUtil.assertState(readTimeoutMills > 0, "readTimeoutMills must be greater than 0"); + + this.appId = appId; + this.instanceId = instanceId; + this.serviceUrls = ensureEndWithSlash(serviceUrls); + AssertUtil.assertNotEmpty(this.serviceUrls, "No available service url"); + this.ruleKey = ruleKey; + this.connectTimeoutMills = connectTimeoutMills; + this.readTimeoutMills = readTimeoutMills; + } + + + private List ensureEndWithSlash(List serviceUrls) { + List newServiceUrls = new ArrayList<>(); + for (String serviceUrl : serviceUrls) { + if (StringUtil.isBlank(serviceUrl)) { + continue; + } + if (!serviceUrl.endsWith("/")) { + serviceUrl = serviceUrl + "/"; + } + newServiceUrls.add(serviceUrl); + } + return newServiceUrls; + } + + @Override + public String readSource() throws Exception { + return fetchStringSourceFromEurekaMetadata(this.appId, this.instanceId, this.serviceUrls, ruleKey); + } + + + private String fetchStringSourceFromEurekaMetadata(String appId, String instanceId, List serviceUrls, + String ruleKey) throws Exception { + List shuffleUrls = new ArrayList<>(serviceUrls.size()); + shuffleUrls.addAll(serviceUrls); + Collections.shuffle(shuffleUrls); + for (int i = 0; i < shuffleUrls.size(); i++) { + String serviceUrl = shuffleUrls.get(i) + String.format("apps/%s/%s", appId, instanceId); + HttpURLConnection conn = null; + try { + conn = (HttpURLConnection) new URL(serviceUrl).openConnection(); + conn.addRequestProperty("Accept", "application/json;charset=utf-8"); + + conn.setConnectTimeout(connectTimeoutMills); + conn.setReadTimeout(readTimeoutMills); + conn.setRequestMethod("GET"); + conn.setDoOutput(true); + conn.connect(); + RecordLog.debug("[EurekaDataSource] Request from eureka server: " + serviceUrl); + if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { + String s = toString(conn.getInputStream()); + String ruleString = JSON.parseObject(s) + .getJSONObject("instance") + .getJSONObject("metadata") + .getString(ruleKey); + return ruleString; + } + RecordLog.warn("[EurekaDataSource] Warn: retrying on another server if available " + + "due to response code: {}, response message: {}", conn.getResponseCode(), toString(conn.getErrorStream())); + } catch (Exception e) { + try { + if (conn != null) { + RecordLog.warn("[EurekaDataSource] Warn: failed to request " + conn.getURL() + " from " + + InetAddress.getByName(conn.getURL().getHost()).getHostAddress(), e); + } + } catch (Exception e1) { + RecordLog.warn("[EurekaDataSource] Warn: failed to request ", e1); + //ignore + } + RecordLog.warn("[EurekaDataSource] Warn: failed to request,retrying on another server if available"); + + } finally { + if (conn != null) { + conn.disconnect(); + } + } + } + throw new EurekaMetadataFetchException("Can't get any data"); + } + + + public static class EurekaMetadataFetchException extends Exception { + + public EurekaMetadataFetchException(String message) { + super(message); + } + } + + + private String toString(InputStream input) throws IOException { + if (input == null) { + return null; + } + InputStreamReader inputStreamReader = new InputStreamReader(input, "utf-8"); + CharArrayWriter sw = new CharArrayWriter(); + copy(inputStreamReader, sw); + return sw.toString(); + } + + private long copy(Reader input, Writer output) throws IOException { + char[] buffer = new char[1 << 12]; + long count = 0; + for (int n = 0; (n = input.read(buffer)) >= 0; ) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + + +} diff --git a/sentinel-extension/sentinel-datasource-eureka/src/test/java/com/alibaba/csp/sentinel/datasource/eureka/EurekaDataSourceTest.java b/sentinel-extension/sentinel-datasource-eureka/src/test/java/com/alibaba/csp/sentinel/datasource/eureka/EurekaDataSourceTest.java new file mode 100644 index 0000000000..05fef90eea --- /dev/null +++ b/sentinel-extension/sentinel-datasource-eureka/src/test/java/com/alibaba/csp/sentinel/datasource/eureka/EurekaDataSourceTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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 com.alibaba.csp.sentinel.datasource.eureka; + +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.TypeReference; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; + +/** + * @author liyang + */ +@RunWith(SpringRunner.class) +@EnableEurekaServer +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +public class EurekaDataSourceTest { + + private static final String SENTINEL_KEY = "sentinel-rules"; + + @Value("${server.port}") + private int port; + + @Value("${eureka.instance.appname}") + private String appname; + + @Value("${eureka.instance.instance-id}") + private String instanceId; + + + @Test + public void testEurekaDataSource() throws Exception { + String url = "http://localhost:" + port + "/eureka"; + + EurekaDataSource> eurekaDataSource = new EurekaDataSource(appname, instanceId, Arrays.asList(url) + , SENTINEL_KEY, new Converter>() { + @Override + public List convert(String source) { + return JSON.parseObject(source, new TypeReference>() { + }); + } + }); + FlowRuleManager.register2Property(eurekaDataSource.getProperty()); + + await().timeout(15, TimeUnit.SECONDS) + .until(new Callable() { + @Override + public Boolean call() throws Exception { + return FlowRuleManager.getRules().size() > 0; + } + }); + Assert.assertTrue(FlowRuleManager.getRules().size() > 0); + } + + +} diff --git a/sentinel-extension/sentinel-datasource-eureka/src/test/java/com/alibaba/csp/sentinel/datasource/eureka/SimpleSpringApplication.java b/sentinel-extension/sentinel-datasource-eureka/src/test/java/com/alibaba/csp/sentinel/datasource/eureka/SimpleSpringApplication.java new file mode 100644 index 0000000000..872c7ca3fa --- /dev/null +++ b/sentinel-extension/sentinel-datasource-eureka/src/test/java/com/alibaba/csp/sentinel/datasource/eureka/SimpleSpringApplication.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2018 the original author or authors. + * + * Licensed 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 com.alibaba.csp.sentinel.datasource.eureka; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author liyang + */ +@SpringBootApplication +public class SimpleSpringApplication { + public static void main(String[] args) { + SpringApplication.run(SimpleSpringApplication.class); + } + +} diff --git a/sentinel-extension/sentinel-datasource-eureka/src/test/resources/application.yml b/sentinel-extension/sentinel-datasource-eureka/src/test/resources/application.yml new file mode 100644 index 0000000000..3aae0ddac0 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-eureka/src/test/resources/application.yml @@ -0,0 +1,14 @@ +server: + port: 8761 +eureka: + instance: + instance-id: instance-0 + appname: testapp + metadata-map: + sentinel-rules: "[{'clusterMode':false,'controlBehavior':0,'count':20.0,'grade':1,'limitApp':'default','maxQueueingTimeMs':500,'resource':'resource-demo-name','strategy':0,'warmUpPeriodSec':10}]" + + client: + register-with-eureka: true + fetch-registry: false + service-url: + defaultZone: http://localhost:8761/eureka/