diff --git a/README.md b/README.md index dc3507f64a..d635a97be9 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ Sentinel Logo -# Sentinel: Sentinel of Your Application +# Sentinel: The Sentinel of Your Microservices [![Travis Build Status](https://travis-ci.org/alibaba/Sentinel.svg?branch=master)](https://travis-ci.org/alibaba/Sentinel) [![Codecov](https://codecov.io/gh/alibaba/Sentinel/branch/master/graph/badge.svg)](https://codecov.io/gh/alibaba/Sentinel) @@ -30,8 +30,9 @@ See the [中文文档](https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB% See the [Wiki](https://github.com/alibaba/Sentinel/wiki) for full documentation, examples, blog posts, operational details and other information. -Sentinel provides integration module for various open-source frameworks and libraries -(e.g. Spring Cloud, Apache Dubbo, gRPC, Spring WebFlux, Reactor). You can refer to [the document](https://github.com/alibaba/Sentinel/wiki/Adapters-to-Popular-Framework) for more information. +Sentinel provides integration modules for various open-source frameworks +(e.g. Spring Cloud, Apache Dubbo, gRPC, Spring WebFlux, Reactor) and service mesh. +You can refer to [the document](https://github.com/alibaba/Sentinel/wiki/Adapters-to-Popular-Framework) for more information. If you are using Sentinel, please [**leave a comment here**](https://github.com/alibaba/Sentinel/issues/18) to tell us your scenario to make Sentinel better. It's also encouraged to add the link of your blog post, tutorial, demo or customized components to [**Awesome Sentinel**](./doc/awesome-sentinel.md). @@ -55,7 +56,7 @@ If your application is build in Maven, just add the following dependency in `pom com.alibaba.csp sentinel-core - 1.6.1 + 1.7.1 ``` @@ -74,6 +75,7 @@ try (Entry entry = SphU.entry("HelloWorld")) { // Handle rejected request. e.printStackTrace(); } +// try-with-resources auto exit ``` So far the code modification is done. We also provide [annotation support module](https://github.com/alibaba/Sentinel/blob/master/sentinel-extension/sentinel-annotation-aspectj/README.md) to define resource easier. @@ -98,18 +100,18 @@ For more information, please refer to [How To Use](https://github.com/alibaba/Se ### 4. Check the Result -After running the demo for a while, you can see the following records in `~/logs/csp/${appName}-metrics.log`. +After running the demo for a while, you can see the following records in `~/logs/csp/${appName}-metrics.log.{date}` (When using the default `DateFileLogHandler`). ``` -|--timestamp-|------date time----|-resource-|p |block|s |e|rt -1529998904000|2018-06-26 15:41:44|HelloWorld|20|0 |20|0|0 -1529998905000|2018-06-26 15:41:45|HelloWorld|20|5579 |20|0|728 -1529998906000|2018-06-26 15:41:46|HelloWorld|20|15698|20|0|0 -1529998907000|2018-06-26 15:41:47|HelloWorld|20|19262|20|0|0 -1529998908000|2018-06-26 15:41:48|HelloWorld|20|19502|20|0|0 -1529998909000|2018-06-26 15:41:49|HelloWorld|20|18386|20|0|0 - -p stands for incoming request, block for blocked by rules, success for success handled by Sentinel, e for exception count, rt for average response time (ms) +|--timestamp-|------date time----|-resource-|p |block|s |e|rt |occupied +1529998904000|2018-06-26 15:41:44|HelloWorld|20|0 |20|0|0 |0 +1529998905000|2018-06-26 15:41:45|HelloWorld|20|5579 |20|0|728 |0 +1529998906000|2018-06-26 15:41:46|HelloWorld|20|15698|20|0|0 |0 +1529998907000|2018-06-26 15:41:47|HelloWorld|20|19262|20|0|0 |0 +1529998908000|2018-06-26 15:41:48|HelloWorld|20|19502|20|0|0 |0 +1529998909000|2018-06-26 15:41:49|HelloWorld|20|18386|20|0|0 |0 + +p stands for incoming request, block for blocked by rules, success for success handled by Sentinel, e for exception count, rt for average response time (ms), occupied stands for occupiedPassQps since 1.5.0 which enable us booking more than 1 shot when entering. ``` This shows that the demo can print "hello world" 20 times per second. @@ -124,11 +126,14 @@ Samples can be found in the [sentinel-demo](https://github.com/alibaba/Sentinel/ Sentinel also provides a simple dashboard application, on which you can monitor the clients and configure the rules in real time. +![dashboard](https://user-images.githubusercontent.com/9434884/55449295-84866d80-55fd-11e9-94e5-d3441f4a2b63.png) + For details please refer to [Dashboard](https://github.com/alibaba/Sentinel/wiki/Dashboard). ## Trouble Shooting and Logs -Sentinel will generate logs for troubleshooting. All the information can be found in [logs](https://github.com/alibaba/Sentinel/wiki/Logs). +Sentinel will generate logs for troubleshooting and real-time monitoring. +All the information can be found in [logs](https://github.com/alibaba/Sentinel/wiki/Logs). ## Bugs and Feedback @@ -163,6 +168,5 @@ These are only part of the companies using Sentinel, for reference only. If you ![亲宝宝](https://stlib.qbb6.com/wclt/img/home_hd/version1/title_logo.png) ![杭州光云科技](https://www.raycloud.com/images/logo.png) ![金汇金融](https://res.jinhui365.com/r/images/logo2.png?v=1.527) -![Vivo](https://user-images.githubusercontent.com/9434884/49355264-c6f87600-f701-11e8-8109-054cf91df868.png) ![闪电购](http://cdn.52shangou.com/shandianbang/official-source/3.1.1/build/images/logo.png) ![拼多多](http://cdn.pinduoduo.com/assets/img/pdd_logo_v3.png) diff --git a/doc/awesome-sentinel.md b/doc/awesome-sentinel.md index db2715777d..e560bf5de5 100644 --- a/doc/awesome-sentinel.md +++ b/doc/awesome-sentinel.md @@ -2,52 +2,74 @@ [![Awesome](https://awesome.re/badge-flat.svg)](https://awesome.re) -A curated list of awesome things (e.g. sample, extensions, blogs) for [Sentinel](https://github.com/alibaba/Sentinel). +A curated list of awesome things (e.g. samples, third-party extensions, blog posts) for [Sentinel](https://github.com/alibaba/Sentinel). -If you want your component to appear here, send a pull request to this repository to add it. +If you want your component to appear here, feel free to submit a pull request to this repository to add it. You can refer to the [awesome contribution guidelines](https://github.com/sentinel-group/sentinel-awesome/blob/master/CONTRIBUTING.md). You can also add to [sentinel-group/sentinel-awesome](https://github.com/sentinel-group/sentinel-awesome). ## Contents +- [Presentations](#presentations) - [Tutorials](#tutorials) -- [Samples / Demos](#samples--demos) +- [Demos](#demos) - [Extensions / Integrations](#extensions--integrations) -- [Blogs](#blogs) +- [Blog Posts](#blog-posts) + +## Presentations + +- Sentinel 1.6.0 网关流控新特性介绍-Eric Zhao (Dubbo Tech Day-201905-Beijing): [PDF](https://github.com/sentinel-group/sentinel-awesome/blob/master/slides/Sentinel%201.6.0%20网关流控新特性介绍-Eric%20Zhao-DTED-201905.pdf) +- Sentinel 微服务流控降级实践-Eric Zhao (Dubbo Tech Day-201907-Shenzhen): [PDF](https://github.com/sentinel-group/sentinel-awesome/blob/master/slides/Sentinel%20微服务流控降级实践-Eric%20Zhao-DTED-201907.pdf) +- Sentinel 1.7.0 新特性展望-Eric Zhao (Dubbo Tech Day-201910-Chengdu): [PDF](https://github.com/sentinel-group/sentinel-awesome/blob/master/slides/Sentinel%201.7.0%20新特性展望-Eric%20Zhao-DTED-201910.pdf) ## Tutorials -## Samples / Demos +- [Sentinel Guides](https://github.com/sentinel-group/sentinel-guides) + +## Demos - [sentinel-zuul-example](https://github.com/tigerMoon/sentinel-zuul-sample): A simple project integration Sentinel to Spring Cloud Zuul which provide Service and API Path level flow control management by [tiger](https://github.com/tigerMoon) ## Extensions / Integrations - [sentinel-support](https://github.com/cdfive/sentinel-support): A support project for convenient Sentinel integration including properties file configuration, ActiveMQ integration and a JdbcDataSource implementation by [cdfive](https://github.com/cdfive) -- [sentinel-multiDataSource-adapter](https://github.com/finefuture/sentinel-dashboard-X): Sentinel-dashborad multi-data source adapter has integrated Apollo configuration center and Nacos configuration center for bidirectional modification persistence, implementation by [finefuture](https://github.com/finefuture) +- [Sentinel dashboard multi-data-source adapter](https://github.com/finefuture/sentinel-dashboard-X): Sentinel dashboard multi-data-source adapter has integrated Apollo and Nacos configuration center for bidirectional modification persistence. Implemented by [finefuture](https://github.com/finefuture) +- [Sentinel Rule Annotation Support](https://github.com/code1986/sentinel-lib): A third-party library that supports configuring flow rule and degrade rule using annotation. Implemented by [code1986](https://github.com/code1986) +- [sentinel-pigeon-adapter](https://github.com/wchswchs/sentinel-pigeon): A RPC framework Pigeon adapter for Sentinel including provider and invoker rate limiting implementation by [wchswchs](https://github.com/wchswchs) -## Blogs +## Blog Posts - [Sentinel 为 Dubbo 服务保驾护航](http://dubbo.apache.org/zh-cn/blog/sentinel-introduction-for-dubbo.html) by [Eric Zhao](https://github.com/sczyh30) -- [Sentinel 与 Hystrix 的对比](https://github.com/alibaba/Sentinel/wiki/Sentinel-%E4%B8%8E-Hystrix-%E7%9A%84%E5%AF%B9%E6%AF%94) by [Eric Zhao](https://github.com/sczyh30) -- [Guideline: 从 Hystrix 迁移到 Sentinel](https://github.com/alibaba/Sentinel/wiki/Guideline:-%E4%BB%8E-Hystrix-%E8%BF%81%E7%A7%BB%E5%88%B0-Sentinel) by [Eric Zhao](https://github.com/sczyh30) -- [Sentinel 控制台监控数据持久化【MySQL】(Spring Data JPA)](https://www.cnblogs.com/cdfive2018/p/9838577.html) by [cdfive](https://github.com/cdfive) +- [在生产环境中使用 Sentinel 控制台](https://github.com/alibaba/Sentinel/wiki/在生产环境中使用-Sentinel) by [Eric Zhao](https://github.com/sczyh30) +- [Sentinel 与 Hystrix 的对比](https://sentinelguard.io/zh-cn/blog/sentinel-vs-hystrix.html) by [Eric Zhao](https://github.com/sczyh30) +- [Guideline: 从 Hystrix 迁移到 Sentinel](https://sentinelguard.io/zh-cn/blog/guideline-migrate-from-hystrix-to-sentinel.html) by [Eric Zhao](https://github.com/sczyh30) +- [Sentinel 控制台监控数据持久化【MySQL】](https://www.cnblogs.com/cdfive2018/p/9838577.html) by [cdfive](https://github.com/cdfive) - [Sentinel 控制台监控数据持久化【InfluxDB】](https://www.cnblogs.com/cdfive2018/p/9914838.html) by [cdfive](https://github.com/cdfive) -- [Sentinel一体化监控解决方案 CrateDB+Grafana](https://blog.csdn.net/huyong1990/article/details/82392386) by [Young Hu](https://github.com/YoungHu) -- [Sentinel 原理-全解析](https://mp.weixin.qq.com/s/7_pCkamNv0269e5l9_Wz7w) by [houyi](https://github.com/all4you) -- [Sentinel 原理-调用链](https://mp.weixin.qq.com/s/UEzwD22YC6jpp02foNSXnw) by [houyi](https://github.com/all4you) -- [Sentinel 原理-滑动窗口](https://mp.weixin.qq.com/s/B1_7Kb_CxeKEAv43kdCWOA) by [houyi](https://github.com/all4you) -- [Sentinel 实战-限流](https://mp.weixin.qq.com/s/rjyU37Dm-sxNln7GUD8tOw) by [houyi](https://github.com/all4you) -- [Sentinel 实战-控制台](https://mp.weixin.qq.com/s/23EDFHMXLwsDqw-4O5dR5A) by [houyi](https://github.com/all4you) -- [Sentinel 实战-规则持久化](https://mp.weixin.qq.com/s/twMFiBfRawKLR-1-N-f1yw) by [houyi](https://github.com/all4you) -- [sentinel 深入浅出之原理篇SlotChain](https://www.jianshu.com/p/a7a405de3a12) by [shxz130](https://github.com/shxz130) -- [sentinel 深入浅出之原理篇Context初始化&Entry初始化](https://www.jianshu.com/p/e39ac47cd893) by [shxz130](https://github.com/shxz130) -- [sentinel 深入浅出之原理篇NodeSelectorSlot](https://www.jianshu.com/p/9a380ba188ab) by [shxz130](https://github.com/shxz130) -- [sentinel 深入浅出之原理篇ClusterBuilderSlot](https://www.jianshu.com/p/0b0b5d8888a2) by [shxz130](https://github.com/shxz130) -- [sentinel 深入浅出之原理篇StatisticSlot&滑动窗口](https://www.jianshu.com/p/9620298fd15a) by [shxz130](https://github.com/shxz130) -- [sentinel 深入浅出之原理篇SystemSlot](https://www.jianshu.com/p/bfad1b7d0cde) by [shxz130](https://github.com/shxz130) -- [sentinel 深入浅出之原理篇AuthoritySlot](https://www.jianshu.com/p/c5312c2242b3) by [shxz130](https://github.com/shxz130) -- [sentinel 深入浅出之原理篇FlowSlot](https://www.jianshu.com/p/53218d0d273e) by [shxz130](https://github.com/shxz130) -- [sentinel 深入浅出之原理篇DegradeSlot](https://www.jianshu.com/p/e910d4840e4a) by [shxz130](https://github.com/shxz130) -- [sentinel 深入浅出之原理篇协议拓展dubbo,grpc,web-servlet](https://www.jianshu.com/p/579bff0f34be) by [shxz130](https://github.com/shxz130) +- [Sentinel 控制台监控数据持久化【Apollo】](https://blog.csdn.net/caodegao/article/details/100009618) by [cookiejoo](https://github.com/cookiejoo) +- [Sentinel一体化监控解决方案 CrateDB + Grafana](https://blog.csdn.net/huyong1990/article/details/82392386) by [Young Hu](https://github.com/YoungHu) +- Sentinel 源码解析系列 by [houyi](https://github.com/all4you) + - [Sentinel 原理-全解析](https://mp.weixin.qq.com/s/7_pCkamNv0269e5l9_Wz7w) + - [Sentinel 原理-调用链](https://mp.weixin.qq.com/s/UEzwD22YC6jpp02foNSXnw) + - [Sentinel 原理-滑动窗口](https://mp.weixin.qq.com/s/B1_7Kb_CxeKEAv43kdCWOA) + - [Sentinel 实战-限流](https://mp.weixin.qq.com/s/rjyU37Dm-sxNln7GUD8tOw) + - [Sentinel 实战-控制台](https://mp.weixin.qq.com/s/23EDFHMXLwsDqw-4O5dR5A) + - [Sentinel 实战-规则持久化](https://mp.weixin.qq.com/s/twMFiBfRawKLR-1-N-f1yw) +- Sentinel 学习笔记 by [ro9er](https://github.com/ro9er) + - [Sentinel 学习笔记(1)-- 流量统计代码解析](https://www.jianshu.com/p/7936d7a57924) + - [Sentinel 学习笔记(2)-- 流量控制代码分析](https://www.jianshu.com/p/938709e94e43) + - [Sentinel 学习笔记(3)-- 上下文统计Node建立分析](https://www.jianshu.com/p/cfdf525248c1) +- [大流量下的服务质量治理 Dubbo Sentinel 初涉](https://mp.weixin.qq.com/s/ergr_siI07VwwSRPFgsLvQ) by [RyuGrade](https://github.com/RyuGrade) +- Sentinel 深入浅出系列 by [shxz130](https://github.com/shxz130) + - [Sentinel 深入浅出之原理篇 SlotChain](https://www.jianshu.com/p/a7a405de3a12) + - [Sentinel 深入浅出之原理篇 Context初始化 & Entry初始化](https://www.jianshu.com/p/e39ac47cd893) + - [Sentinel 深入浅出之原理篇 NodeSelectorSlot](https://www.jianshu.com/p/9a380ba188ab) + - [Sentinel 深入浅出之原理篇 ClusterBuilderSlot](https://www.jianshu.com/p/0b0b5d8888a2) + - [Sentinel 深入浅出之原理篇 StatisticSlot&滑动窗口](https://www.jianshu.com/p/9620298fd15a) + - [Sentinel 深入浅出之原理篇 SystemSlot](https://www.jianshu.com/p/bfad1b7d0cde) + - [Sentinel 深入浅出之原理篇 AuthoritySlot](https://www.jianshu.com/p/c5312c2242b3) + - [Sentinel 深入浅出之原理篇 FlowSlot](https://www.jianshu.com/p/53218d0d273e) + - [Sentinel 深入浅出之原理篇 DegradeSlot](https://www.jianshu.com/p/e910d4840e4a) +- [Alibaba Sentinel RESTful 接口流控处理优化](https://www.jianshu.com/p/96f5980d9798) by [luanlouis](https://github.com/luanlouis) +- [Sentinel 控制台前端开发环境搭建](https://www.cnblogs.com/cdfive2018/p/11084001.html) by [cdfive](https://github.com/cdfive) +- [阿里 Sentinel 源码解析](https://www.javadoop.com/post/sentinel) by [Javadoop](https://www.javadoop.com) diff --git a/pom.xml b/pom.xml index 944bfc7a16..2d80de934b 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.alibaba.csp sentinel-parent - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT pom ${project.artifactId} @@ -41,7 +41,7 @@ - 1.2.56 + 1.2.62 4.12 @@ -120,11 +120,21 @@ sentinel-datasource-apollo ${project.version} + + com.alibaba.csp + sentinel-datasource-etcd + ${project.version} + com.alibaba.csp sentinel-transport-simple-http ${project.version} + + com.alibaba.csp + sentinel-transport-netty-http + ${project.version} + com.alibaba.csp sentinel-transport-common @@ -288,6 +298,11 @@ maven-jar-plugin ${maven.jar.version} + + org.apache.maven.plugins + maven-gpg-plugin + ${maven.gpg.version} + diff --git a/sentinel-adapter/pom.xml b/sentinel-adapter/pom.xml index 038652a4cf..1f152c169a 100755 --- a/sentinel-adapter/pom.xml +++ b/sentinel-adapter/pom.xml @@ -7,7 +7,7 @@ com.alibaba.csp sentinel-parent - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT sentinel-adapter pom @@ -24,6 +24,7 @@ sentinel-spring-webflux-adapter sentinel-api-gateway-adapter-common sentinel-spring-cloud-gateway-adapter + sentinel-spring-webmvc-adapter diff --git a/sentinel-adapter/sentinel-apache-dubbo-adapter/pom.xml b/sentinel-adapter/sentinel-apache-dubbo-adapter/pom.xml index d6a4bab392..5ccf18467c 100644 --- a/sentinel-adapter/sentinel-apache-dubbo-adapter/pom.xml +++ b/sentinel-adapter/sentinel-apache-dubbo-adapter/pom.xml @@ -5,7 +5,7 @@ sentinel-adapter com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 @@ -15,7 +15,7 @@ 1.8 1.8 - 2.7.1 + 2.7.3 diff --git a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/BaseSentinelDubboFilter.java b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/BaseSentinelDubboFilter.java new file mode 100644 index 0000000000..9ea01e59da --- /dev/null +++ b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/BaseSentinelDubboFilter.java @@ -0,0 +1,77 @@ +/* + * 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.adapter.dubbo; + + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.adapter.dubbo.config.DubboConfig; +import com.alibaba.csp.sentinel.context.ContextUtil; +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.constants.CommonConstants; +import org.apache.dubbo.rpc.Invocation; +import org.apache.dubbo.rpc.Invoker; +import org.apache.dubbo.rpc.ListenableFilter; +import org.apache.dubbo.rpc.Result; +import org.apache.dubbo.rpc.RpcContext; + +/** + * Base Class of the {@link SentinelDubboProviderFilter} and {@link SentinelDubboConsumerFilter}. + * + * @author Zechao Zheng + */ + +public abstract class BaseSentinelDubboFilter extends ListenableFilter { + public BaseSentinelDubboFilter() { + this.listener = new SentinelDubboListener(); + } + + static class SentinelDubboListener implements Listener { + + @Override + public void onResponse(Result appResponse, Invoker invoker, Invocation invocation) { + if (DubboConfig.getDubboBizExceptionTraceEnabled()) { + traceAndExit(appResponse.getException(), invoker.getUrl()); + } else { + traceAndExit(null, invoker.getUrl()); + } + } + + @Override + public void onError(Throwable t, Invoker invoker, Invocation invocation) { + traceAndExit(t, invoker.getUrl()); + } + + } + + static void traceAndExit(Throwable throwable, URL url) { + Entry interfaceEntry = (Entry) RpcContext.getContext().get(DubboUtils.DUBBO_INTERFACE_ENTRY_KEY); + Entry methodEntry = (Entry) RpcContext.getContext().get(DubboUtils.DUBBO_METHOD_ENTRY_KEY); + if (methodEntry != null) { + Tracer.traceEntry(throwable, methodEntry); + methodEntry.exit(); + RpcContext.getContext().remove(DubboUtils.DUBBO_METHOD_ENTRY_KEY); + } + if (interfaceEntry != null) { + Tracer.traceEntry(throwable, interfaceEntry); + interfaceEntry.exit(); + RpcContext.getContext().remove(DubboUtils.DUBBO_INTERFACE_ENTRY_KEY); + } + if (CommonConstants.PROVIDER_SIDE.equals(url.getParameter(CommonConstants.SIDE_KEY))) { + ContextUtil.exit(); + } + } +} diff --git a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboAppContextFilter.java b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboAppContextFilter.java index 97ae51bac3..b73542931b 100644 --- a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboAppContextFilter.java +++ b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboAppContextFilter.java @@ -15,7 +15,7 @@ */ package com.alibaba.csp.sentinel.adapter.dubbo; -import org.apache.dubbo.common.Constants; +import org.apache.dubbo.common.constants.CommonConstants; import org.apache.dubbo.common.extension.Activate; import org.apache.dubbo.rpc.Filter; import org.apache.dubbo.rpc.Invocation; @@ -24,17 +24,19 @@ import org.apache.dubbo.rpc.RpcContext; import org.apache.dubbo.rpc.RpcException; +import static org.apache.dubbo.common.constants.CommonConstants.CONSUMER; + /** * Puts current consumer's application name in the attachment of each invocation. * * @author Eric Zhao */ -@Activate(group = "consumer") +@Activate(group = CONSUMER) public class DubboAppContextFilter implements Filter { @Override public Result invoke(Invoker invoker, Invocation invocation) throws RpcException { - String application = invoker.getUrl().getParameter(Constants.APPLICATION_KEY); + String application = invoker.getUrl().getParameter(CommonConstants.APPLICATION_KEY); if (application != null) { RpcContext.getContext().setAttachment(DubboUtils.SENTINEL_DUBBO_APPLICATION_KEY, application); } diff --git a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboUtils.java b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboUtils.java index 03a494f585..d2c325fafd 100644 --- a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboUtils.java +++ b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboUtils.java @@ -15,6 +15,8 @@ */ package com.alibaba.csp.sentinel.adapter.dubbo; +import com.alibaba.csp.sentinel.adapter.dubbo.config.DubboConfig; +import com.alibaba.csp.sentinel.util.StringUtil; import org.apache.dubbo.rpc.Invocation; import org.apache.dubbo.rpc.Invoker; @@ -24,6 +26,8 @@ public final class DubboUtils { public static final String SENTINEL_DUBBO_APPLICATION_KEY = "dubboApplication"; + public static final String DUBBO_METHOD_ENTRY_KEY = "dubboMethodEntry"; + public static final String DUBBO_INTERFACE_ENTRY_KEY = "dubboInterfaceEntry"; public static String getApplication(Invocation invocation, String defaultValue) { if (invocation == null || invocation.getAttachments() == null) { @@ -32,9 +36,14 @@ public static String getApplication(Invocation invocation, String defaultValue) return invocation.getAttachment(SENTINEL_DUBBO_APPLICATION_KEY, defaultValue); } - public static String getResourceName(Invoker invoker, Invocation invocation) { + public static String getResourceName(Invoker invoker, Invocation invocation){ + return getResourceName(invoker, invocation, false); + } + + public static String getResourceName(Invoker invoker, Invocation invocation, Boolean useGroupAndVersion) { StringBuilder buf = new StringBuilder(64); - buf.append(invoker.getInterface().getName()) + String interfaceResource = useGroupAndVersion ? invoker.getUrl().getColonSeparatedKey() : invoker.getInterface().getName(); + buf.append(interfaceResource) .append(":") .append(invocation.getMethodName()) .append("("); @@ -50,5 +59,16 @@ public static String getResourceName(Invoker invoker, Invocation invocation) return buf.toString(); } - private DubboUtils() {} + public static String getResourceName(Invoker invoker, Invocation invocation, String prefix) { + if (StringUtil.isNotBlank(prefix)) { + return new StringBuilder(64) + .append(prefix) + .append(getResourceName(invoker, invocation, DubboConfig.getDubboInterfaceGroupAndVersionEnabled())) + .toString(); + } else { + return getResourceName(invoker, invocation, DubboConfig.getDubboInterfaceGroupAndVersionEnabled()); + } + } + private DubboUtils() { + } } diff --git a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboConsumerFilter.java b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboConsumerFilter.java old mode 100755 new mode 100644 index 2dcbaa79cb..ffa9fba9cf --- a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboConsumerFilter.java +++ b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboConsumerFilter.java @@ -17,22 +17,26 @@ import com.alibaba.csp.sentinel.Entry; import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.ResourceTypeConstants; import com.alibaba.csp.sentinel.SphU; -import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.adapter.dubbo.config.DubboConfig; +import com.alibaba.csp.sentinel.adapter.dubbo.fallback.DubboFallbackRegistry; import com.alibaba.csp.sentinel.log.RecordLog; import com.alibaba.csp.sentinel.slots.block.BlockException; - -import com.alibaba.csp.sentinel.adapter.dubbo.fallback.DubboFallbackRegistry; import org.apache.dubbo.common.extension.Activate; -import org.apache.dubbo.rpc.Filter; import org.apache.dubbo.rpc.Invocation; +import org.apache.dubbo.rpc.InvokeMode; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.Result; +import org.apache.dubbo.rpc.RpcContext; import org.apache.dubbo.rpc.RpcException; +import org.apache.dubbo.rpc.support.RpcUtils; + +import static org.apache.dubbo.common.constants.CommonConstants.CONSUMER; /** *

Dubbo service consumer filter for Sentinel. Auto activated by default.

- * + *

* If you want to disable the consumer filter, you can configure: *

  * <dubbo:consumer filter="-sentinel.dubbo.consumer.filter"/>
@@ -41,8 +45,8 @@
  * @author Carpenter Lee
  * @author Eric Zhao
  */
-@Activate(group = "consumer")
-public class SentinelDubboConsumerFilter implements Filter {
+@Activate(group = CONSUMER)
+public class SentinelDubboConsumerFilter extends BaseSentinelDubboFilter {
 
     public SentinelDubboConsumerFilter() {
         RecordLog.info("Sentinel Apache Dubbo consumer filter initialized");
@@ -52,32 +56,29 @@ public SentinelDubboConsumerFilter() {
     public Result invoke(Invoker invoker, Invocation invocation) throws RpcException {
         Entry interfaceEntry = null;
         Entry methodEntry = null;
+        RpcContext rpcContext = RpcContext.getContext();
         try {
-            String resourceName = DubboUtils.getResourceName(invoker, invocation);
-            interfaceEntry = SphU.entry(invoker.getInterface().getName(), EntryType.OUT);
-            methodEntry = SphU.entry(resourceName, EntryType.OUT);
+            String methodResourceName = DubboUtils.getResourceName(invoker, invocation, DubboConfig.getDubboConsumerPrefix());
+            String interfaceResourceName = DubboConfig.getDubboInterfaceGroupAndVersionEnabled() ? invoker.getUrl().getColonSeparatedKey()
+                    : invoker.getInterface().getName();
+            InvokeMode invokeMode = RpcUtils.getInvokeMode(invoker.getUrl(), invocation);
 
-            Result result = invoker.invoke(invocation);
-            if (result.hasException()) {
-                Throwable e = result.getException();
-                // Record common exception.
-                Tracer.traceEntry(e, interfaceEntry);
-                Tracer.traceEntry(e, methodEntry);
+            if (InvokeMode.SYNC == invokeMode) {
+                interfaceEntry = SphU.entry(interfaceResourceName, ResourceTypeConstants.COMMON_RPC, EntryType.OUT);
+                rpcContext.set(DubboUtils.DUBBO_INTERFACE_ENTRY_KEY, interfaceEntry);
+                methodEntry = SphU.entry(methodResourceName, ResourceTypeConstants.COMMON_RPC, EntryType.OUT, invocation.getArguments());
+            } else {
+                // should generate the AsyncEntry when the invoke model in future or async
+                interfaceEntry = SphU.asyncEntry(interfaceResourceName, ResourceTypeConstants.COMMON_RPC, EntryType.OUT);
+                rpcContext.set(DubboUtils.DUBBO_INTERFACE_ENTRY_KEY, interfaceEntry);
+                methodEntry = SphU.asyncEntry(methodResourceName, ResourceTypeConstants.COMMON_RPC, EntryType.OUT, 1, invocation.getArguments());
             }
-            return result;
+            rpcContext.set(DubboUtils.DUBBO_METHOD_ENTRY_KEY, methodEntry);
+            return invoker.invoke(invocation);
         } catch (BlockException e) {
             return DubboFallbackRegistry.getConsumerFallback().handle(invoker, invocation, e);
-        } catch (RpcException e) {
-            Tracer.traceEntry(e, interfaceEntry);
-            Tracer.traceEntry(e, methodEntry);
-            throw e;
-        } finally {
-            if (methodEntry != null) {
-                methodEntry.exit();
-            }
-            if (interfaceEntry != null) {
-                interfaceEntry.exit();
-            }
         }
     }
 }
+
+
diff --git a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboProviderFilter.java b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboProviderFilter.java
old mode 100755
new mode 100644
index 2020832a0b..c919978694
--- a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboProviderFilter.java
+++ b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboProviderFilter.java
@@ -17,24 +17,26 @@
 
 import com.alibaba.csp.sentinel.Entry;
 import com.alibaba.csp.sentinel.EntryType;
+import com.alibaba.csp.sentinel.ResourceTypeConstants;
 import com.alibaba.csp.sentinel.SphU;
-import com.alibaba.csp.sentinel.Tracer;
+import com.alibaba.csp.sentinel.adapter.dubbo.config.DubboConfig;
 import com.alibaba.csp.sentinel.adapter.dubbo.fallback.DubboFallbackRegistry;
 import com.alibaba.csp.sentinel.context.ContextUtil;
 import com.alibaba.csp.sentinel.log.RecordLog;
 import com.alibaba.csp.sentinel.slots.block.BlockException;
-
 import org.apache.dubbo.common.extension.Activate;
-import org.apache.dubbo.rpc.Filter;
 import org.apache.dubbo.rpc.Invocation;
 import org.apache.dubbo.rpc.Invoker;
 import org.apache.dubbo.rpc.Result;
+import org.apache.dubbo.rpc.RpcContext;
 import org.apache.dubbo.rpc.RpcException;
 
+import static org.apache.dubbo.common.constants.CommonConstants.PROVIDER;
+
 /**
  * 

Apache Dubbo service provider filter that enables integration with Sentinel. Auto activated by default.

*

Note: this only works for Apache Dubbo 2.7.x or above version.

- * + *

* If you want to disable the provider filter, you can configure: *

  * <dubbo:provider filter="-sentinel.dubbo.provider.filter"/>
@@ -43,8 +45,8 @@
  * @author Carpenter Lee
  * @author Eric Zhao
  */
-@Activate(group = "provider")
-public class SentinelDubboProviderFilter implements Filter {
+@Activate(group = PROVIDER)
+public class SentinelDubboProviderFilter extends BaseSentinelDubboFilter {
 
     public SentinelDubboProviderFilter() {
         RecordLog.info("Sentinel Apache Dubbo provider filter initialized");
@@ -54,40 +56,26 @@ public SentinelDubboProviderFilter() {
     public Result invoke(Invoker invoker, Invocation invocation) throws RpcException {
         // Get origin caller.
         String application = DubboUtils.getApplication(invocation, "");
-
+        RpcContext rpcContext = RpcContext.getContext();
         Entry interfaceEntry = null;
         Entry methodEntry = null;
         try {
-            String resourceName = DubboUtils.getResourceName(invoker, invocation);
-            String interfaceName = invoker.getInterface().getName();
+            String methodResourceName = DubboUtils.getResourceName(invoker, invocation, DubboConfig.getDubboProviderPrefix());
+            String interfaceResourceName = DubboConfig.getDubboInterfaceGroupAndVersionEnabled() ? invoker.getUrl().getColonSeparatedKey()
+                    : invoker.getInterface().getName();
             // Only need to create entrance context at provider side, as context will take effect
             // at entrance of invocation chain only (for inbound traffic).
-            ContextUtil.enter(resourceName, application);
-            interfaceEntry = SphU.entry(interfaceName, EntryType.IN);
-            methodEntry = SphU.entry(resourceName, EntryType.IN, 1, invocation.getArguments());
-
-            Result result = invoker.invoke(invocation);
-            if (result.hasException()) {
-                Throwable e = result.getException();
-                // Record common exception.
-                Tracer.traceEntry(e, interfaceEntry);
-                Tracer.traceEntry(e, methodEntry);
-            }
-            return result;
+            ContextUtil.enter(methodResourceName, application);
+            interfaceEntry = SphU.entry(interfaceResourceName, ResourceTypeConstants.COMMON_RPC, EntryType.IN);
+            rpcContext.set(DubboUtils.DUBBO_INTERFACE_ENTRY_KEY, interfaceEntry);
+            methodEntry = SphU.entry(methodResourceName, ResourceTypeConstants.COMMON_RPC, EntryType.IN, invocation.getArguments());
+            rpcContext.set(DubboUtils.DUBBO_METHOD_ENTRY_KEY, methodEntry);
+            return invoker.invoke(invocation);
         } catch (BlockException e) {
             return DubboFallbackRegistry.getProviderFallback().handle(invoker, invocation, e);
-        } catch (RpcException e) {
-            Tracer.traceEntry(e, interfaceEntry);
-            Tracer.traceEntry(e, methodEntry);
-            throw e;
-        } finally {
-            if (methodEntry != null) {
-                methodEntry.exit(1, invocation.getArguments());
-            }
-            if (interfaceEntry != null) {
-                interfaceEntry.exit();
-            }
-            ContextUtil.exit();
         }
     }
+
+
 }
+
diff --git a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/config/DubboConfig.java b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/config/DubboConfig.java
new file mode 100644
index 0000000000..9b2b021828
--- /dev/null
+++ b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/config/DubboConfig.java
@@ -0,0 +1,77 @@
+/*
+ * 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.adapter.dubbo.config;
+
+import com.alibaba.csp.sentinel.config.SentinelConfig;
+import com.alibaba.csp.sentinel.util.StringUtil;
+
+/**
+ * 

+ * Responsible for dubbo service provider, consumer attribute configuration + *

+ * + * @author lianglin + * @since 1.7.0 + */ +public final class DubboConfig { + + public static final String DUBBO_USE_PREFIX = "csp.sentinel.dubbo.resource.use.prefix"; + private static final String TRUE_STR = "true"; + + public static final String DUBBO_PROVIDER_PREFIX = "csp.sentinel.dubbo.resource.provider.prefix"; + public static final String DUBBO_CONSUMER_PREFIX = "csp.sentinel.dubbo.resource.consumer.prefix"; + + private static final String DEFAULT_DUBBO_PROVIDER_PREFIX = "dubbo:provider:"; + private static final String DEFAULT_DUBBO_CONSUMER_PREFIX = "dubbo:consumer:"; + + public static final String DUBBO_INTERFACE_GROUP_VERSION_ENABLED = "csp.sentinel.dubbo.interface.group.version.enabled"; + + public static final String TRACE_BIZ_EXCEPTION_ENABLED = "csp.sentinel.dubbo.trace.biz.exception.enabled"; + + + public static boolean isUsePrefix() { + return TRUE_STR.equalsIgnoreCase(SentinelConfig.getConfig(DUBBO_USE_PREFIX)); + } + + public static String getDubboProviderPrefix() { + if (isUsePrefix()) { + String config = SentinelConfig.getConfig(DUBBO_PROVIDER_PREFIX); + return StringUtil.isNotBlank(config) ? config : DEFAULT_DUBBO_PROVIDER_PREFIX; + } + return null; + } + + public static String getDubboConsumerPrefix() { + if (isUsePrefix()) { + String config = SentinelConfig.getConfig(DUBBO_CONSUMER_PREFIX); + return StringUtil.isNotBlank(config) ? config : DEFAULT_DUBBO_CONSUMER_PREFIX; + } + return null; + } + + public static Boolean getDubboInterfaceGroupAndVersionEnabled() { + return TRUE_STR.equalsIgnoreCase(SentinelConfig.getConfig(DUBBO_INTERFACE_GROUP_VERSION_ENABLED)); + } + + public static Boolean getDubboBizExceptionTraceEnabled() { + String traceBizExceptionEnabled = SentinelConfig.getConfig(TRACE_BIZ_EXCEPTION_ENABLED); + if (StringUtil.isNotBlank(traceBizExceptionEnabled)) { + return TRUE_STR.equalsIgnoreCase(traceBizExceptionEnabled); + } + return true; + } + +} diff --git a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/BaseTest.java b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/BaseTest.java index a61f3b4c80..7793a6567e 100644 --- a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/BaseTest.java +++ b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/BaseTest.java @@ -15,26 +15,73 @@ */ package com.alibaba.csp.sentinel; +import com.alibaba.csp.sentinel.adapter.dubbo.config.DubboConfig; +import com.alibaba.csp.sentinel.adapter.dubbo.provider.DemoService; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; - +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.constants.CommonConstants; +import org.apache.dubbo.rpc.Invocation; +import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.RpcContext; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + /** * Base test class, provide common methods for subClass * The package is same as CtSph, to call CtSph.resetChainMap() method for test - * + *

* Note: Only for test. DO NOT USE IN PRODUCTION! * * @author cdfive */ public class BaseTest { + + protected Invoker invoker; + protected Invocation invocation; + + public void constructInvokerAndInvocation() { + invoker = mock(Invoker.class); + URL url = URL.valueOf("dubbo://127.0.0.1:2181") + .addParameter(CommonConstants.VERSION_KEY, "1.0.0") + .addParameter(CommonConstants.GROUP_KEY, "grp1") + .addParameter(CommonConstants.INTERFACE_KEY, DemoService.class.getName()); + when(invoker.getUrl()).thenReturn(url); + when(invoker.getInterface()).thenReturn(DemoService.class); + + invocation = mock(Invocation.class); + Method method = DemoService.class.getMethods()[0]; + when(invocation.getMethodName()).thenReturn(method.getName()); + when(invocation.getParameterTypes()).thenReturn(method.getParameterTypes()); + + } + /** * Clean up resources for context, clusterNodeMap, processorSlotChainMap */ protected static void cleanUpAll() { - RpcContext.removeContext(); - ClusterBuilderSlot.getClusterNodeMap().clear(); - CtSph.resetChainMap(); + try { + RpcContext.removeContext(); + ClusterBuilderSlot.getClusterNodeMap().clear(); + CtSph.resetChainMap(); + Method method = ContextUtil.class.getDeclaredMethod("resetContextMap"); + method.setAccessible(true); + method.invoke(null, null); + ContextUtil.exit(); + SentinelConfig.setConfig(DubboConfig.DUBBO_INTERFACE_GROUP_VERSION_ENABLED, "true"); + FlowRuleManager.loadRules(new ArrayList<>()); + DegradeRuleManager.loadRules(new ArrayList<>()); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + } } } \ No newline at end of file diff --git a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboUtilsTest.java b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboUtilsTest.java index 8d3636f180..ece46fde3d 100644 --- a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboUtilsTest.java +++ b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboUtilsTest.java @@ -15,30 +15,55 @@ */ package com.alibaba.csp.sentinel.adapter.dubbo; +import com.alibaba.csp.sentinel.adapter.dubbo.config.DubboConfig; +import com.alibaba.csp.sentinel.adapter.dubbo.provider.DemoService; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.constants.CommonConstants; import org.apache.dubbo.rpc.Invocation; import org.apache.dubbo.rpc.Invoker; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import java.lang.reflect.Method; import java.util.HashMap; -import com.alibaba.csp.sentinel.adapter.dubbo.provider.DemoService; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * @author cdfive */ public class DubboUtilsTest { + @Before + public void setUp() { + SentinelConfig.setConfig("csp.sentinel.dubbo.resource.use.prefix", "true"); + SentinelConfig.setConfig(DubboConfig.DUBBO_PROVIDER_PREFIX, ""); + SentinelConfig.setConfig(DubboConfig.DUBBO_CONSUMER_PREFIX, ""); + SentinelConfig.setConfig(DubboConfig.DUBBO_INTERFACE_GROUP_VERSION_ENABLED, "false"); + } + + + @After + public void tearDown() { + SentinelConfig.setConfig("csp.sentinel.dubbo.resource.use.prefix", "false"); + SentinelConfig.setConfig(DubboConfig.DUBBO_PROVIDER_PREFIX, ""); + SentinelConfig.setConfig(DubboConfig.DUBBO_CONSUMER_PREFIX, ""); + SentinelConfig.setConfig(DubboConfig.DUBBO_INTERFACE_GROUP_VERSION_ENABLED, "false"); + } + + @Test public void testGetApplication() { Invocation invocation = mock(Invocation.class); when(invocation.getAttachments()).thenReturn(new HashMap<>()); when(invocation.getAttachment(DubboUtils.SENTINEL_DUBBO_APPLICATION_KEY, "")) - .thenReturn("consumerA"); + .thenReturn("consumerA"); String application = DubboUtils.getApplication(invocation, ""); verify(invocation).getAttachment(DubboUtils.SENTINEL_DUBBO_APPLICATION_KEY, ""); @@ -51,7 +76,7 @@ public void testGetApplicationNoAttachments() { Invocation invocation = mock(Invocation.class); when(invocation.getAttachments()).thenReturn(null); when(invocation.getAttachment(DubboUtils.SENTINEL_DUBBO_APPLICATION_KEY, "")) - .thenReturn("consumerA"); + .thenReturn("consumerA"); DubboUtils.getApplication(invocation, ""); @@ -59,17 +84,66 @@ public void testGetApplicationNoAttachments() { } @Test - public void testGetResourceName() { + public void testGetResourceName() throws NoSuchMethodException { Invoker invoker = mock(Invoker.class); when(invoker.getInterface()).thenReturn(DemoService.class); Invocation invocation = mock(Invocation.class); - Method method = DemoService.class.getMethods()[0]; + Method method = DemoService.class.getDeclaredMethod("sayHello", String.class, int.class); when(invocation.getMethodName()).thenReturn(method.getName()); when(invocation.getParameterTypes()).thenReturn(method.getParameterTypes()); String resourceName = DubboUtils.getResourceName(invoker, invocation); assertEquals("com.alibaba.csp.sentinel.adapter.dubbo.provider.DemoService:sayHello(java.lang.String,int)", resourceName); + + } + + @Test + public void testGetResourceNameWithGroupAndVersion() throws NoSuchMethodException { + Invoker invoker = mock(Invoker.class); + URL url = URL.valueOf("dubbo://127.0.0.1:2181") + .addParameter(CommonConstants.VERSION_KEY, "1.0.0") + .addParameter(CommonConstants.GROUP_KEY, "grp1") + .addParameter(CommonConstants.INTERFACE_KEY, DemoService.class.getName()); + when(invoker.getUrl()).thenReturn(url); + when(invoker.getInterface()).thenReturn(DemoService.class); + + Invocation invocation = mock(Invocation.class); + Method method = DemoService.class.getDeclaredMethod("sayHello", String.class, int.class); + when(invocation.getMethodName()).thenReturn(method.getName()); + when(invocation.getParameterTypes()).thenReturn(method.getParameterTypes()); + + String resourceNameUseGroupAndVersion = DubboUtils.getResourceName(invoker, invocation, true); + + assertEquals("com.alibaba.csp.sentinel.adapter.dubbo.provider.DemoService:1.0.0:grp1:sayHello(java.lang.String,int)", resourceNameUseGroupAndVersion); + } + + + @Test + public void testGetResourceNameWithPrefix() throws NoSuchMethodException { + Invoker invoker = mock(Invoker.class); + when(invoker.getInterface()).thenReturn(DemoService.class); + + Invocation invocation = mock(Invocation.class); + Method method = DemoService.class.getDeclaredMethod("sayHello", String.class, int.class); + when(invocation.getMethodName()).thenReturn(method.getName()); + when(invocation.getParameterTypes()).thenReturn(method.getParameterTypes()); + + //test with default prefix + String resourceName = DubboUtils.getResourceName(invoker, invocation, DubboConfig.getDubboProviderPrefix()); + assertEquals("dubbo:provider:com.alibaba.csp.sentinel.adapter.dubbo.provider.DemoService:sayHello(java.lang.String,int)", resourceName); + resourceName = DubboUtils.getResourceName(invoker, invocation, DubboConfig.getDubboConsumerPrefix()); + assertEquals("dubbo:consumer:com.alibaba.csp.sentinel.adapter.dubbo.provider.DemoService:sayHello(java.lang.String,int)", resourceName); + + + //test with custom prefix + SentinelConfig.setConfig(DubboConfig.DUBBO_PROVIDER_PREFIX, "my:dubbo:provider:"); + SentinelConfig.setConfig(DubboConfig.DUBBO_CONSUMER_PREFIX, "my:dubbo:consumer:"); + resourceName = DubboUtils.getResourceName(invoker, invocation, DubboConfig.getDubboProviderPrefix()); + assertEquals("my:dubbo:provider:com.alibaba.csp.sentinel.adapter.dubbo.provider.DemoService:sayHello(java.lang.String,int)", resourceName); + resourceName = DubboUtils.getResourceName(invoker, invocation, DubboConfig.getDubboConsumerPrefix()); + assertEquals("my:dubbo:consumer:com.alibaba.csp.sentinel.adapter.dubbo.provider.DemoService:sayHello(java.lang.String,int)", resourceName); + } -} +} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboConsumerFilterTest.java b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboConsumerFilterTest.java index e4b30b95e0..f949231bc3 100644 --- a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboConsumerFilterTest.java +++ b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboConsumerFilterTest.java @@ -16,9 +16,12 @@ package com.alibaba.csp.sentinel.adapter.dubbo; import com.alibaba.csp.sentinel.BaseTest; -import com.alibaba.csp.sentinel.Constants; import com.alibaba.csp.sentinel.Entry; import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.adapter.dubbo.config.DubboConfig; +import com.alibaba.csp.sentinel.adapter.dubbo.fallback.DefaultDubboFallback; +import com.alibaba.csp.sentinel.adapter.dubbo.fallback.DubboFallback; +import com.alibaba.csp.sentinel.adapter.dubbo.fallback.DubboFallbackRegistry; import com.alibaba.csp.sentinel.adapter.dubbo.provider.DemoService; import com.alibaba.csp.sentinel.context.Context; import com.alibaba.csp.sentinel.context.ContextUtil; @@ -27,20 +30,40 @@ import com.alibaba.csp.sentinel.node.Node; import com.alibaba.csp.sentinel.node.StatisticNode; import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; - +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import org.apache.dubbo.rpc.AsyncRpcResult; import org.apache.dubbo.rpc.Invocation; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.Result; +import org.apache.dubbo.rpc.RpcContext; +import org.apache.dubbo.rpc.support.RpcUtils; import org.junit.After; import org.junit.Before; import org.junit.Test; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.Set; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static com.alibaba.csp.sentinel.slots.block.RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO; +import static org.apache.dubbo.rpc.Constants.ASYNC_KEY; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * @author cdfive @@ -49,28 +72,191 @@ public class SentinelDubboConsumerFilterTest extends BaseTest { private SentinelDubboConsumerFilter filter = new SentinelDubboConsumerFilter(); + @Before public void setUp() { cleanUpAll(); + initFallback(); + constructInvokerAndInvocation(); } @After public void cleanUp() { cleanUpAll(); + DubboFallbackRegistry.setConsumerFallback(new DefaultDubboFallback()); + } + + public void initFlowRule(String resource) { + FlowRule flowRule = new FlowRule(resource); + flowRule.setCount(1); + flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS); + List flowRules = new ArrayList<>(); + flowRules.add(flowRule); + FlowRuleManager.loadRules(flowRules); + } + + public void initDegradeRule(String resource) { + DegradeRule degradeRule = new DegradeRule(resource) + .setCount(0.5) + .setGrade(DEGRADE_GRADE_EXCEPTION_RATIO); + List degradeRules = new ArrayList<>(); + degradeRules.add(degradeRule); + degradeRule.setTimeWindow(1); + DegradeRuleManager.loadRules(degradeRules); + } + + + public void initFallback() { + DubboFallbackRegistry.setConsumerFallback(new DubboFallback() { + @Override + public Result handle(Invoker invoker, Invocation invocation, BlockException ex) { + boolean async = RpcUtils.isAsync(invoker.getUrl(), invocation); + Result fallbackResult = null; + fallbackResult = AsyncRpcResult.newDefaultAsyncResult("fallback", invocation); + return fallbackResult; + } + }); + } + + @Test + public void testInterfaceLevelFollowControlAsync() throws InterruptedException { + when(invocation.getAttachment(ASYNC_KEY)).thenReturn(Boolean.TRUE.toString()); + initFlowRule(invoker.getUrl().getColonSeparatedKey()); + Result result1 = responseBack(requestGo(false, invocation)); + assertEquals("normal", result1.getValue()); + // should fallback because the qps > 1 + Result result2 = responseBack(requestGo(false, invocation)); + assertEquals("fallback", result2.getValue()); + // sleeping 1000 ms to reset qps + Thread.sleep(1000); + Result result3 = responseBack(requestGo(false, invocation)); + assertEquals("normal", result3.getValue()); + + verifyInvocationStructureForCallFinish(); } @Test - public void testInvoke() { - final Invoker invoker = mock(Invoker.class); - when(invoker.getInterface()).thenReturn(DemoService.class); + public void testDegradeAsync() throws InterruptedException { + when(invocation.getAttachment(ASYNC_KEY)).thenReturn(Boolean.TRUE.toString()); + + initDegradeRule(invoker.getUrl().getColonSeparatedKey()); + Result result = requestGo(false, invocation); + verifyInvocationStructureForAsyncCall(invoker, invocation); + responseBack(result); + assertEquals("normal", result.getValue()); + // inc the clusterNode's exception to trigger the fallback + for (int i = 0; i < 5; i++) { + responseBack(requestGo(true, invocation)); + verifyInvocationStructureForCallFinish(); + } + Result result2 = responseBack(requestGo(false, invocation)); + assertEquals("fallback", result2.getValue()); + // sleeping 1000 ms to reset exception + Thread.sleep(1000); + + Result result3 = responseBack(requestGo(false, invocation)); + assertEquals("normal", result3.getValue()); + + Context context = ContextUtil.getContext(); + assertNull(context); + } + + @Test + public void testDegradeSync() throws InterruptedException { + + initDegradeRule(invoker.getUrl().getColonSeparatedKey()); + Result result = requestGo(false, invocation); + verifyInvocationStructure(invoker, invocation); + responseBack(result); + assertEquals("normal", result.getValue()); + // inc the clusterNode's exception to trigger the fallback + for (int i = 0; i < 5; i++) { + responseBack(requestGo(true, invocation)); + verifyInvocationStructureForCallFinish(); + } + Result result2 = responseBack(requestGo(false, invocation)); + assertEquals("fallback", result2.getValue()); + // sleeping 1000 ms to reset exception + Thread.sleep(1000); + + Result result3 = responseBack(requestGo(false, invocation)); + assertEquals("normal", result3.getValue()); + + Context context = ContextUtil.getContext(); + assertNull(context); + } - final Invocation invocation = mock(Invocation.class); - Method method = DemoService.class.getMethods()[0]; - when(invocation.getMethodName()).thenReturn(method.getName()); - when(invocation.getParameterTypes()).thenReturn(method.getParameterTypes()); + + @Test + public void testMethodFlowControlAsync() { + when(invocation.getAttachment(ASYNC_KEY)).thenReturn(Boolean.TRUE.toString()); + initFlowRule(DubboUtils.getResourceName(invoker, invocation, DubboConfig.getDubboConsumerPrefix())); + responseBack(requestGo(false, invocation)); + + responseBack(requestGo(false, invocation)); + + Invocation invocation2 = mock(Invocation.class); + Method method = DemoService.class.getMethods()[1]; + when(invocation2.getMethodName()).thenReturn(method.getName()); + when(invocation2.getParameterTypes()).thenReturn(method.getParameterTypes()); + Result result2 = responseBack(requestGo(false, invocation2)); + verifyInvocationStructureForCallFinish(); + assertEquals("normal", result2.getValue()); + + // the method of invocation should be blocked + Result fallback = requestGo(false, invocation); + assertNotNull(RpcContext.getContext().get(DubboUtils.DUBBO_INTERFACE_ENTRY_KEY)); + assertNull(RpcContext.getContext().get(DubboUtils.DUBBO_METHOD_ENTRY_KEY)); + responseBack(fallback); + assertEquals("fallback", fallback.getValue()); + verifyInvocationStructureForCallFinish(); + + + } + + public Result requestGo(boolean exception, Invocation currentInvocation) { + AsyncRpcResult result = null; + + if (exception) { + result = AsyncRpcResult.newDefaultAsyncResult(new Exception("error"), currentInvocation); + } else { + result = AsyncRpcResult.newDefaultAsyncResult("normal", currentInvocation); + } + when(invoker.invoke(currentInvocation)).thenReturn(result); + return filter.invoke(invoker, currentInvocation); + } + + public Result responseBack(Result result) { + filter.listener().onResponse(result, invoker, invocation); + return result; + } + + + @Test + public void testInvokeAsync() throws InterruptedException { + + when(invocation.getAttachment(ASYNC_KEY)).thenReturn(Boolean.TRUE.toString()); final Result result = mock(Result.class); when(result.hasException()).thenReturn(false); + when(invoker.invoke(invocation)).thenAnswer(invocationOnMock -> { + verifyInvocationStructureForAsyncCall(invoker, invocation); + return result; + }); + + filter.invoke(invoker, invocation); + verify(invoker).invoke(invocation); + + Context context = ContextUtil.getContext(); + assertNotNull(context); + } + + @Test + public void testInvokeSync() { + + final Result result = mock(Result.class); + when(result.hasException()).thenReturn(false); + when(result.getException()).thenReturn(new Exception()); when(invoker.invoke(invocation)).thenAnswer(invocationOnMock -> { verifyInvocationStructure(invoker, invocation); return result; @@ -79,6 +265,7 @@ public void testInvoke() { filter.invoke(invoker, invocation); verify(invoker).invoke(invocation); + filter.listener().onResponse(result, invoker, invocation); Context context = ContextUtil.getContext(); assertNull(context); } @@ -92,34 +279,35 @@ public void testInvoke() { private void verifyInvocationStructure(Invoker invoker, Invocation invocation) { Context context = ContextUtil.getContext(); assertNotNull(context); - // As not call ContextUtil.enter(resourceName, application) in SentinelDubboConsumerFilter, use default context // In actual project, a consumer is usually also a provider, the context will be created by SentinelDubboProviderFilter // If consumer is on the top of Dubbo RPC invocation chain, use default context - String resourceName = DubboUtils.getResourceName(invoker, invocation); - assertEquals(Constants.CONTEXT_DEFAULT_NAME, context.getName()); + String resourceName = DubboUtils.getResourceName(invoker, invocation, true); + assertEquals(com.alibaba.csp.sentinel.Constants.CONTEXT_DEFAULT_NAME, context.getName()); assertEquals("", context.getOrigin()); DefaultNode entranceNode = context.getEntranceNode(); ResourceWrapper entranceResource = entranceNode.getId(); - assertEquals(Constants.CONTEXT_DEFAULT_NAME, entranceResource.getName()); - assertSame(EntryType.IN, entranceResource.getType()); + + assertEquals(com.alibaba.csp.sentinel.Constants.CONTEXT_DEFAULT_NAME, entranceResource.getName()); + assertSame(EntryType.IN, entranceResource.getEntryType()); // As SphU.entry(interfaceName, EntryType.OUT); Set childList = entranceNode.getChildList(); assertEquals(1, childList.size()); - DefaultNode interfaceNode = (DefaultNode) childList.iterator().next(); + DefaultNode interfaceNode = getNode(invoker.getUrl().getColonSeparatedKey(), entranceNode); ResourceWrapper interfaceResource = interfaceNode.getId(); - assertEquals(DemoService.class.getName(), interfaceResource.getName()); - assertSame(EntryType.OUT, interfaceResource.getType()); + + assertEquals(invoker.getUrl().getColonSeparatedKey(), interfaceResource.getName()); + assertSame(EntryType.OUT, interfaceResource.getEntryType()); // As SphU.entry(resourceName, EntryType.OUT); childList = interfaceNode.getChildList(); assertEquals(1, childList.size()); - DefaultNode methodNode = (DefaultNode) childList.iterator().next(); + DefaultNode methodNode = getNode(resourceName, entranceNode); ResourceWrapper methodResource = methodNode.getId(); assertEquals(resourceName, methodResource.getName()); - assertSame(EntryType.OUT, methodResource.getType()); + assertSame(EntryType.OUT, methodResource.getEntryType()); // Verify curEntry Entry curEntry = context.getCurEntry(); @@ -139,4 +327,82 @@ private void verifyInvocationStructure(Invoker invoker, Invocation invocation) { Map interfaceOriginCountMap = interfaceClusterNode.getOriginCountMap(); assertEquals(0, interfaceOriginCountMap.size()); } + + private void verifyInvocationStructureForAsyncCall(Invoker invoker, Invocation invocation) { + Context context = ContextUtil.getContext(); + assertNotNull(context); + + // As not call ContextUtil.enter(resourceName, application) in SentinelDubboConsumerFilter, use default context + // In actual project, a consumer is usually also a provider, the context will be created by SentinelDubboProviderFilter + // If consumer is on the top of Dubbo RPC invocation chain, use default context + String resourceName = DubboUtils.getResourceName(invoker, invocation, true); + assertEquals(com.alibaba.csp.sentinel.Constants.CONTEXT_DEFAULT_NAME, context.getName()); + assertEquals("", context.getOrigin()); + + DefaultNode entranceNode = context.getEntranceNode(); + ResourceWrapper entranceResource = entranceNode.getId(); + assertEquals(com.alibaba.csp.sentinel.Constants.CONTEXT_DEFAULT_NAME, entranceResource.getName()); + assertSame(EntryType.IN, entranceResource.getEntryType()); + + // As SphU.entry(interfaceName, EntryType.OUT); + Set childList = entranceNode.getChildList(); + assertEquals(2, childList.size()); + DefaultNode interfaceNode = getNode(invoker.getUrl().getColonSeparatedKey(), entranceNode); + ResourceWrapper interfaceResource = interfaceNode.getId(); + assertEquals(invoker.getUrl().getColonSeparatedKey(), interfaceResource.getName()); + assertSame(EntryType.OUT, interfaceResource.getEntryType()); + + // As SphU.entry(resourceName, EntryType.OUT); + childList = interfaceNode.getChildList(); + assertEquals(0, childList.size()); + DefaultNode methodNode = getNode(resourceName, entranceNode); + ResourceWrapper methodResource = methodNode.getId(); + assertEquals(resourceName, methodResource.getName()); + assertSame(EntryType.OUT, methodResource.getEntryType()); + + // Verify curEntry + // nothing will bind to local context when use the AsyncEntry + Entry curEntry = context.getCurEntry(); + assertNull(curEntry); + + // Verify clusterNode + ClusterNode methodClusterNode = methodNode.getClusterNode(); + ClusterNode interfaceClusterNode = interfaceNode.getClusterNode(); + assertNotSame(methodClusterNode, interfaceClusterNode);// Different resource->Different ProcessorSlot->Different ClusterNode + + // As context origin is "", the StatisticNode should not be created in originCountMap of ClusterNode + Map methodOriginCountMap = methodClusterNode.getOriginCountMap(); + assertEquals(0, methodOriginCountMap.size()); + + Map interfaceOriginCountMap = interfaceClusterNode.getOriginCountMap(); + assertEquals(0, interfaceOriginCountMap.size()); + } + + + private void verifyInvocationStructureForCallFinish() { + Context context = ContextUtil.getContext(); + assertNull(context); + Entry interfaceEntry = (Entry) RpcContext.getContext().get(DubboUtils.DUBBO_INTERFACE_ENTRY_KEY); + Entry methodEntry = (Entry) RpcContext.getContext().get(DubboUtils.DUBBO_METHOD_ENTRY_KEY); + assertNull(interfaceEntry); + assertNull(methodEntry); + } + + + public DefaultNode getNode(String resourceName, DefaultNode root) { + + Queue queue = new LinkedList<>(); + queue.offer(root); + while (!queue.isEmpty()) { + DefaultNode temp = queue.poll(); + if (temp.getId().getName().equals(resourceName)) { + return temp; + } + for (Node node : temp.getChildList()) { + queue.offer((DefaultNode) node); + } + } + return null; + } + } diff --git a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboProviderFilterTest.java b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboProviderFilterTest.java index 8b0ab899a9..1a7000f1d6 100644 --- a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboProviderFilterTest.java +++ b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboProviderFilterTest.java @@ -26,7 +26,8 @@ import com.alibaba.csp.sentinel.node.Node; import com.alibaba.csp.sentinel.node.StatisticNode; import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; - +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.constants.CommonConstants; import org.apache.dubbo.rpc.Invocation; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.Result; @@ -38,8 +39,15 @@ import java.util.Map; import java.util.Set; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * @author cdfive @@ -50,6 +58,7 @@ public class SentinelDubboProviderFilterTest extends BaseTest { @Before public void setUp() { + constructInvokerAndInvocation(); cleanUpAll(); } @@ -62,18 +71,16 @@ public void cleanUp() { public void testInvoke() { final String originApplication = "consumerA"; - final Invoker invoker = mock(Invoker.class); - when(invoker.getInterface()).thenReturn(DemoService.class); + URL url = invoker.getUrl() + .addParameter(CommonConstants.SIDE_KEY, CommonConstants.PROVIDER_SIDE); + when(invoker.getUrl()).thenReturn(url); - final Invocation invocation = mock(Invocation.class); - Method method = DemoService.class.getMethods()[0]; - when(invocation.getMethodName()).thenReturn(method.getName()); - when(invocation.getParameterTypes()).thenReturn(method.getParameterTypes()); when(invocation.getAttachment(DubboUtils.SENTINEL_DUBBO_APPLICATION_KEY, "")) - .thenReturn(originApplication); + .thenReturn(originApplication); final Result result = mock(Result.class); when(result.hasException()).thenReturn(false); + when(result.getException()).thenReturn(new Exception()); when(invoker.invoke(invocation)).thenAnswer(invocationOnMock -> { verifyInvocationStructure(originApplication, invoker, invocation); return result; @@ -82,6 +89,7 @@ public void testInvoke() { filter.invoke(invoker, invocation); verify(invoker).invoke(invocation); + filter.listener().onResponse(result, invoker, invocation); Context context = ContextUtil.getContext(); assertNull(context); } @@ -97,22 +105,23 @@ private void verifyInvocationStructure(String originApplication, Invoker invoker assertNotNull(context); // As ContextUtil.enter(resourceName, application) in SentinelDubboProviderFilter - String resourceName = DubboUtils.getResourceName(invoker, invocation); + String resourceName = DubboUtils.getResourceName(invoker, invocation, true); assertEquals(resourceName, context.getName()); assertEquals(originApplication, context.getOrigin()); DefaultNode entranceNode = context.getEntranceNode(); ResourceWrapper entranceResource = entranceNode.getId(); assertEquals(resourceName, entranceResource.getName()); - assertSame(EntryType.IN, entranceResource.getType()); + assertSame(EntryType.IN, entranceResource.getEntryType()); // As SphU.entry(interfaceName, EntryType.IN); Set childList = entranceNode.getChildList(); assertEquals(1, childList.size()); DefaultNode interfaceNode = (DefaultNode) childList.iterator().next(); ResourceWrapper interfaceResource = interfaceNode.getId(); - assertEquals(DemoService.class.getName(), interfaceResource.getName()); - assertSame(EntryType.IN, interfaceResource.getType()); + + assertEquals(invoker.getUrl().getColonSeparatedKey(), interfaceResource.getName()); + assertSame(EntryType.IN, interfaceResource.getEntryType()); // As SphU.entry(resourceName, EntryType.IN, 1, invocation.getArguments()); childList = interfaceNode.getChildList(); @@ -120,7 +129,7 @@ private void verifyInvocationStructure(String originApplication, Invoker invoker DefaultNode methodNode = (DefaultNode) childList.iterator().next(); ResourceWrapper methodResource = methodNode.getId(); assertEquals(resourceName, methodResource.getName()); - assertSame(EntryType.IN, methodResource.getType()); + assertSame(EntryType.IN, methodResource.getEntryType()); // Verify curEntry Entry curEntry = context.getCurEntry(); diff --git a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/fallback/DubboFallbackRegistryTest.java b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/fallback/DubboFallbackRegistryTest.java index bc06ebf179..e1f199b729 100644 --- a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/fallback/DubboFallbackRegistryTest.java +++ b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/fallback/DubboFallbackRegistryTest.java @@ -18,10 +18,11 @@ import com.alibaba.csp.sentinel.slots.block.BlockException; import com.alibaba.csp.sentinel.slots.block.SentinelRpcException; import com.alibaba.csp.sentinel.slots.block.flow.FlowException; - +import org.apache.dubbo.rpc.AsyncRpcResult; import org.apache.dubbo.rpc.Result; -import org.apache.dubbo.rpc.RpcResult; +import org.junit.After; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; /** @@ -29,6 +30,16 @@ */ public class DubboFallbackRegistryTest { + @Before + public void setUp() { + DubboFallbackRegistry.setConsumerFallback(new DefaultDubboFallback()); + } + + @After + public void tearDown() { + DubboFallbackRegistry.setConsumerFallback(new DefaultDubboFallback()); + } + @Test(expected = SentinelRpcException.class) public void testDefaultFallback() { // Test for default. @@ -41,7 +52,7 @@ public void testDefaultFallback() { public void testCustomFallback() { BlockException ex = new FlowException("xxx"); DubboFallbackRegistry.setConsumerFallback( - (invoker, invocation, e) -> new RpcResult("Error: " + e.getClass().getName())); + (invoker, invocation, e) -> AsyncRpcResult.newDefaultAsyncResult("Error: " + e.getClass().getName(), invocation)); Result result = DubboFallbackRegistry.getConsumerFallback() .handle(null, null, ex); Assert.assertFalse("The invocation should not fail", result.hasException()); diff --git a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/provider/DemoService.java b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/provider/DemoService.java index 2a024cc9cb..26e8447997 100644 --- a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/provider/DemoService.java +++ b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/provider/DemoService.java @@ -20,4 +20,5 @@ */ public interface DemoService { String sayHello(String name, int n); + String sayHi(String name,int n); } diff --git a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/provider/impl/DemoServiceImpl.java b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/provider/impl/DemoServiceImpl.java index f804c2c42e..b09e08bea9 100644 --- a/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/provider/impl/DemoServiceImpl.java +++ b/sentinel-adapter/sentinel-apache-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/provider/impl/DemoServiceImpl.java @@ -24,4 +24,9 @@ public class DemoServiceImpl implements DemoService { public String sayHello(String name, int n) { return "Hello " + name + ", " + n; } + + @Override + public String sayHi(String name, int n) { + return "Hi " + name + ", " + n; + } } diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/pom.xml b/sentinel-adapter/sentinel-api-gateway-adapter-common/pom.xml index 5cc4a43f06..6b2d57dcd3 100644 --- a/sentinel-adapter/sentinel-api-gateway-adapter-common/pom.xml +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/pom.xml @@ -5,7 +5,7 @@ sentinel-adapter com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/SentinelGatewayConstants.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/SentinelGatewayConstants.java index 2c56e6b7ce..5e8f78e2a7 100644 --- a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/SentinelGatewayConstants.java +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/SentinelGatewayConstants.java @@ -30,16 +30,22 @@ public final class SentinelGatewayConstants { public static final int PARAM_PARSE_STRATEGY_HOST = 1; public static final int PARAM_PARSE_STRATEGY_HEADER = 2; public static final int PARAM_PARSE_STRATEGY_URL_PARAM = 3; + public static final int PARAM_PARSE_STRATEGY_COOKIE = 4; + + public static final int URL_MATCH_STRATEGY_EXACT = 0; + public static final int URL_MATCH_STRATEGY_PREFIX = 1; + public static final int URL_MATCH_STRATEGY_REGEX = 2; public static final int PARAM_MATCH_STRATEGY_EXACT = 0; public static final int PARAM_MATCH_STRATEGY_PREFIX = 1; public static final int PARAM_MATCH_STRATEGY_REGEX = 2; + public static final int PARAM_MATCH_STRATEGY_CONTAINS = 3; public static final String GATEWAY_CONTEXT_DEFAULT = "sentinel_gateway_context_default"; public static final String GATEWAY_CONTEXT_PREFIX = "sentinel_gateway_context$$"; public static final String GATEWAY_CONTEXT_ROUTE_PREFIX = "sentinel_gateway_context$$route$$"; - public static final String GATEWAY_NOT_MATCH_PARAM = "$$not_match"; + public static final String GATEWAY_NOT_MATCH_PARAM = "$NM"; public static final String GATEWAY_DEFAULT_PARAM = "$D"; private SentinelGatewayConstants() {} diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiPathPredicateItem.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiPathPredicateItem.java index 1cbce589fb..a47b280154 100644 --- a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiPathPredicateItem.java +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiPathPredicateItem.java @@ -17,6 +17,8 @@ import java.util.Objects; +import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; + /** * @author Eric Zhao * @since 1.6.0 @@ -24,7 +26,7 @@ public class ApiPathPredicateItem implements ApiPredicateItem { private String pattern; - private int matchStrategy; + private int matchStrategy = SentinelGatewayConstants.URL_MATCH_STRATEGY_EXACT; public ApiPathPredicateItem setPattern(String pattern) { this.pattern = pattern; diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/command/UpdateGatewayApiDefinitionGroupCommandHandler.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/command/UpdateGatewayApiDefinitionGroupCommandHandler.java index 963561f82a..d480512c28 100644 --- a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/command/UpdateGatewayApiDefinitionGroupCommandHandler.java +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/command/UpdateGatewayApiDefinitionGroupCommandHandler.java @@ -15,19 +15,24 @@ */ package com.alibaba.csp.sentinel.adapter.gateway.common.command; -import java.net.URLDecoder; -import java.util.HashSet; -import java.util.List; - import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition; +import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem; +import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem; import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager; import com.alibaba.csp.sentinel.command.CommandHandler; import com.alibaba.csp.sentinel.command.CommandRequest; import com.alibaba.csp.sentinel.command.CommandResponse; import com.alibaba.csp.sentinel.command.annotation.CommandMapping; +import com.alibaba.csp.sentinel.datasource.WritableDataSource; import com.alibaba.csp.sentinel.log.RecordLog; import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; + +import java.net.URLDecoder; +import java.util.HashSet; +import java.util.Set; /** * @author Eric Zhao @@ -36,6 +41,8 @@ @CommandMapping(name = "gateway/updateApiDefinitions", desc = "") public class UpdateGatewayApiDefinitionGroupCommandHandler implements CommandHandler { + private static WritableDataSource> apiDefinitionWds = null; + @Override public CommandResponse handle(CommandRequest request) { String data = request.getParam("data"); @@ -52,10 +59,67 @@ public CommandResponse handle(CommandRequest request) { RecordLog.info("[API Server] Receiving data change (type: gateway API definition): {0}", data); String result = SUCCESS_MSG; - List apiDefinitions = JSONArray.parseArray(data, ApiDefinition.class); - GatewayApiDefinitionManager.loadApiDefinitions(new HashSet<>(apiDefinitions)); + + Set apiDefinitions = parseJson(data); + GatewayApiDefinitionManager.loadApiDefinitions(apiDefinitions); + if (!writeToDataSource(apiDefinitionWds, apiDefinitions)) { + result = WRITE_DS_FAILURE_MSG; + } return CommandResponse.ofSuccess(result); } private static final String SUCCESS_MSG = "success"; + private static final String WRITE_DS_FAILURE_MSG = "partial success (write data source failed)"; + + /** + * Parse json data to set of {@link ApiDefinition}. + * + * Since the predicateItems of {@link ApiDefinition} is set of interface, + * here we parse predicateItems to {@link ApiPathPredicateItem} temporarily. + */ + private Set parseJson(String data) { + Set apiDefinitions = new HashSet<>(); + JSONArray array = JSON.parseArray(data); + for (Object obj : array) { + JSONObject o = (JSONObject)obj; + ApiDefinition apiDefinition = new ApiDefinition((o.getString("apiName"))); + Set predicateItems = new HashSet<>(); + JSONArray itemArray = o.getJSONArray("predicateItems"); + if (itemArray != null) { + predicateItems.addAll(itemArray.toJavaList(ApiPathPredicateItem.class)); + } + apiDefinition.setPredicateItems(predicateItems); + apiDefinitions.add(apiDefinition); + } + + return apiDefinitions; + } + + /** + * Write target value to given data source. + * + * @param dataSource writable data source + * @param value target value to save + * @param value type + * @return true if write successful or data source is empty; false if error occurs + */ + private boolean writeToDataSource(WritableDataSource dataSource, T value) { + if (dataSource != null) { + try { + dataSource.write(value); + } catch (Exception e) { + RecordLog.warn("Write data source failed", e); + return false; + } + } + return true; + } + + public synchronized static WritableDataSource> getWritableDataSource() { + return apiDefinitionWds; + } + + public synchronized static void setWritableDataSource(WritableDataSource> apiDefinitionWds) { + UpdateGatewayApiDefinitionGroupCommandHandler.apiDefinitionWds = apiDefinitionWds; + } } diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/command/UpdateGatewayRuleCommandHandler.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/command/UpdateGatewayRuleCommandHandler.java index 46e14209b4..b79b78f9cf 100644 --- a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/command/UpdateGatewayRuleCommandHandler.java +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/command/UpdateGatewayRuleCommandHandler.java @@ -15,19 +15,20 @@ */ package com.alibaba.csp.sentinel.adapter.gateway.common.command; -import java.net.URLDecoder; -import java.util.HashSet; -import java.util.List; - import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule; import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager; import com.alibaba.csp.sentinel.command.CommandHandler; import com.alibaba.csp.sentinel.command.CommandRequest; import com.alibaba.csp.sentinel.command.CommandResponse; import com.alibaba.csp.sentinel.command.annotation.CommandMapping; +import com.alibaba.csp.sentinel.datasource.WritableDataSource; import com.alibaba.csp.sentinel.log.RecordLog; import com.alibaba.csp.sentinel.util.StringUtil; -import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.TypeReference; + +import java.net.URLDecoder; +import java.util.Set; /** * @author Eric Zhao @@ -35,6 +36,7 @@ */ @CommandMapping(name = "gateway/updateRules", desc = "Update gateway rules") public class UpdateGatewayRuleCommandHandler implements CommandHandler { + private static WritableDataSource> gatewayFlowWds = null; @Override public CommandResponse handle(CommandRequest request) { @@ -52,10 +54,43 @@ public CommandResponse handle(CommandRequest request) { RecordLog.info(String.format("[API Server] Receiving rule change (type: gateway rule): %s", data)); String result = SUCCESS_MSG; - List flowRules = JSONArray.parseArray(data, GatewayFlowRule.class); - GatewayRuleManager.loadRules(new HashSet<>(flowRules)); + Set flowRules = JSON.parseObject(data, new TypeReference>() { + }); + GatewayRuleManager.loadRules(flowRules); + if (!writeToDataSource(gatewayFlowWds, flowRules)) { + result = WRITE_DS_FAILURE_MSG; + } return CommandResponse.ofSuccess(result); } + /** + * Write target value to given data source. + * + * @param dataSource writable data source + * @param value target value to save + * @param value type + * @return true if write successful or data source is empty; false if error occurs + */ + private boolean writeToDataSource(WritableDataSource dataSource, T value) { + if (dataSource != null) { + try { + dataSource.write(value); + } catch (Exception e) { + RecordLog.warn("Write data source failed", e); + return false; + } + } + return true; + } + + public synchronized static WritableDataSource> getWritableDataSource() { + return gatewayFlowWds; + } + + public synchronized static void setWritableDataSource(WritableDataSource> gatewayFlowWds) { + UpdateGatewayRuleCommandHandler.gatewayFlowWds = gatewayFlowWds; + } + private static final String SUCCESS_MSG = "success"; + private static final String WRITE_DS_FAILURE_MSG = "partial success (write data source failed)"; } \ No newline at end of file diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/GatewayParamParser.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/GatewayParamParser.java index f326468b68..40bc92d71f 100644 --- a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/GatewayParamParser.java +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/GatewayParamParser.java @@ -17,6 +17,7 @@ import java.util.HashSet; import java.util.Set; +import java.util.regex.Pattern; import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule; @@ -42,8 +43,8 @@ public GatewayParamParser(RequestItemParser requestItemParser) { /** * Parse parameters for given resource from the request entity on condition of the rule predicate. * - * @param resource valid resource name - * @param request valid request + * @param resource valid resource name + * @param request valid request * @param rulePredicate rule predicate indicating the rules to refer * @return the parameter array */ @@ -92,6 +93,8 @@ private String parseInternal(GatewayParamFlowItem item, T request) { return parseHeader(item, request); case SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM: return parseUrlParameter(item, request); + case SentinelGatewayConstants.PARAM_PARSE_STRATEGY_COOKIE: + return parseCookie(item, request); default: return null; } @@ -100,7 +103,7 @@ private String parseInternal(GatewayParamFlowItem item, T request) { private String parseClientIp(/*@Valid*/ GatewayParamFlowItem item, T request) { String clientIp = requestItemParser.getRemoteAddress(request); String pattern = item.getPattern(); - if (pattern == null) { + if (StringUtil.isEmpty(pattern)) { return clientIp; } return parseWithMatchStrategyInternal(item.getMatchStrategy(), clientIp, pattern); @@ -111,7 +114,7 @@ private String parseHeader(/*@Valid*/ GatewayParamFlowItem item, T request) { String pattern = item.getPattern(); // TODO: what if the header has multiple values? String headerValue = requestItemParser.getHeader(request, headerKey); - if (pattern == null) { + if (StringUtil.isEmpty(pattern)) { return headerValue; } // Match value according to regex pattern or exact mode. @@ -121,7 +124,7 @@ private String parseHeader(/*@Valid*/ GatewayParamFlowItem item, T request) { private String parseHost(/*@Valid*/ GatewayParamFlowItem item, T request) { String pattern = item.getPattern(); String host = requestItemParser.getHeader(request, "Host"); - if (pattern == null) { + if (StringUtil.isEmpty(pattern)) { return host; } // Match value according to regex pattern or exact mode. @@ -132,7 +135,18 @@ private String parseUrlParameter(/*@Valid*/ GatewayParamFlowItem item, T request String paramName = item.getFieldName(); String pattern = item.getPattern(); String param = requestItemParser.getUrlParam(request, paramName); - if (pattern == null) { + if (StringUtil.isEmpty(pattern)) { + return param; + } + // Match value according to regex pattern or exact mode. + return parseWithMatchStrategyInternal(item.getMatchStrategy(), param, pattern); + } + + private String parseCookie(/*@Valid*/ GatewayParamFlowItem item, T request) { + String cookieName = item.getFieldName(); + String pattern = item.getPattern(); + String param = requestItemParser.getCookieValue(request, cookieName); + if (StringUtil.isEmpty(pattern)) { return param; } // Match value according to regex pattern or exact mode. @@ -140,13 +154,22 @@ private String parseUrlParameter(/*@Valid*/ GatewayParamFlowItem item, T request } private String parseWithMatchStrategyInternal(int matchStrategy, String value, String pattern) { - // TODO: implement here. if (value == null) { return null; } - if (matchStrategy == SentinelGatewayConstants.PARAM_MATCH_STRATEGY_REGEX) { - return value; + switch (matchStrategy) { + case SentinelGatewayConstants.PARAM_MATCH_STRATEGY_EXACT: + return value.equals(pattern) ? value : SentinelGatewayConstants.GATEWAY_NOT_MATCH_PARAM; + case SentinelGatewayConstants.PARAM_MATCH_STRATEGY_CONTAINS: + return value.contains(pattern) ? value : SentinelGatewayConstants.GATEWAY_NOT_MATCH_PARAM; + case SentinelGatewayConstants.PARAM_MATCH_STRATEGY_REGEX: + Pattern regex = GatewayRegexCache.getRegexPattern(pattern); + if (regex == null) { + return value; + } + return regex.matcher(value).matches() ? value : SentinelGatewayConstants.GATEWAY_NOT_MATCH_PARAM; + default: + return value; } - return value; } } diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/GatewayRegexCache.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/GatewayRegexCache.java new file mode 100644 index 0000000000..17d22b9e94 --- /dev/null +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/GatewayRegexCache.java @@ -0,0 +1,58 @@ +/* + * Copyright 1999-2019 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 + * + * https://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.adapter.gateway.common.param; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +import com.alibaba.csp.sentinel.log.RecordLog; + +/** + * @author Eric Zhao + * @since 1.6.2 + */ +public final class GatewayRegexCache { + + private static final Map REGEX_CACHE = new ConcurrentHashMap<>(); + + public static Pattern getRegexPattern(String pattern) { + if (pattern == null) { + return null; + } + return REGEX_CACHE.get(pattern); + } + + public static boolean addRegexPattern(String pattern) { + if (pattern == null) { + return false; + } + try { + Pattern regex = Pattern.compile(pattern); + REGEX_CACHE.put(pattern, regex); + return true; + } catch (Exception ex) { + RecordLog.warn("[GatewayRegexCache] Failed to compile the regex: " + pattern, ex); + return false; + } + } + + public static void clear() { + REGEX_CACHE.clear(); + } + + private GatewayRegexCache() {} +} diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/RequestItemParser.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/RequestItemParser.java index 5b28abe785..7eb71c52be 100644 --- a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/RequestItemParser.java +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/RequestItemParser.java @@ -41,7 +41,7 @@ public interface RequestItemParser { * Get the header associated with the header key. * * @param request valid request - * @param key valid header key + * @param key valid header key * @return the header */ String getHeader(T request, String key); @@ -49,9 +49,19 @@ public interface RequestItemParser { /** * Get the parameter value associated with the parameter name. * - * @param request valid request + * @param request valid request * @param paramName valid parameter name * @return the parameter value */ String getUrlParam(T request, String paramName); + + /** + * Get the cookie value associated with the cookie name. + * + * @param request valid request + * @param cookieName valid cookie name + * @return the cookie value + * @since 1.7.0 + */ + String getCookieValue(T request, String cookieName); } diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleConverter.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleConverter.java index f5531fa24f..814c0bfe20 100644 --- a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleConverter.java +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleConverter.java @@ -15,7 +15,9 @@ */ package com.alibaba.csp.sentinel.adapter.gateway.common.rule; +import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowItem; import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule; /** @@ -32,6 +34,18 @@ static FlowRule toFlowRule(/*@Valid*/ GatewayFlowRule rule) { .setMaxQueueingTimeMs(rule.getMaxQueueingTimeoutMs()); } + static ParamFlowItem generateNonMatchPassParamItem() { + return new ParamFlowItem().setClassType(String.class.getName()) + .setCount(1000_0000) + .setObject(SentinelGatewayConstants.GATEWAY_NOT_MATCH_PARAM); + } + + static ParamFlowItem generateNonMatchBlockParamItem() { + return new ParamFlowItem().setClassType(String.class.getName()) + .setCount(0) + .setObject(SentinelGatewayConstants.GATEWAY_NOT_MATCH_PARAM); + } + static ParamFlowRule applyNonParamToParamRule(/*@Valid*/ GatewayFlowRule gatewayRule, int idx) { return new ParamFlowRule(gatewayRule.getResource()) .setCount(gatewayRule.getCount()) @@ -63,7 +77,11 @@ static ParamFlowRule applyToParamRule(/*@Valid*/ GatewayFlowRule gatewayRule, in GatewayParamFlowItem gatewayItem = gatewayRule.getParamItem(); // Apply the current idx to gateway rule item. gatewayItem.setIndex(idx); - // TODO: implement for pattern-based parameters. + // Apply for pattern-based parameters. + String valuePattern = gatewayItem.getPattern(); + if (valuePattern != null) { + paramRule.getParamFlowItemList().add(generateNonMatchPassParamItem()); + } return paramRule; } diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleManager.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleManager.java index e4880bed2d..8744707d85 100644 --- a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleManager.java +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleManager.java @@ -16,6 +16,7 @@ package com.alibaba.csp.sentinel.adapter.gateway.common.rule; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -24,6 +25,7 @@ import java.util.concurrent.ConcurrentHashMap; import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; +import com.alibaba.csp.sentinel.adapter.gateway.common.param.GatewayRegexCache; import com.alibaba.csp.sentinel.log.RecordLog; import com.alibaba.csp.sentinel.property.DynamicSentinelProperty; import com.alibaba.csp.sentinel.property.PropertyListener; @@ -131,6 +133,16 @@ private int getIdxInternal(Map idxMap, String resourceName) { return idxMap.get(resourceName); } + private void cacheRegexPattern(/*@NonNull*/ GatewayParamFlowItem item) { + String pattern = item.getPattern(); + if (StringUtil.isNotEmpty(pattern) && + item.getMatchStrategy() == SentinelGatewayConstants.PARAM_MATCH_STRATEGY_REGEX) { + if (GatewayRegexCache.getRegexPattern(pattern) == null) { + GatewayRegexCache.addRegexPattern(pattern); + } + } + } + private synchronized void applyGatewayRuleInternal(Set conf) { if (conf == null || conf.isEmpty()) { applyToConvertedParamMap(new HashSet()); @@ -162,6 +174,7 @@ private synchronized void applyGatewayRuleInternal(Set conf) { if (paramFlowRules.add(GatewayRuleConverter.applyToParamRule(rule, idx))) { idxMap.put(rule.getResource(), idx + 1); } + cacheRegexPattern(rule.getParamItem()); } // Apply to the gateway rule map. Set ruleSet = gatewayRuleMap.get(resourceName); @@ -242,14 +255,18 @@ static boolean isValidParamItem(/*@NonNull*/ GatewayParamFlowItem item) { if (item.getParseStrategy() < 0) { return false; } - if (item.getParseStrategy() == SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM || - item.getParseStrategy() == SentinelGatewayConstants.PARAM_PARSE_STRATEGY_HEADER) { - if (StringUtil.isBlank(item.getFieldName())) { - return false; - } + // Check required field name for item types. + if (FIELD_REQUIRED_SET.contains(item.getParseStrategy()) && StringUtil.isBlank(item.getFieldName())) { + return false; } return StringUtil.isEmpty(item.getPattern()) || item.getMatchStrategy() >= 0; } + private static final Set FIELD_REQUIRED_SET = new HashSet<>( + Arrays.asList(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM, + SentinelGatewayConstants.PARAM_PARSE_STRATEGY_HEADER, + SentinelGatewayConstants.PARAM_PARSE_STRATEGY_COOKIE) + ); + private GatewayRuleManager() {} } diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/GatewayParamParserTest.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/GatewayParamParserTest.java index 7edc7844ed..e37c693018 100644 --- a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/GatewayParamParserTest.java +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/GatewayParamParserTest.java @@ -94,6 +94,7 @@ public void testParseParametersWithItems() { final String api1 = "my_test_route_B"; final String headerName = "X-Sentinel-Flag"; final String paramName = "p"; + final String cookieName = "myCookie"; GatewayFlowRule routeRuleNoParam = new GatewayFlowRule(routeId1) .setCount(10) .setIntervalSec(10); @@ -128,6 +129,13 @@ public void testParseParametersWithItems() { .setParamItem(new GatewayParamFlowItem() .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_HOST) ); + GatewayFlowRule routeRule5 = new GatewayFlowRule(routeId1) + .setCount(50) + .setIntervalSec(30) + .setParamItem(new GatewayParamFlowItem() + .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_COOKIE) + .setFieldName(cookieName) + ); GatewayFlowRule apiRule1 = new GatewayFlowRule(api1) .setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME) .setCount(5) @@ -140,6 +148,7 @@ public void testParseParametersWithItems() { rules.add(routeRule2); rules.add(routeRule3); rules.add(routeRule4); + rules.add(routeRule5); rules.add(routeRuleNoParam); rules.add(apiRule1); GatewayRuleManager.loadRules(rules); @@ -148,19 +157,24 @@ public void testParseParametersWithItems() { final String expectedAddress = "66.77.88.99"; final String expectedHeaderValue1 = "Sentinel"; final String expectedUrlParamValue1 = "17"; + final String expectedCookieValue1 = "Sentinel-Foo"; + mockClientHostAddress(itemParser, expectedAddress); Map expectedHeaders = new HashMap() {{ put(headerName, expectedHeaderValue1); put("Host", expectedHost); }}; mockHeaders(itemParser, expectedHeaders); mockSingleUrlParam(itemParser, paramName, expectedUrlParamValue1); + mockSingleCookie(itemParser, cookieName, expectedCookieValue1); + Object[] params = paramParser.parseParameterFor(routeId1, request, routeIdPredicate); - // Param length should be 5 (4 with parameters, 1 normal flow with generated constant) - assertThat(params.length).isEqualTo(5); + // Param length should be 6 (5 with parameters, 1 normal flow with generated constant) + assertThat(params.length).isEqualTo(6); assertThat(params[routeRule1.getParamItem().getIndex()]).isEqualTo(expectedAddress); assertThat(params[routeRule2.getParamItem().getIndex()]).isEqualTo(expectedHeaderValue1); assertThat(params[routeRule3.getParamItem().getIndex()]).isEqualTo(expectedUrlParamValue1); assertThat(params[routeRule4.getParamItem().getIndex()]).isEqualTo(expectedHost); + assertThat(params[routeRule5.getParamItem().getIndex()]).isEqualTo(expectedCookieValue1); assertThat(params[params.length - 1]).isEqualTo(SentinelGatewayConstants.GATEWAY_DEFAULT_PARAM); assertThat(paramParser.parseParameterFor(api1, request, routeIdPredicate).length).isZero(); @@ -172,6 +186,125 @@ public void testParseParametersWithItems() { assertThat(params[apiRule1.getParamItem().getIndex()]).isEqualTo(expectedUrlParamValue2); } + @Test + public void testParseParametersWithEmptyItemPattern() { + RequestItemParser itemParser = mock(RequestItemParser.class); + GatewayParamParser paramParser = new GatewayParamParser<>(itemParser); + // Create a fake request. + Object request = new Object(); + // Prepare gateway rules. + Set rules = new HashSet<>(); + final String routeId = "my_test_route_DS(*H"; + final String headerName = "X-Sentinel-Flag"; + GatewayFlowRule routeRule1 = new GatewayFlowRule(routeId) + .setCount(10) + .setIntervalSec(2) + .setParamItem(new GatewayParamFlowItem() + .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_HEADER) + .setFieldName(headerName) + .setPattern("") + .setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_EXACT) + ); + rules.add(routeRule1); + GatewayRuleManager.loadRules(rules); + + mockSingleHeader(itemParser, headerName, "Sent1nel"); + Object[] params = paramParser.parseParameterFor(routeId, request, routeIdPredicate); + assertThat(params.length).isEqualTo(1); + // Empty pattern should not take effect. + assertThat(params[routeRule1.getParamItem().getIndex()]).isEqualTo("Sent1nel"); + } + + @Test + public void testParseParametersWithItemPatternMatching() { + RequestItemParser itemParser = mock(RequestItemParser.class); + GatewayParamParser paramParser = new GatewayParamParser<>(itemParser); + // Create a fake request. + Object request = new Object(); + + // Prepare gateway rules. + Set rules = new HashSet<>(); + final String routeId1 = "my_test_route_F&@"; + final String api1 = "my_test_route_E5K"; + final String headerName = "X-Sentinel-Flag"; + final String paramName = "p"; + + String nameEquals = "Wow"; + String nameContains = "warn"; + String valueRegex = "\\d+"; + GatewayFlowRule routeRule1 = new GatewayFlowRule(routeId1) + .setCount(10) + .setIntervalSec(1) + .setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER) + .setMaxQueueingTimeoutMs(600) + .setParamItem(new GatewayParamFlowItem() + .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_HEADER) + .setFieldName(headerName) + .setPattern(nameEquals) + .setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_EXACT) + ); + GatewayFlowRule routeRule2 = new GatewayFlowRule(routeId1) + .setCount(20) + .setIntervalSec(1) + .setBurst(5) + .setParamItem(new GatewayParamFlowItem() + .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM) + .setFieldName(paramName) + .setPattern(nameContains) + .setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_CONTAINS) + ); + GatewayFlowRule apiRule1 = new GatewayFlowRule(api1) + .setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME) + .setCount(5) + .setIntervalSec(1) + .setParamItem(new GatewayParamFlowItem() + .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM) + .setFieldName(paramName) + .setPattern(valueRegex) + .setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_REGEX) + ); + rules.add(routeRule1); + rules.add(routeRule2); + rules.add(apiRule1); + GatewayRuleManager.loadRules(rules); + + mockSingleHeader(itemParser, headerName, nameEquals); + mockSingleUrlParam(itemParser, paramName, nameContains); + Object[] params = paramParser.parseParameterFor(routeId1, request, routeIdPredicate); + assertThat(params.length).isEqualTo(2); + assertThat(params[routeRule1.getParamItem().getIndex()]).isEqualTo(nameEquals); + assertThat(params[routeRule2.getParamItem().getIndex()]).isEqualTo(nameContains); + + mockSingleHeader(itemParser, headerName, nameEquals + "_foo"); + mockSingleUrlParam(itemParser, paramName, nameContains + "_foo"); + params = paramParser.parseParameterFor(routeId1, request, routeIdPredicate); + assertThat(params.length).isEqualTo(2); + assertThat(params[routeRule1.getParamItem().getIndex()]) + .isEqualTo(SentinelGatewayConstants.GATEWAY_NOT_MATCH_PARAM); + assertThat(params[routeRule2.getParamItem().getIndex()]) + .isEqualTo(nameContains + "_foo"); + + mockSingleHeader(itemParser, headerName, "foo"); + mockSingleUrlParam(itemParser, paramName, "foo"); + params = paramParser.parseParameterFor(routeId1, request, routeIdPredicate); + assertThat(params.length).isEqualTo(2); + assertThat(params[routeRule1.getParamItem().getIndex()]) + .isEqualTo(SentinelGatewayConstants.GATEWAY_NOT_MATCH_PARAM); + assertThat(params[routeRule2.getParamItem().getIndex()]) + .isEqualTo(SentinelGatewayConstants.GATEWAY_NOT_MATCH_PARAM); + + mockSingleUrlParam(itemParser, paramName, "23"); + params = paramParser.parseParameterFor(api1, request, apiNamePredicate); + assertThat(params.length).isEqualTo(1); + assertThat(params[apiRule1.getParamItem().getIndex()]).isEqualTo("23"); + + mockSingleUrlParam(itemParser, paramName, "some233"); + params = paramParser.parseParameterFor(api1, request, apiNamePredicate); + assertThat(params.length).isEqualTo(1); + assertThat(params[apiRule1.getParamItem().getIndex()]) + .isEqualTo(SentinelGatewayConstants.GATEWAY_NOT_MATCH_PARAM); + } + private void mockClientHostAddress(/*@Mock*/ RequestItemParser parser, String address) { when(parser.getRemoteAddress(any())).thenReturn(address); } @@ -196,15 +329,21 @@ private void mockSingleHeader(/*@Mock*/ RequestItemParser parser, String key, St when(parser.getHeader(any(), eq(key))).thenReturn(value); } + private void mockSingleCookie(/*@Mock*/ RequestItemParser parser, String key, String value) { + when(parser.getCookieValue(any(), eq(key))).thenReturn(value); + } + @Before public void setUp() { GatewayApiDefinitionManager.loadApiDefinitions(new HashSet()); GatewayRuleManager.loadRules(new HashSet()); + GatewayRegexCache.clear(); } @After public void tearDown() { GatewayApiDefinitionManager.loadApiDefinitions(new HashSet()); GatewayRuleManager.loadRules(new HashSet()); + GatewayRegexCache.clear(); } } diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/GatewayRegexCacheTest.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/GatewayRegexCacheTest.java new file mode 100644 index 0000000000..c10b2888fb --- /dev/null +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/GatewayRegexCacheTest.java @@ -0,0 +1,49 @@ +/* + * Copyright 1999-2019 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.adapter.gateway.common.param; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Eric Zhao + */ +public class GatewayRegexCacheTest { + + @Before + public void setUp() { + GatewayRegexCache.clear(); + } + + @After + public void tearDown() { + GatewayRegexCache.clear(); + } + + @Test + public void testAddAndGetRegexPattern() { + // Test for invalid pattern. + assertThat(GatewayRegexCache.addRegexPattern("\\")).isFalse(); + assertThat(GatewayRegexCache.addRegexPattern(null)).isFalse(); + // Test for good pattern. + String goodPattern = "\\d+"; + assertThat(GatewayRegexCache.addRegexPattern(goodPattern)).isTrue(); + assertThat(GatewayRegexCache.getRegexPattern(goodPattern)).isNotNull(); + } +} diff --git a/sentinel-adapter/sentinel-dubbo-adapter/pom.xml b/sentinel-adapter/sentinel-dubbo-adapter/pom.xml index 6bbf6d6a1b..732c8c6caa 100755 --- a/sentinel-adapter/sentinel-dubbo-adapter/pom.xml +++ b/sentinel-adapter/sentinel-dubbo-adapter/pom.xml @@ -6,7 +6,7 @@ com.alibaba.csp sentinel-adapter - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 sentinel-dubbo-adapter diff --git a/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/AbstractDubboFilter.java b/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/AbstractDubboFilter.java index 2c5b077964..aa8248a524 100755 --- a/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/AbstractDubboFilter.java +++ b/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/AbstractDubboFilter.java @@ -15,6 +15,7 @@ */ package com.alibaba.csp.sentinel.adapter.dubbo; +import com.alibaba.csp.sentinel.util.StringUtil; import com.alibaba.dubbo.rpc.Filter; import com.alibaba.dubbo.rpc.Invocation; import com.alibaba.dubbo.rpc.Invoker; @@ -27,9 +28,9 @@ abstract class AbstractDubboFilter implements Filter { protected String getResourceName(Invoker invoker, Invocation invocation) { StringBuilder buf = new StringBuilder(64); buf.append(invoker.getInterface().getName()) - .append(":") - .append(invocation.getMethodName()) - .append("("); + .append(":") + .append(invocation.getMethodName()) + .append("("); boolean isFirst = true; for (Class clazz : invocation.getParameterTypes()) { if (!isFirst) { @@ -41,4 +42,16 @@ protected String getResourceName(Invoker invoker, Invocation invocation) { buf.append(")"); return buf.toString(); } + + protected String getResourceName(Invoker invoker, Invocation invocation, String prefix) { + if (StringUtil.isBlank(prefix)) { + return getResourceName(invoker, invocation); + } + StringBuilder buf = new StringBuilder(64); + return buf.append(prefix) + .append(getResourceName(invoker, invocation)) + .toString(); + + + } } diff --git a/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboAppContextFilter.java b/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboAppContextFilter.java index ceae979d53..e76f2db42e 100644 --- a/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboAppContextFilter.java +++ b/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboAppContextFilter.java @@ -24,12 +24,14 @@ import com.alibaba.dubbo.rpc.RpcContext; import com.alibaba.dubbo.rpc.RpcException; +import static com.alibaba.dubbo.common.Constants.CONSUMER; + /** * Puts current consumer's application name in the attachment of each invocation. * * @author Eric Zhao */ -@Activate(group = "consumer") +@Activate(group = CONSUMER) public class DubboAppContextFilter implements Filter { @Override diff --git a/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboConsumerFilter.java b/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboConsumerFilter.java index 54e067ba2f..e7c36af92e 100755 --- a/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboConsumerFilter.java +++ b/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboConsumerFilter.java @@ -17,10 +17,11 @@ import com.alibaba.csp.sentinel.Entry; import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.ResourceTypeConstants; import com.alibaba.csp.sentinel.SphU; import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.adapter.dubbo.config.DubboConfig; import com.alibaba.csp.sentinel.adapter.dubbo.fallback.DubboFallbackRegistry; -import com.alibaba.csp.sentinel.context.ContextUtil; import com.alibaba.csp.sentinel.log.RecordLog; import com.alibaba.csp.sentinel.slots.block.BlockException; import com.alibaba.dubbo.common.extension.Activate; @@ -30,6 +31,8 @@ import com.alibaba.dubbo.rpc.Result; import com.alibaba.dubbo.rpc.RpcException; +import static com.alibaba.dubbo.common.Constants.CONSUMER; + /** *

Dubbo service consumer filter for Sentinel. Auto activated by default.

* @@ -41,7 +44,7 @@ * @author leyou * @author Eric Zhao */ -@Activate(group = "consumer") +@Activate(group = CONSUMER) public class SentinelDubboConsumerFilter extends AbstractDubboFilter implements Filter { public SentinelDubboConsumerFilter() { @@ -53,20 +56,24 @@ public Result invoke(Invoker invoker, Invocation invocation) throws RpcExcept Entry interfaceEntry = null; Entry methodEntry = null; try { - String resourceName = getResourceName(invoker, invocation); - interfaceEntry = SphU.entry(invoker.getInterface().getName(), EntryType.OUT); - methodEntry = SphU.entry(resourceName, EntryType.OUT); + String resourceName = getResourceName(invoker, invocation, DubboConfig.getDubboConsumerPrefix()); + interfaceEntry = SphU.entry(invoker.getInterface().getName(), ResourceTypeConstants.COMMON_RPC, + EntryType.OUT); + methodEntry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_RPC, EntryType.OUT); Result result = invoker.invoke(invocation); if (result.hasException()) { + Throwable e = result.getException(); // Record common exception. - Tracer.trace(result.getException()); + Tracer.traceEntry(e, interfaceEntry); + Tracer.traceEntry(e, methodEntry); } return result; } catch (BlockException e) { return DubboFallbackRegistry.getConsumerFallback().handle(invoker, invocation, e); } catch (RpcException e) { - Tracer.trace(e); + Tracer.traceEntry(e, interfaceEntry); + Tracer.traceEntry(e, methodEntry); throw e; } finally { if (methodEntry != null) { diff --git a/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboProviderFilter.java b/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboProviderFilter.java index c82afb8bb7..85d566ad99 100755 --- a/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboProviderFilter.java +++ b/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboProviderFilter.java @@ -17,13 +17,14 @@ import com.alibaba.csp.sentinel.Entry; import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.ResourceTypeConstants; import com.alibaba.csp.sentinel.SphU; import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.adapter.dubbo.config.DubboConfig; import com.alibaba.csp.sentinel.adapter.dubbo.fallback.DubboFallbackRegistry; import com.alibaba.csp.sentinel.context.ContextUtil; import com.alibaba.csp.sentinel.log.RecordLog; import com.alibaba.csp.sentinel.slots.block.BlockException; -import com.alibaba.csp.sentinel.slots.block.SentinelRpcException; import com.alibaba.dubbo.common.extension.Activate; import com.alibaba.dubbo.rpc.Filter; import com.alibaba.dubbo.rpc.Invocation; @@ -31,6 +32,8 @@ import com.alibaba.dubbo.rpc.Result; import com.alibaba.dubbo.rpc.RpcException; +import static com.alibaba.dubbo.common.Constants.PROVIDER; + /** *

Dubbo service provider filter for Sentinel. Auto activated by default.

* @@ -42,7 +45,7 @@ * @author leyou * @author Eric Zhao */ -@Activate(group = "provider") +@Activate(group = PROVIDER) public class SentinelDubboProviderFilter extends AbstractDubboFilter implements Filter { public SentinelDubboProviderFilter() { @@ -57,21 +60,26 @@ public Result invoke(Invoker invoker, Invocation invocation) throws RpcExcept Entry interfaceEntry = null; Entry methodEntry = null; try { - String resourceName = getResourceName(invoker, invocation); + String resourceName = getResourceName(invoker, invocation, DubboConfig.getDubboProviderPrefix()); String interfaceName = invoker.getInterface().getName(); ContextUtil.enter(resourceName, application); - interfaceEntry = SphU.entry(interfaceName, EntryType.IN); - methodEntry = SphU.entry(resourceName, EntryType.IN, 1, invocation.getArguments()); + interfaceEntry = SphU.entry(interfaceName, ResourceTypeConstants.COMMON_RPC, EntryType.IN); + methodEntry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_RPC, + EntryType.IN, invocation.getArguments()); Result result = invoker.invoke(invocation); if (result.hasException()) { - Tracer.trace(result.getException()); + Throwable e = result.getException(); + // Record common exception. + Tracer.traceEntry(e, interfaceEntry); + Tracer.traceEntry(e, methodEntry); } return result; } catch (BlockException e) { return DubboFallbackRegistry.getProviderFallback().handle(invoker, invocation, e); } catch (RpcException e) { - Tracer.trace(e); + Tracer.traceEntry(e, interfaceEntry); + Tracer.traceEntry(e, methodEntry); throw e; } finally { if (methodEntry != null) { diff --git a/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/config/DubboConfig.java b/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/config/DubboConfig.java new file mode 100644 index 0000000000..b7658d29a4 --- /dev/null +++ b/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/config/DubboConfig.java @@ -0,0 +1,62 @@ +/* + * 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.adapter.dubbo.config; + +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + *

+ * Responsible for dubbo service provider, consumer attribute configuration + *

+ * + * @author lianglin + * @since 1.7.0 + */ +public final class DubboConfig { + + public static final String DUBBO_USE_PREFIX = "csp.sentinel.dubbo.resource.use.prefix"; + private static final String TRUE_STR = "true"; + + public static final String DUBBO_PROVIDER_PREFIX = "csp.sentinel.dubbo.resource.provider.prefix"; + public static final String DUBBO_CONSUMER_PREFIX = "csp.sentinel.dubbo.resource.consumer.prefix"; + + private static final String DEFAULT_DUBBO_PROVIDER_PREFIX = "dubbo:provider:"; + private static final String DEFAULT_DUBBO_CONSUMER_PREFIX = "dubbo:consumer:"; + + public static boolean isUsePrefix() { + return TRUE_STR.equalsIgnoreCase(SentinelConfig.getConfig(DUBBO_USE_PREFIX)); + } + + + public static String getDubboProviderPrefix() { + if (isUsePrefix()) { + String config = SentinelConfig.getConfig(DUBBO_PROVIDER_PREFIX); + return StringUtil.isNotBlank(config) ? config : DEFAULT_DUBBO_PROVIDER_PREFIX; + } + return null; + } + + public static String getDubboConsumerPrefix() { + if (isUsePrefix()) { + String config = SentinelConfig.getConfig(DUBBO_CONSUMER_PREFIX); + return StringUtil.isNotBlank(config) ? config : DEFAULT_DUBBO_CONSUMER_PREFIX; + } + return null; + } + + +} diff --git a/sentinel-adapter/sentinel-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/AbstractDubboFilterTest.java b/sentinel-adapter/sentinel-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/AbstractDubboFilterTest.java index 69b95c6d54..cb9da8c413 100644 --- a/sentinel-adapter/sentinel-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/AbstractDubboFilterTest.java +++ b/sentinel-adapter/sentinel-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/AbstractDubboFilterTest.java @@ -1,22 +1,42 @@ package com.alibaba.csp.sentinel.adapter.dubbo; +import com.alibaba.csp.sentinel.adapter.dubbo.config.DubboConfig; import com.alibaba.csp.sentinel.adapter.dubbo.provider.DemoService; +import com.alibaba.csp.sentinel.config.SentinelConfig; import com.alibaba.dubbo.rpc.Invocation; import com.alibaba.dubbo.rpc.Invoker; import com.alibaba.dubbo.rpc.Result; import com.alibaba.dubbo.rpc.RpcException; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import java.lang.reflect.Method; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * @author cdfive */ public class AbstractDubboFilterTest { + + @Before + public void setUp() { + SentinelConfig.setConfig("csp.sentinel.dubbo.resource.use.prefix", "true"); + SentinelConfig.setConfig(DubboConfig.DUBBO_PROVIDER_PREFIX, ""); + SentinelConfig.setConfig(DubboConfig.DUBBO_CONSUMER_PREFIX, ""); + } + + @After + public void tearDown() { + SentinelConfig.setConfig("csp.sentinel.dubbo.resource.use.prefix", "false"); + SentinelConfig.setConfig(DubboConfig.DUBBO_PROVIDER_PREFIX, ""); + SentinelConfig.setConfig(DubboConfig.DUBBO_CONSUMER_PREFIX, ""); + } + private AbstractDubboFilter filter = new AbstractDubboFilter() { @Override public Result invoke(Invoker invoker, Invocation invocation) throws RpcException { @@ -24,6 +44,7 @@ public Result invoke(Invoker invoker, Invocation invocation) throws RpcExcept } }; + @Test public void testGetResourceName() { Invoker invoker = mock(Invoker.class); @@ -38,4 +59,32 @@ public void testGetResourceName() { assertEquals("com.alibaba.csp.sentinel.adapter.dubbo.provider.DemoService:sayHello(java.lang.String,int)", resourceName); } + + @Test + public void testGetResourceNameWithPrefix() { + Invoker invoker = mock(Invoker.class); + when(invoker.getInterface()).thenReturn(DemoService.class); + + Invocation invocation = mock(Invocation.class); + Method method = DemoService.class.getMethods()[0]; + when(invocation.getMethodName()).thenReturn(method.getName()); + when(invocation.getParameterTypes()).thenReturn(method.getParameterTypes()); + + //test with default prefix + String resourceName = filter.getResourceName(invoker, invocation, DubboConfig.getDubboProviderPrefix()); + System.out.println("resourceName = " + resourceName); + assertEquals("dubbo:provider:com.alibaba.csp.sentinel.adapter.dubbo.provider.DemoService:sayHello(java.lang.String,int)", resourceName); + resourceName = filter.getResourceName(invoker, invocation, DubboConfig.getDubboConsumerPrefix()); + assertEquals("dubbo:consumer:com.alibaba.csp.sentinel.adapter.dubbo.provider.DemoService:sayHello(java.lang.String,int)", resourceName); + + + //test with custom prefix + SentinelConfig.setConfig(DubboConfig.DUBBO_PROVIDER_PREFIX, "my:dubbo:provider:"); + SentinelConfig.setConfig(DubboConfig.DUBBO_CONSUMER_PREFIX, "my:dubbo:consumer:"); + resourceName = filter.getResourceName(invoker, invocation, DubboConfig.getDubboProviderPrefix()); + assertEquals("my:dubbo:provider:com.alibaba.csp.sentinel.adapter.dubbo.provider.DemoService:sayHello(java.lang.String,int)", resourceName); + resourceName = filter.getResourceName(invoker, invocation, DubboConfig.getDubboConsumerPrefix()); + assertEquals("my:dubbo:consumer:com.alibaba.csp.sentinel.adapter.dubbo.provider.DemoService:sayHello(java.lang.String,int)", resourceName); + + } } diff --git a/sentinel-adapter/sentinel-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboConsumerFilterTest.java b/sentinel-adapter/sentinel-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboConsumerFilterTest.java index a71b0d00fb..58de29d64f 100644 --- a/sentinel-adapter/sentinel-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboConsumerFilterTest.java +++ b/sentinel-adapter/sentinel-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboConsumerFilterTest.java @@ -92,7 +92,7 @@ private void verifyInvocationStructure(Invoker invoker, Invocation invocation) { DefaultNode entranceNode = context.getEntranceNode(); ResourceWrapper entranceResource = entranceNode.getId(); assertEquals(Constants.CONTEXT_DEFAULT_NAME, entranceResource.getName()); - assertSame(EntryType.IN, entranceResource.getType()); + assertSame(EntryType.IN, entranceResource.getEntryType()); // As SphU.entry(interfaceName, EntryType.OUT); Set childList = entranceNode.getChildList(); @@ -100,7 +100,7 @@ private void verifyInvocationStructure(Invoker invoker, Invocation invocation) { DefaultNode interfaceNode = (DefaultNode) childList.iterator().next(); ResourceWrapper interfaceResource = interfaceNode.getId(); assertEquals(DemoService.class.getName(), interfaceResource.getName()); - assertSame(EntryType.OUT, interfaceResource.getType()); + assertSame(EntryType.OUT, interfaceResource.getEntryType()); // As SphU.entry(resourceName, EntryType.OUT); childList = interfaceNode.getChildList(); @@ -108,7 +108,7 @@ private void verifyInvocationStructure(Invoker invoker, Invocation invocation) { DefaultNode methodNode = (DefaultNode) childList.iterator().next(); ResourceWrapper methodResource = methodNode.getId(); assertEquals(resourceName, methodResource.getName()); - assertSame(EntryType.OUT, methodResource.getType()); + assertSame(EntryType.OUT, methodResource.getEntryType()); // Verify curEntry Entry curEntry = context.getCurEntry(); diff --git a/sentinel-adapter/sentinel-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboProviderFilterTest.java b/sentinel-adapter/sentinel-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboProviderFilterTest.java index 809a5c7159..8f1f850f2f 100644 --- a/sentinel-adapter/sentinel-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboProviderFilterTest.java +++ b/sentinel-adapter/sentinel-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboProviderFilterTest.java @@ -92,7 +92,7 @@ private void verifyInvocationStructure(String originApplication, Invoker invoker DefaultNode entranceNode = context.getEntranceNode(); ResourceWrapper entranceResource = entranceNode.getId(); assertEquals(resourceName, entranceResource.getName()); - assertSame(EntryType.IN, entranceResource.getType()); + assertSame(EntryType.IN, entranceResource.getEntryType()); // As SphU.entry(interfaceName, EntryType.IN); Set childList = entranceNode.getChildList(); @@ -100,7 +100,7 @@ private void verifyInvocationStructure(String originApplication, Invoker invoker DefaultNode interfaceNode = (DefaultNode) childList.iterator().next(); ResourceWrapper interfaceResource = interfaceNode.getId(); assertEquals(DemoService.class.getName(), interfaceResource.getName()); - assertSame(EntryType.IN, interfaceResource.getType()); + assertSame(EntryType.IN, interfaceResource.getEntryType()); // As SphU.entry(resourceName, EntryType.IN, 1, invocation.getArguments()); childList = interfaceNode.getChildList(); @@ -108,7 +108,7 @@ private void verifyInvocationStructure(String originApplication, Invoker invoker DefaultNode methodNode = (DefaultNode) childList.iterator().next(); ResourceWrapper methodResource = methodNode.getId(); assertEquals(resourceName, methodResource.getName()); - assertSame(EntryType.IN, methodResource.getType()); + assertSame(EntryType.IN, methodResource.getEntryType()); // Verify curEntry Entry curEntry = context.getCurEntry(); diff --git a/sentinel-adapter/sentinel-grpc-adapter/README.md b/sentinel-adapter/sentinel-grpc-adapter/README.md index 38d60030b6..e3c3aa273a 100755 --- a/sentinel-adapter/sentinel-grpc-adapter/README.md +++ b/sentinel-adapter/sentinel-grpc-adapter/README.md @@ -3,7 +3,6 @@ Sentinel gRPC Adapter provides client and server interceptor for gRPC services. > Note that currently the interceptor only supports unary methods in gRPC. -> In some circumstances (e.g. asynchronous call), the RT metrics might not be accurate. ## Client Interceptor @@ -35,4 +34,3 @@ Server server = ServerBuilder.forPort(port) .intercept(new SentinelGrpcServerInterceptor()) // Add the server interceptor. .build(); ``` - diff --git a/sentinel-adapter/sentinel-grpc-adapter/pom.xml b/sentinel-adapter/sentinel-grpc-adapter/pom.xml index 034b541987..7f4f9f1378 100755 --- a/sentinel-adapter/sentinel-grpc-adapter/pom.xml +++ b/sentinel-adapter/sentinel-grpc-adapter/pom.xml @@ -5,7 +5,7 @@ sentinel-adapter com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 sentinel-grpc-adapter diff --git a/sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptor.java b/sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptor.java index 4e1686279c..20fe36b15a 100755 --- a/sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptor.java +++ b/sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptor.java @@ -15,28 +15,27 @@ */ package com.alibaba.csp.sentinel.adapter.grpc; -import javax.annotation.Nullable; - import com.alibaba.csp.sentinel.Entry; import com.alibaba.csp.sentinel.EntryType; import com.alibaba.csp.sentinel.SphU; import com.alibaba.csp.sentinel.Tracer; -import com.alibaba.csp.sentinel.context.ContextUtil; import com.alibaba.csp.sentinel.slots.block.BlockException; - import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ClientCall; import io.grpc.ClientInterceptor; import io.grpc.ForwardingClientCall; -import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener; +import io.grpc.ForwardingClientCallListener; import io.grpc.Metadata; import io.grpc.MethodDescriptor; import io.grpc.Status; +import javax.annotation.Nullable; +import java.util.concurrent.atomic.AtomicReference; + /** *

gRPC client interceptor for Sentinel. Currently it only works with unary methods.

- * + *

* Example code: *

  * public class ServiceClient {
@@ -52,50 +51,59 @@
  *
  * }
  * 
- * + *

* For server interceptor, see {@link SentinelGrpcServerInterceptor}. * * @author Eric Zhao */ public class SentinelGrpcClientInterceptor implements ClientInterceptor { - private static final Status FLOW_CONTROL_BLOCK = Status.UNAVAILABLE.withDescription( - "Flow control limit exceeded (client side)"); + "Flow control limit exceeded (client side)"); @Override public ClientCall interceptCall(MethodDescriptor methodDescriptor, CallOptions callOptions, Channel channel) { - String resourceName = methodDescriptor.getFullMethodName(); + String fullMethodName = methodDescriptor.getFullMethodName(); Entry entry = null; try { - entry = SphU.entry(resourceName, EntryType.OUT); + entry = SphU.asyncEntry(fullMethodName, EntryType.OUT); + final AtomicReference atomicReferenceEntry = new AtomicReference<>(entry); // Allow access, forward the call. return new ForwardingClientCall.SimpleForwardingClientCall( - channel.newCall(methodDescriptor, callOptions)) { + channel.newCall(methodDescriptor, callOptions)) { @Override public void start(Listener responseListener, Metadata headers) { - super.start(new SimpleForwardingClientCallListener(responseListener) { - @Override - public void onReady() { - super.onReady(); - } - + super.start(new ForwardingClientCallListener.SimpleForwardingClientCallListener(responseListener) { @Override public void onClose(Status status, Metadata trailers) { - super.onClose(status, trailers); - // Record the exception metrics. - if (!status.isOk()) { - recordException(status.asRuntimeException()); + Entry entry = atomicReferenceEntry.get(); + if (entry != null) { + // Record the exception metrics. + if (!status.isOk()) { + Tracer.traceEntry(status.asRuntimeException(), entry); + } + entry.exit(); + atomicReferenceEntry.set(null); } + super.onClose(status, trailers); } }, headers); } + /** + * Some Exceptions will only call cancel. + */ @Override public void cancel(@Nullable String message, @Nullable Throwable cause) { + Entry entry = atomicReferenceEntry.get(); + // Some Exceptions will call onClose and cancel. + if (entry != null) { + // Record the exception metrics. + Tracer.traceEntry(cause, entry); + entry.exit(); + atomicReferenceEntry.set(null); + } super.cancel(message, cause); - // Record the exception metrics. - recordException(cause); } }; } catch (BlockException e) { @@ -108,32 +116,27 @@ public void start(Listener responseListener, Metadata headers) { @Override public void request(int numMessages) { - } @Override public void cancel(@Nullable String message, @Nullable Throwable cause) { - } @Override public void halfClose() { - } @Override public void sendMessage(ReqT message) { - } }; - } finally { + } catch (RuntimeException e) { + // Catch the RuntimeException newCall throws, entry is guaranteed to exit. if (entry != null) { + Tracer.traceEntry(e, entry); entry.exit(); } + throw e; } } - - private void recordException(Throwable t) { - Tracer.trace(t); - } } diff --git a/sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptor.java b/sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptor.java index 1e0d909019..ca1d558d36 100755 --- a/sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptor.java +++ b/sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptor.java @@ -19,21 +19,21 @@ import com.alibaba.csp.sentinel.EntryType; import com.alibaba.csp.sentinel.SphU; import com.alibaba.csp.sentinel.Tracer; -import com.alibaba.csp.sentinel.context.ContextUtil; import com.alibaba.csp.sentinel.slots.block.BlockException; - import io.grpc.ForwardingServerCall; import io.grpc.ForwardingServerCallListener; import io.grpc.Metadata; import io.grpc.ServerCall; -import io.grpc.ServerCall.Listener; import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; import io.grpc.Status; +import io.grpc.StatusRuntimeException; + +import java.util.concurrent.atomic.AtomicReference; /** *

gRPC server interceptor for Sentinel. Currently it only works with unary methods.

- * + *

* Example code: *

  * Server server = ServerBuilder.forPort(port)
@@ -41,50 +41,69 @@
  *      .intercept(new SentinelGrpcServerInterceptor()) // Add the server interceptor.
  *      .build();
  * 
- * + *

* For client interceptor, see {@link SentinelGrpcClientInterceptor}. * * @author Eric Zhao */ public class SentinelGrpcServerInterceptor implements ServerInterceptor { - private static final Status FLOW_CONTROL_BLOCK = Status.UNAVAILABLE.withDescription( - "Flow control limit exceeded (server side)"); + "Flow control limit exceeded (server side)"); + private static final StatusRuntimeException STATUS_RUNTIME_EXCEPTION = new StatusRuntimeException(Status.CANCELLED); @Override - public Listener interceptCall(ServerCall serverCall, Metadata metadata, - ServerCallHandler serverCallHandler) { - String resourceName = serverCall.getMethodDescriptor().getFullMethodName(); + public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { + String fullMethodName = call.getMethodDescriptor().getFullMethodName(); // Remote address: serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR); Entry entry = null; try { - ContextUtil.enter(resourceName); - entry = SphU.entry(resourceName, EntryType.IN); + entry = SphU.asyncEntry(fullMethodName, EntryType.IN); + final AtomicReference atomicReferenceEntry = new AtomicReference<>(entry); // Allow access, forward the call. return new ForwardingServerCallListener.SimpleForwardingServerCallListener( - serverCallHandler.startCall( - new ForwardingServerCall.SimpleForwardingServerCall(serverCall) { - @Override - public void close(Status status, Metadata trailers) { - super.close(status, trailers); - // Record the exception metrics. - if (!status.isOk()) { - recordException(status.asRuntimeException()); - } - } - }, metadata)) {}; + next.startCall( + new ForwardingServerCall.SimpleForwardingServerCall(call) { + @Override + public void close(Status status, Metadata trailers) { + Entry entry = atomicReferenceEntry.get(); + if (entry != null) { + // Record the exception metrics. + if (!status.isOk()) { + Tracer.traceEntry(status.asRuntimeException(), entry); + } + //entry exit when the call be closed + entry.exit(); + } + super.close(status, trailers); + } + }, headers)) { + /** + * If call was canceled, onCancel will be called. and the close will not be called + * so the server is encouraged to abort processing to save resources by onCancel + * @see ServerCall.Listener#onCancel() + */ + @Override + public void onCancel() { + Entry entry = atomicReferenceEntry.get(); + if (entry != null) { + Tracer.traceEntry(STATUS_RUNTIME_EXCEPTION, entry); + entry.exit(); + atomicReferenceEntry.set(null); + } + super.onCancel(); + } + }; } catch (BlockException e) { - serverCall.close(FLOW_CONTROL_BLOCK, new Metadata()); - return new ServerCall.Listener() {}; - } finally { + call.close(FLOW_CONTROL_BLOCK, new Metadata()); + return new ServerCall.Listener() { + }; + } catch (RuntimeException e) { + // Catch the RuntimeException startCall throws, entry is guaranteed to exit. if (entry != null) { + Tracer.traceEntry(e, entry); entry.exit(); } - ContextUtil.exit(); + throw e; } } - - private void recordException(Throwable t) { - Tracer.trace(t); - } } diff --git a/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceClient.java b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceClient.java index dab36678c9..2aaefae957 100755 --- a/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceClient.java +++ b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceClient.java @@ -15,38 +15,36 @@ */ package com.alibaba.csp.sentinel.adapter.grpc; -import java.util.concurrent.TimeUnit; - import com.alibaba.csp.sentinel.adapter.grpc.gen.FooRequest; import com.alibaba.csp.sentinel.adapter.grpc.gen.FooResponse; import com.alibaba.csp.sentinel.adapter.grpc.gen.FooServiceGrpc; - import io.grpc.ClientInterceptor; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; +import java.util.concurrent.TimeUnit; + /** * A simple wrapped gRPC client for FooService. * * @author Eric Zhao */ final class FooServiceClient { - private final ManagedChannel channel; private final FooServiceGrpc.FooServiceBlockingStub blockingStub; FooServiceClient(String host, int port) { this.channel = ManagedChannelBuilder.forAddress(host, port) - .usePlaintext() - .build(); + .usePlaintext() + .build(); this.blockingStub = FooServiceGrpc.newBlockingStub(this.channel); } FooServiceClient(String host, int port, ClientInterceptor interceptor) { this.channel = ManagedChannelBuilder.forAddress(host, port) - .usePlaintext() - .intercept(interceptor) - .build(); + .usePlaintext() + .intercept(interceptor) + .build(); this.blockingStub = FooServiceGrpc.newBlockingStub(this.channel); } diff --git a/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceImpl.java b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceImpl.java index adda5102da..298ec44fe1 100755 --- a/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceImpl.java +++ b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceImpl.java @@ -18,27 +18,59 @@ import com.alibaba.csp.sentinel.adapter.grpc.gen.FooRequest; import com.alibaba.csp.sentinel.adapter.grpc.gen.FooResponse; import com.alibaba.csp.sentinel.adapter.grpc.gen.FooServiceGrpc; - import io.grpc.stub.StreamObserver; /** * Implementation of FooService defined in proto. */ class FooServiceImpl extends FooServiceGrpc.FooServiceImplBase { - @Override public void sayHello(FooRequest request, StreamObserver responseObserver) { - String message = String.format("Hello %s! Your ID is %d.", request.getName(), request.getId()); - FooResponse response = FooResponse.newBuilder().setMessage(message).build(); - responseObserver.onNext(response); - responseObserver.onCompleted(); + int id = request.getId(); + switch (id) { + // Exception test + case -1: + responseObserver.onError(new IllegalAccessException("The id is error!")); + break; + // RT test + case -2: + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + responseObserver.onError(e); + break; + } + default: + String message = String.format("Hello %s! Your ID is %d.", request.getName(), id); + FooResponse response = FooResponse.newBuilder().setMessage(message).build(); + responseObserver.onNext(response); + responseObserver.onCompleted(); + break; + } } @Override public void anotherHello(FooRequest request, StreamObserver responseObserver) { - String message = String.format("Good day, %s (%d)", request.getName(), request.getId()); - FooResponse response = FooResponse.newBuilder().setMessage(message).build(); - responseObserver.onNext(response); - responseObserver.onCompleted(); + int id = request.getId(); + switch (id) { + // Exception test + case -1: + responseObserver.onError(new IllegalAccessException("The id is error!")); + break; + // RT test + case -2: + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + responseObserver.onError(e); + break; + } + default: + String message = String.format("Good day, %s (%d)", request.getName(), id); + FooResponse response = FooResponse.newBuilder().setMessage(message).build(); + responseObserver.onNext(response); + responseObserver.onCompleted(); + break; + } } } diff --git a/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/GrpcTestServer.java b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/GrpcTestServer.java index f46c5718a1..86fe011518 100644 --- a/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/GrpcTestServer.java +++ b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/GrpcTestServer.java @@ -19,6 +19,7 @@ import io.grpc.ServerBuilder; import java.io.IOException; +import java.util.concurrent.Executors; class GrpcTestServer { diff --git a/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptorTest.java b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptorTest.java index e61dd7605c..9b43416884 100755 --- a/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptorTest.java +++ b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptorTest.java @@ -15,8 +15,6 @@ */ package com.alibaba.csp.sentinel.adapter.grpc; -import java.util.Collections; - import com.alibaba.csp.sentinel.EntryType; import com.alibaba.csp.sentinel.adapter.grpc.gen.FooRequest; import com.alibaba.csp.sentinel.adapter.grpc.gen.FooResponse; @@ -25,12 +23,17 @@ import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; - import io.grpc.StatusRuntimeException; import org.junit.After; +import org.junit.Before; import org.junit.Test; -import static org.junit.Assert.*; +import java.util.Collections; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; /** * Test cases for {@link SentinelGrpcClientInterceptor}. @@ -38,48 +41,52 @@ * @author Eric Zhao */ public class SentinelGrpcClientInterceptorTest { - - private final String resourceName = "com.alibaba.sentinel.examples.FooService/sayHello"; - private final int threshold = 2; + private final String fullMethodName = "com.alibaba.sentinel.examples.FooService/sayHello"; private final GrpcTestServer server = new GrpcTestServer(); + private FooServiceClient client; private void configureFlowRule(int count) { FlowRule rule = new FlowRule() - .setCount(count) - .setGrade(RuleConstant.FLOW_GRADE_QPS) - .setResource(resourceName) - .setLimitApp("default") - .as(FlowRule.class); + .setCount(count) + .setGrade(RuleConstant.FLOW_GRADE_QPS) + .setResource(fullMethodName) + .setLimitApp("default") + .as(FlowRule.class); FlowRuleManager.loadRules(Collections.singletonList(rule)); } @Test public void testGrpcClientInterceptor() throws Exception { final int port = 19328; - - configureFlowRule(threshold); server.start(port, false); + client = new FooServiceClient("localhost", port, new SentinelGrpcClientInterceptor()); - FooServiceClient client = new FooServiceClient("localhost", port, new SentinelGrpcClientInterceptor()); - - assertTrue(sendRequest(client)); - ClusterNode clusterNode = ClusterBuilderSlot.getClusterNode(resourceName, EntryType.OUT); + configureFlowRule(Integer.MAX_VALUE); + assertTrue(sendRequest(FooRequest.newBuilder().setName("Sentinel").setId(666).build())); + ClusterNode clusterNode = ClusterBuilderSlot.getClusterNode(fullMethodName, EntryType.OUT); assertNotNull(clusterNode); - assertEquals(1, clusterNode.totalRequest() - clusterNode.blockRequest()); + assertEquals(1, clusterNode.totalPass()); // Not allowed to pass. configureFlowRule(0); - // The second request will be blocked. - assertFalse(sendRequest(client)); + assertFalse(sendRequest(FooRequest.newBuilder().setName("Sentinel").setId(666).build())); assertEquals(1, clusterNode.blockRequest()); + configureFlowRule(Integer.MAX_VALUE); + assertFalse(sendRequest(FooRequest.newBuilder().setName("Sentinel").setId(-1).build())); + assertEquals(1, clusterNode.totalException()); + + configureFlowRule(Integer.MAX_VALUE); + assertTrue(sendRequest(FooRequest.newBuilder().setName("Sentinel").setId(-2).build())); + assertTrue(clusterNode.avgRt() >= 1000); + server.stop(); } - private boolean sendRequest(FooServiceClient client) { + private boolean sendRequest(FooRequest request) { try { - FooResponse response = client.sayHello(FooRequest.newBuilder().setName("Sentinel").setId(666).build()); + FooResponse response = client.sayHello(request); System.out.println("Response: " + response); return true; } catch (StatusRuntimeException ex) { @@ -88,9 +95,15 @@ private boolean sendRequest(FooServiceClient client) { } } + @Before + public void cleanUpBefore() { + FlowRuleManager.loadRules(null); + ClusterBuilderSlot.getClusterNodeMap().clear(); + } + @After - public void cleanUp() { + public void cleanUpAfter() { FlowRuleManager.loadRules(null); ClusterBuilderSlot.getClusterNodeMap().clear(); } -} \ No newline at end of file +} diff --git a/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptorTest.java b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptorTest.java index 004a256357..ceef04fafd 100755 --- a/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptorTest.java +++ b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptorTest.java @@ -15,8 +15,6 @@ */ package com.alibaba.csp.sentinel.adapter.grpc; -import java.util.Collections; - import com.alibaba.csp.sentinel.EntryType; import com.alibaba.csp.sentinel.adapter.grpc.gen.FooRequest; import com.alibaba.csp.sentinel.adapter.grpc.gen.FooResponse; @@ -25,12 +23,17 @@ import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; - import io.grpc.StatusRuntimeException; import org.junit.After; +import org.junit.Before; import org.junit.Test; -import static org.junit.Assert.*; +import java.util.Collections; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; /** * Test cases for {@link SentinelGrpcServerInterceptor}. @@ -38,49 +41,52 @@ * @author Eric Zhao */ public class SentinelGrpcServerInterceptorTest { - private final String resourceName = "com.alibaba.sentinel.examples.FooService/anotherHello"; - private final int threshold = 4; private final GrpcTestServer server = new GrpcTestServer(); - private FooServiceClient client; private void configureFlowRule(int count) { FlowRule rule = new FlowRule() - .setCount(count) - .setGrade(RuleConstant.FLOW_GRADE_QPS) - .setResource(resourceName) - .setLimitApp("default") - .as(FlowRule.class); + .setCount(count) + .setGrade(RuleConstant.FLOW_GRADE_QPS) + .setResource(resourceName) + .setLimitApp("default") + .as(FlowRule.class); FlowRuleManager.loadRules(Collections.singletonList(rule)); } @Test public void testGrpcServerInterceptor() throws Exception { final int port = 19329; - client = new FooServiceClient("localhost", port); - - configureFlowRule(threshold); server.start(port, true); + client = new FooServiceClient("localhost", port); - assertTrue(sendRequest()); + configureFlowRule(Integer.MAX_VALUE); + assertTrue(sendRequest(FooRequest.newBuilder().setName("Sentinel").setId(666).build())); ClusterNode clusterNode = ClusterBuilderSlot.getClusterNode(resourceName, EntryType.IN); assertNotNull(clusterNode); - assertEquals(1, clusterNode.totalRequest() - clusterNode.blockRequest()); + assertEquals(1, clusterNode.totalPass()); // Not allowed to pass. configureFlowRule(0); - // The second request will be blocked. - assertFalse(sendRequest()); + assertFalse(sendRequest(FooRequest.newBuilder().setName("Sentinel").setId(666).build())); assertEquals(1, clusterNode.blockRequest()); + configureFlowRule(Integer.MAX_VALUE); + assertFalse(sendRequest(FooRequest.newBuilder().setName("Sentinel").setId(-1).build())); + assertEquals(1, clusterNode.totalException()); + + configureFlowRule(Integer.MAX_VALUE); + assertTrue(sendRequest(FooRequest.newBuilder().setName("Sentinel").setId(-2).build())); + assertTrue(clusterNode.avgRt() >= 1000); + server.stop(); } - private boolean sendRequest() { + private boolean sendRequest(FooRequest request) { try { - FooResponse response = client.anotherHello(FooRequest.newBuilder().setName("Sentinel").setId(666).build()); + FooResponse response = client.anotherHello(request); System.out.println("Response: " + response); return true; } catch (StatusRuntimeException ex) { @@ -89,10 +95,15 @@ private boolean sendRequest() { } } + @Before + public void cleanUpBefore() { + FlowRuleManager.loadRules(null); + ClusterBuilderSlot.getClusterNodeMap().clear(); + } @After - public void cleanUp() { + public void cleanUpAfter() { FlowRuleManager.loadRules(null); ClusterBuilderSlot.getClusterNodeMap().clear(); } -} \ No newline at end of file +} diff --git a/sentinel-adapter/sentinel-grpc-adapter/src/test/proto/example.proto b/sentinel-adapter/sentinel-grpc-adapter/src/test/proto/example.proto index 61858510b6..daf70e3c19 100755 --- a/sentinel-adapter/sentinel-grpc-adapter/src/test/proto/example.proto +++ b/sentinel-adapter/sentinel-grpc-adapter/src/test/proto/example.proto @@ -18,6 +18,5 @@ message FooResponse { // Example service definition. service FooService { rpc sayHello(FooRequest) returns (FooResponse) {} - rpc anotherHello(FooRequest) returns (FooResponse) {} -} \ No newline at end of file +} diff --git a/sentinel-adapter/sentinel-reactor-adapter/pom.xml b/sentinel-adapter/sentinel-reactor-adapter/pom.xml index 2e483c8933..c02c5c5e8b 100644 --- a/sentinel-adapter/sentinel-reactor-adapter/pom.xml +++ b/sentinel-adapter/sentinel-reactor-adapter/pom.xml @@ -5,7 +5,7 @@ sentinel-adapter com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 diff --git a/sentinel-adapter/sentinel-reactor-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/reactor/EntryConfig.java b/sentinel-adapter/sentinel-reactor-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/reactor/EntryConfig.java index be677d532a..2113d6d231 100644 --- a/sentinel-adapter/sentinel-reactor-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/reactor/EntryConfig.java +++ b/sentinel-adapter/sentinel-reactor-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/reactor/EntryConfig.java @@ -18,6 +18,7 @@ import java.util.Arrays; import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.ResourceTypeConstants; import com.alibaba.csp.sentinel.util.AssertUtil; /** @@ -28,6 +29,8 @@ public class EntryConfig { private final String resourceName; private final EntryType entryType; + private final int resourceType; + private final int acquireCount; private final Object[] args; private final ContextConfig contextConfig; @@ -44,17 +47,31 @@ public EntryConfig(String resourceName, EntryType entryType, ContextConfig conte this(resourceName, entryType, 1, new Object[0], contextConfig); } + public EntryConfig(String resourceName, int resourceType, EntryType entryType, ContextConfig contextConfig) { + this(resourceName, resourceType, entryType, 1, new Object[0], contextConfig); + } + public EntryConfig(String resourceName, EntryType entryType, int acquireCount, Object[] args) { this(resourceName, entryType, acquireCount, args, null); } public EntryConfig(String resourceName, EntryType entryType, int acquireCount, Object[] args, ContextConfig contextConfig) { + this(resourceName, ResourceTypeConstants.COMMON, entryType, acquireCount, args, contextConfig); + } + + public EntryConfig(String resourceName, int resourceType, EntryType entryType, int acquireCount, Object[] args) { + this(resourceName, resourceType, entryType, acquireCount, args, null); + } + + public EntryConfig(String resourceName, int resourceType, EntryType entryType, int acquireCount, Object[] args, + ContextConfig contextConfig) { AssertUtil.assertNotBlank(resourceName, "resourceName cannot be blank"); AssertUtil.notNull(entryType, "entryType cannot be null"); AssertUtil.isTrue(acquireCount > 0, "acquireCount should be positive"); this.resourceName = resourceName; this.entryType = entryType; + this.resourceType = resourceType; this.acquireCount = acquireCount; this.args = args; // Constructed ContextConfig should be valid here. Null is allowed here. @@ -81,11 +98,19 @@ public ContextConfig getContextConfig() { return contextConfig; } + /** + * @since 1.7.0 + */ + public int getResourceType() { + return resourceType; + } + @Override public String toString() { return "EntryConfig{" + "resourceName='" + resourceName + '\'' + ", entryType=" + entryType + + ", resourceType=" + resourceType + ", acquireCount=" + acquireCount + ", args=" + Arrays.toString(args) + ", contextConfig=" + contextConfig + diff --git a/sentinel-adapter/sentinel-reactor-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/reactor/SentinelReactorSubscriber.java b/sentinel-adapter/sentinel-reactor-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/reactor/SentinelReactorSubscriber.java index 3a3be77404..00a4418850 100644 --- a/sentinel-adapter/sentinel-reactor-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/reactor/SentinelReactorSubscriber.java +++ b/sentinel-adapter/sentinel-reactor-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/reactor/SentinelReactorSubscriber.java @@ -89,8 +89,8 @@ private void entryWhenSubscribed() { ContextUtil.enter(sentinelContextConfig.getContextName(), sentinelContextConfig.getOrigin()); } try { - AsyncEntry entry = SphU.asyncEntry(entryConfig.getResourceName(), entryConfig.getEntryType(), - entryConfig.getAcquireCount(), entryConfig.getArgs()); + AsyncEntry entry = SphU.asyncEntry(entryConfig.getResourceName(), entryConfig.getResourceType(), + entryConfig.getEntryType(), entryConfig.getAcquireCount(), entryConfig.getArgs()); this.currentEntry = entry; actual.onSubscribe(this); } catch (BlockException ex) { @@ -155,7 +155,7 @@ protected void hookOnError(Throwable t) { @Override protected void hookOnCancel() { - + tryCompleteEntry(); } private boolean tryCompleteEntry() { diff --git a/sentinel-adapter/sentinel-reactor-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/reactor/MonoSentinelOperatorIntegrationTest.java b/sentinel-adapter/sentinel-reactor-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/reactor/MonoSentinelOperatorIntegrationTest.java index e682723a92..ba867839cd 100644 --- a/sentinel-adapter/sentinel-reactor-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/reactor/MonoSentinelOperatorIntegrationTest.java +++ b/sentinel-adapter/sentinel-reactor-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/reactor/MonoSentinelOperatorIntegrationTest.java @@ -162,6 +162,28 @@ public void testEmitSingleError() { assertEquals(1, cn.totalException()); } + @Test + public void testMultipleReactorTransformerLatterFlowControl() { + String resourceName1 = createResourceName("testMultipleReactorTransformerLatterFlowControl1"); + String resourceName2 = createResourceName("testMultipleReactorTransformerLatterFlowControl2"); + FlowRuleManager.loadRules(Collections.singletonList( + new FlowRule(resourceName2).setCount(0) + )); + StepVerifier.create(Mono.just(2) + .transform(new SentinelReactorTransformer<>(resourceName1)) + .transform(new SentinelReactorTransformer<>(resourceName2))) + .expectError(BlockException.class) + .verify(); + + ClusterNode cn1 = ClusterBuilderSlot.getClusterNode(resourceName1); + assertNotNull(cn1); + ClusterNode cn2 = ClusterBuilderSlot.getClusterNode(resourceName2); + assertNotNull(cn2); + assertEquals(1, cn2.blockRequest()); + assertEquals(1, cn1.totalSuccess()); + FlowRuleManager.loadRules(new ArrayList<>()); + } + private String createResourceName(String resourceName) { return "reactor_test_mono_" + resourceName; } diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/pom.xml b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/pom.xml index b221cc7b99..6c3b13128f 100644 --- a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/pom.xml +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/pom.xml @@ -5,7 +5,7 @@ sentinel-adapter com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/SentinelGatewayFilter.java b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/SentinelGatewayFilter.java index dbe9c13089..3dfbaa5a41 100644 --- a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/SentinelGatewayFilter.java +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/SentinelGatewayFilter.java @@ -20,6 +20,7 @@ import java.util.stream.Collectors; import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.ResourceTypeConstants; import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; import com.alibaba.csp.sentinel.adapter.gateway.common.param.GatewayParamParser; import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; @@ -34,6 +35,7 @@ import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.route.Route; import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; +import org.springframework.core.Ordered; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @@ -41,7 +43,17 @@ * @author Eric Zhao * @since 1.6.0 */ -public class SentinelGatewayFilter implements GatewayFilter, GlobalFilter { +public class SentinelGatewayFilter implements GatewayFilter, GlobalFilter, Ordered { + + private final int order; + + public SentinelGatewayFilter() { + this(Ordered.HIGHEST_PRECEDENCE); + } + + public SentinelGatewayFilter(int order) { + this.order = order; + } private final GatewayParamParser paramParser = new GatewayParamParser<>( new ServerWebExchangeItemParser()); @@ -59,8 +71,8 @@ public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { .map(f -> f.apply(exchange)) .orElse(""); asyncResult = asyncResult.transform( - new SentinelReactorTransformer<>(new EntryConfig(routeId, EntryType.IN, - 1, params, new ContextConfig(contextName(routeId), origin))) + new SentinelReactorTransformer<>(new EntryConfig(routeId, ResourceTypeConstants.COMMON_API_GATEWAY, + EntryType.IN, 1, params, new ContextConfig(contextName(routeId), origin))) ); } @@ -69,7 +81,8 @@ public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { Object[] params = paramParser.parseParameterFor(apiName, exchange, r -> r.getResourceMode() == SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME); asyncResult = asyncResult.transform( - new SentinelReactorTransformer<>(new EntryConfig(apiName, EntryType.IN, 1, params)) + new SentinelReactorTransformer<>(new EntryConfig(apiName, ResourceTypeConstants.COMMON_API_GATEWAY, + EntryType.IN, 1, params)) ); } @@ -87,4 +100,9 @@ Set pickMatchingApiDefinitions(ServerWebExchange exchange) { .map(WebExchangeApiMatcher::getApiName) .collect(Collectors.toSet()); } + + @Override + public int getOrder() { + return order; + } } diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/ServerWebExchangeItemParser.java b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/ServerWebExchangeItemParser.java index 6088cd2d7e..fcf00ecb3e 100644 --- a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/ServerWebExchangeItemParser.java +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/ServerWebExchangeItemParser.java @@ -16,9 +16,11 @@ package com.alibaba.csp.sentinel.adapter.gateway.sc; import java.net.InetSocketAddress; +import java.util.Optional; import com.alibaba.csp.sentinel.adapter.gateway.common.param.RequestItemParser; +import org.springframework.http.HttpCookie; import org.springframework.web.server.ServerWebExchange; /** @@ -50,4 +52,11 @@ public String getHeader(ServerWebExchange exchange, String key) { public String getUrlParam(ServerWebExchange exchange, String paramName) { return exchange.getRequest().getQueryParams().getFirst(paramName); } + + @Override + public String getCookieValue(ServerWebExchange exchange, String cookieName) { + return Optional.ofNullable(exchange.getResponse().getCookies().getFirst(cookieName)) + .map(HttpCookie::getValue) + .orElse(null); + } } diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/api/matcher/WebExchangeApiMatcher.java b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/api/matcher/WebExchangeApiMatcher.java index 7abae52b62..3a671c489c 100644 --- a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/api/matcher/WebExchangeApiMatcher.java +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/api/matcher/WebExchangeApiMatcher.java @@ -59,9 +59,9 @@ private Optional> fromApiPathPredicate(/*@Valid*/ A return Optional.empty(); } switch (item.getMatchStrategy()) { - case SentinelGatewayConstants.PARAM_MATCH_STRATEGY_REGEX: + case SentinelGatewayConstants.URL_MATCH_STRATEGY_REGEX: return Optional.of(RouteMatchers.regexPath(pattern)); - case SentinelGatewayConstants.PARAM_MATCH_STRATEGY_PREFIX: + case SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX: return Optional.of(RouteMatchers.antPath(pattern)); default: return Optional.of(RouteMatchers.exactPath(pattern)); diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/sc/SentinelGatewayFilterTest.java b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/sc/SentinelGatewayFilterTest.java index 078bfec6e4..1bf86ea021 100644 --- a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/sc/SentinelGatewayFilterTest.java +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/sc/SentinelGatewayFilterTest.java @@ -44,14 +44,14 @@ public void testPickMatchingApiDefinitions() { ApiDefinition api1 = new ApiDefinition(apiName1) .setPredicateItems(Collections.singleton( new ApiPathPredicateItem().setPattern("/product/**") - .setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_PREFIX) + .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX) )); String apiName2 = "another_customized_api"; ApiDefinition api2 = new ApiDefinition(apiName2) .setPredicateItems(new HashSet() {{ add(new ApiPathPredicateItem().setPattern("/something")); add(new ApiPathPredicateItem().setPattern("/other/**") - .setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_PREFIX)); + .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX)); }}); apiDefinitions.add(api1); apiDefinitions.add(api2); diff --git a/sentinel-adapter/sentinel-spring-webflux-adapter/README.md b/sentinel-adapter/sentinel-spring-webflux-adapter/README.md index ed47f5d7c8..158937fe93 100644 --- a/sentinel-adapter/sentinel-spring-webflux-adapter/README.md +++ b/sentinel-adapter/sentinel-spring-webflux-adapter/README.md @@ -50,7 +50,7 @@ public class WebFluxConfig { You can register various customized callback in `WebFluxCallbackManager`: - `setBlockHandler`: register a customized `BlockRequestHandler` to handle the blocked request. The default implementation is `DefaultBlockRequestHandler`, which returns default message like `Blocked by Sentinel: FlowException`. -- `setUrlCleaner`: used for normalization of URL. The function type is `(ServerWebExchange, String) → String`, which means `(webExchange, originalUrl) → finalUrl`. +- `setUrlCleaner`: used for normalization of URL. The function type is `(ServerWebExchange, String) → String`, which means `(webExchange, originalUrl) → finalUrl`, if the finalUrl is `"""` or `null`, the URLs will be excluded (since Sentinel 1.7.0).. - `setRequestOriginParser`: used to resolve the origin from the HTTP request. The function type is `ServerWebExchange → String`. You can also refer to the demo: [sentinel-demo-spring-webflux](https://github.com/alibaba/Sentinel/tree/master/sentinel-demo/sentinel-demo-spring-webflux). \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-webflux-adapter/pom.xml b/sentinel-adapter/sentinel-spring-webflux-adapter/pom.xml index 3c8a8d12e0..c940192811 100644 --- a/sentinel-adapter/sentinel-spring-webflux-adapter/pom.xml +++ b/sentinel-adapter/sentinel-spring-webflux-adapter/pom.xml @@ -5,7 +5,7 @@ sentinel-adapter com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 diff --git a/sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/SentinelWebFluxFilter.java b/sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/SentinelWebFluxFilter.java index b83767d2ed..7d9cbf2860 100644 --- a/sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/SentinelWebFluxFilter.java +++ b/sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/SentinelWebFluxFilter.java @@ -18,10 +18,12 @@ import java.util.Optional; import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.ResourceTypeConstants; import com.alibaba.csp.sentinel.adapter.reactor.ContextConfig; import com.alibaba.csp.sentinel.adapter.reactor.EntryConfig; import com.alibaba.csp.sentinel.adapter.reactor.SentinelReactorTransformer; import com.alibaba.csp.sentinel.adapter.spring.webflux.callback.WebFluxCallbackManager; +import com.alibaba.csp.sentinel.util.StringUtil; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; @@ -36,24 +38,25 @@ public class SentinelWebFluxFilter implements WebFilter { @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { - return chain.filter(exchange) - .transform(buildSentinelTransformer(exchange)); - } - - private SentinelReactorTransformer buildSentinelTransformer(ServerWebExchange exchange) { // Maybe we can get the URL pattern elsewhere via: // exchange.getAttributeOrDefault(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, path) - String path = exchange.getRequest().getPath().value(); - String finalPath = Optional.ofNullable(WebFluxCallbackManager.getUrlCleaner()) - .map(f -> f.apply(exchange, path)) - .orElse(path); + + String finalPath = WebFluxCallbackManager.getUrlCleaner().apply(exchange, path); + if (StringUtil.isEmpty(finalPath)) { + return chain.filter(exchange); + } + return chain.filter(exchange) + .transform(buildSentinelTransformer(exchange, finalPath)); + } + + private SentinelReactorTransformer buildSentinelTransformer(ServerWebExchange exchange, String finalPath) { String origin = Optional.ofNullable(WebFluxCallbackManager.getRequestOriginParser()) .map(f -> f.apply(exchange)) .orElse(EMPTY_ORIGIN); - return new SentinelReactorTransformer<>( - new EntryConfig(finalPath, EntryType.IN, new ContextConfig(finalPath, origin))); + return new SentinelReactorTransformer<>(new EntryConfig(finalPath, ResourceTypeConstants.COMMON_WEB, + EntryType.IN, new ContextConfig(finalPath, origin))); } private static final String EMPTY_ORIGIN = ""; diff --git a/sentinel-adapter/sentinel-spring-webflux-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webflux/SentinelWebFluxIntegrationTest.java b/sentinel-adapter/sentinel-spring-webflux-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webflux/SentinelWebFluxIntegrationTest.java index 91d7b923fd..2e0ec9e46d 100644 --- a/sentinel-adapter/sentinel-spring-webflux-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webflux/SentinelWebFluxIntegrationTest.java +++ b/sentinel-adapter/sentinel-spring-webflux-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webflux/SentinelWebFluxIntegrationTest.java @@ -101,6 +101,26 @@ public void testCustomizedUrlCleaner() throws Exception { WebFluxCallbackManager.resetUrlCleaner(); } + @Test + public void testCustomizedIgnoreUrlCleaner() throws Exception { + final String fooPrefix = "/foo/"; + String url1 = fooPrefix + 1; + WebFluxCallbackManager.setUrlCleaner(((exchange, originUrl) -> { + if (originUrl.startsWith(fooPrefix)) { + return ""; + } + return originUrl; + })); + this.webClient.get() + .uri(url1) + .exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("Hello 1"); + + assertNull(ClusterBuilderSlot.getClusterNode(url1)); + WebFluxCallbackManager.resetUrlCleaner(); + } + @Test public void testCustomizedBlockRequestHandler() throws Exception { String url = "/error"; @@ -166,4 +186,4 @@ public void cleanUp() { FlowRuleManager.loadRules(new ArrayList<>()); ClusterBuilderSlot.resetClusterNodes(); } -} \ No newline at end of file +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/README.md b/sentinel-adapter/sentinel-spring-webmvc-adapter/README.md new file mode 100755 index 0000000000..941daed42e --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/README.md @@ -0,0 +1,106 @@ +# Sentinel Spring MVC Adapter + +## Introduction + +Sentinel provides integration for Spring Web to enable flow control for web requests. + +Add the following dependency in `pom.xml` (if you are using Maven): + +```xml + + com.alibaba.csp + sentinel-spring-webmvc-adapter + x.y.z + +``` + +Then we could add a configuration bean to configure the interceptor: + +```java +@Configuration +public class InterceptorConfig implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + SentinelWebMvcConfig config = new SentinelWebMvcConfig(); + // Enable the HTTP method prefix. + config.setHttpMethodSpecify(true); + // Add to the interceptor list. + registry.addInterceptor(new SentinelWebInterceptor(config)).addPathPatterns("/**"); + } +} +``` + +Then Sentinel will extract URL patterns defined in Web Controller as the web resource (e.g. `/foo/{id}`). + +## Configuration + +### Block handling + +Sentinel Spring Web adapter provides a `BlockExceptionHandler` interface to handle the blocked requests. +We could set the handler via `SentinelWebMvcTotalConfig#setBlockExceptionHandler()` method. + +By default the interceptor will throw out the `BlockException`. +We need to set a global exception handler function in Spring to handle it. An example: + +```java +@ControllerAdvice +@Order(0) +public class SentinelBlockExceptionHandlerConfig { + private Logger logger = LoggerFactory.getLogger(this.getClass()); + + @ExceptionHandler(BlockException.class) + @ResponseBody + public String sentinelBlockHandler(BlockException e) { + AbstractRule rule = e.getRule(); + logger.info("Blocked by Sentinel: {}", rule.toString()); + return "Blocked by Sentinel"; + } +} +``` + +We've provided a `DefaultBlockExceptionHandler`. When a request is blocked, the handler will return a default page +indicating the request is rejected (`Blocked by Sentinel (flow limiting)`). +The HTTP status code of the default block page is **429 (Too Many Requests)**. + +We could also implement our implementation of the `BlockExceptionHandler` interface and +set to the config object. An example: + +```java +SentinelWebMvcConfig config = new SentinelWebMvcConfig(); +config.setBlockExceptionHandler((request, response, e) -> { + String resourceName = e.getRule().getResource(); + // Depending on your situation, you can choose to process or throw + if ("/hello".equals(resourceName)) { + // Do something ...... + response.getWriter().write("Blocked by Sentinel"); + } else { + // Handle it in global exception handling + throw e; + } +}); +``` + +### Customized configuration + +- Common configuration in `SentinelWebMvcConfig` and `SentinelWebMvcTotalConfig`: + +| name | description | type | default value | +|------|------------|------|-------| +| `blockExceptionHandler`| The handler that handles the block request | `BlockExceptionHandler` | null (throw out the BlockException) | +| `originParser` | Extracting request origin (e.g. IP or appName from HTTP Header) from HTTP request | `RequestOriginParser` | - | + +- `SentinelWebMvcConfig` configuration: + +| name | description | type | default value | +|------|------------|------|-------| +| urlCleaner | The `UrlCleaner` interface is designed for clean and unify the URL resource. | `UrlCleaner` | - | +| requestAttributeName | Attribute key in request used by Sentinel (internal) | `String` | `$$sentinel_spring_web_entry_attr` | +| httpMethodSpecify | Specify whether the URL resource name should contain the HTTP method prefix (e.g. `POST:`). | `boolean` | `false` | + +- `SentinelWebMvcTotalConfig` configuration: + +| name | description | type | default value | +|------|------------|------|-------| +| totalResourceName | The resource name in `SentinelTotalInterceptor` | `String` | `spring-mvc-total-url-request` | +| requestAttributeName | Attribute key in request used by Sentinel (internal) | `String` | `$$sentinel_spring_web_total_entry_attr` | \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/pom.xml b/sentinel-adapter/sentinel-spring-webmvc-adapter/pom.xml new file mode 100644 index 0000000000..b74d2cc475 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/pom.xml @@ -0,0 +1,56 @@ + + + + com.alibaba.csp + sentinel-adapter + 1.7.2-SNAPSHOT + + 4.0.0 + + sentinel-spring-webmvc-adapter + jar + + + 5.1.8.RELEASE + 2.1.3.RELEASE + 3.1.0 + + + + + com.alibaba.csp + sentinel-core + + + javax.servlet + javax.servlet-api + ${servlet.api.version} + provided + + + org.springframework + spring-webmvc + ${spring.version} + provided + + + org.springframework.boot + spring-boot-starter-web + ${spring.boot.version} + test + + + org.springframework.boot + spring-boot-starter-test + ${spring.boot.version} + test + + + com.alibaba + fastjson + test + + + \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/AbstractSentinelInterceptor.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/AbstractSentinelInterceptor.java new file mode 100644 index 0000000000..e9f0a39ee6 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/AbstractSentinelInterceptor.java @@ -0,0 +1,147 @@ +/* + * Copyright 1999-2019 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 + * + * https://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.adapter.spring.webmvc; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.ResourceTypeConstants; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.config.BaseWebMvcConfig; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.csp.sentinel.util.StringUtil; + +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +/** + * @author kaizi2009 + * @since 1.7.1 + */ +public abstract class AbstractSentinelInterceptor implements HandlerInterceptor { + + public static final String SENTINEL_SPRING_WEB_CONTEXT_NAME = "sentinel_spring_web_context"; + private static final String EMPTY_ORIGIN = ""; + + private final BaseWebMvcConfig baseWebMvcConfig; + + public AbstractSentinelInterceptor(BaseWebMvcConfig config) { + AssertUtil.notNull(config, "BaseWebMvcConfig should not be null"); + AssertUtil.assertNotBlank(config.getRequestAttributeName(), "requestAttributeName should not be blank"); + this.baseWebMvcConfig = config; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + try { + String resourceName = getResourceName(request); + + if (StringUtil.isNotEmpty(resourceName)) { + // Parse the request origin using registered origin parser. + String origin = parseOrigin(request); + ContextUtil.enter(SENTINEL_SPRING_WEB_CONTEXT_NAME, origin); + Entry entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.IN); + + setEntryInRequest(request, baseWebMvcConfig.getRequestAttributeName(), entry); + } + return true; + } catch (BlockException e) { + handleBlockException(request, response, e); + return false; + } + } + + /** + * Return the resource name of the target web resource. + * + * @param request web request + * @return the resource name of the target web resource. + */ + protected abstract String getResourceName(HttpServletRequest request); + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, + Object handler, Exception ex) throws Exception { + Entry entry = getEntryInRequest(request, baseWebMvcConfig.getRequestAttributeName()); + if (entry != null) { + traceExceptionAndExit(entry, ex); + removeEntryInRequest(request); + } + ContextUtil.exit(); + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + ModelAndView modelAndView) throws Exception { + } + + protected void setEntryInRequest(HttpServletRequest request, String name, Entry entry) { + Object attrVal = request.getAttribute(name); + if (attrVal != null) { + RecordLog.warn("[{}] The attribute key '{0}' already exists in request, please set `requestAttributeName`", + getClass().getSimpleName(), name); + } else { + request.setAttribute(name, entry); + } + } + + protected Entry getEntryInRequest(HttpServletRequest request, String attrKey) { + Object entryObject = request.getAttribute(attrKey); + return entryObject == null ? null : (Entry)entryObject; + } + + protected void removeEntryInRequest(HttpServletRequest request) { + request.removeAttribute(baseWebMvcConfig.getRequestAttributeName()); + } + + protected void traceExceptionAndExit(Entry entry, Exception ex) { + if (entry != null) { + if (ex != null) { + Tracer.traceEntry(ex, entry); + } + entry.exit(); + } + } + + protected void handleBlockException(HttpServletRequest request, HttpServletResponse response, BlockException e) + throws Exception { + if (baseWebMvcConfig.getBlockExceptionHandler() != null) { + baseWebMvcConfig.getBlockExceptionHandler().handle(request, response, e); + } else { + // Throw BlockException directly. Users need to handle it in Spring global exception handler. + throw e; + } + } + + protected String parseOrigin(HttpServletRequest request) { + String origin = EMPTY_ORIGIN; + if (baseWebMvcConfig.getOriginParser() != null) { + origin = baseWebMvcConfig.getOriginParser().parseOrigin(request); + if (StringUtil.isEmpty(origin)) { + return EMPTY_ORIGIN; + } + } + return origin; + } + +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelWebInterceptor.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelWebInterceptor.java new file mode 100644 index 0000000000..b01b2c358a --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelWebInterceptor.java @@ -0,0 +1,69 @@ +/* + * Copyright 1999-2019 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 + * + * https://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.adapter.spring.webmvc; + +import com.alibaba.csp.sentinel.adapter.spring.webmvc.config.SentinelWebMvcConfig; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.UrlCleaner; + +import javax.servlet.http.HttpServletRequest; + +import com.alibaba.csp.sentinel.util.StringUtil; +import org.springframework.web.servlet.HandlerMapping; + +/** + * Spring Web MVC interceptor that integrates with Sentinel. + * + * @author kaizi2009 + * @since 1.7.1 + */ +public class SentinelWebInterceptor extends AbstractSentinelInterceptor { + + private final SentinelWebMvcConfig config; + + public SentinelWebInterceptor() { + this(new SentinelWebMvcConfig()); + } + + public SentinelWebInterceptor(SentinelWebMvcConfig config) { + super(config); + if (config == null) { + // Use the default config by default. + this.config = new SentinelWebMvcConfig(); + } else { + this.config = config; + } + } + + @Override + protected String getResourceName(HttpServletRequest request) { + // Resolve the Spring Web URL pattern from the request attribute. + Object resourceNameObject = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); + if (resourceNameObject == null || !(resourceNameObject instanceof String)) { + return null; + } + String resourceName = (String) resourceNameObject; + UrlCleaner urlCleaner = config.getUrlCleaner(); + if (urlCleaner != null) { + resourceName = urlCleaner.clean(resourceName); + } + // Add method specification if necessary + if (StringUtil.isNotEmpty(resourceName) && config.isHttpMethodSpecify()) { + resourceName = request.getMethod().toUpperCase() + ":" + resourceName; + } + return resourceName; + } + +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelWebTotalInterceptor.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelWebTotalInterceptor.java new file mode 100644 index 0000000000..d356ca36ff --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelWebTotalInterceptor.java @@ -0,0 +1,50 @@ +/* + * Copyright 1999-2019 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 + * + * https://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.adapter.spring.webmvc; + +import com.alibaba.csp.sentinel.adapter.spring.webmvc.config.SentinelWebMvcTotalConfig; + +import javax.servlet.http.HttpServletRequest; + +/** + * The web interceptor for all requests, which will unify all URL as + * a single resource name (configured in {@link SentinelWebMvcTotalConfig}). + * + * @author kaizi2009 + * @since 1.7.1 + */ +public class SentinelWebTotalInterceptor extends AbstractSentinelInterceptor { + + private final SentinelWebMvcTotalConfig config; + + public SentinelWebTotalInterceptor(SentinelWebMvcTotalConfig config) { + super(config); + if (config == null) { + this.config = new SentinelWebMvcTotalConfig(); + } else { + this.config = config; + } + } + + public SentinelWebTotalInterceptor() { + this(new SentinelWebMvcTotalConfig()); + } + + @Override + protected String getResourceName(HttpServletRequest request) { + return config.getTotalResourceName(); + } +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/BlockExceptionHandler.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/BlockExceptionHandler.java new file mode 100644 index 0000000000..b40a06ebe6 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/BlockExceptionHandler.java @@ -0,0 +1,40 @@ +/* + * Copyright 1999-2019 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 + * + * https://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.adapter.spring.webmvc.callback; + +import com.alibaba.csp.sentinel.slots.block.BlockException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Handler for the blocked request. + * + * @author kaizi2009 + */ +public interface BlockExceptionHandler { + + /** + * Handle the request when blocked. + * + * @param request Servlet request + * @param response Servlet response + * @param e the block exception + * @throws Exception users may throw out the BlockException or other error occurs + */ + void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception; + +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/DefaultBlockExceptionHandler.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/DefaultBlockExceptionHandler.java new file mode 100644 index 0000000000..acb74d0e43 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/DefaultBlockExceptionHandler.java @@ -0,0 +1,49 @@ +/* + * Copyright 1999-2019 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 + * + * https://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.adapter.spring.webmvc.callback; + +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.util.StringUtil; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.PrintWriter; + +/** + * Default handler for the blocked request. + * + * @author kaizi2009 + */ +public class DefaultBlockExceptionHandler implements BlockExceptionHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception { + // Return 429 (Too Many Requests) by default. + response.setStatus(429); + + StringBuffer url = request.getRequestURL(); + + if ("GET".equals(request.getMethod()) && StringUtil.isNotBlank(request.getQueryString())) { + url.append("?").append(request.getQueryString()); + } + + PrintWriter out = response.getWriter(); + out.print("Blocked by Sentinel (flow limiting)"); + out.flush(); + out.close(); + } + +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/RequestOriginParser.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/RequestOriginParser.java new file mode 100644 index 0000000000..647e018363 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/RequestOriginParser.java @@ -0,0 +1,34 @@ +/* + * 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.adapter.spring.webmvc.callback; + +import javax.servlet.http.HttpServletRequest; + +/** + * The origin parser parses request origin (e.g. IP, user, appName) from HTTP request. + * + * @author kaizi2009 + */ +public interface RequestOriginParser { + + /** + * Parse the origin from given HTTP request. + * + * @param request HTTP request + * @return parsed origin + */ + String parseOrigin(HttpServletRequest request); +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/UrlCleaner.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/UrlCleaner.java new file mode 100644 index 0000000000..8541d097b6 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/callback/UrlCleaner.java @@ -0,0 +1,32 @@ +/* + * Copyright 1999-2019 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 + * + * https://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.adapter.spring.webmvc.callback; + +/** + * Unify the resource target. + * + * @author kaizi2009 + */ +public interface UrlCleaner { + + /** + * Unify the resource target. + * + * @param originUrl the original URL + * @return the unified resource name + */ + String clean(String originUrl); +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/BaseWebMvcConfig.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/BaseWebMvcConfig.java new file mode 100644 index 0000000000..03ff0ebc7b --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/BaseWebMvcConfig.java @@ -0,0 +1,57 @@ +/* + * Copyright 1999-2019 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.adapter.spring.webmvc.config; + +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser; +import com.alibaba.csp.sentinel.util.AssertUtil; + +/** + * Common base configuration for Spring Web MVC adapter. + * + * @author kaizi2009 + * @since 1.7.1 + */ +public abstract class BaseWebMvcConfig { + + protected String requestAttributeName; + protected BlockExceptionHandler blockExceptionHandler; + protected RequestOriginParser originParser; + + public String getRequestAttributeName() { + return requestAttributeName; + } + + public void setRequestAttributeName(String requestAttributeName) { + this.requestAttributeName = requestAttributeName; + } + + public BlockExceptionHandler getBlockExceptionHandler() { + return blockExceptionHandler; + } + + public void setBlockExceptionHandler(BlockExceptionHandler blockExceptionHandler) { + this.blockExceptionHandler = blockExceptionHandler; + } + + public RequestOriginParser getOriginParser() { + return originParser; + } + + public void setOriginParser(RequestOriginParser originParser) { + this.originParser = originParser; + } +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelWebMvcConfig.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelWebMvcConfig.java new file mode 100755 index 0000000000..29a2891ca6 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelWebMvcConfig.java @@ -0,0 +1,70 @@ +/* + * 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.adapter.spring.webmvc.config; + +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.UrlCleaner; + +/** + * @author kaizi2009 + * @since 1.7.1 + */ +public class SentinelWebMvcConfig extends BaseWebMvcConfig { + + public static final String DEFAULT_REQUEST_ATTRIBUTE_NAME = "$$sentinel_spring_web_entry_attr"; + + /** + * Specify the URL cleaner that unifies the URL resources. + */ + private UrlCleaner urlCleaner; + /** + * Specify whether the URL resource name should contain the HTTP method prefix (e.g. {@code POST:}). + */ + private boolean httpMethodSpecify; + + public SentinelWebMvcConfig() { + super(); + setRequestAttributeName(DEFAULT_REQUEST_ATTRIBUTE_NAME); + } + + public UrlCleaner getUrlCleaner() { + return urlCleaner; + } + + public SentinelWebMvcConfig setUrlCleaner(UrlCleaner urlCleaner) { + this.urlCleaner = urlCleaner; + return this; + } + + public boolean isHttpMethodSpecify() { + return httpMethodSpecify; + } + + public SentinelWebMvcConfig setHttpMethodSpecify(boolean httpMethodSpecify) { + this.httpMethodSpecify = httpMethodSpecify; + return this; + } + + @Override + public String toString() { + return "SentinelWebMvcConfig{" + + "urlCleaner=" + urlCleaner + + ", httpMethodSpecify=" + httpMethodSpecify + + ", requestAttributeName='" + requestAttributeName + '\'' + + ", blockExceptionHandler=" + blockExceptionHandler + + ", originParser=" + originParser + + '}'; + } +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelWebMvcTotalConfig.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelWebMvcTotalConfig.java new file mode 100644 index 0000000000..dc8b4af026 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelWebMvcTotalConfig.java @@ -0,0 +1,52 @@ +/* + * Copyright 1999-2019 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 + * + * https://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.adapter.spring.webmvc.config; + +/** + * @author kaizi2009 + * @since 1.7.1 + */ +public class SentinelWebMvcTotalConfig extends BaseWebMvcConfig { + + public static final String DEFAULT_TOTAL_RESOURCE_NAME = "spring-mvc-total-url-request"; + public static final String DEFAULT_REQUEST_ATTRIBUTE_NAME = "$$sentinel_spring_web_total_entry_attr"; + + private String totalResourceName = DEFAULT_TOTAL_RESOURCE_NAME; + + public SentinelWebMvcTotalConfig() { + super(); + setRequestAttributeName(DEFAULT_REQUEST_ATTRIBUTE_NAME); + } + + public String getTotalResourceName() { + return totalResourceName; + } + + public SentinelWebMvcTotalConfig setTotalResourceName(String totalResourceName) { + this.totalResourceName = totalResourceName; + return this; + } + + @Override + public String toString() { + return "SentinelWebMvcTotalConfig{" + + "totalResourceName='" + totalResourceName + '\'' + + ", requestAttributeName='" + requestAttributeName + '\'' + + ", blockExceptionHandler=" + blockExceptionHandler + + ", originParser=" + originParser + + '}'; + } +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/ResultWrapper.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/ResultWrapper.java new file mode 100644 index 0000000000..66121fcd60 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/ResultWrapper.java @@ -0,0 +1,53 @@ +/* + * Copyright 1999-2019 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 + * + * https://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.adapter.spring.webmvc; + +import com.alibaba.fastjson.JSONObject; + +/** + * @author kaizi2009 + */ +public class ResultWrapper { + + private Integer code; + private String message; + + public ResultWrapper(Integer code, String message) { + this.code = code; + this.message = message; + } + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public static ResultWrapper error() { + + return new ResultWrapper(-1, "System error"); + } + + public static ResultWrapper blocked() { + return new ResultWrapper(-2, "Blocked by Sentinel"); + } + + public String toJsonString() { + return JSONObject.toJSONString(this); + } +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java new file mode 100644 index 0000000000..f6a4b30fba --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelSpringMvcIntegrationTest.java @@ -0,0 +1,155 @@ +/* + * Copyright 1999-2019 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 + * + * https://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.adapter.spring.webmvc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; +import com.alibaba.csp.sentinel.util.StringUtil; + +import java.util.Collections; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +/** + * @author kaizi2009 + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = TestApplication.class) +@AutoConfigureMockMvc +public class SentinelSpringMvcIntegrationTest { + + private static final String HELLO_STR = "Hello!"; + @Autowired + private MockMvc mvc; + + @Test + public void testBase() throws Exception { + String url = "/hello"; + this.mvc.perform(get(url)) + .andExpect(status().isOk()) + .andExpect(content().string(HELLO_STR)); + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(url); + assertNotNull(cn); + assertEquals(1, cn.passQps(), 0.01); + } + + @Test + public void testOriginParser() throws Exception { + String springMvcPathVariableUrl = "/foo/{id}"; + String limitOrigin = "userA"; + final String headerName = "S-User"; + configureRulesFor(springMvcPathVariableUrl, 0, limitOrigin); + + this.mvc.perform(get("/foo/1").accept(MediaType.TEXT_PLAIN).header(headerName, "userB")) + .andExpect(status().isOk()) + .andExpect(content().string("foo 1")); + + // This will be blocked and reponse json. + this.mvc.perform( + get("/foo/2").accept(MediaType.APPLICATION_JSON).header(headerName, limitOrigin)) + .andExpect(status().isOk()) + .andExpect(content().json(ResultWrapper.blocked().toJsonString())); + this.mvc.perform(get("/foo/3").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().string(ResultWrapper.blocked().toJsonString())); + + FlowRuleManager.loadRules(null); + } + + @Test + public void testTotalInterceptor() throws Exception { + String url = "/hello"; + String totalTarget = "my_spring_mvc_total_url_request"; + for (int i = 0; i < 3; i++) { + this.mvc.perform(get(url)) + .andExpect(status().isOk()) + .andExpect(content().string(HELLO_STR)); + } + ClusterNode cn = ClusterBuilderSlot.getClusterNode(totalTarget); + assertNotNull(cn); + assertEquals(3, cn.passQps(), 0.01); + } + + @Test + public void testRuntimeException() throws Exception { + String url = "/runtimeException"; + configureExceptionRulesFor(url, 3, null); + int repeat = 3; + for (int i = 0; i < repeat; i++) { + this.mvc.perform(get(url)) + .andExpect(status().isOk()) + .andExpect(content().string(ResultWrapper.error().toJsonString())); + ClusterNode cn = ClusterBuilderSlot.getClusterNode(url); + assertNotNull(cn); + assertEquals(i + 1, cn.passQps(), 0.01); + } + + // This will be blocked and reponse json. + this.mvc.perform(get(url)) + .andExpect(status().isOk()) + .andExpect(content().string(ResultWrapper.blocked().toJsonString())); + ClusterNode cn = ClusterBuilderSlot.getClusterNode(url); + assertNotNull(cn); + assertEquals(repeat, cn.passQps(), 0.01); + assertEquals(1, cn.blockRequest(), 1); + } + + private void configureRulesFor(String resource, int count, String limitApp) { + FlowRule rule = new FlowRule() + .setCount(count) + .setGrade(RuleConstant.FLOW_GRADE_QPS); + rule.setResource(resource); + if (StringUtil.isNotBlank(limitApp)) { + rule.setLimitApp(limitApp); + } + FlowRuleManager.loadRules(Collections.singletonList(rule)); + } + + private void configureExceptionRulesFor(String resource, int count, String limitApp) { + FlowRule rule = new FlowRule() + .setCount(count) + .setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO); + rule.setResource(resource); + if (StringUtil.isNotBlank(limitApp)) { + rule.setLimitApp(limitApp); + } + FlowRuleManager.loadRules(Collections.singletonList(rule)); + } + + @After + public void cleanUp() { + FlowRuleManager.loadRules(null); + ClusterBuilderSlot.resetClusterNodes(); + } +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelWebInterceptorTest.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelWebInterceptorTest.java new file mode 100644 index 0000000000..506d9b2d46 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/SentinelWebInterceptorTest.java @@ -0,0 +1,35 @@ +/* + * 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.adapter.spring.webmvc; + +import com.alibaba.csp.sentinel.adapter.spring.webmvc.config.SentinelWebMvcConfig; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author Eric Zhao + */ +public class SentinelWebInterceptorTest { + + @Test(expected = IllegalArgumentException.class) + public void testPassIllegalConfig() { + SentinelWebMvcConfig config = new SentinelWebMvcConfig(); + config.setRequestAttributeName(null); + SentinelWebInterceptor interceptor = new SentinelWebInterceptor(config); + } +} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/TestApplication.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/TestApplication.java new file mode 100644 index 0000000000..da9b6bba93 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/TestApplication.java @@ -0,0 +1,31 @@ +/* + * Copyright 1999-2019 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 + * + * https://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.adapter.spring.webmvc; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +/** + * @author kaizi2009 + */ +@SpringBootApplication +public class TestApplication { + public static void main(String[] args) { + SpringApplication.run(TestApplication.class); + } +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/InterceptorConfig.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/InterceptorConfig.java new file mode 100644 index 0000000000..1ff24fd2cf --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/InterceptorConfig.java @@ -0,0 +1,91 @@ +/* + * Copyright 1999-2019 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 + * + * https://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.adapter.spring.webmvc.config; + +import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelWebInterceptor; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelWebTotalInterceptor; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Config sentinel interceptor + * + * @author kaizi2009 + */ +@Configuration +public class InterceptorConfig implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + //Add sentinel interceptor + addSpringMvcInterceptor(registry); + + //If you want to sentinel the total flow, you can add total interceptor + addSpringMvcTotalInterceptor(registry); + } + + private void addSpringMvcInterceptor(InterceptorRegistry registry) { + //Config + SentinelWebMvcConfig config = new SentinelWebMvcConfig(); + + config.setBlockExceptionHandler(new BlockExceptionHandler() { + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception { + String resourceName = e.getRule().getResource(); + //Depending on your situation, you can choose to process or throw + if ("/hello".equals(resourceName)) { + //Do something ...... + //Write string or json string; + response.getWriter().write("/Blocked by sentinel"); + } else { + //Handle in global exception handling + throw e; + } + } + }); + + //Custom configuration if necessary + config.setHttpMethodSpecify(false); + config.setOriginParser(new RequestOriginParser() { + @Override + public String parseOrigin(HttpServletRequest request) { + return request.getHeader("S-user"); + } + }); + + //Add sentinel interceptor + registry.addInterceptor(new SentinelWebInterceptor(config)).addPathPatterns("/**"); + } + + private void addSpringMvcTotalInterceptor(InterceptorRegistry registry) { + //Configure + SentinelWebMvcTotalConfig config = new SentinelWebMvcTotalConfig(); + + //Custom configuration if necessary + config.setRequestAttributeName("my_sentinel_spring_mvc_total_entity_container"); + config.setTotalResourceName("my_spring_mvc_total_url_request"); + + //Add sentinel interceptor + registry.addInterceptor(new SentinelWebTotalInterceptor(config)).addPathPatterns("/**"); + } +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelSpringMvcBlockHandlerConfig.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelSpringMvcBlockHandlerConfig.java new file mode 100644 index 0000000000..b8e4660377 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/config/SentinelSpringMvcBlockHandlerConfig.java @@ -0,0 +1,54 @@ +/* + * Copyright 1999-2019 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 + * + * https://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.adapter.spring.webmvc.config; + +import com.alibaba.csp.sentinel.adapter.spring.webmvc.ResultWrapper; +import com.alibaba.csp.sentinel.slots.block.AbstractRule; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.Order; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * Config 'BlockException' handler, handler it in spring veb 'ExceptionHandler' + * + * @author kaizi2009 + */ +@ControllerAdvice +@Order(0) +public class SentinelSpringMvcBlockHandlerConfig { + private Logger logger = LoggerFactory.getLogger(this.getClass()); + + @ExceptionHandler(BlockException.class) + @ResponseBody + public ResultWrapper sentinelBlockHandler(BlockException e) { + AbstractRule rule = e.getRule(); + //Log + logger.info("Blocked by sentinel, {}", rule.toString()); + //Return object + return ResultWrapper.blocked(); + } + + @ExceptionHandler(Exception.class) + @ResponseBody + public ResultWrapper exceptionHandler(Exception e) { + logger.error("System error", e.getMessage()); + return new ResultWrapper(-1, "System error"); + } +} diff --git a/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/controller/TestController.java b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/controller/TestController.java new file mode 100644 index 0000000000..d8a42ab8fb --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webmvc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/spring/webmvc/controller/TestController.java @@ -0,0 +1,55 @@ +/* + * Copyright 1999-2019 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 + * + * https://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.adapter.spring.webmvc.controller; + + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author kaizi2009 + */ +@RestController +public class TestController { + + @GetMapping("/hello") + public String apiHello() { + return "Hello!"; + } + + @GetMapping("/err") + public String apiError() { + return "Oops..."; + } + + @GetMapping("/foo/{id}") + public String apiFoo(@PathVariable("id") Long id) { + return "foo " + id; + } + + @GetMapping("/runtimeException") + public String runtimeException() { + int i = 1 / 0; + return "runtimeException"; + } + + @GetMapping("/exclude/{id}") + public String apiExclude(@PathVariable("id") Long id) { + return "Exclude " + id; + } + +} diff --git a/sentinel-adapter/sentinel-web-servlet/README.md b/sentinel-adapter/sentinel-web-servlet/README.md index f73451a3c7..1d56d21b6e 100755 --- a/sentinel-adapter/sentinel-web-servlet/README.md +++ b/sentinel-adapter/sentinel-web-servlet/README.md @@ -1,6 +1,7 @@ # Sentinel Web Servlet Filter -Sentinel provides Servlet filter integration to enable flow control for web requests. Add the following dependency in `pom.xml` (if you are using Maven): +Sentinel provides Servlet filter integration to enable flow control for web requests. +Add the following dependency in `pom.xml` (if you are using Maven): ```xml @@ -10,7 +11,7 @@ Sentinel provides Servlet filter integration to enable flow control for web requ ``` -To use the filter, you can simply configure your `web.xml` with: +To activate the filter, you can simply configure your `web.xml` with: ```xml @@ -34,16 +35,21 @@ public class FilterConfig { public FilterRegistrationBean sentinelFilterRegistration() { FilterRegistrationBean registration = new FilterRegistrationBean<>(); registration.setFilter(new CommonFilter()); + // Set the matching URL pattern for the filter. registration.addUrlPatterns("/*"); - registration.setName("sentinelFilter"); + registration.setName("sentinelCommonFilter"); registration.setOrder(1); - + // Set whether to support the specified HTTP method prefix for the filter. + registration.addInitParameter(CommonFilter.HTTP_METHOD_SPECIFY, "false"); return registration; } } ``` -When a request is blocked, Sentinel servlet filter will give a default page indicating the request blocked. +When a request is blocked, Sentinel servlet filter will display a default page indicating the request is rejected. +The HTTP status code of the default block page is **429 (Too Many Requests)**. You can customize it +via the `csp.sentinel.web.servlet.block.status` configuration item (since 1.7.0). + If customized block page is set (via `WebServletConfig.setBlockPage(blockPage)` method), the filter will redirect the request to provided URL. You can also implement your own block handler (the `UrlBlockHandler` interface) and register to `WebCallbackManager`. @@ -52,5 +58,9 @@ The `UrlCleaner` interface is designed for clean and unify the URL resource. For REST APIs, you have to clean the URL resource (e.g. `/foo/1` and `/foo/2` -> `/foo/:id`), or the amount of context and resources will exceed the threshold. -`RequestOriginParser` interface is useful for extracting request origin (e.g. IP or appName from HTTP Header) -from HTTP request. You can implement your own `RequestOriginParser` and register to `WebCallbackManager`. \ No newline at end of file +If you need to exclude some URLs (that should not be recorded as Sentinel resources), you could also +leverage the `UrlCleaner` interface. You may unify the unwanted URLs to the empty string `""` or `null`, +then the URLs will be excluded (since Sentinel 1.6.3). + +The `RequestOriginParser` interface is useful for extracting request origin (e.g. IP or appName from HTTP Header) +from HTTP request. You can implement your own `RequestOriginParser` and register to `WebCallbackManager`. diff --git a/sentinel-adapter/sentinel-web-servlet/pom.xml b/sentinel-adapter/sentinel-web-servlet/pom.xml index fc82802ed2..32c6b0d3fa 100755 --- a/sentinel-adapter/sentinel-web-servlet/pom.xml +++ b/sentinel-adapter/sentinel-web-servlet/pom.xml @@ -6,7 +6,7 @@ com.alibaba.csp sentinel-adapter - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT sentinel-web-servlet diff --git a/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilter.java b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilter.java index f24bbe55e2..e246e06c65 100755 --- a/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilter.java +++ b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilter.java @@ -28,40 +28,58 @@ import com.alibaba.csp.sentinel.Entry; import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.ResourceTypeConstants; import com.alibaba.csp.sentinel.SphU; import com.alibaba.csp.sentinel.Tracer; import com.alibaba.csp.sentinel.adapter.servlet.callback.RequestOriginParser; import com.alibaba.csp.sentinel.adapter.servlet.callback.UrlCleaner; import com.alibaba.csp.sentinel.adapter.servlet.callback.WebCallbackManager; +import com.alibaba.csp.sentinel.adapter.servlet.config.WebServletConfig; import com.alibaba.csp.sentinel.adapter.servlet.util.FilterUtil; import com.alibaba.csp.sentinel.context.ContextUtil; import com.alibaba.csp.sentinel.slots.block.BlockException; import com.alibaba.csp.sentinel.util.StringUtil; -/*** +/** * Servlet filter that integrates with Sentinel. * * @author youji.zj * @author Eric Zhao + * @author zhaoyuguang */ public class CommonFilter implements Filter { - private final static String HTTP_METHOD_SPECIFY = "HTTP_METHOD_SPECIFY"; + /** + * Specify whether the URL resource name should contain the HTTP method prefix (e.g. {@code POST:}). + */ + public static final String HTTP_METHOD_SPECIFY = "HTTP_METHOD_SPECIFY"; + /** + * If enabled, use the URL path as the context name, or else use the default + * {@link WebServletConfig#WEB_SERVLET_CONTEXT_NAME}. Please pay attention to the number of context (EntranceNode), + * which may affect the memory footprint. + * + * @since 1.7.0 + */ + public static final String WEB_CONTEXT_UNIFY = "WEB_CONTEXT_UNIFY"; + private final static String COLON = ":"; + private boolean httpMethodSpecify = false; + private boolean webContextUnify = true; @Override public void init(FilterConfig filterConfig) { httpMethodSpecify = Boolean.parseBoolean(filterConfig.getInitParameter(HTTP_METHOD_SPECIFY)); + if (filterConfig.getInitParameter(WEB_CONTEXT_UNIFY) != null) { + webContextUnify = Boolean.parseBoolean(filterConfig.getInitParameter(WEB_CONTEXT_UNIFY)); + } } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest sRequest = (HttpServletRequest) request; - Entry entry = null; - - Entry methodEntry = null; + Entry urlEntry = null; try { String target = FilterUtil.filterTarget(sRequest); @@ -73,39 +91,33 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha target = urlCleaner.clean(target); } - // Parse the request origin using registered origin parser. - String origin = parseOrigin(sRequest); - - ContextUtil.enter(target, origin); - entry = SphU.entry(target, EntryType.IN); - - - // Add method specification if necessary - if (httpMethodSpecify) { - methodEntry = SphU.entry(sRequest.getMethod().toUpperCase() + COLON + target, - EntryType.IN); + // If you intend to exclude some URLs, you can convert the URLs to the empty string "" + // in the UrlCleaner implementation. + if (!StringUtil.isEmpty(target)) { + // Parse the request origin using registered origin parser. + String origin = parseOrigin(sRequest); + String contextName = webContextUnify ? WebServletConfig.WEB_SERVLET_CONTEXT_NAME : target; + ContextUtil.enter(contextName, origin); + + if (httpMethodSpecify) { + // Add HTTP method prefix if necessary. + String pathWithHttpMethod = sRequest.getMethod().toUpperCase() + COLON + target; + urlEntry = SphU.entry(pathWithHttpMethod, ResourceTypeConstants.COMMON_WEB, EntryType.IN); + } else { + urlEntry = SphU.entry(target, ResourceTypeConstants.COMMON_WEB, EntryType.IN); + } } - chain.doFilter(request, response); } catch (BlockException e) { HttpServletResponse sResponse = (HttpServletResponse) response; // Return the block page, or redirect to another URL. WebCallbackManager.getUrlBlockHandler().blocked(sRequest, sResponse, e); - } catch (IOException e2) { - Tracer.trace(e2); + } catch (IOException | ServletException | RuntimeException e2) { + Tracer.traceEntry(e2, urlEntry); throw e2; - } catch (ServletException e3) { - Tracer.trace(e3); - throw e3; - } catch (RuntimeException e4) { - Tracer.trace(e4); - throw e4; } finally { - if (methodEntry != null) { - methodEntry.exit(); - } - if (entry != null) { - entry.exit(); + if (urlEntry != null) { + urlEntry.exit(); } ContextUtil.exit(); } diff --git a/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonTotalFilter.java b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonTotalFilter.java index 803607d5b7..47161e0f30 100755 --- a/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonTotalFilter.java +++ b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonTotalFilter.java @@ -27,10 +27,11 @@ import javax.servlet.http.HttpServletResponse; import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.ResourceTypeConstants; import com.alibaba.csp.sentinel.SphU; import com.alibaba.csp.sentinel.Tracer; import com.alibaba.csp.sentinel.adapter.servlet.callback.WebCallbackManager; -import com.alibaba.csp.sentinel.adapter.servlet.util.FilterUtil; +import com.alibaba.csp.sentinel.adapter.servlet.config.WebServletConfig; import com.alibaba.csp.sentinel.context.ContextUtil; import com.alibaba.csp.sentinel.slots.block.BlockException; @@ -52,26 +53,18 @@ public void init(FilterConfig filterConfig) { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest sRequest = (HttpServletRequest)request; - String target = FilterUtil.filterTarget(sRequest); - target = WebCallbackManager.getUrlCleaner().clean(target); Entry entry = null; try { - ContextUtil.enter(target); - entry = SphU.entry(TOTAL_URL_REQUEST); + ContextUtil.enter(WebServletConfig.WEB_SERVLET_CONTEXT_NAME); + entry = SphU.entry(TOTAL_URL_REQUEST, ResourceTypeConstants.COMMON_WEB); chain.doFilter(request, response); } catch (BlockException e) { HttpServletResponse sResponse = (HttpServletResponse)response; WebCallbackManager.getUrlBlockHandler().blocked(sRequest, sResponse, e); - } catch (IOException e2) { + } catch (IOException | ServletException | RuntimeException e2) { Tracer.trace(e2); throw e2; - } catch (ServletException e3) { - Tracer.trace(e3); - throw e3; - } catch (RuntimeException e4) { - Tracer.trace(e4); - throw e4; } finally { if (entry != null) { entry.exit(); diff --git a/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/config/WebServletConfig.java b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/config/WebServletConfig.java index bcaafe0786..dc6f66c42a 100755 --- a/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/config/WebServletConfig.java +++ b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/config/WebServletConfig.java @@ -18,13 +18,23 @@ import com.alibaba.csp.sentinel.adapter.servlet.CommonFilter; import com.alibaba.csp.sentinel.adapter.servlet.CommonTotalFilter; import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.util.StringUtil; /** + * The configuration center for Web Servlet adapter. + * * @author leyou + * @author zhaoyuguang */ -public class WebServletConfig { +public final class WebServletConfig { + + public static final String WEB_SERVLET_CONTEXT_NAME = "sentinel_web_servlet_context"; - public static final String BLOCK_PAGE = "csp.sentinel.web.servlet.block.page"; + public static final String BLOCK_PAGE_URL_CONF_KEY = "csp.sentinel.web.servlet.block.page"; + public static final String BLOCK_PAGE_HTTP_STATUS_CONF_KEY = "csp.sentinel.web.servlet.block.status"; + + private static final int HTTP_STATUS_TOO_MANY_REQUESTS = 429; /** * Get redirecting page when Sentinel blocking for {@link CommonFilter} or @@ -33,10 +43,52 @@ public class WebServletConfig { * @return the block page URL, maybe null if not configured. */ public static String getBlockPage() { - return SentinelConfig.getConfig(BLOCK_PAGE); + return SentinelConfig.getConfig(BLOCK_PAGE_URL_CONF_KEY); } public static void setBlockPage(String blockPage) { - SentinelConfig.setConfig(BLOCK_PAGE, blockPage); + SentinelConfig.setConfig(BLOCK_PAGE_URL_CONF_KEY, blockPage); + } + + /** + *

Get the HTTP status when using the default block page.

+ *

You can set the status code with the {@code -Dcsp.sentinel.web.servlet.block.status} + * property. When the property is empty or invalid, Sentinel will use 429 (Too Many Requests) + * as the default status code.

+ * + * @return the HTTP status of the default block page + * @since 1.7.0 + */ + public static int getBlockPageHttpStatus() { + String value = SentinelConfig.getConfig(BLOCK_PAGE_HTTP_STATUS_CONF_KEY); + if (StringUtil.isEmpty(value)) { + return HTTP_STATUS_TOO_MANY_REQUESTS; + } + try { + int s = Integer.parseInt(value); + if (s <= 0) { + throw new IllegalArgumentException("Invalid status code: " + s); + } + return s; + } catch (Exception e) { + RecordLog.warn("[WebServletConfig] Invalid block HTTP status (" + value + "), using default 429"); + setBlockPageHttpStatus(HTTP_STATUS_TOO_MANY_REQUESTS); + } + return HTTP_STATUS_TOO_MANY_REQUESTS; } + + /** + * Set the HTTP status of the default block page. + * + * @param httpStatus the HTTP status of the default block page + * @since 1.7.0 + */ + public static void setBlockPageHttpStatus(int httpStatus) { + if (httpStatus <= 0) { + throw new IllegalArgumentException("Invalid HTTP status code: " + httpStatus); + } + SentinelConfig.setConfig(BLOCK_PAGE_HTTP_STATUS_CONF_KEY, String.valueOf(httpStatus)); + } + + private WebServletConfig() {} } diff --git a/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/util/FilterUtil.java b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/util/FilterUtil.java index 626aecf16e..a4781bead3 100755 --- a/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/util/FilterUtil.java +++ b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/util/FilterUtil.java @@ -27,6 +27,7 @@ /** * Util class for web servlet filter. * + * @author zhaoyuguang * @author youji.zj * @author Eric Zhao */ @@ -65,7 +66,7 @@ public static void blockRequest(HttpServletRequest request, HttpServletResponse } if (StringUtil.isBlank(WebServletConfig.getBlockPage())) { - writeDefaultBlockedPage(response); + writeDefaultBlockedPage(response, WebServletConfig.getBlockPageHttpStatus()); } else { String redirectUrl = WebServletConfig.getBlockPage() + "?http_referer=" + url.toString(); // Redirect to the customized block page. @@ -73,7 +74,8 @@ public static void blockRequest(HttpServletRequest request, HttpServletResponse } } - private static void writeDefaultBlockedPage(HttpServletResponse response) throws IOException { + private static void writeDefaultBlockedPage(HttpServletResponse response, int httpStatus) throws IOException { + response.setStatus(httpStatus); PrintWriter out = response.getWriter(); out.print(DEFAULT_BLOCK_MSG); out.flush(); diff --git a/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilterTest.java b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilterTest.java index bd42a89b06..a505ebf809 100644 --- a/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilterTest.java +++ b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilterTest.java @@ -19,6 +19,7 @@ import javax.servlet.http.HttpServletRequest; +import com.alibaba.csp.sentinel.Constants; import com.alibaba.csp.sentinel.adapter.servlet.callback.DefaultUrlCleaner; import com.alibaba.csp.sentinel.adapter.servlet.callback.RequestOriginParser; import com.alibaba.csp.sentinel.adapter.servlet.callback.UrlCleaner; @@ -26,6 +27,8 @@ import com.alibaba.csp.sentinel.adapter.servlet.config.WebServletConfig; import com.alibaba.csp.sentinel.adapter.servlet.util.FilterUtil; import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.node.EntranceNode; +import com.alibaba.csp.sentinel.node.Node; import com.alibaba.csp.sentinel.slots.block.RuleConstant; import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; @@ -38,15 +41,17 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import static org.junit.Assert.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; /** + * @author zhaoyuguang * @author Eric Zhao */ @RunWith(SpringRunner.class) @@ -76,6 +81,7 @@ private void configureRulesFor(String resource, int count, String limitApp) { @Test public void testCommonFilterMiscellaneous() throws Exception { + Constants.ROOT.removeChildList(); String url = "/hello"; this.mvc.perform(get(url)) .andExpect(status().isOk()) @@ -85,22 +91,39 @@ public void testCommonFilterMiscellaneous() throws Exception { assertNotNull(cn); assertEquals(1, cn.passQps(), 0.01); + String context = ""; + for (Node n : Constants.ROOT.getChildList()) { + if (n instanceof EntranceNode) { + String id = ((EntranceNode) n).getId().getName(); + if (url.equals(id)) { + context = ((EntranceNode) n).getId().getName(); + } + } + } + assertEquals("", context); + testCommonBlockAndRedirectBlockPage(url, cn); // Test for url cleaner. testUrlCleaner(); - + testUrlExclusion(); testCustomOriginParser(); } private void testCommonBlockAndRedirectBlockPage(String url, ClusterNode cn) throws Exception { configureRulesFor(url, 0); // The request will be blocked and response is default block message. + WebServletConfig.setBlockPageHttpStatus(HttpStatus.OK.value()); this.mvc.perform(get(url).accept(MediaType.TEXT_PLAIN)) .andExpect(status().isOk()) .andExpect(content().string(FilterUtil.DEFAULT_BLOCK_MSG)); assertEquals(1, cn.blockQps(), 0.01); + WebServletConfig.setBlockPageHttpStatus(HttpStatus.TOO_MANY_REQUESTS.value()); + this.mvc.perform(get(url).accept(MediaType.TEXT_PLAIN)) + .andExpect(status().isTooManyRequests()) + .andExpect(content().string(FilterUtil.DEFAULT_BLOCK_MSG)); + // Test for redirect. String redirectUrl = "http://some-location.com"; WebServletConfig.setBlockPage(redirectUrl); @@ -139,6 +162,25 @@ public String clean(String originUrl) { WebCallbackManager.setUrlCleaner(new DefaultUrlCleaner()); } + private void testUrlExclusion() throws Exception { + final String excludePrefix = "/exclude/"; + String url = excludePrefix + 1; + WebCallbackManager.setUrlCleaner(new UrlCleaner() { + @Override + public String clean(String originUrl) { + if(originUrl.startsWith(excludePrefix)) { + return ""; + } + return originUrl; + } + }); + this.mvc.perform(get(url).accept(MediaType.TEXT_PLAIN)) + .andExpect(status().isOk()) + .andExpect(content().string("Exclude 1")); + assertNull(ClusterBuilderSlot.getClusterNode(url)); + WebCallbackManager.setUrlCleaner(new DefaultUrlCleaner()); + } + private void testCustomOriginParser() throws Exception { String url = "/hello"; String limitOrigin = "userA"; @@ -158,7 +200,7 @@ public String parseOrigin(HttpServletRequest request) { .andExpect(content().string(HELLO_STR)); // This will be blocked. this.mvc.perform(get(url).accept(MediaType.TEXT_PLAIN).header(headerName, limitOrigin)) - .andExpect(status().isOk()) + .andExpect(status().isTooManyRequests()) .andExpect(content().string(FilterUtil.DEFAULT_BLOCK_MSG)); this.mvc.perform(get(url).accept(MediaType.TEXT_PLAIN)) .andExpect(status().isOk()) diff --git a/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/TestController.java b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/TestController.java index 6221190b67..dce4e2ebfe 100644 --- a/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/TestController.java +++ b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/TestController.java @@ -39,4 +39,9 @@ public String apiError() { public String apiFoo(@PathVariable("id") Long id) { return "Hello " + id; } + + @GetMapping("/exclude/{id}") + public String apiExclude(@PathVariable("id") Long id) { + return "Exclude " + id; + } } diff --git a/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/CommonFilterContextTest.java b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/CommonFilterContextTest.java new file mode 100644 index 0000000000..10c215d995 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/CommonFilterContextTest.java @@ -0,0 +1,100 @@ +/* + * 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.adapter.servletcontext; + +import com.alibaba.csp.sentinel.Constants; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.node.EntranceNode; +import com.alibaba.csp.sentinel.node.Node; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Collections; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author zhaoyuguang + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = TestContextApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +public class CommonFilterContextTest { + + private static final String HELLO_STR = "Hello!"; + + @Autowired + private MockMvc mvc; + + private void configureRulesFor(String resource, int count) { + configureRulesFor(resource, count, "default"); + } + + private void configureRulesFor(String resource, int count, String limitApp) { + FlowRule rule = new FlowRule() + .setCount(count) + .setGrade(RuleConstant.FLOW_GRADE_QPS); + rule.setResource(resource); + if (StringUtil.isNotBlank(limitApp)) { + rule.setLimitApp(limitApp); + } + FlowRuleManager.loadRules(Collections.singletonList(rule)); + } + + @Test + public void testCommonFilterMiscellaneous() throws Exception { + String url = "/hello"; + this.mvc.perform(get(url)) + .andExpect(status().isOk()) + .andExpect(content().string(HELLO_STR)); + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(url); + assertNotNull(cn); + assertEquals(1, cn.passQps(), 0.01); + String context = ""; + for (Node n : Constants.ROOT.getChildList()) { + if (n instanceof EntranceNode) { + String id = ((EntranceNode) n).getId().getName(); + if (url.equals(id)) { + context = ((EntranceNode) n).getId().getName(); + } + } + } + assertEquals(url, context); + } + + @After + public void cleanUp() { + FlowRuleManager.loadRules(null); + ClusterBuilderSlot.resetClusterNodes(); + } +} diff --git a/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/FilterContextConfig.java b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/FilterContextConfig.java new file mode 100644 index 0000000000..aff3a5db9d --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/FilterContextConfig.java @@ -0,0 +1,40 @@ +/* + * 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.adapter.servletcontext; + +import com.alibaba.csp.sentinel.adapter.servlet.CommonFilter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author zhaoyuguang + */ +@Configuration +public class FilterContextConfig { + + @Bean + public FilterRegistrationBean sentinelFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setFilter(new CommonFilter()); + registration.addUrlPatterns("/*"); + registration.addInitParameter(CommonFilter.WEB_CONTEXT_UNIFY, "false"); + registration.setName("sentinelFilter"); + registration.setOrder(1); + + return registration; + } +} diff --git a/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/TestContextApplication.java b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/TestContextApplication.java new file mode 100644 index 0000000000..fbf16c6ee7 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/TestContextApplication.java @@ -0,0 +1,30 @@ +/* + * 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.adapter.servletcontext; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author zhaoyuguang + */ +@SpringBootApplication +public class TestContextApplication { + + public static void main(String[] args) { + SpringApplication.run(TestContextApplication.class, args); + } +} diff --git a/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/TestContextController.java b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/TestContextController.java new file mode 100644 index 0000000000..39fc81a104 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/TestContextController.java @@ -0,0 +1,31 @@ +/* + * 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.adapter.servletcontext; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author zhaoyuguang + */ +@RestController +public class TestContextController { + + @GetMapping("/hello") + public String apiHello() { + return "Hello!"; + } +} diff --git a/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/CommonFilterMethodTest.java b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/CommonFilterMethodTest.java index 5d9a006a53..b220961032 100644 --- a/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/CommonFilterMethodTest.java +++ b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/CommonFilterMethodTest.java @@ -41,6 +41,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; /** + * @author zhaoyuguang * @author Roger Law */ @RunWith(SpringRunner.class) @@ -107,7 +108,7 @@ private void testCommonBlockAndRedirectBlockPage(String url, ClusterNode cnGet, configureRulesFor(GET + ":" + url, 0); // The request will be blocked and response is default block message. this.mvc.perform(get(url).accept(MediaType.TEXT_PLAIN)) - .andExpect(status().isOk()) + .andExpect(status().isTooManyRequests()) .andExpect(content().string(FilterUtil.DEFAULT_BLOCK_MSG)); assertEquals(1, cnGet.blockQps(), 0.01); diff --git a/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/FilterMethodConfig.java b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/FilterMethodConfig.java index d2c9d21fa0..5759a75977 100644 --- a/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/FilterMethodConfig.java +++ b/sentinel-adapter/sentinel-web-servlet/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/FilterMethodConfig.java @@ -16,7 +16,7 @@ public FilterRegistrationBean sentinelFilterRegistration() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(new CommonFilter()); registration.addUrlPatterns("/*"); - registration.addInitParameter("HTTP_METHOD_SPECIFY", "true"); + registration.addInitParameter(CommonFilter.HTTP_METHOD_SPECIFY, "true"); registration.setName("sentinelFilter"); registration.setOrder(1); diff --git a/sentinel-adapter/sentinel-zuul-adapter/pom.xml b/sentinel-adapter/sentinel-zuul-adapter/pom.xml index a00190ff24..6857cb8e8e 100755 --- a/sentinel-adapter/sentinel-zuul-adapter/pom.xml +++ b/sentinel-adapter/sentinel-zuul-adapter/pom.xml @@ -5,7 +5,7 @@ sentinel-adapter com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 sentinel-zuul-adapter diff --git a/sentinel-adapter/sentinel-zuul-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/zuul/RequestContextItemParser.java b/sentinel-adapter/sentinel-zuul-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/zuul/RequestContextItemParser.java index c1e081e8b1..aaa5bb2f76 100644 --- a/sentinel-adapter/sentinel-zuul-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/zuul/RequestContextItemParser.java +++ b/sentinel-adapter/sentinel-zuul-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/zuul/RequestContextItemParser.java @@ -15,6 +15,8 @@ */ package com.alibaba.csp.sentinel.adapter.gateway.zuul; +import javax.servlet.http.Cookie; + import com.alibaba.csp.sentinel.adapter.gateway.common.param.RequestItemParser; import com.netflix.zuul.context.RequestContext; @@ -44,4 +46,18 @@ public String getHeader(RequestContext requestContext, String headerKey) { public String getUrlParam(RequestContext requestContext, String paramName) { return requestContext.getRequest().getParameter(paramName); } + + @Override + public String getCookieValue(RequestContext requestContext, String cookieName) { + Cookie[] cookies = requestContext.getRequest().getCookies(); + if (cookies == null || cookieName == null) { + return null; + } + for (Cookie cookie : cookies) { + if (cookie != null && cookieName.equals(cookie.getName())) { + return cookie.getValue(); + } + } + return null; + } } diff --git a/sentinel-adapter/sentinel-zuul-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/zuul/api/matcher/RequestContextApiMatcher.java b/sentinel-adapter/sentinel-zuul-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/zuul/api/matcher/RequestContextApiMatcher.java index 90718db194..162874d4bb 100644 --- a/sentinel-adapter/sentinel-zuul-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/zuul/api/matcher/RequestContextApiMatcher.java +++ b/sentinel-adapter/sentinel-zuul-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/zuul/api/matcher/RequestContextApiMatcher.java @@ -61,9 +61,9 @@ private Predicate fromApiPathPredicate(/*@Valid*/ ApiPathPredica return null; } switch (item.getMatchStrategy()) { - case SentinelGatewayConstants.PARAM_MATCH_STRATEGY_REGEX: + case SentinelGatewayConstants.URL_MATCH_STRATEGY_REGEX: return ZuulRouteMatchers.regexPath(pattern); - case SentinelGatewayConstants.PARAM_MATCH_STRATEGY_PREFIX: + case SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX: return ZuulRouteMatchers.antPath(pattern); default: return ZuulRouteMatchers.exactPath(pattern); diff --git a/sentinel-adapter/sentinel-zuul-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/zuul/filters/EntryHolder.java b/sentinel-adapter/sentinel-zuul-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/zuul/filters/EntryHolder.java new file mode 100644 index 0000000000..c67708b5af --- /dev/null +++ b/sentinel-adapter/sentinel-zuul-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/zuul/filters/EntryHolder.java @@ -0,0 +1,41 @@ +/* + * Copyright 1999-2019 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 + * + * https://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.adapter.gateway.zuul.filters; + +import com.alibaba.csp.sentinel.Entry; + +/** + * @author wavesZh + */ +class EntryHolder { + + final private Entry entry; + + final private Object[] params; + + public EntryHolder(Entry entry, Object[] params) { + this.entry = entry; + this.params = params; + } + + public Entry getEntry() { + return entry; + } + + public Object[] getParams() { + return params; + } +} diff --git a/sentinel-adapter/sentinel-zuul-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/zuul/filters/SentinelEntryUtils.java b/sentinel-adapter/sentinel-zuul-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/zuul/filters/SentinelEntryUtils.java index 8c93a1b7ec..48e91e38fb 100644 --- a/sentinel-adapter/sentinel-zuul-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/zuul/filters/SentinelEntryUtils.java +++ b/sentinel-adapter/sentinel-zuul-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/zuul/filters/SentinelEntryUtils.java @@ -17,7 +17,7 @@ import java.util.Deque; -import com.alibaba.csp.sentinel.AsyncEntry; +import com.alibaba.csp.sentinel.Entry; import com.alibaba.csp.sentinel.Tracer; import com.alibaba.csp.sentinel.adapter.gateway.zuul.constants.ZuulConstant; import com.alibaba.csp.sentinel.context.ContextUtil; @@ -34,11 +34,11 @@ final class SentinelEntryUtils { static void tryExitFromCurrentContext() { RequestContext ctx = RequestContext.getCurrentContext(); if (ctx.containsKey(ZuulConstant.ZUUL_CTX_SENTINEL_ENTRIES_KEY)) { - Deque asyncEntries = (Deque) ctx.get(ZuulConstant.ZUUL_CTX_SENTINEL_ENTRIES_KEY); - AsyncEntry entry; - while (!asyncEntries.isEmpty()) { - entry = asyncEntries.pop(); - entry.exit(); + Deque holders = (Deque) ctx.get(ZuulConstant.ZUUL_CTX_SENTINEL_ENTRIES_KEY); + EntryHolder holder; + while (!holders.isEmpty()) { + holder = holders.pop(); + exit(holder); } ctx.remove(ZuulConstant.ZUUL_CTX_SENTINEL_ENTRIES_KEY); } @@ -50,17 +50,22 @@ static void tryExitFromCurrentContext() { static void tryTraceExceptionThenExitFromCurrentContext(Throwable t) { RequestContext ctx = RequestContext.getCurrentContext(); if (ctx.containsKey(ZuulConstant.ZUUL_CTX_SENTINEL_ENTRIES_KEY)) { - Deque asyncEntries = (Deque) ctx.get(ZuulConstant.ZUUL_CTX_SENTINEL_ENTRIES_KEY); - AsyncEntry entry; - while (!asyncEntries.isEmpty()) { - entry = asyncEntries.pop(); - Tracer.traceEntry(t, entry); - entry.exit(); + Deque holders = (Deque) ctx.get(ZuulConstant.ZUUL_CTX_SENTINEL_ENTRIES_KEY); + EntryHolder holder; + while (!holders.isEmpty()) { + holder = holders.pop(); + Tracer.traceEntry(t, holder.getEntry()); + exit(holder); } ctx.remove(ZuulConstant.ZUUL_CTX_SENTINEL_ENTRIES_KEY); } ContextUtil.exit(); } + static void exit(EntryHolder holder) { + Entry entry = holder.getEntry(); + entry.exit(1, holder.getParams()); + } + private SentinelEntryUtils() {} } diff --git a/sentinel-adapter/sentinel-zuul-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/zuul/filters/SentinelZuulPreFilter.java b/sentinel-adapter/sentinel-zuul-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/zuul/filters/SentinelZuulPreFilter.java index 39f85ff0dc..649ffa19d5 100644 --- a/sentinel-adapter/sentinel-zuul-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/zuul/filters/SentinelZuulPreFilter.java +++ b/sentinel-adapter/sentinel-zuul-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/zuul/filters/SentinelZuulPreFilter.java @@ -23,6 +23,7 @@ import com.alibaba.csp.sentinel.AsyncEntry; import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.ResourceTypeConstants; import com.alibaba.csp.sentinel.SphU; import com.alibaba.csp.sentinel.adapter.gateway.common.param.GatewayParamParser; import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule; @@ -88,7 +89,7 @@ public boolean shouldFilter() { } private void doSentinelEntry(String resourceName, final int resType, RequestContext requestContext, - Deque asyncEntries) throws BlockException { + Deque holders) throws BlockException { Object[] params = paramParser.parseParameterFor(resourceName, requestContext, new Predicate() { @Override @@ -96,8 +97,10 @@ public boolean test(GatewayFlowRule r) { return r.getResourceMode() == resType; } }); - AsyncEntry entry = SphU.asyncEntry(resourceName, EntryType.IN, 1, params); - asyncEntries.push(entry); + AsyncEntry entry = SphU.asyncEntry(resourceName, ResourceTypeConstants.COMMON_API_GATEWAY, + EntryType.IN, params); + EntryHolder holder = new EntryHolder(entry, params); + holders.push(holder); } @Override @@ -106,12 +109,12 @@ public Object run() throws ZuulException { String origin = parseOrigin(ctx.getRequest()); String routeId = (String)ctx.get(ZuulConstant.PROXY_ID_KEY); - Deque asyncEntries = new ArrayDeque<>(); + Deque holders = new ArrayDeque<>(); String fallBackRoute = routeId; try { if (StringUtil.isNotBlank(routeId)) { ContextUtil.enter(GATEWAY_CONTEXT_ROUTE_PREFIX + routeId, origin); - doSentinelEntry(routeId, RESOURCE_MODE_ROUTE_ID, ctx, asyncEntries); + doSentinelEntry(routeId, RESOURCE_MODE_ROUTE_ID, ctx, holders); } Set matchingApis = pickMatchingApiDefinitions(ctx); @@ -120,7 +123,7 @@ public Object run() throws ZuulException { } for (String apiName : matchingApis) { fallBackRoute = apiName; - doSentinelEntry(apiName, RESOURCE_MODE_CUSTOM_API_NAME, ctx, asyncEntries); + doSentinelEntry(apiName, RESOURCE_MODE_CUSTOM_API_NAME, ctx, holders); } } catch (BlockException ex) { ZuulBlockFallbackProvider zuulBlockFallbackProvider = ZuulBlockFallbackManager.getFallbackProvider( @@ -138,8 +141,8 @@ public Object run() throws ZuulException { } finally { // We don't exit the entry here. We need to exit the entries in post filter to record Rt correctly. // So here the entries will be carried in the request context. - if (!asyncEntries.isEmpty()) { - ctx.put(ZuulConstant.ZUUL_CTX_SENTINEL_ENTRIES_KEY, asyncEntries); + if (!holders.isEmpty()) { + ctx.put(ZuulConstant.ZUUL_CTX_SENTINEL_ENTRIES_KEY, holders); } } return null; diff --git a/sentinel-benchmark/pom.xml b/sentinel-benchmark/pom.xml index 7f7e9a88b1..50410f2dbd 100644 --- a/sentinel-benchmark/pom.xml +++ b/sentinel-benchmark/pom.xml @@ -5,7 +5,7 @@ sentinel-parent com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 diff --git a/sentinel-cluster/pom.xml b/sentinel-cluster/pom.xml index c58ee305bc..52170486a0 100644 --- a/sentinel-cluster/pom.xml +++ b/sentinel-cluster/pom.xml @@ -5,7 +5,7 @@ sentinel-parent com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 pom @@ -23,6 +23,7 @@ sentinel-cluster-client-default sentinel-cluster-server-default sentinel-cluster-common-default + sentinel-cluster-server-envoy-rls diff --git a/sentinel-cluster/sentinel-cluster-client-default/pom.xml b/sentinel-cluster/sentinel-cluster-client-default/pom.xml index 80bf32dffe..0775cb3196 100644 --- a/sentinel-cluster/sentinel-cluster-client-default/pom.xml +++ b/sentinel-cluster/sentinel-cluster-client-default/pom.xml @@ -5,7 +5,7 @@ sentinel-cluster com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 @@ -53,5 +53,10 @@ mockito-core test + + org.assertj + assertj-core + test + \ No newline at end of file diff --git a/sentinel-cluster/sentinel-cluster-client-default/src/main/java/com/alibaba/csp/sentinel/cluster/client/codec/data/ParamFlowRequestDataWriter.java b/sentinel-cluster/sentinel-cluster-client-default/src/main/java/com/alibaba/csp/sentinel/cluster/client/codec/data/ParamFlowRequestDataWriter.java index 657223e952..8dc51a5eec 100644 --- a/sentinel-cluster/sentinel-cluster-client-default/src/main/java/com/alibaba/csp/sentinel/cluster/client/codec/data/ParamFlowRequestDataWriter.java +++ b/sentinel-cluster/sentinel-cluster-client-default/src/main/java/com/alibaba/csp/sentinel/cluster/client/codec/data/ParamFlowRequestDataWriter.java @@ -15,16 +15,17 @@ */ package com.alibaba.csp.sentinel.cluster.client.codec.data; -import java.util.Collection; - import com.alibaba.csp.sentinel.cluster.ClusterConstants; import com.alibaba.csp.sentinel.cluster.codec.EntityWriter; import com.alibaba.csp.sentinel.cluster.request.data.ParamFlowRequestData; import com.alibaba.csp.sentinel.log.RecordLog; import com.alibaba.csp.sentinel.util.AssertUtil; - import io.netty.buffer.ByteBuf; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + /** * @author jialiang.linjl * @author Eric Zhao @@ -50,41 +51,67 @@ public void writeTo(ParamFlowRequestData entity, ByteBuf target) { Collection params = entity.getParams(); - // Write parameter amount. - int amount = calculateParamAmount(params); - target.writeInt(amount); + params = resolveValidParams(params); + target.writeInt(params.size()); // Serialize parameters with type flag. - for (Object param : entity.getParams()) { + for (Object param : params) { encodeValue(param, target); } } + /** + * Get valid parameters in provided parameter list + * + * @param params + * @return + */ + public List resolveValidParams(Collection params) { + List validParams = new ArrayList<>(); + int size = 0; + for (Object param : params) { + int s = calculateParamTransportSize(param); + if (s <= 0) { + RecordLog.warn("[ParamFlowRequestDataWriter] WARN: Non-primitive type detected in params of " + + "cluster parameter flow control, which is not supported: " + param); + continue; + } + if (size + s > maxParamByteSize) { + RecordLog.warn("[ParamFlowRequestDataWriter] WARN: params size is too big." + + " the configure value is : " + maxParamByteSize + ", the params size is: " + params.size()); + break; + } + size += s; + validParams.add(param); + } + return validParams; + } + private void encodeValue(Object param, ByteBuf target) { // Handle primitive type. if (param instanceof Integer || int.class.isInstance(param)) { target.writeByte(ClusterConstants.PARAM_TYPE_INTEGER); - target.writeInt((Integer)param); + target.writeInt((Integer) param); } else if (param instanceof String) { - encodeString((String)param, target); + encodeString((String) param, target); } else if (boolean.class.isInstance(param) || param instanceof Boolean) { target.writeByte(ClusterConstants.PARAM_TYPE_BOOLEAN); - target.writeBoolean((Boolean)param); + target.writeBoolean((Boolean) param); } else if (long.class.isInstance(param) || param instanceof Long) { target.writeByte(ClusterConstants.PARAM_TYPE_LONG); - target.writeLong((Long)param); + target.writeLong((Long) param); } else if (double.class.isInstance(param) || param instanceof Double) { target.writeByte(ClusterConstants.PARAM_TYPE_DOUBLE); - target.writeDouble((Double)param); + target.writeDouble((Double) param); } else if (float.class.isInstance(param) || param instanceof Float) { target.writeByte(ClusterConstants.PARAM_TYPE_FLOAT); - target.writeFloat((Float)param); + target.writeFloat((Float) param); } else if (byte.class.isInstance(param) || param instanceof Byte) { target.writeByte(ClusterConstants.PARAM_TYPE_BYTE); - target.writeByte((Byte)param); + target.writeByte((Byte) param); } else if (short.class.isInstance(param) || param instanceof Short) { target.writeByte(ClusterConstants.PARAM_TYPE_SHORT); - target.writeShort((Short)param); + target.writeShort((Short) param); } else { // Unexpected type, drop. } @@ -97,30 +124,6 @@ private void encodeString(String param, ByteBuf target) { target.writeBytes(tmpChars); } - /** - * Calculate amount of valid parameters in provided parameter list. - * - * @param params non-empty parameter list - * @return amount of valid parameters - */ - int calculateParamAmount(/*@NonEmpty*/ Collection params) { - int size = 0; - int length = 0; - for (Object param : params) { - int s = calculateParamTransportSize(param); - if (s <= 0) { - RecordLog.warn("[ParamFlowRequestDataWriter] WARN: Non-primitive type detected in params of " - + "cluster parameter flow control, which is not supported: " + param); - continue; - } - if (size + s > maxParamByteSize) { - break; - } - size += s; - length++; - } - return length; - } int calculateParamTransportSize(Object value) { if (value == null) { @@ -132,7 +135,7 @@ int calculateParamTransportSize(Object value) { return 5; } else if (value instanceof String) { // Layout for string: |type flag(1)|length(4)|string content| - String tmpValue = (String)value; + String tmpValue = (String) value; byte[] tmpChars = tmpValue.getBytes(); return 1 + 4 + tmpChars.length; } else if (boolean.class.isInstance(value) || value instanceof Boolean) { diff --git a/sentinel-cluster/sentinel-cluster-client-default/src/main/java/com/alibaba/csp/sentinel/cluster/client/codec/data/PingResponseDataDecoder.java b/sentinel-cluster/sentinel-cluster-client-default/src/main/java/com/alibaba/csp/sentinel/cluster/client/codec/data/PingResponseDataDecoder.java index e12395f0de..73c4a62bcb 100644 --- a/sentinel-cluster/sentinel-cluster-client-default/src/main/java/com/alibaba/csp/sentinel/cluster/client/codec/data/PingResponseDataDecoder.java +++ b/sentinel-cluster/sentinel-cluster-client-default/src/main/java/com/alibaba/csp/sentinel/cluster/client/codec/data/PingResponseDataDecoder.java @@ -27,9 +27,14 @@ public class PingResponseDataDecoder implements EntityDecoder @Override public Integer decode(ByteBuf source) { - if (source.readableBytes() >= 1) { + int size = source.readableBytes(); + if (size == 1) { + // Compatible with old version (< 1.7.0). return (int) source.readByte(); } + if (size >= 4) { + return source.readInt(); + } return -1; } } diff --git a/sentinel-cluster/sentinel-cluster-client-default/src/main/java/com/alibaba/csp/sentinel/cluster/client/config/ClusterClientStartUpConfig.java b/sentinel-cluster/sentinel-cluster-client-default/src/main/java/com/alibaba/csp/sentinel/cluster/client/config/ClusterClientStartUpConfig.java new file mode 100644 index 0000000000..8da2813a1c --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-client-default/src/main/java/com/alibaba/csp/sentinel/cluster/client/config/ClusterClientStartUpConfig.java @@ -0,0 +1,49 @@ +/* + * 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.cluster.client.config; + +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.log.RecordLog; + +/** + *

+ * this class dedicated to reading startup configurations of cluster client + *

+ * + * @author lianglin + * @since 1.7.0 + */ +public class ClusterClientStartUpConfig { + + private static final String MAX_PARAM_BYTE_SIZE = "csp.sentinel.cluster.max.param.byte.size"; + + /** + * Get the max bytes params can be serialized + * + * @return the max bytes, may be null + */ + public static Integer getMaxParamByteSize() { + String maxParamByteSize = SentinelConfig.getConfig(MAX_PARAM_BYTE_SIZE); + try { + return maxParamByteSize == null ? null : Integer.valueOf(maxParamByteSize); + } catch (Exception ex) { + RecordLog.warn("[ClusterClientStartUpConfig] Failed to parse maxParamByteSize: " + maxParamByteSize); + return null; + } + } + + +} diff --git a/sentinel-cluster/sentinel-cluster-client-default/src/main/java/com/alibaba/csp/sentinel/cluster/client/init/DefaultClusterClientInitFunc.java b/sentinel-cluster/sentinel-cluster-client-default/src/main/java/com/alibaba/csp/sentinel/cluster/client/init/DefaultClusterClientInitFunc.java index d5851bc01f..5b9467c7d5 100644 --- a/sentinel-cluster/sentinel-cluster-client-default/src/main/java/com/alibaba/csp/sentinel/cluster/client/init/DefaultClusterClientInitFunc.java +++ b/sentinel-cluster/sentinel-cluster-client-default/src/main/java/com/alibaba/csp/sentinel/cluster/client/init/DefaultClusterClientInitFunc.java @@ -23,6 +23,7 @@ import com.alibaba.csp.sentinel.cluster.client.codec.data.PingResponseDataDecoder; import com.alibaba.csp.sentinel.cluster.client.codec.registry.RequestDataWriterRegistry; import com.alibaba.csp.sentinel.cluster.client.codec.registry.ResponseDataDecodeRegistry; +import com.alibaba.csp.sentinel.cluster.client.config.ClusterClientStartUpConfig; import com.alibaba.csp.sentinel.init.InitFunc; import com.alibaba.csp.sentinel.init.InitOrder; @@ -42,7 +43,12 @@ public void init() throws Exception { private void initDefaultEntityWriters() { RequestDataWriterRegistry.addWriter(ClientConstants.TYPE_PING, new PingRequestDataWriter()); RequestDataWriterRegistry.addWriter(ClientConstants.TYPE_FLOW, new FlowRequestDataWriter()); - RequestDataWriterRegistry.addWriter(ClientConstants.TYPE_PARAM_FLOW, new ParamFlowRequestDataWriter()); + Integer maxParamByteSize = ClusterClientStartUpConfig.getMaxParamByteSize(); + if (maxParamByteSize == null) { + RequestDataWriterRegistry.addWriter(ClientConstants.TYPE_PARAM_FLOW, new ParamFlowRequestDataWriter()); + } else { + RequestDataWriterRegistry.addWriter(ClientConstants.TYPE_PARAM_FLOW, new ParamFlowRequestDataWriter(maxParamByteSize)); + } } private void initDefaultEntityDecoders() { diff --git a/sentinel-cluster/sentinel-cluster-client-default/src/test/java/com/alibaba/csp/sentinel/cluster/client/codec/data/ParamFlowRequestDataWriterTest.java b/sentinel-cluster/sentinel-cluster-client-default/src/test/java/com/alibaba/csp/sentinel/cluster/client/codec/data/ParamFlowRequestDataWriterTest.java index 314020203c..de74dd8b8a 100644 --- a/sentinel-cluster/sentinel-cluster-client-default/src/test/java/com/alibaba/csp/sentinel/cluster/client/codec/data/ParamFlowRequestDataWriterTest.java +++ b/sentinel-cluster/sentinel-cluster-client-default/src/test/java/com/alibaba/csp/sentinel/cluster/client/codec/data/ParamFlowRequestDataWriterTest.java @@ -1,9 +1,10 @@ package com.alibaba.csp.sentinel.cluster.client.codec.data; -import java.util.ArrayList; - import org.junit.Test; +import java.util.ArrayList; +import java.util.List; + import static org.junit.Assert.*; /** @@ -27,35 +28,33 @@ public void testCalculateParamTransportSize() { } @Test - public void testCalculateParamAmountExceedsMaxSize() { - final int maxSize = 10; + public void testResolveValidParams() { + + final int maxSize = 15; ParamFlowRequestDataWriter writer = new ParamFlowRequestDataWriter(maxSize); - assertEquals(1, writer.calculateParamAmount(new ArrayList() {{ + + ArrayList params = new ArrayList() {{ add(1); - }})); - assertEquals(2, writer.calculateParamAmount(new ArrayList() {{ - add(1); add(64); - }})); - assertEquals(2, writer.calculateParamAmount(new ArrayList() {{ - add(1); add(64); add(3); - }})); - } + add(64); + add(3); + }}; - @Test - public void testCalculateParamAmount() { - ParamFlowRequestDataWriter writer = new ParamFlowRequestDataWriter(); - assertEquals(6, writer.calculateParamAmount(new ArrayList() {{ - add(1); add(1d); add(1f); add((byte) 1); add("123"); add(true); - }})); - // POJO (non-primitive type) should not be regarded as a valid parameter. - assertEquals(0, writer.calculateParamAmount(new ArrayList() {{ + List validParams = writer.resolveValidParams(params); + assertTrue(validParams.contains(1) && validParams.contains(64) && validParams.contains(3)); + + //when over maxSize, the exceed number should not be contained + params.add(5); + assertFalse(writer.resolveValidParams(params).contains(5)); + + + //POJO (non-primitive type) should not be regarded as a valid parameter + assertTrue(writer.resolveValidParams(new ArrayList() {{ add(new SomePojo()); - }})); - assertEquals(1, writer.calculateParamAmount(new ArrayList() {{ - add(new Object()); add(1); - }})); + }}).size() == 0); + } + private static class SomePojo { private String param1; diff --git a/sentinel-cluster/sentinel-cluster-client-default/src/test/java/com/alibaba/csp/sentinel/cluster/client/codec/data/PingResponseDataDecoderTest.java b/sentinel-cluster/sentinel-cluster-client-default/src/test/java/com/alibaba/csp/sentinel/cluster/client/codec/data/PingResponseDataDecoderTest.java new file mode 100644 index 0000000000..f2b04ec662 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-client-default/src/test/java/com/alibaba/csp/sentinel/cluster/client/codec/data/PingResponseDataDecoderTest.java @@ -0,0 +1,44 @@ +/* + * 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.cluster.client.codec.data; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Eric Zhao + */ +public class PingResponseDataDecoderTest { + + @Test + public void testDecodePingResponseData() { + ByteBuf buf = Unpooled.buffer(); + PingResponseDataDecoder decoder = new PingResponseDataDecoder(); + + int big = Integer.MAX_VALUE; + buf.writeInt(big); + assertThat(decoder.decode(buf)).isEqualTo(big); + + byte small = 12; + buf.writeByte(small); + assertThat(decoder.decode(buf)).isEqualTo(small); + + buf.release(); + } +} diff --git a/sentinel-cluster/sentinel-cluster-common-default/pom.xml b/sentinel-cluster/sentinel-cluster-common-default/pom.xml index cc1a988060..59549847b3 100644 --- a/sentinel-cluster/sentinel-cluster-common-default/pom.xml +++ b/sentinel-cluster/sentinel-cluster-common-default/pom.xml @@ -5,7 +5,7 @@ sentinel-cluster com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 diff --git a/sentinel-cluster/sentinel-cluster-server-default/pom.xml b/sentinel-cluster/sentinel-cluster-server-default/pom.xml index 6522b710fb..6443bab92d 100644 --- a/sentinel-cluster/sentinel-cluster-server-default/pom.xml +++ b/sentinel-cluster/sentinel-cluster-server-default/pom.xml @@ -5,7 +5,7 @@ sentinel-cluster com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 @@ -52,5 +52,10 @@ mockito-core test + + org.assertj + assertj-core + test + \ No newline at end of file diff --git a/sentinel-cluster/sentinel-cluster-server-default/src/main/java/com/alibaba/csp/sentinel/cluster/flow/ClusterFlowChecker.java b/sentinel-cluster/sentinel-cluster-server-default/src/main/java/com/alibaba/csp/sentinel/cluster/flow/ClusterFlowChecker.java index ab1147999d..df69bef9dc 100644 --- a/sentinel-cluster/sentinel-cluster-server-default/src/main/java/com/alibaba/csp/sentinel/cluster/flow/ClusterFlowChecker.java +++ b/sentinel-cluster/sentinel-cluster-server-default/src/main/java/com/alibaba/csp/sentinel/cluster/flow/ClusterFlowChecker.java @@ -64,7 +64,7 @@ static TokenResult acquireClusterToken(/*@Valid*/ FlowRule rule, int acquireCoun return new TokenResult(TokenResultStatus.FAIL); } - double latestQps = metric.getAvg(ClusterFlowEvent.PASS_REQUEST); + double latestQps = metric.getAvg(ClusterFlowEvent.PASS); double globalThreshold = calcGlobalThreshold(rule) * ClusterServerConfigManager.getExceedCount(); double nextRemaining = globalThreshold - latestQps - acquireCount; diff --git a/sentinel-cluster/sentinel-cluster-server-default/src/main/java/com/alibaba/csp/sentinel/cluster/server/TokenServiceProvider.java b/sentinel-cluster/sentinel-cluster-server-default/src/main/java/com/alibaba/csp/sentinel/cluster/server/TokenServiceProvider.java index 969e540037..1f9cd1c785 100644 --- a/sentinel-cluster/sentinel-cluster-server-default/src/main/java/com/alibaba/csp/sentinel/cluster/server/TokenServiceProvider.java +++ b/sentinel-cluster/sentinel-cluster-server-default/src/main/java/com/alibaba/csp/sentinel/cluster/server/TokenServiceProvider.java @@ -15,13 +15,10 @@ */ package com.alibaba.csp.sentinel.cluster.server; -import java.util.ArrayList; -import java.util.List; -import java.util.ServiceLoader; - import com.alibaba.csp.sentinel.cluster.TokenService; import com.alibaba.csp.sentinel.cluster.flow.DefaultTokenService; import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.util.SpiLoader; /** * @author Eric Zhao @@ -31,8 +28,6 @@ public final class TokenServiceProvider { private static TokenService service = null; - private static final ServiceLoader LOADER = ServiceLoader.load(TokenService.class); - static { resolveTokenServiceSpi(); } @@ -42,24 +37,12 @@ public static TokenService getService() { } private static void resolveTokenServiceSpi() { - boolean hasOther = false; - List list = new ArrayList(); - for (TokenService service : LOADER) { - if (service.getClass() != DefaultTokenService.class) { - hasOther = true; - list.add(service); - } - } - - if (hasOther) { - // Pick the first. - service = list.get(0); + service = SpiLoader.loadFirstInstanceOrDefault(TokenService.class, DefaultTokenService.class); + if (service != null) { + RecordLog.info("[TokenServiceProvider] Global token service resolved: " + + service.getClass().getCanonicalName()); } else { - // No custom token service, using default. - service = new DefaultTokenService(); + RecordLog.warn("[TokenServiceProvider] Unable to resolve TokenService: no SPI found"); } - - RecordLog.info("[TokenServiceProvider] Global token service resolved: " - + service.getClass().getCanonicalName()); } } diff --git a/sentinel-cluster/sentinel-cluster-server-default/src/main/java/com/alibaba/csp/sentinel/cluster/server/codec/data/PingResponseDataWriter.java b/sentinel-cluster/sentinel-cluster-server-default/src/main/java/com/alibaba/csp/sentinel/cluster/server/codec/data/PingResponseDataWriter.java index 26e31ecd2f..f380b61dd3 100644 --- a/sentinel-cluster/sentinel-cluster-server-default/src/main/java/com/alibaba/csp/sentinel/cluster/server/codec/data/PingResponseDataWriter.java +++ b/sentinel-cluster/sentinel-cluster-server-default/src/main/java/com/alibaba/csp/sentinel/cluster/server/codec/data/PingResponseDataWriter.java @@ -31,6 +31,6 @@ public void writeTo(Integer entity, ByteBuf target) { if (entity == null || target == null) { return; } - target.writeByte(entity); + target.writeInt(entity); } } diff --git a/sentinel-cluster/sentinel-cluster-server-default/src/main/java/com/alibaba/csp/sentinel/cluster/server/processor/RequestProcessorProvider.java b/sentinel-cluster/sentinel-cluster-server-default/src/main/java/com/alibaba/csp/sentinel/cluster/server/processor/RequestProcessorProvider.java index bd572f27c5..ca1bd87d52 100644 --- a/sentinel-cluster/sentinel-cluster-server-default/src/main/java/com/alibaba/csp/sentinel/cluster/server/processor/RequestProcessorProvider.java +++ b/sentinel-cluster/sentinel-cluster-server-default/src/main/java/com/alibaba/csp/sentinel/cluster/server/processor/RequestProcessorProvider.java @@ -20,6 +20,7 @@ import java.util.concurrent.ConcurrentHashMap; import com.alibaba.csp.sentinel.cluster.annotation.RequestType; +import com.alibaba.csp.sentinel.spi.ServiceLoaderUtil; import com.alibaba.csp.sentinel.util.AssertUtil; /** @@ -30,7 +31,8 @@ public final class RequestProcessorProvider { private static final Map PROCESSOR_MAP = new ConcurrentHashMap<>(); - private static final ServiceLoader SERVICE_LOADER = ServiceLoader.load(RequestProcessor.class); + private static final ServiceLoader SERVICE_LOADER = ServiceLoaderUtil.getServiceLoader( + RequestProcessor.class); static { loadAndInit(); @@ -71,6 +73,5 @@ static void addProcessor(int type, RequestProcessor processor) { PROCESSOR_MAP.put(type, processor); } - private RequestProcessorProvider() {} } diff --git a/sentinel-cluster/sentinel-cluster-server-default/src/test/java/com/alibaba/csp/sentinel/cluster/server/codec/data/PingResponseDataWriterTest.java b/sentinel-cluster/sentinel-cluster-server-default/src/test/java/com/alibaba/csp/sentinel/cluster/server/codec/data/PingResponseDataWriterTest.java new file mode 100644 index 0000000000..96191cfa15 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-default/src/test/java/com/alibaba/csp/sentinel/cluster/server/codec/data/PingResponseDataWriterTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 1999-2019 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.cluster.server.codec.data; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test cases for {@link PingResponseDataWriter}. + * + * @author Eric Zhao + */ +public class PingResponseDataWriterTest { + + @Test + public void testWritePingResponseAndParse() { + ByteBuf buf = Unpooled.buffer(); + PingResponseDataWriter writer = new PingResponseDataWriter(); + + int small = 120; + writer.writeTo(small, buf); + assertThat(buf.readableBytes()).isGreaterThanOrEqualTo(4); + assertThat(buf.readInt()).isEqualTo(small); + + int big = Integer.MAX_VALUE; + writer.writeTo(big, buf); + assertThat(buf.readableBytes()).isGreaterThanOrEqualTo(4); + assertThat(buf.readInt()).isEqualTo(big); + + buf.release(); + } +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/Dockerfile b/sentinel-cluster/sentinel-cluster-server-envoy-rls/Dockerfile new file mode 100644 index 0000000000..7a1f24408b --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/Dockerfile @@ -0,0 +1,10 @@ +FROM java:8-jre + +ENV SENTINEL_HOME /sentinel + +COPY target/sentinel-envoy-rls-token-server.jar $SENTINEL_HOME/ + +WORKDIR $SENTINEL_HOME + +ENTRYPOINT ["sh", "-c"] +CMD ["java -Dcsp.sentinel.log.dir=/sentinel/logs/ ${JAVA_OPTS} -jar sentinel-envoy-rls-token-server.jar"] \ No newline at end of file diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/README.md b/sentinel-cluster/sentinel-cluster-server-envoy-rls/README.md new file mode 100644 index 0000000000..e610043686 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/README.md @@ -0,0 +1,59 @@ +# Sentinel Token Server (Envoy RLS implementation) + +This module provides the [Envoy rate limiting gRPC service](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/global_rate_limiting#arch-overview-rate-limit) implementation +with Sentinel token server. + +> Note: the gRPC stub classes for Envoy RLS service is generated via `protobuf-maven-plugin` during the `compile` goal. +> The generated classes is located in the directory: `target/generated-sources/protobuf`. + +## Build + +Build the executable jar: + +```bash +mvn clean package -P prod +``` + +## Rule configuration + +Currently Sentinel RLS token server supports dynamic rule configuration via the yaml file. +The file may provide rules for one *domain* (defined in Envoy's conf file). +In Envoy, one rate limit request might carry multiple *rate limit descriptors* +(which will be generated from [Envoy rate limit actions](https://www.envoyproxy.io/docs/envoy/latest/api-v2/api/v2/route/route.proto#envoy-api-msg-route-ratelimit)). +One rate limit descriptor may have multiple entries (key-value pair). +We may set different threshold for each rate limit descriptors. + +A sample rule configuration file: + +```yaml +domain: foo +descriptors: + - resources: + - key: "destination_cluster" + value: "service_httpbin" + count: 1 +``` + +This rule only takes effect for domain `foo`. It will limit the max QPS to 1 for +all requests targeted to the `service_httpbin` cluster. + +We need to provide the path to yaml file via the `SENTINEL_RLS_RULE_FILE_PATH` env +(or `-Dcsp.sentinel.rls.rule.file` opts). Then as soon as the content in the rule file has been changed, +Sentinel will reload the new rules from the file to the `EnvoyRlsRuleManager`. + +We may check the logs in `~/logs/csp/sentinel-record.log.xxx` to see whether the rules has been loaded. +We may also retrieve the converted `FlowRule` via the command API `localhost:8719/cluster/server/flowRules`. + +## Configuration items + +The configuration list: + +| Item (env) | Item (JVM property) | Description | Default Value | Required | +|--------|--------|--------|--------|--------| +| `SENTINEL_RLS_GRPC_PORT` | `csp.sentinel.grpc.server.port` | The RLS gRPC server port | **10240** | false | +| `SENTINEL_RLS_RULE_FILE_PATH` | `csp.sentinel.rls.rule.file` | The path of the RLS rule yaml file | - | **true** | +| `SENTINEL_RLS_ACCESS_LOG` | - | Whether to enable the access log (`on` for enable) | off | false | + +## Samples + +- [Kubernetes sample](./sample/k8s) diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/pom.xml b/sentinel-cluster/sentinel-cluster-server-envoy-rls/pom.xml new file mode 100644 index 0000000000..3c84641c79 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/pom.xml @@ -0,0 +1,156 @@ + + + + sentinel-cluster + com.alibaba.csp + 1.7.2-SNAPSHOT + + 4.0.0 + + sentinel-cluster-server-envoy-rls + + + 1.8 + 1.8 + + 3.10.0 + 1.24.0 + + 3.2.1 + + + + + com.alibaba.csp + sentinel-cluster-server-default + ${project.version} + + + com.alibaba.csp + sentinel-datasource-extension + + + com.alibaba.csp + sentinel-transport-simple-http + + + + io.grpc + grpc-netty + ${grpc.version} + + + io.grpc + grpc-protobuf + ${grpc.version} + + + io.grpc + grpc-stub + ${grpc.version} + + + com.google.protobuf + protobuf-java + ${protobuf.version} + + + + org.yaml + snakeyaml + 1.25 + + + + junit + junit + test + + + org.mockito + mockito-core + test + + + + + + + kr.motd.maven + os-maven-plugin + 1.6.2 + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + 0.6.1 + + com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} + + grpc-java + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + + + + + + compile + compile-custom + + + + + + + org.apache.maven.plugins + maven-pmd-plugin + ${maven.pmd.version} + + + target/generated-sources + + + + + + + + + prod + + + + org.apache.maven.plugins + maven-shade-plugin + ${maven.shade.version} + + + package + + shade + + + sentinel-envoy-rls-token-server + + + + com.alibaba.csp.sentinel.cluster.server.envoy.rls.SentinelEnvoyRlsServer + + + + + + + + + + + + + \ No newline at end of file diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/sample/k8s/README.md b/sentinel-cluster/sentinel-cluster-server-envoy-rls/sample/k8s/README.md new file mode 100644 index 0000000000..3fd58ba887 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/sample/k8s/README.md @@ -0,0 +1,105 @@ +# Sentinel Envoy RLS - Kubernetes sample + +This sample will illustrate how to use Sentinel RLS token server with Envoy in Kubernetes clusters. + +## Build the Docker image + +We could use the pre-built Docker image: `registry.cn-hangzhou.aliyuncs.com/sentinel-docker-repo/sentinel-envoy-rls-server:latest` + +We can also manually build the Docker image in the `sentinel-cluster-server-envoy-rls` directory: + +```bash +docker build -t "sentinel/sentinel-envoy-rls-server:latest" -f ./Dockerfile . +``` + +## Deploy Sentinel RLS token server + +Next we could deploy the Sentinel RLS token server in the K8S cluster. +We've provided a deployment template for Sentinel RLS token server in `sentinel-rls.yml`. +It includes: + +- A `ConfigMap` that contains the cluster flow control rule for Envoy global rate limiting. + This will be mounted as a file in the target `Deployment`, so that the Sentinel RLS token server + could load the rules dynamically as soon as the rule in the `ConfigMap` has been updated. +- A `Deployment` for Sentinel RLS token server. It will mount the `ConfigMap` as a volume + for dynamic rule configuration. +- A `Service` that exports the Sentinel command port (8719) and the RLS gRPC port (by default 10245) + on a cluster-internal IP so that the Envoy pods could communicate with the RLS server. + +The sample rate limiting rule in the `sentinel-rule-cm`: + +```yaml +domain: foo +descriptors: + # For requests to the "service_httpbin" cluster, limit the max QPS to 1 + - resources: + - key: "destination_cluster" + value: "service_httpbin" + count: 1 +``` + +You may enable the access log in the Sentinel RLS token server (output to console) +via the `SENTINEL_RLS_ACCESS_LOG` env: + +```yaml +env: + - name: SENTINEL_RLS_ACCESS_LOG + value: "on" +``` + +You may also append JVM opts via the `JAVA_OPTS` env. + +After preparing the yaml template, you may deploy the Sentinel RLS token server: + +```bash +kubectl apply -f sample/k8s/sentinel-rls.yml +``` + +## Deploy Envoy + +Next we could deploy the Envoy instances in the K8S cluster. If you've already had Envoy instances running, +you could configure the address (`sentinel-rls-service`) and the port (`10245`) +of the rate limit cluster in your Envoy configuration. + +We've provided a deployment template for Envoy in `envoy.yml`. +It includes: + +- A `ConfigMap` that contains the configuration for Envoy. + This will be mounted as a file in the target `Deployment`, which will be loaded as the configuration + file by Envoy. +- A `Deployment` for Envoy. It will mount the `ConfigMap` as a volume + for configuration. +- A `Service` that exports the Envoy endpoint port (by default 10000) on a cluster-internal IP + so that it could be accessible from other pods. If you need external access, you could choose the + `LoadBalancer` type or add a frontend ingress. + +In the sample, we have two [Envoy clusters](https://www.envoyproxy.io/docs/envoy/latest/api-v2/clusters/clusters): + +- `service_httpbin`: HTTP proxy to `httpbin.org` +- `rate_limit_cluster`: the cluster of the Sentinel RLS token server + +This route configuration tells Envoy to route incoming requests to `httpbin.org`. In the `http_filters` conf item, +we added the `envoy.rate_limit` filter to the filter chain so that the global rate limiting is enabled. +We set the rate limit domain as `foo`, which matches the item in the Envoy RLS rule. +In the `route_config`, we provide the rate limit action: `{destination_cluster: {}}`, which will +generate the rate limit descriptor containing the actual target cluster name (e.g. `service_httpbin`). +Then we could set rate limit rules for each target clusters. + +After preparing the yaml template, you may deploy the Envoy instance: + +```bash +kubectl apply -f sample/k8s/envoy.yml +``` + +## Test the rate limiting + +Now it's show time! We could visit the URL `envoy-service:10000/json` in K8S pods. +Since we set the max QPS to 1, we could emit concurrent requests to the URL, and we +could see the first request passes, while the latter requests are blocked (status 429): + +![image](https://user-images.githubusercontent.com/9434884/68571798-d0a46500-049e-11ea-8488-5e90f56f23a5.png) + +## Update the rules dynamically + +You could update the rules in the `sentinel-rule-cm` ConfigMap. Once the content is updated, +Sentinel will perceive the changes and load the new rules to `EnvoyRlsRuleManager`. diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/sample/k8s/envoy.yml b/sentinel-cluster/sentinel-cluster-server-envoy-rls/sample/k8s/envoy.yml new file mode 100644 index 0000000000..b732615804 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/sample/k8s/envoy.yml @@ -0,0 +1,132 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: envoy-cm +data: + envoy-yml: |- + admin: + access_log_path: /tmp/admin_access.log + address: + socket_address: + protocol: TCP + address: 127.0.0.1 + port_value: 9901 + static_resources: + listeners: + - name: listener_0 + address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 10000 + filter_chains: + - filters: + - name: envoy.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: ["*"] + routes: + - match: + prefix: "/" + route: + host_rewrite: httpbin.org + cluster: service_httpbin + rate_limits: + - stage: 0 + actions: + - {destination_cluster: {}} + http_filters: + - name: envoy.rate_limit + config: + domain: foo + stage: 0 + rate_limit_service: + grpc_service: + envoy_grpc: + cluster_name: rate_limit_cluster + timeout: 0.25s + - name: envoy.router + clusters: + - name: service_httpbin + connect_timeout: 0.5s + type: LOGICAL_DNS + # Comment out the following line to test on v6 networks + dns_lookup_family: V4_ONLY + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: service_httpbin + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: httpbin.org + port_value: 80 + - name: rate_limit_cluster + type: LOGICAL_DNS + connect_timeout: 0.25s + lb_policy: ROUND_ROBIN + http2_protocol_options: {} + load_assignment: + cluster_name: rate_limit_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: sentinel-rls-service + port_value: 10245 +--- +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: envoy-deployment-basic + labels: + app: envoy +spec: + replicas: 1 + selector: + matchLabels: + app: envoy + template: + metadata: + labels: + app: envoy + spec: + containers: + - name: envoy + image: envoyproxy/envoy:v1.12.0 + ports: + - containerPort: 10000 + command: ["envoy"] + args: ["-c", "/tmp/envoy/envoy.yaml"] + volumeMounts: + - name: envoy-config + mountPath: /tmp/envoy + volumes: + - name: envoy-config + configMap: + name: envoy-cm + items: + - key: envoy-yml + path: envoy.yaml +--- +apiVersion: v1 +kind: Service +metadata: + name: envoy-service + labels: + name: envoy-service +spec: + type: ClusterIP + ports: + - port: 10000 + targetPort: 10000 + protocol: TCP + selector: + app: envoy \ No newline at end of file diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/sample/k8s/sentinel-rls.yml b/sentinel-cluster/sentinel-cluster-server-envoy-rls/sample/k8s/sentinel-rls.yml new file mode 100644 index 0000000000..1023932e37 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/sample/k8s/sentinel-rls.yml @@ -0,0 +1,68 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: sentinel-rule-cm +data: + rule-yaml: |- + domain: foo + descriptors: + - resources: + - key: "destination_cluster" + value: "service_httpbin" + count: 1 +--- +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: sentinel-rls-server + labels: + app: sentinel +spec: + replicas: 1 + selector: + matchLabels: + app: sentinel + template: + metadata: + labels: + app: sentinel + spec: + containers: + - name: sentinelserver + # You could replace the image with your own image here + image: "registry.cn-hangzhou.aliyuncs.com/sentinel-docker-repo/sentinel-envoy-rls-server:latest" + imagePullPolicy: Always + ports: + - containerPort: 10245 + - containerPort: 8719 + volumeMounts: + - name: sentinel-rule-config + mountPath: /tmp/sentinel + env: + - name: SENTINEL_RLS_RULE_FILE_PATH + value: "/tmp/sentinel/rule.yaml" + volumes: + - name: sentinel-rule-config + configMap: + name: sentinel-rule-cm + items: + - key: rule-yaml + path: rule.yaml +--- +apiVersion: v1 +kind: Service +metadata: + name: sentinel-rls-service + labels: + name: sentinel-rls-service +spec: + type: ClusterIP + ports: + - port: 8719 + targetPort: 8719 + name: sentinel-command + - port: 10245 + targetPort: 10245 + name: sentinel-grpc + selector: + app: sentinel \ No newline at end of file diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsConstants.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsConstants.java new file mode 100644 index 0000000000..369fe3e05f --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsConstants.java @@ -0,0 +1,34 @@ +/* + * Copyright 1999-2019 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.cluster.server.envoy.rls; + +/** + * @author Eric Zhao + */ +public final class SentinelEnvoyRlsConstants { + + public static final int DEFAULT_GRPC_PORT = 10245; + public static final String SERVER_APP_NAME = "sentinel-rls-token-server"; + + public static final String GRPC_PORT_ENV_KEY = "SENTINEL_RLS_GRPC_PORT"; + public static final String GRPC_PORT_PROPERTY_KEY = "csp.sentinel.grpc.server.port"; + public static final String RULE_FILE_PATH_ENV_KEY = "SENTINEL_RLS_RULE_FILE_PATH"; + public static final String RULE_FILE_PATH_PROPERTY_KEY = "csp.sentinel.rls.rule.file"; + + public static final String ENABLE_ACCESS_LOG_ENV_KEY = "SENTINEL_RLS_ACCESS_LOG"; + + private SentinelEnvoyRlsConstants() {} +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServer.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServer.java new file mode 100644 index 0000000000..102add2d89 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServer.java @@ -0,0 +1,73 @@ +/* + * Copyright 1999-2019 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.cluster.server.envoy.rls; + +import java.util.Optional; + +import com.alibaba.csp.sentinel.cluster.server.envoy.rls.datasource.EnvoyRlsRuleDataSourceService; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.init.InitExecutor; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * @author Eric Zhao + */ +public class SentinelEnvoyRlsServer { + + public static void main(String[] args) throws Exception { + System.setProperty("project.name", SentinelEnvoyRlsConstants.SERVER_APP_NAME); + + EnvoyRlsRuleDataSourceService dataSourceService = new EnvoyRlsRuleDataSourceService(); + dataSourceService.init(); + + int port = resolvePort(); + SentinelRlsGrpcServer server = new SentinelRlsGrpcServer(port); + server.start(); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.err.println("[SentinelEnvoyRlsServer] Shutting down gRPC RLS server since JVM is shutting down"); + server.shutdown(); + dataSourceService.onShutdown(); + System.err.println("[SentinelEnvoyRlsServer] Server has been shut down"); + })); + InitExecutor.doInit(); + + server.blockUntilShutdown(); + } + + private static int resolvePort() { + final int defaultPort = SentinelEnvoyRlsConstants.DEFAULT_GRPC_PORT; + // Order: system env > property + String portStr = Optional.ofNullable(System.getenv(SentinelEnvoyRlsConstants.GRPC_PORT_ENV_KEY)) + .orElse(SentinelConfig.getConfig(SentinelEnvoyRlsConstants.GRPC_PORT_PROPERTY_KEY)); + if (StringUtil.isBlank(portStr)) { + return defaultPort; + } + try { + int port = Integer.parseInt(portStr); + if (port <= 0 || port > 65535) { + RecordLog.warn("[SentinelEnvoyRlsServer] Invalid port <" + portStr + ">, using default" + defaultPort); + return defaultPort; + } + return port; + } catch (Exception ex) { + RecordLog.warn("[SentinelEnvoyRlsServer] Failed to resolve port, using default " + defaultPort); + System.err.println("[SentinelEnvoyRlsServer] Failed to resolve port, using default " + defaultPort); + return defaultPort; + } + } +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServiceImpl.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServiceImpl.java new file mode 100644 index 0000000000..7e0bc7b95b --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServiceImpl.java @@ -0,0 +1,135 @@ +/* + * Copyright 1999-2019 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.cluster.server.envoy.rls; + +import java.util.ArrayList; +import java.util.List; + +import com.alibaba.csp.sentinel.cluster.TokenResult; +import com.alibaba.csp.sentinel.cluster.TokenResultStatus; +import com.alibaba.csp.sentinel.cluster.flow.rule.ClusterFlowRuleManager; +import com.alibaba.csp.sentinel.cluster.server.envoy.rls.flow.SimpleClusterFlowChecker; +import com.alibaba.csp.sentinel.cluster.server.envoy.rls.log.RlsAccessLogger; +import com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule.EnvoySentinelRuleConverter; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.util.function.Tuple2; + +import com.google.protobuf.TextFormat; +import io.envoyproxy.envoy.api.v2.ratelimit.RateLimitDescriptor; +import io.envoyproxy.envoy.api.v2.ratelimit.RateLimitDescriptor.Entry; +import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitRequest; +import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitResponse; +import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitResponse.Code; +import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitResponse.DescriptorStatus; +import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitResponse.RateLimit; +import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitResponse.RateLimit.Unit; +import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitServiceGrpc; +import io.grpc.stub.StreamObserver; + +import static com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule.EnvoySentinelRuleConverter.SEPARATOR; + +/** + * @author Eric Zhao + * @since 1.7.0 + */ +public class SentinelEnvoyRlsServiceImpl extends RateLimitServiceGrpc.RateLimitServiceImplBase { + + @Override + public void shouldRateLimit(RateLimitRequest request, StreamObserver responseObserver) { + int acquireCount = request.getHitsAddend(); + if (acquireCount < 0) { + responseObserver.onError(new IllegalArgumentException( + "acquireCount should be positive, but actual: " + acquireCount)); + return; + } + if (acquireCount == 0) { + // Not present, use the default "1" by default. + acquireCount = 1; + } + + String domain = request.getDomain(); + boolean blocked = false; + List statusList = new ArrayList<>(request.getDescriptorsCount()); + for (RateLimitDescriptor descriptor : request.getDescriptorsList()) { + Tuple2 t = checkToken(domain, descriptor, acquireCount); + TokenResult r = t.r2; + + printAccessLogIfNecessary(domain, descriptor, r); + + if (r.getStatus() == TokenResultStatus.NO_RULE_EXISTS) { + // If the rule of the descriptor is absent, the request will pass directly. + r.setStatus(TokenResultStatus.OK); + } + + if (!blocked && r.getStatus() != TokenResultStatus.OK) { + blocked = true; + } + + Code statusCode = r.getStatus() == TokenResultStatus.OK ? Code.OK : Code.OVER_LIMIT; + DescriptorStatus.Builder descriptorStatusBuilder = DescriptorStatus.newBuilder() + .setCode(statusCode); + if (t.r1 != null) { + descriptorStatusBuilder + .setCurrentLimit(RateLimit.newBuilder().setUnit(Unit.SECOND) + .setRequestsPerUnit((int)t.r1.getCount()) + .build()) + .setLimitRemaining(r.getRemaining()); + } + statusList.add(descriptorStatusBuilder.build()); + } + + Code overallStatus = blocked ? Code.OVER_LIMIT : Code.OK; + RateLimitResponse response = RateLimitResponse.newBuilder() + .setOverallCode(overallStatus) + .addAllStatuses(statusList) + .build(); + + responseObserver.onNext(response); + responseObserver.onCompleted(); + } + + private void printAccessLogIfNecessary(String domain, RateLimitDescriptor descriptor, TokenResult result) { + if (!RlsAccessLogger.isEnabled()) { + return; + } + String message = new StringBuilder("[RlsAccessLog] domain=").append(domain) + .append(", descriptor=").append(TextFormat.shortDebugString(descriptor)) + .append(", checkStatus=").append(result.getStatus()) + .append(", remaining=").append(result.getRemaining()) + .toString(); + RlsAccessLogger.log(message); + } + + protected Tuple2 checkToken(String domain, RateLimitDescriptor descriptor, int acquireCount) { + long ruleId = EnvoySentinelRuleConverter.generateFlowId(generateKey(domain, descriptor)); + + FlowRule rule = ClusterFlowRuleManager.getFlowRuleById(ruleId); + if (rule == null) { + // Pass if the target rule is absent. + return Tuple2.of(null, new TokenResult(TokenResultStatus.NO_RULE_EXISTS)); + } + // If the rule is present, it should be valid. + return Tuple2.of(rule, SimpleClusterFlowChecker.acquireClusterToken(rule, acquireCount)); + } + + private String generateKey(String domain, RateLimitDescriptor descriptor) { + StringBuilder sb = new StringBuilder(domain); + for (Entry resource : descriptor.getEntriesList()) { + sb.append(SEPARATOR).append(resource.getKey()).append(SEPARATOR).append(resource.getValue()); + } + return sb.toString(); + } +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelRlsGrpcServer.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelRlsGrpcServer.java new file mode 100644 index 0000000000..459540ad69 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelRlsGrpcServer.java @@ -0,0 +1,59 @@ +/* + * Copyright 1999-2019 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.cluster.server.envoy.rls; + +import java.io.IOException; + +import com.alibaba.csp.sentinel.log.RecordLog; + +import io.grpc.Server; +import io.grpc.ServerBuilder; + +/** + * @author Eric Zhao + */ +public class SentinelRlsGrpcServer { + + private final Server server; + + public SentinelRlsGrpcServer(int port) { + ServerBuilder builder = ServerBuilder.forPort(port) + .addService(new SentinelEnvoyRlsServiceImpl()); + server = builder.build(); + } + + public void start() throws IOException { + // The gRPC server has already checked the start status, so we don't check here. + server.start(); + String message = "[SentinelRlsGrpcServer] RLS server is running at port " + server.getPort(); + RecordLog.info(message); + System.out.println(message); + } + + public void shutdown() { + server.shutdownNow(); + } + + public boolean isShutdown() { + return server.isShutdown(); + } + + public void blockUntilShutdown() throws InterruptedException { + if (server != null) { + server.awaitTermination(); + } + } +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/datasource/EnvoyRlsRuleDataSourceService.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/datasource/EnvoyRlsRuleDataSourceService.java new file mode 100644 index 0000000000..4cf0f07e72 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/datasource/EnvoyRlsRuleDataSourceService.java @@ -0,0 +1,80 @@ +/* + * Copyright 1999-2019 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.cluster.server.envoy.rls.datasource; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import com.alibaba.csp.sentinel.cluster.server.envoy.rls.SentinelEnvoyRlsConstants; +import com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule.EnvoyRlsRule; +import com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule.EnvoyRlsRuleManager; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.datasource.FileRefreshableDataSource; +import com.alibaba.csp.sentinel.datasource.ReadableDataSource; +import com.alibaba.csp.sentinel.util.StringUtil; + +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.representer.Representer; + +/** + * @author Eric Zhao + * @since 1.7.0 + */ +public class EnvoyRlsRuleDataSourceService { + + private final Yaml yaml; + private ReadableDataSource> ds; + + public EnvoyRlsRuleDataSourceService() { + this.yaml = createYamlParser(); + } + + private Yaml createYamlParser() { + Representer representer = new Representer(); + representer.getPropertyUtils().setSkipMissingProperties(true); + return new Yaml(representer); + } + + public synchronized void init() throws Exception { + if (ds != null) { + return; + } + String configPath = getRuleConfigPath(); + if (StringUtil.isBlank(configPath)) { + throw new IllegalStateException("Empty rule config path, please set the file path in the env: " + + SentinelEnvoyRlsConstants.RULE_FILE_PATH_ENV_KEY); + } + + this.ds = new FileRefreshableDataSource<>(configPath, s -> Arrays.asList(yaml.loadAs(s, EnvoyRlsRule.class))); + EnvoyRlsRuleManager.register2Property(ds.getProperty()); + } + + public synchronized void onShutdown() { + if (ds != null) { + try { + ds.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private String getRuleConfigPath() { + return Optional.ofNullable(System.getenv(SentinelEnvoyRlsConstants.RULE_FILE_PATH_ENV_KEY)) + .orElse(SentinelConfig.getConfig(SentinelEnvoyRlsConstants.RULE_FILE_PATH_PROPERTY_KEY)); + } +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/flow/SimpleClusterFlowChecker.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/flow/SimpleClusterFlowChecker.java new file mode 100644 index 0000000000..efa307cd34 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/flow/SimpleClusterFlowChecker.java @@ -0,0 +1,74 @@ +/* + * 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.cluster.server.envoy.rls.flow; + +import com.alibaba.csp.sentinel.cluster.TokenResult; +import com.alibaba.csp.sentinel.cluster.TokenResultStatus; +import com.alibaba.csp.sentinel.cluster.flow.statistic.ClusterMetricStatistics; +import com.alibaba.csp.sentinel.cluster.flow.statistic.data.ClusterFlowEvent; +import com.alibaba.csp.sentinel.cluster.flow.statistic.metric.ClusterMetric; +import com.alibaba.csp.sentinel.cluster.server.config.ClusterServerConfigManager; +import com.alibaba.csp.sentinel.cluster.server.log.ClusterServerStatLogUtil; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; + +/** + * @author Eric Zhao + * @since 1.7.0 + */ +public final class SimpleClusterFlowChecker { + + public static TokenResult acquireClusterToken(/*@Valid*/ FlowRule rule, int acquireCount) { + Long id = rule.getClusterConfig().getFlowId(); + + ClusterMetric metric = ClusterMetricStatistics.getMetric(id); + if (metric == null) { + return new TokenResult(TokenResultStatus.FAIL); + } + + double latestQps = metric.getAvg(ClusterFlowEvent.PASS); + double globalThreshold = rule.getCount() * ClusterServerConfigManager.getExceedCount(); + double nextRemaining = globalThreshold - latestQps - acquireCount; + + if (nextRemaining >= 0) { + metric.add(ClusterFlowEvent.PASS, acquireCount); + metric.add(ClusterFlowEvent.PASS_REQUEST, 1); + + ClusterServerStatLogUtil.log("flow|pass|" + id, acquireCount); + ClusterServerStatLogUtil.log("flow|pass_request|" + id, 1); + + // Remaining count is cut down to a smaller integer. + return new TokenResult(TokenResultStatus.OK) + .setRemaining((int) nextRemaining) + .setWaitInMs(0); + } else { + // Blocked. + metric.add(ClusterFlowEvent.BLOCK, acquireCount); + metric.add(ClusterFlowEvent.BLOCK_REQUEST, 1); + ClusterServerStatLogUtil.log("flow|block|" + id, acquireCount); + ClusterServerStatLogUtil.log("flow|block_request|" + id, 1); + + return blockedResult(); + } + } + + private static TokenResult blockedResult() { + return new TokenResult(TokenResultStatus.BLOCKED) + .setRemaining(0) + .setWaitInMs(0); + } + + private SimpleClusterFlowChecker() {} +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/log/RlsAccessLogger.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/log/RlsAccessLogger.java new file mode 100644 index 0000000000..7dc23b46f5 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/log/RlsAccessLogger.java @@ -0,0 +1,45 @@ +/* + * 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.cluster.server.envoy.rls.log; + +import com.alibaba.csp.sentinel.cluster.server.envoy.rls.SentinelEnvoyRlsConstants; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * @author Eric Zhao + */ +public final class RlsAccessLogger { + + private static boolean enabled = false; + + static { + try { + enabled = "on".equalsIgnoreCase(System.getenv(SentinelEnvoyRlsConstants.ENABLE_ACCESS_LOG_ENV_KEY)); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + public static boolean isEnabled() { + return enabled; + } + + public static void log(String info) { + if (enabled && StringUtil.isNotEmpty(info)) { + System.out.println(info); + } + } +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoyRlsRule.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoyRlsRule.java new file mode 100644 index 0000000000..fe66d4fd98 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoyRlsRule.java @@ -0,0 +1,147 @@ +/* + * Copyright 1999-2019 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.cluster.server.envoy.rls.rule; + +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import com.alibaba.csp.sentinel.util.AssertUtil; + +/** + * @author Eric Zhao + * @since 1.7.0 + */ +public class EnvoyRlsRule { + + private String domain; + private List descriptors; + + public String getDomain() { + return domain; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + public List getDescriptors() { + return descriptors; + } + + public void setDescriptors(List descriptors) { + this.descriptors = descriptors; + } + + @Override + public String toString() { + return "EnvoyRlsRule{" + + "domain='" + domain + '\'' + + ", descriptors=" + descriptors + + '}'; + } + + public static class ResourceDescriptor { + + private Set resources; + + private Double count; + + public ResourceDescriptor() {} + + public ResourceDescriptor(Set resources, Double count) { + this.resources = resources; + this.count = count; + } + + public Set getResources() { + return resources; + } + + public void setResources(Set resources) { + this.resources = resources; + } + + public Double getCount() { + return count; + } + + public void setCount(Double count) { + this.count = count; + } + + @Override + public String toString() { + return "ResourceDescriptor{" + + "resources=" + resources + + ", count=" + count + + '}'; + } + } + + public static class KeyValueResource { + + private String key; + private String value; + + public KeyValueResource() {} + + public KeyValueResource(String key, String value) { + AssertUtil.assertNotBlank(key, "key cannot be blank"); + AssertUtil.assertNotBlank(value, "value cannot be blank"); + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + KeyValueResource that = (KeyValueResource)o; + return Objects.equals(key, that.key) && + Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(key, value); + } + + @Override + public String toString() { + return "KeyValueResource{" + + "key='" + key + '\'' + + ", value='" + value + '\'' + + '}'; + } + } +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoyRlsRuleManager.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoyRlsRuleManager.java new file mode 100644 index 0000000000..1acca60040 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoyRlsRuleManager.java @@ -0,0 +1,153 @@ +/* + * Copyright 1999-2019 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.cluster.server.envoy.rls.rule; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; + +import com.alibaba.csp.sentinel.cluster.flow.rule.ClusterFlowRuleManager; +import com.alibaba.csp.sentinel.cluster.server.ServerConstants; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.property.DynamicSentinelProperty; +import com.alibaba.csp.sentinel.property.PropertyListener; +import com.alibaba.csp.sentinel.property.SentinelProperty; +import com.alibaba.csp.sentinel.property.SimplePropertyListener; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * @author Eric Zhao + * @since 1.7.0 + */ +public final class EnvoyRlsRuleManager { + + private static final ConcurrentMap RULE_MAP = new ConcurrentHashMap<>(); + + private static final PropertyListener> PROPERTY_LISTENER = new EnvoyRlsRulePropertyListener(); + private static SentinelProperty> currentProperty = new DynamicSentinelProperty<>(); + + static { + currentProperty.addListener(PROPERTY_LISTENER); + } + + /** + * Listen to the {@link SentinelProperty} for Envoy RLS rules. The property is the source of {@link EnvoyRlsRule}. + * + * @param property the property to listen + */ + public static void register2Property(SentinelProperty> property) { + AssertUtil.notNull(property, "property cannot be null"); + synchronized (PROPERTY_LISTENER) { + RecordLog.info("[EnvoyRlsRuleManager] Registering new property to Envoy rate limit service rule manager"); + currentProperty.removeListener(PROPERTY_LISTENER); + property.addListener(PROPERTY_LISTENER); + currentProperty = property; + } + } + + /** + * Load Envoy RLS rules, while former rules will be replaced. + * + * @param rules new rules to load + * @return true if there are actual changes, otherwise false + */ + public static boolean loadRules(List rules) { + return currentProperty.updateValue(rules); + } + + public static List getRules() { + return new ArrayList<>(RULE_MAP.values()); + } + + static final class EnvoyRlsRulePropertyListener extends SimplePropertyListener> { + + @Override + public synchronized void configUpdate(List conf) { + Map ruleMap = generateRuleMap(conf); + + List flowRules = ruleMap.values().stream() + .flatMap(e -> EnvoySentinelRuleConverter.toSentinelFlowRules(e).stream()) + .collect(Collectors.toList()); + + RULE_MAP.clear(); + RULE_MAP.putAll(ruleMap); + RecordLog.info("[EnvoyRlsRuleManager] Envoy RLS rules loaded: " + flowRules); + + // Use the "default" namespace. + ClusterFlowRuleManager.loadRules(ServerConstants.DEFAULT_NAMESPACE, flowRules); + } + + Map generateRuleMap(List conf) { + if (conf == null || conf.isEmpty()) { + return new HashMap<>(2); + } + Map map = new HashMap<>(conf.size()); + for (EnvoyRlsRule rule : conf) { + if (!isValidRule(rule)) { + RecordLog.warn("[EnvoyRlsRuleManager] Ignoring invalid rule when loading new RLS rules: " + rule); + continue; + } + if (map.containsKey(rule.getDomain())) { + RecordLog.warn("[EnvoyRlsRuleManager] Ignoring duplicate RLS rule for specific domain: " + rule); + continue; + } + map.put(rule.getDomain(), rule); + } + return map; + } + } + + /** + * Check whether the given Envoy RLS rule is valid. + * + * @param rule the rule to check + * @return true if the rule is valid, otherwise false + */ + public static boolean isValidRule(EnvoyRlsRule rule) { + if (rule == null || StringUtil.isBlank(rule.getDomain())) { + return false; + } + List descriptors = rule.getDescriptors(); + if (descriptors == null || descriptors.isEmpty()) { + return false; + } + for (EnvoyRlsRule.ResourceDescriptor descriptor : descriptors) { + if (descriptor == null || descriptor.getCount() == null || descriptor.getCount() < 0) { + return false; + } + Set resources = descriptor.getResources(); + if (resources == null || resources.isEmpty()) { + return false; + } + for (EnvoyRlsRule.KeyValueResource resource : resources) { + if (resource == null || + StringUtil.isBlank(resource.getKey()) || StringUtil.isBlank(resource.getValue())) { + return false; + } + } + } + return true; + } + + private EnvoyRlsRuleManager() {} +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoySentinelRuleConverter.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoySentinelRuleConverter.java new file mode 100644 index 0000000000..8ae33c2cde --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoySentinelRuleConverter.java @@ -0,0 +1,88 @@ +/* + * Copyright 1999-2019 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.cluster.server.envoy.rls.rule; + +import java.util.List; +import java.util.stream.Collectors; + +import com.alibaba.csp.sentinel.slots.block.ClusterRuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.ClusterFlowConfig; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * @author Eric Zhao + * @since 1.7.0 + */ +public final class EnvoySentinelRuleConverter { + + /** + * Currently we use "|" to separate each key/value entries. + */ + public static final String SEPARATOR = "|"; + + /** + * Convert the {@link EnvoyRlsRule} to a list of Sentinel flow rules. + * + * @param rule a valid Envoy RLS rule + * @return converted rules + */ + public static List toSentinelFlowRules(EnvoyRlsRule rule) { + if (!EnvoyRlsRuleManager.isValidRule(rule)) { + throw new IllegalArgumentException("Not a valid RLS rule"); + } + return rule.getDescriptors().stream() + .map(e -> toSentinelFlowRule(rule.getDomain(), e)) + .collect(Collectors.toList()); + } + + public static FlowRule toSentinelFlowRule(String domain, EnvoyRlsRule.ResourceDescriptor descriptor) { + // One descriptor could have only one rule. + String identifier = generateKey(domain, descriptor); + long flowId = generateFlowId(identifier); + return new FlowRule(identifier) + .setCount(descriptor.getCount()) + .setClusterMode(true) + .setClusterConfig(new ClusterFlowConfig() + .setFlowId(flowId) + .setThresholdType(ClusterRuleConstant.FLOW_THRESHOLD_GLOBAL) + .setSampleCount(1) + .setFallbackToLocalWhenFail(false)); + } + + public static long generateFlowId(String key) { + if (StringUtil.isBlank(key)) { + return -1L; + } + // Add offset to avoid negative ID. + return (long) Integer.MAX_VALUE + key.hashCode(); + } + + public static String generateKey(String domain, EnvoyRlsRule.ResourceDescriptor descriptor) { + AssertUtil.assertNotBlank(domain, "domain cannot be blank"); + AssertUtil.notNull(descriptor, "EnvoyRlsRule.ResourceDescriptor cannot be null"); + AssertUtil.assertNotEmpty(descriptor.getResources(), "resources in descriptor cannot be null"); + + StringBuilder sb = new StringBuilder(domain); + for (EnvoyRlsRule.KeyValueResource resource : descriptor.getResources()) { + sb.append(SEPARATOR).append(resource.getKey()).append(SEPARATOR).append(resource.getValue()); + } + return sb.toString(); + } + + private EnvoySentinelRuleConverter() {} +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/api/v2/core/base.proto b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/api/v2/core/base.proto new file mode 100644 index 0000000000..4903df1858 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/api/v2/core/base.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package envoy.api.v2.core; + +option java_outer_classname = "BaseProto"; +option java_multiple_files = true; +option java_package = "io.envoyproxy.envoy.api.v2.core"; + +import "google/protobuf/any.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/wrappers.proto"; + +import "validate/validate.proto"; + +// Header name/value pair. +message HeaderValue { + // Header name. + string key = 1 [(validate.rules).string = {min_bytes: 1 max_bytes: 16384}]; + + // Header value. + // + // The same :ref:`format specifier ` as used for + // :ref:`HTTP access logging ` applies here, however + // unknown header values are replaced with the empty string instead of `-`. + string value = 2 [(validate.rules).string = {max_bytes: 16384}]; +} \ No newline at end of file diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/api/v2/ratelimit/ratelimit.proto b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/api/v2/ratelimit/ratelimit.proto new file mode 100644 index 0000000000..dfcd4174eb --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/api/v2/ratelimit/ratelimit.proto @@ -0,0 +1,65 @@ +syntax = "proto3"; + +package envoy.api.v2.ratelimit; + +option java_outer_classname = "RatelimitProto"; +option java_multiple_files = true; +option java_package = "io.envoyproxy.envoy.api.v2.ratelimit"; + +import "validate/validate.proto"; + +// [#protodoc-title: Common rate limit components] + +// A RateLimitDescriptor is a list of hierarchical entries that are used by the service to +// determine the final rate limit key and overall allowed limit. Here are some examples of how +// they might be used for the domain "envoy". +// +// .. code-block:: cpp +// +// ["authenticated": "false"], ["remote_address": "10.0.0.1"] +// +// What it does: Limits all unauthenticated traffic for the IP address 10.0.0.1. The +// configuration supplies a default limit for the *remote_address* key. If there is a desire to +// raise the limit for 10.0.0.1 or block it entirely it can be specified directly in the +// configuration. +// +// .. code-block:: cpp +// +// ["authenticated": "false"], ["path": "/foo/bar"] +// +// What it does: Limits all unauthenticated traffic globally for a specific path (or prefix if +// configured that way in the service). +// +// .. code-block:: cpp +// +// ["authenticated": "false"], ["path": "/foo/bar"], ["remote_address": "10.0.0.1"] +// +// What it does: Limits unauthenticated traffic to a specific path for a specific IP address. +// Like (1) we can raise/block specific IP addresses if we want with an override configuration. +// +// .. code-block:: cpp +// +// ["authenticated": "true"], ["client_id": "foo"] +// +// What it does: Limits all traffic for an authenticated client "foo" +// +// .. code-block:: cpp +// +// ["authenticated": "true"], ["client_id": "foo"], ["path": "/foo/bar"] +// +// What it does: Limits traffic to a specific path for an authenticated client "foo" +// +// The idea behind the API is that (1)/(2)/(3) and (4)/(5) can be sent in 1 request if desired. +// This enables building complex application scenarios with a generic backend. +message RateLimitDescriptor { + message Entry { + // Descriptor key. + string key = 1 [(validate.rules).string = {min_bytes: 1}]; + + // Descriptor value. + string value = 2 [(validate.rules).string = {min_bytes: 1}]; + } + + // Descriptor entries. + repeated Entry entries = 1 [(validate.rules).repeated = {min_items: 1}]; +} \ No newline at end of file diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/service/ratelimit/v2/rls.proto b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/service/ratelimit/v2/rls.proto new file mode 100644 index 0000000000..a737a51e07 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/service/ratelimit/v2/rls.proto @@ -0,0 +1,109 @@ +syntax = "proto3"; + +package envoy.service.ratelimit.v2; + +option java_outer_classname = "RlsProto"; +option java_multiple_files = true; +option java_package = "io.envoyproxy.envoy.service.ratelimit.v2"; +option java_generic_services = true; + +import "envoy/api/v2/core/base.proto"; +import "envoy/api/v2/ratelimit/ratelimit.proto"; + +import "validate/validate.proto"; + +// [#protodoc-title: Rate Limit Service (RLS)] + +service RateLimitService { + // Determine whether rate limiting should take place. + rpc ShouldRateLimit(RateLimitRequest) returns (RateLimitResponse) { + } +} + +// Main message for a rate limit request. The rate limit service is designed to be fully generic +// in the sense that it can operate on arbitrary hierarchical key/value pairs. The loaded +// configuration will parse the request and find the most specific limit to apply. In addition, +// a RateLimitRequest can contain multiple "descriptors" to limit on. When multiple descriptors +// are provided, the server will limit on *ALL* of them and return an OVER_LIMIT response if any +// of them are over limit. This enables more complex application level rate limiting scenarios +// if desired. +message RateLimitRequest { + // All rate limit requests must specify a domain. This enables the configuration to be per + // application without fear of overlap. E.g., "envoy". + string domain = 1; + + // All rate limit requests must specify at least one RateLimitDescriptor. Each descriptor is + // processed by the service (see below). If any of the descriptors are over limit, the entire + // request is considered to be over limit. + repeated api.v2.ratelimit.RateLimitDescriptor descriptors = 2; + + // Rate limit requests can optionally specify the number of hits a request adds to the matched + // limit. If the value is not set in the message, a request increases the matched limit by 1. + uint32 hits_addend = 3; +} + +// A response from a ShouldRateLimit call. +message RateLimitResponse { + enum Code { + // The response code is not known. + UNKNOWN = 0; + + // The response code to notify that the number of requests are under limit. + OK = 1; + + // The response code to notify that the number of requests are over limit. + OVER_LIMIT = 2; + } + + // Defines an actual rate limit in terms of requests per unit of time and the unit itself. + message RateLimit { + enum Unit { + // The time unit is not known. + UNKNOWN = 0; + + // The time unit representing a second. + SECOND = 1; + + // The time unit representing a minute. + MINUTE = 2; + + // The time unit representing an hour. + HOUR = 3; + + // The time unit representing a day. + DAY = 4; + } + + // The number of requests per unit of time. + uint32 requests_per_unit = 1; + + // The unit of time. + Unit unit = 2; + } + + message DescriptorStatus { + // The response code for an individual descriptor. + Code code = 1; + + // The current limit as configured by the server. Useful for debugging, etc. + RateLimit current_limit = 2; + + // The limit remaining in the current time unit. + uint32 limit_remaining = 3; + } + + // The overall response code which takes into account all of the descriptors that were passed + // in the RateLimitRequest message. + Code overall_code = 1; + + // A list of DescriptorStatus messages which matches the length of the descriptor list passed + // in the RateLimitRequest. This can be used by the caller to determine which individual + // descriptors failed and/or what the currently configured limits are for all of them. + repeated DescriptorStatus statuses = 2; + + // [#next-major-version: rename to response_headers_to_add] + repeated api.v2.core.HeaderValue headers = 3; + + // A list of headers to add to the request when forwarded + repeated api.v2.core.HeaderValue request_headers_to_add = 4; +} \ No newline at end of file diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/validate/validate.proto b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/validate/validate.proto new file mode 100644 index 0000000000..b662551352 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/validate/validate.proto @@ -0,0 +1,763 @@ +syntax = "proto2"; +package validate; + +option go_package = "github.com/lyft/protoc-gen-validate/validate"; +option java_package = "com.lyft.pgv.validate"; + +import "google/protobuf/descriptor.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; + +// Validation rules applied at the message level +extend google.protobuf.MessageOptions { + // Disabled nullifies any validation rules for this message, including any + // message fields associated with it that do support validation. + optional bool disabled = 919191; +} + +// Validation rules applied at the oneof level +extend google.protobuf.OneofOptions { + // Required ensures that exactly one the field options in a oneof is set; + // validation fails if no fields in the oneof are set. + optional bool required = 919191; +} + +// Validation rules applied at the field level +extend google.protobuf.FieldOptions { + // Rules specify the validations to be performed on this field. By default, + // no validation is performed against a field. + optional FieldRules rules = 919191; +} + +// FieldRules encapsulates the rules for each type of field. Depending on the +// field, the correct set should be used to ensure proper validations. +message FieldRules { + oneof type { + // Scalar Field Types + FloatRules float = 1; + DoubleRules double = 2; + Int32Rules int32 = 3; + Int64Rules int64 = 4; + UInt32Rules uint32 = 5; + UInt64Rules uint64 = 6; + SInt32Rules sint32 = 7; + SInt64Rules sint64 = 8; + Fixed32Rules fixed32 = 9; + Fixed64Rules fixed64 = 10; + SFixed32Rules sfixed32 = 11; + SFixed64Rules sfixed64 = 12; + BoolRules bool = 13; + StringRules string = 14; + BytesRules bytes = 15; + + // Complex Field Types + EnumRules enum = 16; + MessageRules message = 17; + RepeatedRules repeated = 18; + MapRules map = 19; + + // Well-Known Field Types + AnyRules any = 20; + DurationRules duration = 21; + TimestampRules timestamp = 22; + } +} + +// FloatRules describes the constraints applied to `float` values +message FloatRules { + // Const specifies that this field must be exactly the specified value + optional float const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional float lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional float lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional float gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional float gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated float in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated float not_in = 7; +} + +// DoubleRules describes the constraints applied to `double` values +message DoubleRules { + // Const specifies that this field must be exactly the specified value + optional double const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional double lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional double lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional double gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional double gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated double in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated double not_in = 7; +} + +// Int32Rules describes the constraints applied to `int32` values +message Int32Rules { + // Const specifies that this field must be exactly the specified value + optional int32 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional int32 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional int32 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional int32 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional int32 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated int32 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated int32 not_in = 7; +} + +// Int64Rules describes the constraints applied to `int64` values +message Int64Rules { + // Const specifies that this field must be exactly the specified value + optional int64 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional int64 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional int64 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional int64 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional int64 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated int64 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated int64 not_in = 7; +} + +// UInt32Rules describes the constraints applied to `uint32` values +message UInt32Rules { + // Const specifies that this field must be exactly the specified value + optional uint32 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional uint32 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional uint32 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional uint32 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional uint32 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated uint32 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated uint32 not_in = 7; +} + +// UInt64Rules describes the constraints applied to `uint64` values +message UInt64Rules { + // Const specifies that this field must be exactly the specified value + optional uint64 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional uint64 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional uint64 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional uint64 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional uint64 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated uint64 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated uint64 not_in = 7; +} + +// SInt32Rules describes the constraints applied to `sint32` values +message SInt32Rules { + // Const specifies that this field must be exactly the specified value + optional sint32 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional sint32 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional sint32 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional sint32 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional sint32 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated sint32 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated sint32 not_in = 7; +} + +// SInt64Rules describes the constraints applied to `sint64` values +message SInt64Rules { + // Const specifies that this field must be exactly the specified value + optional sint64 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional sint64 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional sint64 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional sint64 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional sint64 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated sint64 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated sint64 not_in = 7; +} + +// Fixed32Rules describes the constraints applied to `fixed32` values +message Fixed32Rules { + // Const specifies that this field must be exactly the specified value + optional fixed32 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional fixed32 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional fixed32 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional fixed32 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional fixed32 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated fixed32 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated fixed32 not_in = 7; +} + +// Fixed64Rules describes the constraints applied to `fixed64` values +message Fixed64Rules { + // Const specifies that this field must be exactly the specified value + optional fixed64 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional fixed64 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional fixed64 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional fixed64 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional fixed64 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated fixed64 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated fixed64 not_in = 7; +} + +// SFixed32Rules describes the constraints applied to `sfixed32` values +message SFixed32Rules { + // Const specifies that this field must be exactly the specified value + optional sfixed32 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional sfixed32 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional sfixed32 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional sfixed32 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional sfixed32 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated sfixed32 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated sfixed32 not_in = 7; +} + +// SFixed64Rules describes the constraints applied to `sfixed64` values +message SFixed64Rules { + // Const specifies that this field must be exactly the specified value + optional sfixed64 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional sfixed64 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional sfixed64 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional sfixed64 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional sfixed64 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated sfixed64 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated sfixed64 not_in = 7; +} + +// BoolRules describes the constraints applied to `bool` values +message BoolRules { + // Const specifies that this field must be exactly the specified value + optional bool const = 1; +} + +// StringRules describe the constraints applied to `string` values +message StringRules { + // Const specifies that this field must be exactly the specified value + optional string const = 1; + + // Len specifies that this field must be the specified number of + // characters (Unicode code points). Note that the number of + // characters may differ from the number of bytes in the string. + optional uint64 len = 19; + + // MinLen specifies that this field must be the specified number of + // characters (Unicode code points) at a minimum. Note that the number of + // characters may differ from the number of bytes in the string. + optional uint64 min_len = 2; + + // MaxLen specifies that this field must be the specified number of + // characters (Unicode code points) at a maximum. Note that the number of + // characters may differ from the number of bytes in the string. + optional uint64 max_len = 3; + + // LenBytes specifies that this field must be the specified number of bytes + // at a minimum + optional uint64 len_bytes = 20; + + // MinBytes specifies that this field must be the specified number of bytes + // at a minimum + optional uint64 min_bytes = 4; + + // MaxBytes specifies that this field must be the specified number of bytes + // at a maximum + optional uint64 max_bytes = 5; + + // Pattern specifes that this field must match against the specified + // regular expression (RE2 syntax). The included expression should elide + // any delimiters. + optional string pattern = 6; + + // Prefix specifies that this field must have the specified substring at + // the beginning of the string. + optional string prefix = 7; + + // Suffix specifies that this field must have the specified substring at + // the end of the string. + optional string suffix = 8; + + // Contains specifies that this field must have the specified substring + // anywhere in the string. + optional string contains = 9; + + // In specifies that this field must be equal to one of the specified + // values + repeated string in = 10; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated string not_in = 11; + + // WellKnown rules provide advanced constraints against common string + // patterns + oneof well_known { + // Email specifies that the field must be a valid email address as + // defined by RFC 5322 + bool email = 12; + + // Hostname specifies that the field must be a valid hostname as + // defined by RFC 1034. This constraint does not support + // internationalized domain names (IDNs). + bool hostname = 13; + + // Ip specifies that the field must be a valid IP (v4 or v6) address. + // Valid IPv6 addresses should not include surrounding square brackets. + bool ip = 14; + + // Ipv4 specifies that the field must be a valid IPv4 address. + bool ipv4 = 15; + + // Ipv6 specifies that the field must be a valid IPv6 address. Valid + // IPv6 addresses should not include surrounding square brackets. + bool ipv6 = 16; + + // Uri specifies that the field must be a valid, absolute URI as defined + // by RFC 3986 + bool uri = 17; + + // UriRef specifies that the field must be a valid URI as defined by RFC + // 3986 and may be relative or absolute. + bool uri_ref = 18; + } +} + +// BytesRules describe the constraints applied to `bytes` values +message BytesRules { + // Const specifies that this field must be exactly the specified value + optional bytes const = 1; + + // Len specifies that this field must be the specified number of bytes + optional uint64 len = 13; + + // MinLen specifies that this field must be the specified number of bytes + // at a minimum + optional uint64 min_len = 2; + + // MaxLen specifies that this field must be the specified number of bytes + // at a maximum + optional uint64 max_len = 3; + + // Pattern specifes that this field must match against the specified + // regular expression (RE2 syntax). The included expression should elide + // any delimiters. + optional string pattern = 4; + + // Prefix specifies that this field must have the specified bytes at the + // beginning of the string. + optional bytes prefix = 5; + + // Suffix specifies that this field must have the specified bytes at the + // end of the string. + optional bytes suffix = 6; + + // Contains specifies that this field must have the specified bytes + // anywhere in the string. + optional bytes contains = 7; + + // In specifies that this field must be equal to one of the specified + // values + repeated bytes in = 8; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated bytes not_in = 9; + + // WellKnown rules provide advanced constraints against common byte + // patterns + oneof well_known { + // Ip specifies that the field must be a valid IP (v4 or v6) address in + // byte format + bool ip = 10; + + // Ipv4 specifies that the field must be a valid IPv4 address in byte + // format + bool ipv4 = 11; + + // Ipv6 specifies that the field must be a valid IPv6 address in byte + // format + bool ipv6 = 12; + } +} + +// EnumRules describe the constraints applied to enum values +message EnumRules { + // Const specifies that this field must be exactly the specified value + optional int32 const = 1; + + // DefinedOnly specifies that this field must be only one of the defined + // values for this enum, failing on any undefined value. + optional bool defined_only = 2; + + // In specifies that this field must be equal to one of the specified + // values + repeated int32 in = 3; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated int32 not_in = 4; +} + +// MessageRules describe the constraints applied to embedded message values. +// For message-type fields, validation is performed recursively. +message MessageRules { + // Skip specifies that the validation rules of this field should not be + // evaluated + optional bool skip = 1; + + // Required specifies that this field must be set + optional bool required = 2; +} + +// RepeatedRules describe the constraints applied to `repeated` values +message RepeatedRules { + // MinItems specifies that this field must have the specified number of + // items at a minimum + optional uint64 min_items = 1; + + // MaxItems specifies that this field must have the specified number of + // items at a maximum + optional uint64 max_items = 2; + + // Unique specifies that all elements in this field must be unique. This + // contraint is only applicable to scalar and enum types (messages are not + // supported). + optional bool unique = 3; + + // Items specifies the contraints to be applied to each item in the field. + // Repeated message fields will still execute validation against each item + // unless skip is specified here. + optional FieldRules items = 4; +} + +// MapRules describe the constraints applied to `map` values +message MapRules { + // MinPairs specifies that this field must have the specified number of + // KVs at a minimum + optional uint64 min_pairs = 1; + + // MaxPairs specifies that this field must have the specified number of + // KVs at a maximum + optional uint64 max_pairs = 2; + + // NoSparse specifies values in this field cannot be unset. This only + // applies to map's with message value types. + optional bool no_sparse = 3; + + // Keys specifies the constraints to be applied to each key in the field. + optional FieldRules keys = 4; + + // Values specifies the constraints to be applied to the value of each key + // in the field. Message values will still have their validations evaluated + // unless skip is specified here. + optional FieldRules values = 5; +} + +// AnyRules describe constraints applied exclusively to the +// `google.protobuf.Any` well-known type +message AnyRules { + // Required specifies that this field must be set + optional bool required = 1; + + // In specifies that this field's `type_url` must be equal to one of the + // specified values. + repeated string in = 2; + + // NotIn specifies that this field's `type_url` must not be equal to any of + // the specified values. + repeated string not_in = 3; +} + +// DurationRules describe the constraints applied exclusively to the +// `google.protobuf.Duration` well-known type +message DurationRules { + // Required specifies that this field must be set + optional bool required = 1; + + // Const specifies that this field must be exactly the specified value + optional google.protobuf.Duration const = 2; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional google.protobuf.Duration lt = 3; + + // Lt specifies that this field must be less than the specified value, + // inclusive + optional google.protobuf.Duration lte = 4; + + // Gt specifies that this field must be greater than the specified value, + // exclusive + optional google.protobuf.Duration gt = 5; + + // Gte specifies that this field must be greater than the specified value, + // inclusive + optional google.protobuf.Duration gte = 6; + + // In specifies that this field must be equal to one of the specified + // values + repeated google.protobuf.Duration in = 7; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated google.protobuf.Duration not_in = 8; +} + +// TimestampRules describe the constraints applied exclusively to the +// `google.protobuf.Timestamp` well-known type +message TimestampRules { + // Required specifies that this field must be set + optional bool required = 1; + + // Const specifies that this field must be exactly the specified value + optional google.protobuf.Timestamp const = 2; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional google.protobuf.Timestamp lt = 3; + + // Lte specifies that this field must be less than the specified value, + // inclusive + optional google.protobuf.Timestamp lte = 4; + + // Gt specifies that this field must be greater than the specified value, + // exclusive + optional google.protobuf.Timestamp gt = 5; + + // Gte specifies that this field must be greater than the specified value, + // inclusive + optional google.protobuf.Timestamp gte = 6; + + // LtNow specifies that this must be less than the current time. LtNow + // can only be used with the Within rule. + optional bool lt_now = 7; + + // GtNow specifies that this must be greater than the current time. GtNow + // can only be used with the Within rule. + optional bool gt_now = 8; + + // Within specifies that this field must be within this duration of the + // current time. This constraint can be used alone or with the LtNow and + // GtNow rules. + optional google.protobuf.Duration within = 9; +} \ No newline at end of file diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/test/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServiceImplTest.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/test/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServiceImplTest.java new file mode 100644 index 0000000000..d72618961d --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/test/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServiceImplTest.java @@ -0,0 +1,119 @@ +/* + * Copyright 1999-2019 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.cluster.server.envoy.rls; + +import com.alibaba.csp.sentinel.cluster.TokenResult; +import com.alibaba.csp.sentinel.cluster.TokenResultStatus; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.util.function.Tuple2; + +import io.envoyproxy.envoy.api.v2.ratelimit.RateLimitDescriptor; +import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitRequest; +import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitResponse; +import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitResponse.Code; +import io.grpc.stub.StreamObserver; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * @author Eric Zhao + */ +public class SentinelEnvoyRlsServiceImplTest { + + @Test + public void testShouldRateLimitPass() { + SentinelEnvoyRlsServiceImpl rlsService = mock(SentinelEnvoyRlsServiceImpl.class); + StreamObserver streamObserver = mock(StreamObserver.class); + String domain = "testShouldRateLimitPass"; + int acquireCount = 1; + + RateLimitDescriptor descriptor1 = RateLimitDescriptor.newBuilder() + .addEntries(RateLimitDescriptor.Entry.newBuilder().setKey("a1").setValue("b1").build()) + .build(); + RateLimitDescriptor descriptor2 = RateLimitDescriptor.newBuilder() + .addEntries(RateLimitDescriptor.Entry.newBuilder().setKey("a2").setValue("b2").build()) + .addEntries(RateLimitDescriptor.Entry.newBuilder().setKey("a3").setValue("b3").build()) + .build(); + + ArgumentCaptor responseCapture = ArgumentCaptor.forClass(RateLimitResponse.class); + doNothing().when(streamObserver) + .onNext(responseCapture.capture()); + + doCallRealMethod().when(rlsService).shouldRateLimit(any(), any()); + when(rlsService.checkToken(eq(domain), same(descriptor1), eq(acquireCount))) + .thenReturn(Tuple2.of(new FlowRule(), new TokenResult(TokenResultStatus.OK))); + when(rlsService.checkToken(eq(domain), same(descriptor2), eq(acquireCount))) + .thenReturn(Tuple2.of(new FlowRule(), new TokenResult(TokenResultStatus.OK))); + + RateLimitRequest rateLimitRequest = RateLimitRequest.newBuilder() + .addDescriptors(descriptor1) + .addDescriptors(descriptor2) + .setDomain(domain) + .setHitsAddend(acquireCount) + .build(); + rlsService.shouldRateLimit(rateLimitRequest, streamObserver); + + RateLimitResponse response = responseCapture.getValue(); + assertEquals(Code.OK, response.getOverallCode()); + response.getStatusesList() + .forEach(e -> assertEquals(Code.OK, e.getCode())); + } + + @Test + public void testShouldRatePartialBlock() { + SentinelEnvoyRlsServiceImpl rlsService = mock(SentinelEnvoyRlsServiceImpl.class); + StreamObserver streamObserver = mock(StreamObserver.class); + String domain = "testShouldRatePartialBlock"; + int acquireCount = 1; + + RateLimitDescriptor descriptor1 = RateLimitDescriptor.newBuilder() + .addEntries(RateLimitDescriptor.Entry.newBuilder().setKey("a1").setValue("b1").build()) + .build(); + RateLimitDescriptor descriptor2 = RateLimitDescriptor.newBuilder() + .addEntries(RateLimitDescriptor.Entry.newBuilder().setKey("a2").setValue("b2").build()) + .addEntries(RateLimitDescriptor.Entry.newBuilder().setKey("a3").setValue("b3").build()) + .build(); + + ArgumentCaptor responseCapture = ArgumentCaptor.forClass(RateLimitResponse.class); + doNothing().when(streamObserver) + .onNext(responseCapture.capture()); + + doCallRealMethod().when(rlsService).shouldRateLimit(any(), any()); + when(rlsService.checkToken(eq(domain), same(descriptor1), eq(acquireCount))) + .thenReturn(Tuple2.of(new FlowRule(), new TokenResult(TokenResultStatus.BLOCKED))); + when(rlsService.checkToken(eq(domain), same(descriptor2), eq(acquireCount))) + .thenReturn(Tuple2.of(new FlowRule(), new TokenResult(TokenResultStatus.OK))); + + RateLimitRequest rateLimitRequest = RateLimitRequest.newBuilder() + .addDescriptors(descriptor1) + .addDescriptors(descriptor2) + .setDomain(domain) + .setHitsAddend(acquireCount) + .build(); + rlsService.shouldRateLimit(rateLimitRequest, streamObserver); + + RateLimitResponse response = responseCapture.getValue(); + assertEquals(Code.OVER_LIMIT, response.getOverallCode()); + assertEquals(2, response.getStatusesCount()); + assertTrue(response.getStatusesList().stream() + .anyMatch(e -> e.getCode().equals(Code.OVER_LIMIT))); + assertFalse(response.getStatusesList().stream() + .allMatch(e -> e.getCode().equals(Code.OVER_LIMIT))); + } +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/test/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoySentinelRuleConverterTest.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/test/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoySentinelRuleConverterTest.java new file mode 100644 index 0000000000..554b38c717 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/test/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoySentinelRuleConverterTest.java @@ -0,0 +1,72 @@ +/* + * Copyright 1999-2019 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.cluster.server.envoy.rls.rule; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +import com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule.EnvoyRlsRule.KeyValueResource; +import com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule.EnvoyRlsRule.ResourceDescriptor; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; + +import org.junit.Test; + +import static com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule.EnvoySentinelRuleConverter.SEPARATOR; +import static org.junit.Assert.*; + +/** + * @author Eric Zhao + */ +public class EnvoySentinelRuleConverterTest { + + @Test + public void testConvertToSentinelFlowRules() { + String domain = "testConvertToSentinelFlowRules"; + EnvoyRlsRule rlsRule = new EnvoyRlsRule(); + rlsRule.setDomain(domain); + List descriptors = new ArrayList<>(); + ResourceDescriptor d1 = new ResourceDescriptor(); + d1.setCount(10d); + d1.setResources(Collections.singleton(new KeyValueResource("k1", "v1"))); + descriptors.add(d1); + ResourceDescriptor d2 = new ResourceDescriptor(); + d2.setCount(20d); + d2.setResources(new HashSet<>(Arrays.asList( + new KeyValueResource("k2", "v2"), + new KeyValueResource("k3", "v3") + ))); + descriptors.add(d2); + rlsRule.setDescriptors(descriptors); + + List rules = EnvoySentinelRuleConverter.toSentinelFlowRules(rlsRule); + final String expectedK1 = domain + SEPARATOR + "k1" + SEPARATOR + "v1"; + FlowRule r1 = rules.stream() + .filter(e -> e.getResource().equals(expectedK1)) + .findAny() + .orElseThrow(() -> new AssertionError("the converted rule does not exist, expected key: " + expectedK1)); + assertEquals(10d, r1.getCount(), 0.01); + + final String expectedK2 = domain + SEPARATOR + "k2" + SEPARATOR + "v2" + SEPARATOR + "k3" + SEPARATOR + "v3"; + FlowRule r2 = rules.stream() + .filter(e -> e.getResource().equals(expectedK2)) + .findAny() + .orElseThrow(() -> new AssertionError("the converted rule does not exist, expected key: " + expectedK2)); + assertEquals(20d, r2.getCount(), 0.01); + } +} diff --git a/sentinel-core/pom.xml b/sentinel-core/pom.xml index 9228ee7e3e..1cb08f7d44 100755 --- a/sentinel-core/pom.xml +++ b/sentinel-core/pom.xml @@ -6,7 +6,7 @@ com.alibaba.csp sentinel-parent - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT sentinel-core jar diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/AsyncEntry.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/AsyncEntry.java index 84b7c7e3c5..98ccb85a22 100644 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/AsyncEntry.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/AsyncEntry.java @@ -52,7 +52,11 @@ void cleanCurrentEntryInLocal() { ((CtEntry)parent).child = null; } } else { - throw new IllegalStateException("Bad async context state"); + String curEntryName = curEntry == null ? "none" + : curEntry.resourceWrapper.getName() + "@" + curEntry.hashCode(); + String msg = String.format("Bad async context state, expected entry: %s, but actual: %s", + getResourceWrapper().getName() + "@" + hashCode(), curEntryName); + throw new IllegalStateException(msg); } } } @@ -74,7 +78,8 @@ void initAsyncContext() { .setOrigin(context.getOrigin()) .setCurEntry(this); } else { - RecordLog.warn("[AsyncEntry] Duplicate initialize of async context for entry: " + resourceWrapper.getName()); + RecordLog.warn( + "[AsyncEntry] Duplicate initialize of async context for entry: " + resourceWrapper.getName()); } } diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/Constants.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/Constants.java index 6e728bb96e..c3bbadf091 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/Constants.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/Constants.java @@ -15,22 +15,23 @@ */ package com.alibaba.csp.sentinel; -import com.alibaba.csp.sentinel.config.SentinelConfig; import com.alibaba.csp.sentinel.node.ClusterNode; import com.alibaba.csp.sentinel.node.DefaultNode; import com.alibaba.csp.sentinel.node.EntranceNode; import com.alibaba.csp.sentinel.slotchain.StringResourceWrapper; -import com.alibaba.csp.sentinel.slots.system.SystemRule; import com.alibaba.csp.sentinel.util.VersionUtil; /** + * Universal constants of Sentinel. + * * @author qinan.qn * @author youji.zj * @author jialiang.linjl + * @author Eric Zhao */ public final class Constants { - public static final String SENTINEL_VERSION = VersionUtil.getVersion("1.7.0"); + public static final String SENTINEL_VERSION = VersionUtil.getVersion("1.7.2"); public final static int MAX_CONTEXT_NAME_SIZE = 2000; public final static int MAX_SLOT_CHAIN_SIZE = 6000; @@ -57,18 +58,12 @@ public final class Constants { * Global ROOT statistic node that represents the universal parent node. */ public final static DefaultNode ROOT = new EntranceNode(new StringResourceWrapper(ROOT_ID, EntryType.IN), - new ClusterNode()); - - /** - * Global statistic node for inbound traffic. Usually used for {@link SystemRule} checking. - */ - public final static ClusterNode ENTRY_NODE = new ClusterNode(); + new ClusterNode(ROOT_ID, ResourceTypeConstants.COMMON)); /** - * Response time that exceeds TIME_DROP_VALVE will be calculated as TIME_DROP_VALVE. Default value is 4900 ms. - * It can be configured by property file or JVM parameter via {@code -Dcsp.sentinel.statistic.max.rt=xxx}. + * Global statistic node for inbound traffic. Usually used for {@code SystemRule} checking. */ - public static final int TIME_DROP_VALVE = SentinelConfig.statisticMaxRt(); + public final static ClusterNode ENTRY_NODE = new ClusterNode(TOTAL_IN_RESOURCE_NAME, ResourceTypeConstants.COMMON); /** * The global switch for Sentinel. diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java index 0bb9c411f6..5c5b137b1d 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java @@ -71,7 +71,7 @@ private AsyncEntry asyncEntryWithPriorityInternal(ResourceWrapper resourceWrappe } if (context == null) { // Using default context. - context = MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType()); + context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME); } // Global switch is turned off, so no rule checking will be done. @@ -125,7 +125,7 @@ private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, bool if (context == null) { // Using default context. - context = MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType()); + context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME); } // Global switch is close, no rule checking will do. @@ -245,8 +245,12 @@ static Map getChainMap() { /** * This class is used for skip context name checking. */ - private final static class MyContextUtil extends ContextUtil { - static Context myEnter(String name, String origin, EntryType type) { + private final static class InternalContextUtil extends ContextUtil { + static Context internalEnter(String name) { + return trueEnter(name, ""); + } + + static Context internalEnter(String name, String origin) { return trueEnter(name, origin); } } @@ -329,4 +333,24 @@ public Entry entryWithPriority(String name, EntryType type, int count, boolean p StringResourceWrapper resource = new StringResourceWrapper(name, type); return entryWithPriority(resource, count, prioritized, args); } + + @Override + public Entry entryWithType(String name, int resourceType, EntryType entryType, int count, Object[] args) + throws BlockException { + return entryWithType(name, resourceType, entryType, count, false, args); + } + + @Override + public Entry entryWithType(String name, int resourceType, EntryType entryType, int count, boolean prioritized, + Object[] args) throws BlockException { + StringResourceWrapper resource = new StringResourceWrapper(name, entryType, resourceType); + return entryWithPriority(resource, count, prioritized, args); + } + + @Override + public AsyncEntry asyncEntryWithType(String name, int resourceType, EntryType entryType, int count, + boolean prioritized, Object[] args) throws BlockException { + StringResourceWrapper resource = new StringResourceWrapper(name, entryType, resourceType); + return asyncEntryWithPriorityInternal(resource, count, prioritized, args); + } } diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/ResourceTypeConstants.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/ResourceTypeConstants.java new file mode 100644 index 0000000000..1ae421cc58 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/ResourceTypeConstants.java @@ -0,0 +1,31 @@ +/* + * Copyright 1999-2019 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 + * + * https://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; + +/** + * @author Eric Zhao + * @since 1.7.0 + */ +public final class ResourceTypeConstants { + + public static final int COMMON = 0; + public static final int COMMON_WEB = 1; + public static final int COMMON_RPC = 2; + public static final int COMMON_API_GATEWAY = 3; + public static final int COMMON_DB_SQL = 4; + + private ResourceTypeConstants() {} +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/Sph.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/Sph.java index bedbd33bf3..32643b4697 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/Sph.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/Sph.java @@ -29,7 +29,7 @@ * @author leyou * @author Eric Zhao */ -public interface Sph { +public interface Sph extends SphResourceTypeSupport { /** * Create a protected resource. diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/SphResourceTypeSupport.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/SphResourceTypeSupport.java new file mode 100644 index 0000000000..4349473f6c --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/SphResourceTypeSupport.java @@ -0,0 +1,69 @@ +/* + * Copyright 1999-2019 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 + * + * https://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; + +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/** + * @author Eric Zhao + * @since 1.7.0 + */ +public interface SphResourceTypeSupport { + + /** + * Create a protected resource with provided classification. + * + * @param name the unique name of the protected resource + * @param resourceType the classification of the resource + * @param entryType the traffic entry type (IN/OUT) of the resource + * @param count tokens required + * @param args extra parameters + * @return new entry of the resource + * @throws BlockException if the block criteria is met + */ + Entry entryWithType(String name, int resourceType, EntryType entryType, int count, Object[] args) + throws BlockException; + + /** + * Create a protected resource with provided classification. + * + * @param name the unique name of the protected resource + * @param resourceType the classification of the resource + * @param entryType the traffic entry type (IN/OUT) of the resource + * @param count tokens required + * @param prioritized whether the entry is prioritized + * @param args extra parameters + * @return new entry of the resource + * @throws BlockException if the block criteria is met + */ + Entry entryWithType(String name, int resourceType, EntryType entryType, int count, boolean prioritized, + Object[] args) throws BlockException; + + /** + * Create an asynchronous resource with provided classification. + * + * @param name the unique name of the protected resource + * @param resourceType the classification of the resource + * @param entryType the traffic entry type (IN/OUT) of the resource + * @param count tokens required + * @param prioritized whether the entry is prioritized + * @param args extra parameters + * @return new entry of the resource + * @throws BlockException if the block criteria is met + */ + AsyncEntry asyncEntryWithType(String name, int resourceType, EntryType entryType, int count, boolean prioritized, + Object[] args) throws BlockException; +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/SphU.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/SphU.java index 696b686fe6..a7ad2aa19d 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/SphU.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/SphU.java @@ -76,6 +76,8 @@ public class SphU { private static final Object[] OBJECTS0 = new Object[0]; + private SphU() {} + /** * Checking all {@link Rule}s about the resource. * @@ -246,7 +248,7 @@ public static AsyncEntry asyncEntry(String name, EntryType type, int count, Obje /** * Checking all {@link Rule}s related the resource. The entry is prioritized. * - * @param name the unique name for the protected resource + * @param name the unique name for the protected resource * @throws BlockException if the block criteria is met, eg. when any rule's threshold is exceeded. * @since 1.4.0 */ @@ -257,14 +259,97 @@ public static Entry entryWithPriority(String name) throws BlockException { /** * Checking all {@link Rule}s related the resource. The entry is prioritized. * - * @param name the unique name for the protected resource - * @param type the resource is an inbound or an outbound method. This is used - * to mark whether it can be blocked when the system is unstable, - * only inbound traffic could be blocked by {@link SystemRule} + * @param name the unique name for the protected resource + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable, + * only inbound traffic could be blocked by {@link SystemRule} * @throws BlockException if the block criteria is met, eg. when any rule's threshold is exceeded. * @since 1.4.0 */ public static Entry entryWithPriority(String name, EntryType type) throws BlockException { return Env.sph.entryWithPriority(name, type, 1, true); } + + /** + * Record statistics and check all rules of the resource. + * + * @param name the unique name for the protected resource + * @param resourceType classification of the resource (e.g. Web or RPC) + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable, + * only inbound traffic could be blocked by {@link SystemRule} + * @throws BlockException if the block criteria is met, eg. when any rule's threshold is exceeded + * @since 1.7.0 + */ + public static Entry entry(String name, int resourceType, EntryType type) throws BlockException { + return Env.sph.entryWithType(name, resourceType, type, 1, OBJECTS0); + } + + /** + * Record statistics and check all rules of the resource. + * + * @param name the unique name for the protected resource + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable, + * only inbound traffic could be blocked by {@link SystemRule} + * @param resourceType classification of the resource (e.g. Web or RPC) + * @param args extra parameters. + * @throws BlockException if the block criteria is met, eg. when any rule's threshold is exceeded + * @since 1.7.0 + */ + public static Entry entry(String name, int resourceType, EntryType type, Object[] args) + throws BlockException { + return Env.sph.entryWithType(name, resourceType, type, 1, args); + } + + /** + * Record statistics and check all rules of the resource. + * + * @param name the unique name for the protected resource + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable, + * only inbound traffic could be blocked by {@link SystemRule} + * @param resourceType classification of the resource (e.g. Web or RPC) + * @throws BlockException if the block criteria is met, eg. when any rule's threshold is exceeded + * @since 1.7.0 + */ + public static AsyncEntry asyncEntry(String name, int resourceType, EntryType type) + throws BlockException { + return Env.sph.asyncEntryWithType(name, resourceType, type, 1, false, OBJECTS0); + } + + /** + * Record statistics and check all rules of the resource. + * + * @param name the unique name for the protected resource + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable, + * only inbound traffic could be blocked by {@link SystemRule} + * @param resourceType classification of the resource (e.g. Web or RPC) + * @param args extra parameters + * @throws BlockException if the block criteria is met, eg. when any rule's threshold is exceeded + * @since 1.7.0 + */ + public static AsyncEntry asyncEntry(String name, int resourceType, EntryType type, Object[] args) + throws BlockException { + return Env.sph.asyncEntryWithType(name, resourceType, type, 1, false, args); + } + + /** + * Record statistics and check all rules of the resource. + * + * @param name the unique name for the protected resource + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable, + * only inbound traffic could be blocked by {@link SystemRule} + * @param resourceType classification of the resource (e.g. Web or RPC) + * @param acquireCount tokens required + * @param args extra parameters + * @throws BlockException if the block criteria is met, eg. when any rule's threshold is exceeded + * @since 1.7.0 + */ + public static AsyncEntry asyncEntry(String name, int resourceType, EntryType type, int acquireCount, + Object[] args) throws BlockException { + return Env.sph.asyncEntryWithType(name, resourceType, type, acquireCount, false, args); + } } diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/annotation/SentinelResource.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/annotation/SentinelResource.java index a0fa11d675..95f6584841 100644 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/annotation/SentinelResource.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/annotation/SentinelResource.java @@ -40,6 +40,12 @@ */ EntryType entryType() default EntryType.OUT; + /** + * @return the classification (type) of the resource + * @since 1.7.0 + */ + int resourceType() default 0; + /** * @return name of the block exception function, empty by default */ diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/config/SentinelConfig.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/config/SentinelConfig.java index 5079191238..a0d7c2268d 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/config/SentinelConfig.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/config/SentinelConfig.java @@ -15,26 +15,23 @@ */ package com.alibaba.csp.sentinel.config; -import java.io.File; -import java.io.FileInputStream; -import java.util.Map; -import java.util.Properties; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArraySet; - -import com.alibaba.csp.sentinel.log.LogBase; import com.alibaba.csp.sentinel.log.RecordLog; import com.alibaba.csp.sentinel.util.AppNameUtil; import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.csp.sentinel.util.StringUtil; + +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; /** - * The universal local config center of Sentinel. The config is retrieved from command line arguments - * and {@code ${user.home}/logs/csp/${appName}.properties} file by default. + * The universal local configuration center of Sentinel. The config is retrieved from command line arguments + * and customized properties file by default. * * @author leyou * @author Eric Zhao */ -public class SentinelConfig { +public final class SentinelConfig { /** * The default application type. @@ -52,18 +49,19 @@ public class SentinelConfig { public static final String TOTAL_METRIC_FILE_COUNT = "csp.sentinel.metric.file.total.count"; public static final String COLD_FACTOR = "csp.sentinel.flow.cold.factor"; public static final String STATISTIC_MAX_RT = "csp.sentinel.statistic.max.rt"; + public static final String SPI_CLASSLOADER = "csp.sentinel.spi.classloader"; static final String DEFAULT_CHARSET = "UTF-8"; static final long DEFAULT_SINGLE_METRIC_FILE_SIZE = 1024 * 1024 * 50; static final int DEFAULT_TOTAL_METRIC_FILE_COUNT = 6; static final int DEFAULT_COLD_FACTOR = 3; - static final int DEFAULT_STATISTIC_MAX_RT = 4900; + + public static final int DEFAULT_STATISTIC_MAX_RT = 4900; static { try { initialize(); loadProps(); - resolveAppType(); RecordLog.info("[SentinelConfig] Application type resolved: " + appType); } catch (Throwable ex) { @@ -90,48 +88,17 @@ private static void resolveAppType() { private static void initialize() { // Init default properties. - SentinelConfig.setConfig(CHARSET, DEFAULT_CHARSET); - SentinelConfig.setConfig(SINGLE_METRIC_FILE_SIZE, String.valueOf(DEFAULT_SINGLE_METRIC_FILE_SIZE)); - SentinelConfig.setConfig(TOTAL_METRIC_FILE_COUNT, String.valueOf(DEFAULT_TOTAL_METRIC_FILE_COUNT)); - SentinelConfig.setConfig(COLD_FACTOR, String.valueOf(DEFAULT_COLD_FACTOR)); - SentinelConfig.setConfig(STATISTIC_MAX_RT, String.valueOf(DEFAULT_STATISTIC_MAX_RT)); + setConfig(CHARSET, DEFAULT_CHARSET); + setConfig(SINGLE_METRIC_FILE_SIZE, String.valueOf(DEFAULT_SINGLE_METRIC_FILE_SIZE)); + setConfig(TOTAL_METRIC_FILE_COUNT, String.valueOf(DEFAULT_TOTAL_METRIC_FILE_COUNT)); + setConfig(COLD_FACTOR, String.valueOf(DEFAULT_COLD_FACTOR)); + setConfig(STATISTIC_MAX_RT, String.valueOf(DEFAULT_STATISTIC_MAX_RT)); } private static void loadProps() { - // Resolve app name. - AppNameUtil.resolveAppName(); - try { - String appName = AppNameUtil.getAppName(); - if (appName == null) { - appName = ""; - } - // We first retrieve the properties from the property file. - String fileName = LogBase.getLogBaseDir() + appName + ".properties"; - File file = new File(fileName); - if (file.exists()) { - RecordLog.info("[SentinelConfig] Reading config from " + fileName); - FileInputStream fis = new FileInputStream(fileName); - Properties fileProps = new Properties(); - fileProps.load(fis); - fis.close(); - - for (Object key : fileProps.keySet()) { - SentinelConfig.setConfig((String)key, (String)fileProps.get(key)); - } - } - } catch (Throwable ioe) { - RecordLog.info(ioe.getMessage(), ioe); - } - - // JVM parameter override file config. - for (Map.Entry entry : new CopyOnWriteArraySet<>(System.getProperties().entrySet())) { - String configKey = entry.getKey().toString(); - String configValue = entry.getValue().toString(); - String configValueOld = getConfig(configKey); - SentinelConfig.setConfig(configKey, configValue); - if (configValueOld != null) { - RecordLog.info("[SentinelConfig] JVM parameter overrides {0}: {1} -> {2}", configKey, configValueOld, configValue); - } + Properties properties = SentinelConfigLoader.getProperties(); + for (Object key : properties.keySet()) { + setConfig((String) key, (String) properties.get(key)); } } @@ -189,7 +156,7 @@ public static long singleMetricFileSize() { return Long.parseLong(props.get(SINGLE_METRIC_FILE_SIZE)); } catch (Throwable throwable) { RecordLog.warn("[SentinelConfig] Parse singleMetricFileSize fail, use default value: " - + DEFAULT_SINGLE_METRIC_FILE_SIZE, throwable); + + DEFAULT_SINGLE_METRIC_FILE_SIZE, throwable); return DEFAULT_SINGLE_METRIC_FILE_SIZE; } } @@ -199,7 +166,7 @@ public static int totalMetricFileCount() { return Integer.parseInt(props.get(TOTAL_METRIC_FILE_COUNT)); } catch (Throwable throwable) { RecordLog.warn("[SentinelConfig] Parse totalMetricFileCount fail, use default value: " - + DEFAULT_TOTAL_METRIC_FILE_COUNT, throwable); + + DEFAULT_TOTAL_METRIC_FILE_COUNT, throwable); return DEFAULT_TOTAL_METRIC_FILE_COUNT; } } @@ -221,13 +188,28 @@ public static int coldFactor() { } } + /** + *

Get the max RT value that Sentinel could accept.

+ *

Response time that exceeds {@code statisticMaxRt} will be recorded as this value. + * The default value is {@link #DEFAULT_STATISTIC_MAX_RT}.

+ * + * @return the max allowed RT value + * @since 1.4.1 + */ public static int statisticMaxRt() { + String v = props.get(STATISTIC_MAX_RT); try { - return Integer.parseInt(props.get(STATISTIC_MAX_RT)); + if (StringUtil.isEmpty(v)) { + return DEFAULT_STATISTIC_MAX_RT; + } + return Integer.parseInt(v); } catch (Throwable throwable) { - RecordLog.warn("[SentinelConfig] Parse statisticMaxRt fail, use default value: " - + DEFAULT_STATISTIC_MAX_RT, throwable); + RecordLog.warn("[SentinelConfig] Invalid statisticMaxRt value: {0}, using the default value instead: " + + DEFAULT_STATISTIC_MAX_RT, v, throwable); + SentinelConfig.setConfig(STATISTIC_MAX_RT, String.valueOf(DEFAULT_STATISTIC_MAX_RT)); return DEFAULT_STATISTIC_MAX_RT; } } + + private SentinelConfig() {} } diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/config/SentinelConfigLoader.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/config/SentinelConfigLoader.java new file mode 100644 index 0000000000..67cd4805df --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/config/SentinelConfigLoader.java @@ -0,0 +1,100 @@ +/* + * 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.config; + +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.util.AppNameUtil; +import com.alibaba.csp.sentinel.util.ConfigUtil; +import com.alibaba.csp.sentinel.util.StringUtil; + +import java.io.File; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.CopyOnWriteArraySet; + +import static com.alibaba.csp.sentinel.util.ConfigUtil.addSeparator; + +/** + *

The loader that responsible for loading Sentinel common configurations.

+ * + * @author lianglin + * @since 1.7.0 + */ +public final class SentinelConfigLoader { + + public static final String SENTINEL_CONFIG_ENV_KEY = "CSP_SENTINEL_CONFIG_FILE"; + public static final String SENTINEL_CONFIG_PROPERTY_KEY = "csp.sentinel.config.file"; + + private static final String DIR_NAME = "logs" + File.separator + "csp"; + private static final String USER_HOME = "user.home"; + + private static final String DEFAULT_SENTINEL_CONFIG_FILE = "classpath:sentinel.properties"; + + private static Properties properties = new Properties(); + + static { + try { + load(); + } catch (Throwable t) { + RecordLog.warn("[SentinelConfigLoader] Failed to initialize configuration items", t); + } + } + + private static void load() { + // Order: system property -> system env -> default file (classpath:sentinel.properties) -> legacy path + String fileName = System.getProperty(SENTINEL_CONFIG_PROPERTY_KEY); + if (StringUtil.isBlank(fileName)) { + fileName = System.getenv(SENTINEL_CONFIG_ENV_KEY); + if (StringUtil.isBlank(fileName)) { + fileName = DEFAULT_SENTINEL_CONFIG_FILE; + } + } + + Properties p = ConfigUtil.loadProperties(fileName); + + // Compatible with legacy config file path. + if (p == null) { + String path = addSeparator(System.getProperty(USER_HOME)) + DIR_NAME + File.separator; + fileName = path + AppNameUtil.getAppName() + ".properties"; + File file = new File(fileName); + if (file.exists()) { + p = ConfigUtil.loadProperties(fileName); + } + } + + if (p != null && !p.isEmpty()) { + RecordLog.info("[SentinelConfigLoader] Loading Sentinel config from " + fileName); + properties.putAll(p); + } + + for (Map.Entry entry : new CopyOnWriteArraySet<>(System.getProperties().entrySet())) { + String configKey = entry.getKey().toString(); + String newConfigValue = entry.getValue().toString(); + String oldConfigValue = properties.getProperty(configKey); + properties.put(configKey, newConfigValue); + if (oldConfigValue != null) { + RecordLog.info("[SentinelConfigLoader] JVM parameter overrides {0}: {1} -> {2}", + configKey, oldConfigValue, newConfigValue); + } + } + } + + + public static Properties getProperties() { + return properties; + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/init/InitExecutor.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/init/InitExecutor.java index 0d84ad78fb..e68fd599e3 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/init/InitExecutor.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/init/InitExecutor.java @@ -21,6 +21,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.spi.ServiceLoaderUtil; /** * Load registered init functions and execute in order. @@ -42,7 +43,7 @@ public static void doInit() { return; } try { - ServiceLoader loader = ServiceLoader.load(InitFunc.class); + ServiceLoader loader = ServiceLoaderUtil.getServiceLoader(InitFunc.class); List initList = new ArrayList(); for (InitFunc initFunc : loader) { RecordLog.info("[InitExecutor] Found init func: " + initFunc.getClass().getCanonicalName()); diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/ConsoleHandler.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/ConsoleHandler.java new file mode 100644 index 0000000000..7654fa2a03 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/ConsoleHandler.java @@ -0,0 +1,85 @@ +/* + * 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.log; + +import java.io.UnsupportedEncodingException; +import java.util.logging.*; + +/** + * This Handler publishes log records to console by using {@link java.util.logging.StreamHandler}. + * + * Print log of WARNING level or above to System.err, + * and print log of INFO level or below to System.out. + * + * To use this handler, add the following VM argument: + *
+ * -Dcsp.sentinel.log.output.type=console
+ * 
+ * + * @author cdfive + */ +class ConsoleHandler extends Handler { + + /** + * A Handler which publishes log records to System.out. + */ + private StreamHandler stdoutHandler; + + /** + * A Handler which publishes log records to System.err. + */ + private StreamHandler stderrHandler; + + public ConsoleHandler() { + this.stdoutHandler = new StreamHandler(System.out, new CspFormatter()); + this.stderrHandler = new StreamHandler(System.err, new CspFormatter()); + } + + @Override + public synchronized void setFormatter(Formatter newFormatter) throws SecurityException { + this.stdoutHandler.setFormatter(newFormatter); + this.stderrHandler.setFormatter(newFormatter); + } + + @Override + public synchronized void setEncoding(String encoding) throws SecurityException, UnsupportedEncodingException { + this.stdoutHandler.setEncoding(encoding); + this.stderrHandler.setEncoding(encoding); + } + + @Override + public void publish(LogRecord record) { + if (record.getLevel().intValue() >= Level.WARNING.intValue()) { + stderrHandler.publish(record); + stderrHandler.flush(); + } else { + stdoutHandler.publish(record); + stdoutHandler.flush(); + } + } + + @Override + public void flush() { + stdoutHandler.flush(); + stderrHandler.flush(); + } + + @Override + public void close() throws SecurityException { + stdoutHandler.close(); + stderrHandler.close(); + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/CspFormatter.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/CspFormatter.java index b2a8897962..5d24c21217 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/CspFormatter.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/CspFormatter.java @@ -28,7 +28,7 @@ class CspFormatter extends Formatter { private final ThreadLocal dateFormatThreadLocal = new ThreadLocal() { @Override public SimpleDateFormat initialValue() { - return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); } }; @@ -37,6 +37,7 @@ public String format(LogRecord record) { final DateFormat df = dateFormatThreadLocal.get(); StringBuilder builder = new StringBuilder(1000); builder.append(df.format(new Date(record.getMillis()))).append(" "); + builder.append(record.getLevel().getName()).append(" "); builder.append(formatMessage(record)); String throwable = ""; diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/LogBase.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/LogBase.java index ae71b1a415..2de0bddd4f 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/LogBase.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/LogBase.java @@ -15,83 +15,136 @@ */ package com.alibaba.csp.sentinel.log; +import com.alibaba.csp.sentinel.util.PidUtil; + import java.io.File; import java.io.IOException; +import java.util.Properties; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.Logger; -import com.alibaba.csp.sentinel.util.PidUtil; +import static com.alibaba.csp.sentinel.util.ConfigUtil.addSeparator; /** - * Default log base dir is ${user.home}/logs/csp/, we can use {@link #LOG_DIR} System property to override it. - * Default log file name dose not contain pid, but if multi instances of the same app are running in the same - * machine, we may want to distinguish the log file by pid number, in this case, {@link #LOG_NAME_USE_PID} - * System property could be configured as "true" to turn on this switch. + *

The base class for logging.

+ * + *

+ * The default log base directory is {@code ${user.home}/logs/csp/}. We can use the {@link #LOG_DIR} + * property to override it. The default log file name dose not contain pid, but if multi-instances of the same service + * are running in the same machine, we may want to distinguish the log file by process ID number. + * In this case, {@link #LOG_NAME_USE_PID} property could be configured as "true" to turn on this switch. + *

* * @author leyou */ public class LogBase { - public static final String LOG_CHARSET = "utf-8"; + public static final String LOG_DIR = "csp.sentinel.log.dir"; + public static final String LOG_NAME_USE_PID = "csp.sentinel.log.use.pid"; + public static final String LOG_OUTPUT_TYPE = "csp.sentinel.log.output.type"; + public static final String LOG_CHARSET = "csp.sentinel.log.charset"; + + /** + * Output biz log (e.g. RecordLog and CommandCenterLog) to file. + */ + public static final String LOG_OUTPUT_TYPE_FILE = "file"; + /** + * Output biz log (e.g. RecordLog and CommandCenterLog) to console. + */ + public static final String LOG_OUTPUT_TYPE_CONSOLE = "console"; + public static final String LOG_CHARSET_UTF8 = "utf-8"; private static final String DIR_NAME = "logs" + File.separator + "csp"; private static final String USER_HOME = "user.home"; - public static final String LOG_DIR = "csp.sentinel.log.dir"; - public static final String LOG_NAME_USE_PID = "csp.sentinel.log.use.pid"; - - private static boolean logNameUsePid = false; + private static boolean logNameUsePid; + private static String logOutputType; private static String logBaseDir; + private static String logCharSet; static { try { - init(); + initialize(); + loadProperties(); } catch (Throwable t) { System.err.println("[LogBase] FATAL ERROR when initializing log class"); t.printStackTrace(); } } - private static void init() { - // first use -D, then use user home. - String logDir = System.getProperty(LOG_DIR); + private static void initialize() { + logNameUsePid = false; + logOutputType = LOG_OUTPUT_TYPE_FILE; + logBaseDir = addSeparator(System.getProperty(USER_HOME)) + DIR_NAME + File.separator; + logCharSet = LOG_CHARSET_UTF8; + } + + private static void loadProperties() { + Properties properties = LogConfigLoader.getProperties(); - if (logDir == null || logDir.isEmpty()) { - logDir = System.getProperty(USER_HOME); - logDir = addSeparator(logDir) + DIR_NAME + File.separator; + logOutputType = properties.get(LOG_OUTPUT_TYPE) == null ? logOutputType : properties.getProperty(LOG_OUTPUT_TYPE); + if (!LOG_OUTPUT_TYPE_FILE.equalsIgnoreCase(logOutputType) && !LOG_OUTPUT_TYPE_CONSOLE.equalsIgnoreCase(logOutputType)) { + logOutputType = LOG_OUTPUT_TYPE_FILE; } - logDir = addSeparator(logDir); - File dir = new File(logDir); + System.out.println("INFO: log output type is: " + logOutputType); + + logCharSet = properties.getProperty(LOG_CHARSET) == null ? logCharSet : properties.getProperty(LOG_CHARSET); + System.out.println("INFO: log charset is: " + logCharSet); + + + logBaseDir = properties.getProperty(LOG_DIR) == null ? logBaseDir : properties.getProperty(LOG_DIR); + logBaseDir = addSeparator(logBaseDir); + File dir = new File(logBaseDir); if (!dir.exists()) { if (!dir.mkdirs()) { - System.err.println("ERROR: create log base dir error: " + logDir); + System.err.println("ERROR: create log base dir error: " + logBaseDir); } } - // logBaseDir must end with File.separator - logBaseDir = logDir; System.out.println("INFO: log base dir is: " + logBaseDir); - String usePid = System.getProperty(LOG_NAME_USE_PID, ""); + + String usePid = properties.getProperty(LOG_NAME_USE_PID); logNameUsePid = "true".equalsIgnoreCase(usePid); System.out.println("INFO: log name use pid is: " + logNameUsePid); } + /** - * Whether log file name should contain pid. This switch is configured by {@link #LOG_NAME_USE_PID} System property. + * Whether log file name should contain pid. This switch is configured by {@link #LOG_NAME_USE_PID} system property. * - * @return if log file name should contain pid, return true, otherwise return false. + * @return true if log file name should contain pid, return true, otherwise false */ public static boolean isLogNameUsePid() { return logNameUsePid; } - private static String addSeparator(String logDir) { - if (!logDir.endsWith(File.separator)) { - logDir += File.separator; - } - return logDir; + /** + * Get the log file base directory path, which is guaranteed ended with {@link File#separator}. + * + * @return log file base directory path + */ + public static String getLogBaseDir() { + return logBaseDir; + } + + /** + * Get the log file output type. + * + * @return log output type, "file" by default + */ + public static String getLogOutputType() { + return logOutputType; + } + + /** + * Get the log file charset. + * + * @return the log file charset, "utf-8" by default + */ + public static String getLogCharset() { + return logCharSet; } protected static void log(Logger logger, Handler handler, Level level, String detail, Object... params) { @@ -114,33 +167,45 @@ protected static void log(Logger logger, Handler handler, Level level, String de logger.log(level, detail, throwable); } - /** - * Get log file base directory path, the returned path is guaranteed end with {@link File#separator} - * - * @return log file base directory path. - */ - public static String getLogBaseDir() { - return logBaseDir; - } protected static Handler makeLogger(String logName, Logger heliumRecordLog) { CspFormatter formatter = new CspFormatter(); - String fileName = LogBase.getLogBaseDir() + logName; - if (isLogNameUsePid()) { - fileName += ".pid" + PidUtil.getPid(); - } + Handler handler = null; - try { - handler = new DateFileLogHandler(fileName + ".%d", 1024 * 1024 * 200, 4, true); - handler.setFormatter(formatter); - handler.setEncoding(LOG_CHARSET); - } catch (IOException e) { - e.printStackTrace(); + + // Create handler according to logOutputType, set formatter to CspFormatter, set encoding to LOG_CHARSET + switch (logOutputType) { + case LOG_OUTPUT_TYPE_FILE: + String fileName = LogBase.getLogBaseDir() + logName; + if (isLogNameUsePid()) { + fileName += ".pid" + PidUtil.getPid(); + } + try { + handler = new DateFileLogHandler(fileName + ".%d", 1024 * 1024 * 200, 4, true); + handler.setFormatter(formatter); + handler.setEncoding(logCharSet); + } catch (IOException e) { + e.printStackTrace(); + } + break; + case LOG_OUTPUT_TYPE_CONSOLE: + try { + handler = new ConsoleHandler(); + handler.setFormatter(formatter); + handler.setEncoding(logCharSet); + } catch (IOException e) { + e.printStackTrace(); + } + break; + default: + break; } + if (handler != null) { LoggerUtils.disableOtherHandlers(heliumRecordLog, handler); } heliumRecordLog.setLevel(Level.ALL); return handler; } + } diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/LogConfigLoader.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/LogConfigLoader.java new file mode 100644 index 0000000000..3cdb541bb1 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/LogConfigLoader.java @@ -0,0 +1,76 @@ +/* + * 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.log; + +import com.alibaba.csp.sentinel.util.ConfigUtil; +import com.alibaba.csp.sentinel.util.StringUtil; + +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + *

The loader that responsible for loading Sentinel log configurations.

+ * + * @author lianglin + * @since 1.7.0 + */ +public class LogConfigLoader { + + public static final String LOG_CONFIG_ENV_KEY = "CSP_SENTINEL_CONFIG_FILE"; + public static final String LOG_CONFIG_PROPERTY_KEY = "csp.sentinel.config.file"; + + private static final String DEFAULT_LOG_CONFIG_FILE = "classpath:sentinel.properties"; + + private static final Properties properties = new Properties(); + + static { + try { + load(); + } catch (Throwable t) { + // NOTE: do not use RecordLog here, or there will be circular class dependency! + System.err.println("[LogConfigLoader] Failed to initialize configuration items"); + t.printStackTrace(); + } + } + + private static void load() { + // Order: system property -> system env -> default file (classpath:sentinel.properties) -> legacy path + String fileName = System.getProperty(LOG_CONFIG_PROPERTY_KEY); + if (StringUtil.isBlank(fileName)) { + fileName = System.getenv(LOG_CONFIG_ENV_KEY); + if (StringUtil.isBlank(fileName)) { + fileName = DEFAULT_LOG_CONFIG_FILE; + } + } + + Properties p = ConfigUtil.loadProperties(fileName); + if (p != null && !p.isEmpty()) { + properties.putAll(p); + } + + CopyOnWriteArraySet> copy = new CopyOnWriteArraySet<>(System.getProperties().entrySet()); + for (Map.Entry entry : copy) { + String configKey = entry.getKey().toString(); + String newConfigValue = entry.getValue().toString(); + properties.put(configKey, newConfigValue); + } + } + + public static Properties getProperties() { + return properties; + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/ClusterNode.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/ClusterNode.java index c342697de2..6f5f4ef4f8 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/ClusterNode.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/ClusterNode.java @@ -19,8 +19,10 @@ import java.util.Map; import java.util.concurrent.locks.ReentrantLock; +import com.alibaba.csp.sentinel.ResourceTypeConstants; import com.alibaba.csp.sentinel.context.ContextUtil; import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.util.AssertUtil; /** *

@@ -42,6 +44,19 @@ */ public class ClusterNode extends StatisticNode { + private final String name; + private final int resourceType; + + public ClusterNode(String name) { + this(name, ResourceTypeConstants.COMMON); + } + + public ClusterNode(String name, int resourceType) { + AssertUtil.notEmpty(name, "name cannot be empty"); + this.name = name; + this.resourceType = resourceType; + } + /** *

The origin map holds the pair: (origin, originNode) for one specific resource.

*

@@ -50,10 +65,30 @@ public class ClusterNode extends StatisticNode { * at the very beginning while concurrent map will hold the lock all the time. *

*/ - private Map originCountMap = new HashMap(); + private Map originCountMap = new HashMap<>(); private final ReentrantLock lock = new ReentrantLock(); + /** + * Get resource name of the resource node. + * + * @return resource name + * @since 1.7.0 + */ + public String getName() { + return name; + } + + /** + * Get classification (type) of the resource. + * + * @return resource type + * @since 1.7.0 + */ + public int getResourceType() { + return resourceType; + } + /** *

Get {@link Node} of the specific origin. Usually the origin is the Service Consumer's app name.

*

If the origin node for given origin is absent, then a new {@link StatisticNode} @@ -84,7 +119,7 @@ public Node getOrCreateOriginNode(String origin) { return statisticNode; } - public synchronized Map getOriginCountMap() { + public Map getOriginCountMap() { return originCountMap; } diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/Node.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/Node.java index 0673915499..0927d8e43c 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/Node.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/Node.java @@ -15,11 +15,13 @@ */ package com.alibaba.csp.sentinel.node; +import java.util.List; import java.util.Map; import com.alibaba.csp.sentinel.Entry; import com.alibaba.csp.sentinel.node.metric.MetricNode; import com.alibaba.csp.sentinel.slots.statistic.metric.DebugSupport; +import com.alibaba.csp.sentinel.util.function.Predicate; /** * Holds real-time statistics for resources. @@ -146,6 +148,15 @@ public interface Node extends OccupySupport, DebugSupport { */ Map metrics(); + /** + * Fetch all raw metric items that satisfies the time predicate. + * + * @param timePredicate time predicate + * @return raw metric items that satisfies the time predicate + * @since 1.7.0 + */ + List rawMetricsInMin(Predicate timePredicate); + /** * Add pass count. * diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/StatisticNode.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/StatisticNode.java index 80a8443cad..a722269afd 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/StatisticNode.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/StatisticNode.java @@ -20,6 +20,7 @@ import com.alibaba.csp.sentinel.slots.statistic.metric.ArrayMetric; import com.alibaba.csp.sentinel.slots.statistic.metric.Metric; import com.alibaba.csp.sentinel.util.TimeUtil; +import com.alibaba.csp.sentinel.util.function.Predicate; import java.util.List; import java.util.Map; @@ -131,6 +132,11 @@ public Map metrics() { return metrics; } + @Override + public List rawMetricsInMin(Predicate timePredicate) { + return rollingCounterInMinute.detailsOnCondition(timePredicate); + } + private boolean isNodeInTime(MetricNode node, long currentTime) { return node.getTimestamp() > lastFetchTime && node.getTimestamp() < currentTime; } @@ -207,7 +213,8 @@ public double successQps() { @Override public double maxSuccessQps() { - return rollingCounterInSecond.maxSuccess() * rollingCounterInSecond.getSampleCount(); + return (double) rollingCounterInSecond.maxSuccess() * rollingCounterInSecond.getSampleCount() + / rollingCounterInSecond.getWindowIntervalInSec(); } @Override diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricNode.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricNode.java index e1a6707c27..2f8de6bc2f 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricNode.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricNode.java @@ -27,6 +27,13 @@ */ public class MetricNode { + private String resource; + /** + * Resource classification (e.g. SQL or RPC) + * @since 1.7.0 + */ + private int classification; + private long timestamp; private long passQps; private long blockQps; @@ -38,8 +45,10 @@ public class MetricNode { * @since 1.5.0 */ private long occupiedPassQps; - - private String resource; + /** + * @since 1.7.0 + */ + private int concurrency; public long getTimestamp() { return timestamp; @@ -105,12 +114,38 @@ public void setResource(String resource) { this.resource = resource; } + public int getClassification() { + return classification; + } + + public MetricNode setClassification(int classification) { + this.classification = classification; + return this; + } + + public int getConcurrency() { + return concurrency; + } + + public MetricNode setConcurrency(int concurrency) { + this.concurrency = concurrency; + return this; + } + @Override public String toString() { - return "MetricNode{" + "timestamp=" + timestamp + ", passQps=" + passQps + ", blockQps=" + blockQps - + ", successQps=" + successQps + ", exceptionQps=" + exceptionQps + ", rt=" + rt - + ", occupiedPassQps=" + occupiedPassQps + ", resource='" - + resource + '\'' + '}'; + return "MetricNode{" + + "resource='" + resource + '\'' + + ", classification=" + classification + + ", timestamp=" + timestamp + + ", passQps=" + passQps + + ", blockQps=" + blockQps + + ", successQps=" + successQps + + ", exceptionQps=" + exceptionQps + + ", rt=" + rt + + ", concurrency=" + concurrency + + ", occupiedPassQps=" + occupiedPassQps + + '}'; } /** @@ -132,7 +167,9 @@ public String toThinString() { sb.append(successQps).append("|"); sb.append(exceptionQps).append("|"); sb.append(rt).append("|"); - sb.append(occupiedPassQps); + sb.append(occupiedPassQps).append("|"); + sb.append(concurrency).append("|"); + sb.append(classification); return sb.toString(); } @@ -152,9 +189,15 @@ public static MetricNode fromThinString(String line) { node.setSuccessQps(Long.parseLong(strs[4])); node.setExceptionQps(Long.parseLong(strs[5])); node.setRt(Long.parseLong(strs[6])); - if (strs.length == 8) { + if (strs.length >= 8) { node.setOccupiedPassQps(Long.parseLong(strs[7])); } + if (strs.length >= 9) { + node.setConcurrency(Integer.parseInt(strs[8])); + } + if (strs.length == 10) { + node.setClassification(Integer.parseInt(strs[9])); + } return node; } @@ -180,7 +223,9 @@ public String toFatString() { sb.append(getSuccessQps()).append("|"); sb.append(getExceptionQps()).append("|"); sb.append(getRt()).append("|"); - sb.append(getOccupiedPassQps()); + sb.append(getOccupiedPassQps()).append("|"); + sb.append(concurrency).append("|"); + sb.append(classification); sb.append('\n'); return sb.toString(); } @@ -202,9 +247,15 @@ public static MetricNode fromFatString(String line) { node.setSuccessQps(Long.parseLong(strs[5])); node.setExceptionQps(Long.parseLong(strs[6])); node.setRt(Long.parseLong(strs[7])); - if (strs.length == 9) { + if (strs.length >= 9) { node.setOccupiedPassQps(Long.parseLong(strs[8])); } + if (strs.length >= 10) { + node.setConcurrency(Integer.parseInt(strs[9])); + } + if (strs.length == 11) { + node.setClassification(Integer.parseInt(strs[10])); + } return node; } diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricTimerListener.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricTimerListener.java index 4e3d3a1ef8..93992cc9b6 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricTimerListener.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricTimerListener.java @@ -38,14 +38,13 @@ public class MetricTimerListener implements Runnable { @Override public void run() { - Map> maps = new TreeMap>(); + Map> maps = new TreeMap<>(); for (Entry e : ClusterBuilderSlot.getClusterNodeMap().entrySet()) { - String name = e.getKey().getName(); ClusterNode node = e.getValue(); Map metrics = node.metrics(); - aggregate(maps, metrics, name); + aggregate(maps, metrics, node); } - aggregate(maps, Constants.ENTRY_NODE.metrics(), Constants.TOTAL_IN_RESOURCE_NAME); + aggregate(maps, Constants.ENTRY_NODE.metrics(), Constants.ENTRY_NODE); if (!maps.isEmpty()) { for (Entry> entry : maps.entrySet()) { try { @@ -57,11 +56,12 @@ public void run() { } } - private void aggregate(Map> maps, Map metrics, String resourceName) { + private void aggregate(Map> maps, Map metrics, ClusterNode node) { for (Entry entry : metrics.entrySet()) { long time = entry.getKey(); MetricNode metricNode = entry.getValue(); - metricNode.setResource(resourceName); + metricNode.setResource(node.getName()); + metricNode.setClassification(node.getResourceType()); if (maps.get(time) == null) { maps.put(time, new ArrayList()); } diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/MethodResourceWrapper.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/MethodResourceWrapper.java index 91e20a4c5d..5f89daf7e4 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/MethodResourceWrapper.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/MethodResourceWrapper.java @@ -18,6 +18,7 @@ import java.lang.reflect.Method; import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.ResourceTypeConstants; import com.alibaba.csp.sentinel.util.IdUtil; import com.alibaba.csp.sentinel.util.MethodUtil; @@ -28,17 +29,15 @@ */ public class MethodResourceWrapper extends ResourceWrapper { - private transient Method method; + private final transient Method method; - public MethodResourceWrapper(Method method, EntryType type) { - this.method = method; - this.name = MethodUtil.resolveMethodName(method); - this.type = type; + public MethodResourceWrapper(Method method, EntryType e) { + this(method, e, ResourceTypeConstants.COMMON); } - @Override - public String getName() { - return name; + public MethodResourceWrapper(Method method, EntryType e, int resType) { + super(MethodUtil.resolveMethodName(method), e, resType); + this.method = method; } public Method getMethod() { @@ -47,12 +46,15 @@ public Method getMethod() { @Override public String getShowName() { - return IdUtil.truncate(this.name); + return name; } @Override - public EntryType getType() { - return type; + public String toString() { + return "MethodResourceWrapper{" + + "name='" + name + '\'' + + ", entryType=" + entryType + + ", resourceType=" + resourceType + + '}'; } - } diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ResourceWrapper.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ResourceWrapper.java index a796344c89..a3eb0196f1 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ResourceWrapper.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ResourceWrapper.java @@ -16,28 +16,64 @@ package com.alibaba.csp.sentinel.slotchain; import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.util.AssertUtil; /** - * A wrapper of resource name and {@link EntryType}. + * A wrapper of resource name and type. * * @author qinan.qn * @author jialiang.linjl + * @author Eric Zhao */ public abstract class ResourceWrapper { - protected String name; - protected EntryType type = EntryType.OUT; + protected final String name; - public abstract String getName(); + protected final EntryType entryType; + protected final int resourceType; - public abstract String getShowName(); + public ResourceWrapper(String name, EntryType entryType, int resourceType) { + AssertUtil.notEmpty(name, "resource name cannot be empty"); + AssertUtil.notNull(entryType, "entryType cannot be null"); + this.name = name; + this.entryType = entryType; + this.resourceType = resourceType; + } + + /** + * Get the resource name. + * + * @return the resource name + */ + public String getName() { + return name; + } /** * Get {@link EntryType} of this wrapper. * * @return {@link EntryType} of this wrapper. */ - public abstract EntryType getType(); + public EntryType getEntryType() { + return entryType; + } + + /** + * Get the classification of this resource. + * + * @return the classification of this resource + * @since 1.7.0 + */ + public int getResourceType() { + return resourceType; + } + + /** + * Get the beautified resource name to be showed. + * + * @return the beautified resource name + */ + public abstract String getShowName(); /** * Only {@link #getName()} is considered. diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/SlotChainProvider.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/SlotChainProvider.java index dedad3c19d..9aabbb3376 100644 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/SlotChainProvider.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/SlotChainProvider.java @@ -15,12 +15,9 @@ */ package com.alibaba.csp.sentinel.slotchain; -import java.util.ArrayList; -import java.util.List; -import java.util.ServiceLoader; - import com.alibaba.csp.sentinel.log.RecordLog; import com.alibaba.csp.sentinel.slots.DefaultSlotChainBuilder; +import com.alibaba.csp.sentinel.util.SpiLoader; /** * A provider for creating slot chains via resolved slot chain builder SPI. @@ -30,9 +27,7 @@ */ public final class SlotChainProvider { - private static volatile SlotChainBuilder builder = null; - - private static final ServiceLoader LOADER = ServiceLoader.load(SlotChainBuilder.class); + private static volatile SlotChainBuilder slotChainBuilder = null; /** * The load and pick process is not thread-safe, but it's okay since the method should be only invoked @@ -41,37 +36,22 @@ public final class SlotChainProvider { * @return new created slot chain */ public static ProcessorSlotChain newSlotChain() { - if (builder != null) { - return builder.build(); + if (slotChainBuilder != null) { + return slotChainBuilder.build(); } - resolveSlotChainBuilder(); + // Resolve the slot chain builder SPI. + slotChainBuilder = SpiLoader.loadFirstInstanceOrDefault(SlotChainBuilder.class, DefaultSlotChainBuilder.class); - if (builder == null) { + if (slotChainBuilder == null) { + // Should not go through here. RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default"); - builder = new DefaultSlotChainBuilder(); - } - return builder.build(); - } - - private static void resolveSlotChainBuilder() { - List list = new ArrayList(); - boolean hasOther = false; - for (SlotChainBuilder builder : LOADER) { - if (builder.getClass() != DefaultSlotChainBuilder.class) { - hasOther = true; - list.add(builder); - } - } - if (hasOther) { - builder = list.get(0); + slotChainBuilder = new DefaultSlotChainBuilder(); } else { - // No custom builder, using default. - builder = new DefaultSlotChainBuilder(); + RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: " + + slotChainBuilder.getClass().getCanonicalName()); } - - RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: " - + builder.getClass().getCanonicalName()); + return slotChainBuilder.build(); } private SlotChainProvider() {} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/StringResourceWrapper.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/StringResourceWrapper.java index d45f088258..dd4ff6919e 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/StringResourceWrapper.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/StringResourceWrapper.java @@ -16,26 +16,22 @@ package com.alibaba.csp.sentinel.slotchain; import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.ResourceTypeConstants; /** - * Common resource wrapper. + * Common string resource wrapper. * * @author qinan.qn * @author jialiang.linjl */ public class StringResourceWrapper extends ResourceWrapper { - public StringResourceWrapper(String name, EntryType type) { - if (name == null) { - throw new IllegalArgumentException("Resource name cannot be null"); - } - this.name = name; - this.type = type; + public StringResourceWrapper(String name, EntryType e) { + super(name, e, ResourceTypeConstants.COMMON); } - @Override - public String getName() { - return name; + public StringResourceWrapper(String name, EntryType e, int resType) { + super(name, e, resType); } @Override @@ -43,16 +39,12 @@ public String getShowName() { return name; } - @Override - public EntryType getType() { - return type; - } - @Override public String toString() { return "StringResourceWrapper{" + "name='" + name + '\'' + - ", type=" + type + + ", entryType=" + entryType + + ", resourceType=" + resourceType + '}'; } } diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/DefaultSlotChainBuilder.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/DefaultSlotChainBuilder.java index 025d12791d..797206bb7b 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/DefaultSlotChainBuilder.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/DefaultSlotChainBuilder.java @@ -37,7 +37,8 @@ public class DefaultSlotChainBuilder implements SlotChainBuilder { public ProcessorSlotChain build() { ProcessorSlotChain chain = new DefaultProcessorSlotChain(); - List sortedSlotList = SpiLoader.loadInstanceListSorted(ProcessorSlot.class); + // Note: the instances of ProcessorSlot should be different, since they are not stateless. + List sortedSlotList = SpiLoader.loadDifferentInstanceListSorted(ProcessorSlot.class); for (ProcessorSlot slot : sortedSlotList) { if (!(slot instanceof AbstractLinkedProcessorSlot)) { RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName() + ") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain"); diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/RuleConstant.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/RuleConstant.java index 978b169fbd..e712ff343c 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/RuleConstant.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/RuleConstant.java @@ -36,6 +36,9 @@ public final class RuleConstant { */ public static final int DEGRADE_GRADE_EXCEPTION_COUNT = 2; + public static final int DEGRADE_DEFAULT_SLOW_REQUEST_AMOUNT = 5; + public static final int DEGRADE_DEFAULT_MIN_REQUEST_AMOUNT = 5; + public static final int AUTHORITY_WHITE = 0; public static final int AUTHORITY_BLACK = 1; diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeRule.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeRule.java index f29c42d115..aee71ff543 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeRule.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeRule.java @@ -15,12 +15,6 @@ */ package com.alibaba.csp.sentinel.slots.block.degrade; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; - import com.alibaba.csp.sentinel.concurrent.NamedThreadFactory; import com.alibaba.csp.sentinel.context.Context; import com.alibaba.csp.sentinel.node.ClusterNode; @@ -29,6 +23,12 @@ import com.alibaba.csp.sentinel.slots.block.RuleConstant; import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + /** *

* Degrade is used when the resources are in an unstable state, these resources @@ -55,8 +55,6 @@ */ public class DegradeRule extends AbstractRule { - private static final int RT_MAX_EXCEED_N = 5; - @SuppressWarnings("PMD.ThreadPoolCreationRule") private static ScheduledExecutorService pool = Executors.newScheduledThreadPool( Runtime.getRuntime().availableProcessors(), new NamedThreadFactory("sentinel-degrade-reset-task", true)); @@ -78,11 +76,23 @@ public DegradeRule(String resourceName) { private int timeWindow; /** - * Degrade strategy (0: average RT, 1: exception ratio). + * Degrade strategy (0: average RT, 1: exception ratio, 2: exception count). */ private int grade = RuleConstant.DEGRADE_GRADE_RT; - private final AtomicBoolean cut = new AtomicBoolean(false); + /** + * Minimum number of consecutive slow requests that can trigger RT circuit breaking. + * + * @since 1.7.0 + */ + private int rtSlowRequestAmount = RuleConstant.DEGRADE_DEFAULT_SLOW_REQUEST_AMOUNT; + + /** + * Minimum number of requests (in an active statistic time span) that can trigger circuit breaking. + * + * @since 1.7.0 + */ + private int minRequestAmount = RuleConstant.DEGRADE_DEFAULT_MIN_REQUEST_AMOUNT; public int getGrade() { return grade; @@ -93,8 +103,6 @@ public DegradeRule setGrade(int grade) { return this; } - private AtomicLong passCount = new AtomicLong(0); - public double getCount() { return count; } @@ -104,51 +112,44 @@ public DegradeRule setCount(double count) { return this; } - private boolean isCut() { - return cut.get(); + public int getTimeWindow() { + return timeWindow; } - private void setCut(boolean cut) { - this.cut.set(cut); + public DegradeRule setTimeWindow(int timeWindow) { + this.timeWindow = timeWindow; + return this; } - public AtomicLong getPassCount() { - return passCount; + public int getRtSlowRequestAmount() { + return rtSlowRequestAmount; } - public int getTimeWindow() { - return timeWindow; + public DegradeRule setRtSlowRequestAmount(int rtSlowRequestAmount) { + this.rtSlowRequestAmount = rtSlowRequestAmount; + return this; } - public DegradeRule setTimeWindow(int timeWindow) { - this.timeWindow = timeWindow; + public int getMinRequestAmount() { + return minRequestAmount; + } + + public DegradeRule setMinRequestAmount(int minRequestAmount) { + this.minRequestAmount = minRequestAmount; return this; } @Override public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof DegradeRule)) { - return false; - } - if (!super.equals(o)) { - return false; - } - - DegradeRule that = (DegradeRule)o; - - if (count != that.count) { - return false; - } - if (timeWindow != that.timeWindow) { - return false; - } - if (grade != that.grade) { - return false; - } - return true; + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + if (!super.equals(o)) { return false; } + DegradeRule that = (DegradeRule) o; + return Double.compare(that.count, count) == 0 && + timeWindow == that.timeWindow && + grade == that.grade && + rtSlowRequestAmount == that.rtSlowRequestAmount && + minRequestAmount == that.minRequestAmount; } @Override @@ -157,9 +158,29 @@ public int hashCode() { result = 31 * result + new Double(count).hashCode(); result = 31 * result + timeWindow; result = 31 * result + grade; + result = 31 * result + rtSlowRequestAmount; + result = 31 * result + minRequestAmount; return result; } + @Override + public String toString() { + return "DegradeRule{" + + "resource=" + getResource() + + ", grade=" + grade + + ", count=" + count + + ", limitApp=" + getLimitApp() + + ", timeWindow=" + timeWindow + + ", rtSlowRequestAmount=" + rtSlowRequestAmount + + ", minRequestAmount=" + minRequestAmount + + "}"; + } + + // Internal implementation (will be deprecated and moved outside). + + private AtomicLong passCount = new AtomicLong(0); + private final AtomicBoolean cut = new AtomicBoolean(false); + @Override public boolean passCheck(Context context, DefaultNode node, int acquireCount, Object... args) { if (cut.get()) { @@ -179,20 +200,22 @@ public boolean passCheck(Context context, DefaultNode node, int acquireCount, Ob } // Sentinel will degrade the service only if count exceeds. - if (passCount.incrementAndGet() < RT_MAX_EXCEED_N) { + if (passCount.incrementAndGet() < rtSlowRequestAmount) { return true; } } else if (grade == RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO) { double exception = clusterNode.exceptionQps(); double success = clusterNode.successQps(); double total = clusterNode.totalQps(); - // if total qps less than RT_MAX_EXCEED_N, pass. - if (total < RT_MAX_EXCEED_N) { + // If total amount is less than minRequestAmount, the request will pass. + if (total < minRequestAmount) { return true; } + // In the same aligned statistic time window, + // "success" (aka. completed count) = exception count + non-exception count (realSuccess) double realSuccess = success - exception; - if (realSuccess <= 0 && exception < RT_MAX_EXCEED_N) { + if (realSuccess <= 0 && exception < minRequestAmount) { return true; } @@ -214,17 +237,6 @@ public boolean passCheck(Context context, DefaultNode node, int acquireCount, Ob return false; } - @Override - public String toString() { - return "DegradeRule{" + - "resource=" + getResource() + - ", grade=" + grade + - ", count=" + count + - ", limitApp=" + getLimitApp() + - ", timeWindow=" + timeWindow + - "}"; - } - private static final class ResetTask implements Runnable { private DegradeRule rule; @@ -235,9 +247,8 @@ private static final class ResetTask implements Runnable { @Override public void run() { - rule.getPassCount().set(0); + rule.passCount.set(0); rule.cut.set(false); } } } - diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeRuleManager.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeRuleManager.java index 9eb7e58226..6cb09f8e97 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeRuleManager.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeRuleManager.java @@ -23,7 +23,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import com.alibaba.csp.sentinel.Constants; +import com.alibaba.csp.sentinel.config.SentinelConfig; import com.alibaba.csp.sentinel.context.Context; import com.alibaba.csp.sentinel.log.RecordLog; import com.alibaba.csp.sentinel.node.DefaultNode; @@ -211,16 +211,22 @@ public static boolean isValidRule(DegradeRule rule) { if (!baseValid) { return false; } - // Warn for RT mode that exceeds the {@code TIME_DROP_VALVE}. - int maxAllowedRt = Constants.TIME_DROP_VALVE; - if (rule.getGrade() == RuleConstant.DEGRADE_GRADE_RT && rule.getCount() > maxAllowedRt) { - RecordLog.warn(String.format("[DegradeRuleManager] WARN: setting large RT threshold (%.1f ms) in RT mode" - + " will not take effect since it exceeds the max allowed value (%d ms)", rule.getCount(), - maxAllowedRt)); + int maxAllowedRt = SentinelConfig.statisticMaxRt(); + if (rule.getGrade() == RuleConstant.DEGRADE_GRADE_RT) { + if (rule.getRtSlowRequestAmount() <= 0) { + return false; + } + // Warn for RT mode that exceeds the {@code TIME_DROP_VALVE}. + if (rule.getCount() > maxAllowedRt) { + RecordLog.warn(String.format("[DegradeRuleManager] WARN: setting large RT threshold (%.1f ms)" + + " in RT mode will not take effect since it exceeds the max allowed value (%d ms)", + rule.getCount(), maxAllowedRt)); + } } + // Check exception ratio mode. - if (rule.getGrade() == RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO && rule.getCount() > 1) { - return false; + if (rule.getGrade() == RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO) { + return rule.getCount() <= 1 && rule.getMinRequestAmount() > 0; } return true; } diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowRuleUtil.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowRuleUtil.java index 2c776d15fb..6a79e074d9 100644 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowRuleUtil.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowRuleUtil.java @@ -195,7 +195,7 @@ private static boolean checkClusterField(/*@NonNull*/ FlowRule rule) { if (!isWindowConfigValid(clusterConfig.getSampleCount(), clusterConfig.getWindowIntervalMs())) { return false; } - switch (rule.getStrategy()) { + switch (clusterConfig.getStrategy()) { case ClusterRuleConstant.FLOW_CLUSTER_STRATEGY_NORMAL: return true; default: diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/clusterbuilder/ClusterBuilderSlot.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/clusterbuilder/ClusterBuilderSlot.java index 1cd5f138a0..f25289c9f6 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/clusterbuilder/ClusterBuilderSlot.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/clusterbuilder/ClusterBuilderSlot.java @@ -80,7 +80,7 @@ public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode synchronized (lock) { if (clusterNode == null) { // Create the cluster node. - clusterNode = new ClusterNode(); + clusterNode = new ClusterNode(resourceWrapper.getName(), resourceWrapper.getResourceType()); HashMap newMap = new HashMap<>(Math.max(clusterNodeMap.size(), 16)); newMap.putAll(clusterNodeMap); newMap.put(node.getId(), clusterNode); diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/StatisticSlot.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/StatisticSlot.java index 20bdee2465..9a6b683b53 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/StatisticSlot.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/StatisticSlot.java @@ -17,6 +17,7 @@ import java.util.Collection; +import com.alibaba.csp.sentinel.config.SentinelConfig; import com.alibaba.csp.sentinel.slotchain.ProcessorSlotEntryCallback; import com.alibaba.csp.sentinel.slotchain.ProcessorSlotExitCallback; import com.alibaba.csp.sentinel.slots.block.flow.PriorityWaitException; @@ -67,7 +68,7 @@ public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode context.getCurEntry().getOriginNode().addPassRequest(count); } - if (resourceWrapper.getType() == EntryType.IN) { + if (resourceWrapper.getEntryType() == EntryType.IN) { // Add count for global inbound entry node for global statistics. Constants.ENTRY_NODE.increaseThreadNum(); Constants.ENTRY_NODE.addPassRequest(count); @@ -84,7 +85,7 @@ public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode context.getCurEntry().getOriginNode().increaseThreadNum(); } - if (resourceWrapper.getType() == EntryType.IN) { + if (resourceWrapper.getEntryType() == EntryType.IN) { // Add count for global inbound entry node for global statistics. Constants.ENTRY_NODE.increaseThreadNum(); } @@ -102,7 +103,7 @@ public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode context.getCurEntry().getOriginNode().increaseBlockQps(count); } - if (resourceWrapper.getType() == EntryType.IN) { + if (resourceWrapper.getEntryType() == EntryType.IN) { // Add count for global inbound entry node for global statistics. Constants.ENTRY_NODE.increaseBlockQps(count); } @@ -123,7 +124,7 @@ public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode context.getCurEntry().getOriginNode().increaseExceptionQps(count); } - if (resourceWrapper.getType() == EntryType.IN) { + if (resourceWrapper.getEntryType() == EntryType.IN) { Constants.ENTRY_NODE.increaseExceptionQps(count); } throw e; @@ -135,10 +136,11 @@ public void exit(Context context, ResourceWrapper resourceWrapper, int count, Ob DefaultNode node = (DefaultNode)context.getCurNode(); if (context.getCurEntry().getError() == null) { - // Calculate response time (max RT is TIME_DROP_VALVE). + // Calculate response time (max RT is statisticMaxRt from SentinelConfig). long rt = TimeUtil.currentTimeMillis() - context.getCurEntry().getCreateTime(); - if (rt > Constants.TIME_DROP_VALVE) { - rt = Constants.TIME_DROP_VALVE; + int maxStatisticRt = SentinelConfig.statisticMaxRt(); + if (rt > maxStatisticRt) { + rt = maxStatisticRt; } // Record response time and success count. @@ -153,7 +155,7 @@ public void exit(Context context, ResourceWrapper resourceWrapper, int count, Ob context.getCurEntry().getOriginNode().decreaseThreadNum(); } - if (resourceWrapper.getType() == EntryType.IN) { + if (resourceWrapper.getEntryType() == EntryType.IN) { Constants.ENTRY_NODE.addRtAndSuccess(rt, count); Constants.ENTRY_NODE.decreaseThreadNum(); } diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/LongAdder.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/LongAdder.java index beeaedcf67..e0e638a181 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/LongAdder.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/LongAdder.java @@ -46,6 +46,7 @@ public class LongAdder extends Striped64 implements Serializable { /** * Version of plus for use in retryUpdate */ + @Override final long fn(long v, long x) { return v + x; } /** @@ -153,6 +154,7 @@ public long sumThenReset() { * * @return the String representation of the {@link #sum} */ + @Override public String toString() { return Long.toString(sum()); } @@ -162,6 +164,7 @@ public String toString() { * * @return the sum */ + @Override public long longValue() { return sum(); } @@ -170,6 +173,7 @@ public long longValue() { * Returns the {@link #sum} as an {@code int} after a narrowing * primitive conversion. */ + @Override public int intValue() { return (int)sum(); } @@ -178,6 +182,7 @@ public int intValue() { * Returns the {@link #sum} as a {@code float} * after a widening primitive conversion. */ + @Override public float floatValue() { return (float)sum(); } @@ -186,6 +191,7 @@ public float floatValue() { * Returns the {@link #sum} as a {@code double} after a widening * primitive conversion. */ + @Override public double doubleValue() { return (double)sum(); } diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/Striped64.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/Striped64.java index e4489e8ca6..cab42f0a42 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/Striped64.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/Striped64.java @@ -136,6 +136,7 @@ static final class HashCode { * The corresponding ThreadLocal class */ static final class ThreadHashCode extends ThreadLocal { + @Override public HashCode initialValue() { return new HashCode(); } } @@ -330,6 +331,7 @@ private static sun.misc.Unsafe getUnsafe() { return java.security.AccessController.doPrivileged (new java.security .PrivilegedExceptionAction() { + @Override public sun.misc.Unsafe run() throws Exception { java.lang.reflect.Field f = sun.misc .Unsafe.class.getDeclaredField("theUnsafe"); diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/data/MetricBucket.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/data/MetricBucket.java index ec3f2afaac..6c35e174b3 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/data/MetricBucket.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/data/MetricBucket.java @@ -15,7 +15,7 @@ */ package com.alibaba.csp.sentinel.slots.statistic.data; -import com.alibaba.csp.sentinel.Constants; +import com.alibaba.csp.sentinel.config.SentinelConfig; import com.alibaba.csp.sentinel.slots.statistic.MetricEvent; import com.alibaba.csp.sentinel.slots.statistic.base.LongAdder; @@ -50,7 +50,7 @@ public MetricBucket reset(MetricBucket bucket) { } private void initMinRt() { - this.minRt = Constants.TIME_DROP_VALVE; + this.minRt = SentinelConfig.statisticMaxRt(); } /** diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/ArrayMetric.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/ArrayMetric.java index 8647d99461..c21223a78c 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/ArrayMetric.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/ArrayMetric.java @@ -18,13 +18,14 @@ import java.util.ArrayList; import java.util.List; -import com.alibaba.csp.sentinel.Constants; +import com.alibaba.csp.sentinel.config.SentinelConfig; import com.alibaba.csp.sentinel.node.metric.MetricNode; import com.alibaba.csp.sentinel.slots.statistic.MetricEvent; import com.alibaba.csp.sentinel.slots.statistic.base.LeapArray; import com.alibaba.csp.sentinel.slots.statistic.data.MetricBucket; import com.alibaba.csp.sentinel.slots.statistic.base.WindowWrap; import com.alibaba.csp.sentinel.slots.statistic.metric.occupy.OccupiableBucketLeapArray; +import com.alibaba.csp.sentinel.util.function.Predicate; /** * The basic metric class in Sentinel using a {@link BucketLeapArray} internal. @@ -140,7 +141,7 @@ public long rt() { @Override public long minRt() { data.currentWindow(); - long rt = Constants.TIME_DROP_VALVE; + long rt = SentinelConfig.statisticMaxRt(); List list = data.values(); for (MetricBucket window : list) { if (window.minRt() < rt) { @@ -153,33 +154,56 @@ public long minRt() { @Override public List details() { - List details = new ArrayList(); + List details = new ArrayList<>(); data.currentWindow(); List> list = data.list(); for (WindowWrap window : list) { if (window == null) { continue; } - MetricNode node = new MetricNode(); - node.setBlockQps(window.value().block()); - node.setExceptionQps(window.value().exception()); - node.setPassQps(window.value().pass()); - long successQps = window.value().success(); - node.setSuccessQps(successQps); - if (successQps != 0) { - node.setRt(window.value().rt() / successQps); - } else { - node.setRt(window.value().rt()); + + details.add(fromBucket(window)); + } + + return details; + } + + @Override + public List detailsOnCondition(Predicate timePredicate) { + List details = new ArrayList<>(); + data.currentWindow(); + List> list = data.list(); + for (WindowWrap window : list) { + if (window == null) { + continue; + } + if (timePredicate != null && !timePredicate.test(window.windowStart())) { + continue; } - node.setTimestamp(window.windowStart()); - node.setOccupiedPassQps(window.value().occupiedPass()); - details.add(node); + details.add(fromBucket(window)); } return details; } + private MetricNode fromBucket(WindowWrap wrap) { + MetricNode node = new MetricNode(); + node.setBlockQps(wrap.value().block()); + node.setExceptionQps(wrap.value().exception()); + node.setPassQps(wrap.value().pass()); + long successQps = wrap.value().success(); + node.setSuccessQps(successQps); + if (successQps != 0) { + node.setRt(wrap.value().rt() / successQps); + } else { + node.setRt(wrap.value().rt()); + } + node.setTimestamp(wrap.windowStart()); + node.setOccupiedPassQps(wrap.value().occupiedPass()); + return node; + } + @Override public MetricBucket[] windows() { data.currentWindow(); diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/Metric.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/Metric.java index f6a73f5f9c..b79cd881bf 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/Metric.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/Metric.java @@ -19,6 +19,7 @@ import com.alibaba.csp.sentinel.node.metric.MetricNode; import com.alibaba.csp.sentinel.slots.statistic.data.MetricBucket; +import com.alibaba.csp.sentinel.util.function.Predicate; /** * Represents a basic structure recording invocation metrics of protected resources. @@ -84,6 +85,15 @@ public interface Metric extends DebugSupport { */ List details(); + /** + * Generate aggregated metric items that satisfies the time predicate. + * + * @param timePredicate time predicate + * @return aggregated metric items + * @since 1.7.0 + */ + List detailsOnCondition(Predicate timePredicate); + /** * Get the raw window array. * diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemRuleManager.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemRuleManager.java index 7a6091fe57..d860344e2f 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemRuleManager.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemRuleManager.java @@ -63,7 +63,7 @@ * @author jialiang.linjl * @author leyou */ -public class SystemRuleManager { +public final class SystemRuleManager { private static volatile double highestSystemLoad = Double.MAX_VALUE; /** @@ -95,7 +95,7 @@ public class SystemRuleManager { static { checkSystemStatus.set(false); statusListener = new SystemStatusListener(); - scheduler.scheduleAtFixedRate(statusListener, 5, 1, TimeUnit.SECONDS); + scheduler.scheduleAtFixedRate(statusListener, 0, 1, TimeUnit.SECONDS); currentProperty.addListener(listener); } @@ -107,6 +107,7 @@ public class SystemRuleManager { */ public static void register2Property(SentinelProperty> property) { synchronized (listener) { + RecordLog.info("[SystemRuleManager] Registering new property to system rule manager"); currentProperty.removeListener(listener); property.addListener(listener); currentProperty = property; @@ -167,26 +168,22 @@ public static List getRules() { return result; } - public static double getQps() { + public static double getInboundQpsThreshold() { return qps; } - public static void setQps(double qps) { - SystemRuleManager.qps = qps; - } - - public static long getMaxRt() { + public static long getRtThreshold() { return maxRt; } - public static long getMaxThread() { + public static long getMaxThreadThreshold() { return maxThread; } static class SystemPropertyListener extends SimplePropertyListener> { @Override - public void configUpdate(List rules) { + public synchronized void configUpdate(List rules) { restoreSetting(); // systemRules = rules; if (rules != null && rules.size() >= 1) { @@ -234,14 +231,10 @@ public static Boolean getCheckSystemStatus() { return checkSystemStatus.get(); } - public static double getHighestSystemLoad() { + public static double getSystemLoadThreshold() { return highestSystemLoad; } - public static void setHighestSystemLoad(double highestSystemLoad) { - SystemRuleManager.highestSystemLoad = highestSystemLoad; - } - public static double getCpuUsageThreshold() { return highestCpuUsage; } @@ -257,9 +250,14 @@ public static void loadSystemConf(SystemRule rule) { } if (rule.getHighestCpuUsage() >= 0) { - highestCpuUsage = Math.min(highestCpuUsage, rule.getHighestCpuUsage()); - highestCpuUsageIsSet = true; - checkStatus = true; + if (rule.getHighestCpuUsage() > 1) { + RecordLog.warn(String.format("[SystemRuleManager] Ignoring invalid SystemRule: " + + "highestCpuUsage %.3f > 1", rule.getHighestCpuUsage())); + } else { + highestCpuUsage = Math.min(highestCpuUsage, rule.getHighestCpuUsage()); + highestCpuUsageIsSet = true; + checkStatus = true; + } } if (rule.getAvgRt() >= 0) { @@ -290,13 +288,16 @@ public static void loadSystemConf(SystemRule rule) { * @throws BlockException when any system rule's threshold is exceeded. */ public static void checkSystem(ResourceWrapper resourceWrapper) throws BlockException { + if (resourceWrapper == null) { + return; + } // Ensure the checking switch is on. if (!checkSystemStatus.get()) { return; } // for inbound traffic only - if (resourceWrapper.getType() != EntryType.IN) { + if (resourceWrapper.getEntryType() != EntryType.IN) { return; } @@ -326,9 +327,7 @@ public static void checkSystem(ResourceWrapper resourceWrapper) throws BlockExce // cpu usage if (highestCpuUsageIsSet && getCurrentCpuUsage() > highestCpuUsage) { - if (!checkBbr(currentThread)) { - throw new SystemBlockException(resourceWrapper.getName(), "cpu"); - } + throw new SystemBlockException(resourceWrapper.getName(), "cpu"); } } @@ -347,4 +346,4 @@ public static double getCurrentSystemAvgLoad() { public static double getCurrentCpuUsage() { return statusListener.getCpuUsage(); } -} \ No newline at end of file +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemStatusListener.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemStatusListener.java index e02f0c858b..2310160a16 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemStatusListener.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemStatusListener.java @@ -46,32 +46,35 @@ public void run() { try { OperatingSystemMXBean osBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class); currentLoad = osBean.getSystemLoadAverage(); - /** + /* * Java Doc copied from {@link OperatingSystemMXBean#getSystemCpuLoad()}:
* Returns the "recent cpu usage" for the whole system. This value is a double in the [0.0,1.0] interval. * A value of 0.0 means that all CPUs were idle during the recent period of time observed, while a value * of 1.0 means that all CPUs were actively running 100% of the time during the recent period being - * observed. All values betweens 0.0 and 1.0 are possible depending of the activities going on in the + * observed. All values between 0.0 and 1.0 are possible depending of the activities going on in the * system. If the system recent cpu usage is not available, the method returns a negative value. */ currentCpuUsage = osBean.getSystemCpuLoad(); - StringBuilder sb = new StringBuilder(); - if (currentLoad > SystemRuleManager.getHighestSystemLoad()) { - sb.append("load:").append(currentLoad).append(";"); - sb.append("cpu:").append(currentCpuUsage).append(";"); - sb.append("qps:").append(Constants.ENTRY_NODE.passQps()).append(";"); - sb.append("rt:").append(Constants.ENTRY_NODE.avgRt()).append(";"); - sb.append("thread:").append(Constants.ENTRY_NODE.curThreadNum()).append(";"); - sb.append("success:").append(Constants.ENTRY_NODE.successQps()).append(";"); - sb.append("minRt:").append(Constants.ENTRY_NODE.minRt()).append(";"); - sb.append("maxSuccess:").append(Constants.ENTRY_NODE.maxSuccessQps()).append(";"); - RecordLog.info(sb.toString()); + if (currentLoad > SystemRuleManager.getSystemLoadThreshold()) { + writeSystemStatusLog(); } - } catch (Throwable e) { - RecordLog.info("could not get system error ", e); + RecordLog.warn("[SystemStatusListener] Failed to get system metrics from JMX", e); } } + private void writeSystemStatusLog() { + StringBuilder sb = new StringBuilder(); + sb.append("Load exceeds the threshold: "); + sb.append("load:").append(String.format("%.4f", currentLoad)).append("; "); + sb.append("cpuUsage:").append(String.format("%.4f", currentCpuUsage)).append("; "); + sb.append("qps:").append(String.format("%.4f", Constants.ENTRY_NODE.passQps())).append("; "); + sb.append("rt:").append(String.format("%.4f", Constants.ENTRY_NODE.avgRt())).append("; "); + sb.append("thread:").append(Constants.ENTRY_NODE.curThreadNum()).append("; "); + sb.append("success:").append(String.format("%.4f", Constants.ENTRY_NODE.successQps())).append("; "); + sb.append("minRt:").append(String.format("%.2f", Constants.ENTRY_NODE.minRt())).append("; "); + sb.append("maxSuccess:").append(String.format("%.2f", Constants.ENTRY_NODE.maxSuccessQps())).append("; "); + RecordLog.info(sb.toString()); + } } diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/spi/ServiceLoaderUtil.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/spi/ServiceLoaderUtil.java new file mode 100644 index 0000000000..f7fe6fde03 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/spi/ServiceLoaderUtil.java @@ -0,0 +1,45 @@ +/* + * Copyright 1999-2019 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.spi; + +import java.util.ServiceLoader; + +import com.alibaba.csp.sentinel.config.SentinelConfig; + +/** + * @author Eric Zhao + * @since 1.7.0 + */ +public final class ServiceLoaderUtil { + + private static final String CLASSLOADER_DEFAULT = "default"; + private static final String CLASSLOADER_CONTEXT = "context"; + + public static ServiceLoader getServiceLoader(Class clazz) { + if (shouldUseContextClassloader()) { + return ServiceLoader.load(clazz); + } else { + return ServiceLoader.load(clazz, clazz.getClassLoader()); + } + } + + public static boolean shouldUseContextClassloader() { + String classloaderConf = SentinelConfig.getConfig(SentinelConfig.SPI_CLASSLOADER); + return CLASSLOADER_CONTEXT.equalsIgnoreCase(classloaderConf); + } + + private ServiceLoaderUtil() {} +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/AppNameUtil.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/AppNameUtil.java index 0cde3e85d2..5cfa766ee2 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/AppNameUtil.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/AppNameUtil.java @@ -15,10 +15,10 @@ */ package com.alibaba.csp.sentinel.util; -import java.io.File; - import com.alibaba.csp.sentinel.log.RecordLog; +import java.io.File; + /** * Util class for getting application name. This class uses the flowing order to get app's name: * diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/AssertUtil.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/AssertUtil.java index 4c67b738ff..545bcc4d28 100644 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/AssertUtil.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/AssertUtil.java @@ -15,8 +15,12 @@ */ package com.alibaba.csp.sentinel.util; +import java.util.Collection; + /** * Util class for checking arguments. + * + * @author Eric Zhao */ public class AssertUtil { @@ -28,6 +32,12 @@ public static void notEmpty(String string, String message) { } } + public static void assertNotEmpty(Collection collection, String message) { + if (collection == null || collection.isEmpty()) { + throw new IllegalArgumentException(message); + } + } + public static void assertNotBlank(String string, String message) { if (StringUtil.isBlank(string)) { throw new IllegalArgumentException(message); diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/ConfigUtil.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/ConfigUtil.java new file mode 100644 index 0000000000..a5182b363e --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/ConfigUtil.java @@ -0,0 +1,154 @@ +/* + * 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.util; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Properties; + +/** + *

+ * Util class for loading configuration from file or command arguments. + *

+ * + * @author lianglin + * @since 1.7.0 + */ +public final class ConfigUtil { + + public static final String CLASSPATH_FILE_FLAG = "classpath:"; + + /** + *

Load the properties from provided file.

+ *

Currently it supports reading from classpath file or local file.

+ * + * @param fileName valid file path + * @return the retrieved properties from the file; null if the file not exist + */ + public static Properties loadProperties(String fileName) { + if (StringUtil.isNotBlank(fileName)) { + if (absolutePathStart(fileName)) { + return loadPropertiesFromAbsoluteFile(fileName); + } else if (fileName.startsWith(CLASSPATH_FILE_FLAG)) { + return loadPropertiesFromClasspathFile(fileName); + } else { + return loadPropertiesFromRelativeFile(fileName); + } + } else { + return null; + } + } + + private static Properties loadPropertiesFromAbsoluteFile(String fileName) { + Properties properties = null; + try { + + File file = new File(fileName); + if (!file.exists()) { + return null; + } + + try (BufferedReader bufferedReader = + new BufferedReader(new InputStreamReader(new FileInputStream(file), getCharset()))) { + properties = new Properties(); + properties.load(bufferedReader); + } + } catch (Throwable e) { + e.printStackTrace(); + } + return properties; + } + + private static boolean absolutePathStart(String path) { + File[] files = File.listRoots(); + for (File file : files) { + if (path.startsWith(file.getPath())) { + return true; + } + } + return false; + } + + + private static Properties loadPropertiesFromClasspathFile(String fileName) { + fileName = fileName.substring(CLASSPATH_FILE_FLAG.length()).trim(); + + List list = new ArrayList<>(); + try { + Enumeration urls = getClassLoader().getResources(fileName); + list = new ArrayList<>(); + while (urls.hasMoreElements()) { + list.add(urls.nextElement()); + } + } catch (Throwable e) { + e.printStackTrace(); + } + + if (list.isEmpty()) { + return null; + } + + Properties properties = new Properties(); + for (URL url : list) { + try (BufferedReader bufferedReader = + new BufferedReader(new InputStreamReader(url.openStream(), getCharset()))) { + Properties p = new Properties(); + p.load(bufferedReader); + properties.putAll(p); + } catch (Throwable e) { + e.printStackTrace(); + } + } + return properties; + } + + private static Properties loadPropertiesFromRelativeFile(String fileName) { + String userDir = System.getProperty("user.dir"); + String realFilePath = addSeparator(userDir) + fileName; + return loadPropertiesFromAbsoluteFile(realFilePath); + } + + private static ClassLoader getClassLoader() { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + if (classLoader == null) { + classLoader = ConfigUtil.class.getClassLoader(); + } + return classLoader; + } + + private static Charset getCharset() { + // avoid static loop dependencies: SentinelConfig -> SentinelConfigLoader -> ConfigUtil -> SentinelConfig + // so not use SentinelConfig.charset() + return Charset.forName(System.getProperty("csp.sentinel.charset", StandardCharsets.UTF_8.name())); + } + + public static String addSeparator(String dir) { + if (!dir.endsWith(File.separator)) { + dir += File.separator; + } + return dir; + } + + private ConfigUtil() {} +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/SpiLoader.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/SpiLoader.java index af89ea7f83..a632924461 100644 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/SpiLoader.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/SpiLoader.java @@ -23,6 +23,7 @@ import java.util.concurrent.ConcurrentHashMap; import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.spi.ServiceLoaderUtil; import com.alibaba.csp.sentinel.spi.SpiOrder; /** @@ -34,12 +35,13 @@ public final class SpiLoader { private static final Map SERVICE_LOADER_MAP = new ConcurrentHashMap(); public static T loadFirstInstance(Class clazz) { + AssertUtil.notNull(clazz, "SPI class cannot be null"); try { String key = clazz.getName(); // Not thread-safe, as it's expected to be resolved in a thread-safe context. ServiceLoader serviceLoader = SERVICE_LOADER_MAP.get(key); if (serviceLoader == null) { - serviceLoader = ServiceLoader.load(clazz); + serviceLoader = ServiceLoaderUtil.getServiceLoader(clazz); SERVICE_LOADER_MAP.put(key, serviceLoader); } @@ -56,6 +58,41 @@ public static T loadFirstInstance(Class clazz) { } } + /** + * Load the first-found specific SPI instance (excluding provided default SPI class). + * If no other SPI implementation found, then create a default SPI instance. + * + * @param clazz class of the SPI interface + * @param defaultClass class of the default SPI implementation (if no other implementation found) + * @param SPI type + * @return the first specific SPI instance if exists, or else the default SPI instance + * @since 1.7.0 + */ + public static T loadFirstInstanceOrDefault(Class clazz, Class defaultClass) { + AssertUtil.notNull(clazz, "SPI class cannot be null"); + AssertUtil.notNull(defaultClass, "default SPI class cannot be null"); + try { + String key = clazz.getName(); + // Not thread-safe, as it's expected to be resolved in a thread-safe context. + ServiceLoader serviceLoader = SERVICE_LOADER_MAP.get(key); + if (serviceLoader == null) { + serviceLoader = ServiceLoaderUtil.getServiceLoader(clazz); + SERVICE_LOADER_MAP.put(key, serviceLoader); + } + + for (T instance : serviceLoader) { + if (instance.getClass() != defaultClass) { + return instance; + } + } + return defaultClass.newInstance(); + } catch (Throwable t) { + RecordLog.warn("[SpiLoader] ERROR: loadFirstInstanceOrDefault failed", t); + t.printStackTrace(); + return null; + } + } + /** * Load the SPI instance with highest priority. * @@ -70,7 +107,7 @@ public static T loadHighestPriorityInstance(Class clazz) { // Not thread-safe, as it's expected to be resolved in a thread-safe context. ServiceLoader serviceLoader = SERVICE_LOADER_MAP.get(key); if (serviceLoader == null) { - serviceLoader = ServiceLoader.load(clazz); + serviceLoader = ServiceLoaderUtil.getServiceLoader(clazz); SERVICE_LOADER_MAP.put(key, serviceLoader); } @@ -92,7 +129,6 @@ public static T loadHighestPriorityInstance(Class clazz) { } /** - * Load and sorted SPI instance list. * Load the SPI instance list for provided SPI interface. * * @param clazz class of the SPI @@ -106,7 +142,7 @@ public static List loadInstanceList(Class clazz) { // Not thread-safe, as it's expected to be resolved in a thread-safe context. ServiceLoader serviceLoader = SERVICE_LOADER_MAP.get(key); if (serviceLoader == null) { - serviceLoader = ServiceLoader.load(clazz); + serviceLoader = ServiceLoaderUtil.getServiceLoader(clazz); SERVICE_LOADER_MAP.put(key, serviceLoader); } @@ -125,8 +161,6 @@ public static List loadInstanceList(Class clazz) { /** * Load the sorted SPI instance list for provided SPI interface. * - * Note: each call return new instances. - * * @param clazz class of the SPI * @param SPI type * @return sorted SPI instance list @@ -134,8 +168,13 @@ public static List loadInstanceList(Class clazz) { */ public static List loadInstanceListSorted(Class clazz) { try { - // Call ServiceLoader.load(clazz) to get new instance - ServiceLoader serviceLoader = ServiceLoader.load(clazz); + String key = clazz.getName(); + // Not thread-safe, as it's expected to be resolved in a thread-safe context. + ServiceLoader serviceLoader = SERVICE_LOADER_MAP.get(key); + if (serviceLoader == null) { + serviceLoader = ServiceLoaderUtil.getServiceLoader(clazz); + SERVICE_LOADER_MAP.put(key, serviceLoader); + } List> orderWrappers = new ArrayList<>(); for (T spi : serviceLoader) { @@ -157,6 +196,41 @@ public static List loadInstanceListSorted(Class clazz) { } } + /** + * Load the sorted SPI instance list for provided SPI interface. + * + * @param clazz class of the SPI + * @param SPI type + * @return sorted SPI instance list, and instances are different from prev call + * @since 1.7.2 + */ + public static List loadDifferentInstanceListSorted(Class clazz) { + try { + String key = clazz.getName(); + + // To make sure load new instance + ServiceLoader serviceLoader = ServiceLoaderUtil.getServiceLoader(clazz); + + List> orderWrappers = new ArrayList<>(); + for (T spi : serviceLoader) { + int order = SpiOrderResolver.resolveOrder(spi); + // Since SPI is lazy initialized in ServiceLoader, we use online sort algorithm here. + SpiOrderResolver.insertSorted(orderWrappers, spi, order); + RecordLog.info("[SpiLoader] Found {0} SPI: {1} with order " + order, clazz.getSimpleName(), + spi.getClass().getCanonicalName()); + } + List list = new ArrayList<>(); + for (int i = 0; i < orderWrappers.size(); i++) { + list.add(i, orderWrappers.get(i).spi); + } + return list; + } catch (Throwable t) { + RecordLog.warn("[SpiLoader] ERROR: loadInstanceListSorted failed", t); + t.printStackTrace(); + return new ArrayList<>(); + } + } + private static class SpiOrderResolver { private static void insertSorted(List> list, T spi, int order) { int idx = 0; diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/function/Consumer.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/function/Consumer.java new file mode 100644 index 0000000000..310fb0c509 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/function/Consumer.java @@ -0,0 +1,29 @@ +/* + * Copyright 1999-2019 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 + * + * https://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.util.function; + +/** + * Consumer interface from JDK 8. + */ +public interface Consumer { + + /** + * Performs this operation on the given argument. + * + * @param t the input argument + */ + void accept(T t); +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/ConstantsTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/ConstantsTest.java deleted file mode 100644 index 03084270b1..0000000000 --- a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/ConstantsTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.alibaba.csp.sentinel; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -/** - * Test cases for {@link Constants}. - * - * @author cdfive - */ -public class ConstantsTest { - - @Test - public void testDefaultTimeDropValue() { - assertEquals(4900, Constants.TIME_DROP_VALVE); - } - -// add JVM parameter -// -Dcsp.sentinel.statistic.max.rt=10000 -// @Test - public void testCustomTimeDropValue() { - assertEquals(10000, Constants.TIME_DROP_VALVE); - } -} \ No newline at end of file diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java index 8c203f00cb..1d910ee76a 100644 --- a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/CtSphTest.java @@ -41,7 +41,7 @@ private void testCustomContextEntryWithFullContextSize(String resourceName, bool Entry entry = null; try { if (async) { - entry = ctSph.asyncEntry(resourceName, resourceWrapper.getType(), 1); + entry = ctSph.asyncEntry(resourceName, resourceWrapper.getEntryType(), 1); } else { entry = ctSph.entry(resourceWrapper, 1); } @@ -80,7 +80,7 @@ private void testDefaultContextEntryWithFullContextSize(String resourceName, boo if (!async) { entry = ctSph.entry(resourceWrapper, 1); } else { - entry = ctSph.asyncEntry(resourceName, resourceWrapper.getType(), 1); + entry = ctSph.asyncEntry(resourceName, resourceWrapper.getEntryType(), 1); Context asyncContext = ((AsyncEntry)entry).getAsyncContext(); assertTrue(ContextUtil.isDefaultContext(asyncContext)); assertTrue(asyncContext.isAsync()); @@ -127,7 +127,7 @@ public void testEntryAndAsyncEntryWhenSwitchOff() { AsyncEntry asyncEntry = null; try { entry = ctSph.entry(resourceWrapperA, 1); - asyncEntry = ctSph.asyncEntry(resourceNameB, resourceWrapperB.getType(), 1); + asyncEntry = ctSph.asyncEntry(resourceNameB, resourceWrapperB.getEntryType(), 1); } catch (BlockException ex) { fail("Unexpected blocked: " + ex.getClass().getCanonicalName()); } finally { diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/RecordLogTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/RecordLogTest.java index e2e4bd995f..50f9b63fa1 100755 --- a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/RecordLogTest.java +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/RecordLogTest.java @@ -15,14 +15,14 @@ */ package com.alibaba.csp.sentinel; -import java.io.File; - import com.alibaba.csp.sentinel.log.LogBase; import com.alibaba.csp.sentinel.log.RecordLog; import com.alibaba.csp.sentinel.util.PidUtil; - +import org.junit.Assert; import org.junit.Test; +import java.io.File; + import static org.junit.Assert.assertTrue; /** @@ -44,21 +44,27 @@ public void testLogRolling() { } } - @Test + //Change LogBase It is not not work when integration Testing + //Because LogBase.LOG_DIR can be just static init for once and it will not be changed + //@Test public void testChangeLogBase() { + String userHome = System.getProperty("user.home"); String newLogBase = userHome + File.separator + "tmpLogDir" + System.currentTimeMillis(); System.setProperty(LogBase.LOG_DIR, newLogBase); RecordLog.info("testChangeLogBase"); String logFileName = RecordLog.getLogBaseDir(); + Assert.assertTrue(newLogBase.equals(logFileName)); File[] files = new File(logFileName).listFiles(); assertTrue(files != null && files.length > 0); + deleteLogDir(new File(newLogBase)); + + } @Test public void testLogBaseDir() { - RecordLog.info("testLogBaseDir"); assertTrue(RecordLog.getLogBaseDir().startsWith(System.getProperty("user.home"))); } @@ -88,4 +94,17 @@ public void testLogNameUsePid() { } } + private void deleteLogDir(File logDirFile) { + if (logDirFile != null && logDirFile.isDirectory()) { + if (logDirFile.listFiles() != null) { + for (File file : logDirFile.listFiles()) { + file.delete(); + } + } + logDirFile.delete(); + } + } + + + } \ No newline at end of file diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/SphOTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/SphOTest.java index 7f8787a512..4233a70241 100755 --- a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/SphOTest.java +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/SphOTest.java @@ -63,7 +63,7 @@ public void testStringEntryCount() { try { assertTrue(StringUtil.equalsIgnoreCase( ContextUtil.getContext().getCurEntry().getResourceWrapper().getName(), "resourceName")); - assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getType(), EntryType.OUT); + assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getEntryType(), EntryType.OUT); } finally { SphO.exit(2); } @@ -78,7 +78,7 @@ public void testMethodEntryCount() throws NoSuchMethodException, SecurityExcepti assertTrue(StringUtil.equalsIgnoreCase( ContextUtil.getContext().getCurEntry().getResourceWrapper().getName(), "com.alibaba.csp.sentinel.SphOTest:testMethodEntryCount()")); - assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getType(), EntryType.OUT); + assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getEntryType(), EntryType.OUT); } finally { SphO.exit(2); } @@ -91,7 +91,7 @@ public void testStringEntryType() { try { assertTrue(StringUtil.equalsIgnoreCase( ContextUtil.getContext().getCurEntry().getResourceWrapper().getName(), "resourceName")); - assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getType(), EntryType.IN); + assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getEntryType(), EntryType.IN); } finally { SphO.exit(); } @@ -106,7 +106,7 @@ public void testMethodEntryType() throws NoSuchMethodException, SecurityExceptio assertTrue(StringUtil.equalsIgnoreCase( ContextUtil.getContext().getCurEntry().getResourceWrapper().getName(), "com.alibaba.csp.sentinel.SphOTest:testMethodEntryType()")); - assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getType(), EntryType.IN); + assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getEntryType(), EntryType.IN); } finally { SphO.exit(); } @@ -119,7 +119,7 @@ public void testStringEntryTypeCount() { try { assertTrue(StringUtil.equalsIgnoreCase( ContextUtil.getContext().getCurEntry().getResourceWrapper().getName(), "resourceName")); - assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getType(), EntryType.IN); + assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getEntryType(), EntryType.IN); } finally { SphO.exit(2); } @@ -134,7 +134,7 @@ public void testMethodEntryTypeCount() throws NoSuchMethodException, SecurityExc assertTrue(StringUtil.equalsIgnoreCase( ContextUtil.getContext().getCurEntry().getResourceWrapper().getName(), "com.alibaba.csp.sentinel.SphOTest:testMethodEntryTypeCount()")); - assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getType(), EntryType.IN); + assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getEntryType(), EntryType.IN); } finally { SphO.exit(2); } @@ -147,7 +147,7 @@ public void testStringEntryAll() { try { assertTrue(StringUtil.equalsIgnoreCase( ContextUtil.getContext().getCurEntry().getResourceWrapper().getName(), "resourceName")); - assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getType(), EntryType.IN); + assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getEntryType(), EntryType.IN); } finally { SphO.exit(2, "hello1", "hello2"); } @@ -162,7 +162,7 @@ public void testMethodEntryAll() throws NoSuchMethodException, SecurityException assertTrue(StringUtil.equalsIgnoreCase( ContextUtil.getContext().getCurEntry().getResourceWrapper().getName(), "com.alibaba.csp.sentinel.SphOTest:testMethodEntryAll()")); - assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getType(), EntryType.IN); + assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getEntryType(), EntryType.IN); } finally { SphO.exit(2, "hello1", "hello2"); } diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/SphUTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/SphUTest.java index a1a58a1a96..e546f87c7f 100755 --- a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/SphUTest.java +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/SphUTest.java @@ -38,7 +38,7 @@ public void testStringEntryNormal() throws BlockException { assertNotNull(e); assertEquals(e.resourceWrapper.getName(), "resourceName"); - assertEquals(e.resourceWrapper.getType(), EntryType.OUT); + assertEquals(e.resourceWrapper.getEntryType(), EntryType.OUT); assertEquals(ContextUtil.getContext().getName(), Constants.CONTEXT_DEFAULT_NAME); e.exit(); @@ -53,7 +53,7 @@ public void testMethodEntryNormal() throws BlockException, NoSuchMethodException assertTrue(StringUtil .equalsIgnoreCase(e.resourceWrapper.getName(), "com.alibaba.csp.sentinel.SphUTest:testMethodEntryNormal()")); - assertEquals(e.resourceWrapper.getType(), EntryType.OUT); + assertEquals(e.resourceWrapper.getEntryType(), EntryType.OUT); assertEquals(ContextUtil.getContext().getName(), Constants.CONTEXT_DEFAULT_NAME); e.exit(); @@ -78,7 +78,7 @@ public void testStringEntryCount() throws BlockException { assertNotNull(e); assertEquals("resourceName", e.resourceWrapper.getName()); - assertEquals(e.resourceWrapper.getType(), EntryType.OUT); + assertEquals(e.resourceWrapper.getEntryType(), EntryType.OUT); assertEquals(ContextUtil.getContext().getName(), Constants.CONTEXT_DEFAULT_NAME); e.exit(2); @@ -93,7 +93,7 @@ public void testMethodEntryCount() throws BlockException, NoSuchMethodException, assertTrue(StringUtil .equalsIgnoreCase(e.resourceWrapper.getName(), "com.alibaba.csp.sentinel.SphUTest:testMethodEntryNormal()")); - assertEquals(e.resourceWrapper.getType(), EntryType.OUT); + assertEquals(e.resourceWrapper.getEntryType(), EntryType.OUT); e.exit(2); } @@ -102,7 +102,7 @@ public void testMethodEntryCount() throws BlockException, NoSuchMethodException, public void testStringEntryType() throws BlockException { Entry e = SphU.entry("resourceName", EntryType.IN); - assertSame(e.resourceWrapper.getType(), EntryType.IN); + assertSame(e.resourceWrapper.getEntryType(), EntryType.IN); e.exit(); } @@ -112,7 +112,7 @@ public void testMethodEntryType() throws BlockException, NoSuchMethodException, Method method = SphUTest.class.getMethod("testMethodEntryNormal"); Entry e = SphU.entry(method, EntryType.IN); - assertSame(e.resourceWrapper.getType(), EntryType.IN); + assertSame(e.resourceWrapper.getEntryType(), EntryType.IN); e.exit(); } @@ -121,7 +121,7 @@ public void testMethodEntryType() throws BlockException, NoSuchMethodException, public void testStringEntryCountType() throws BlockException { Entry e = SphU.entry("resourceName", EntryType.IN, 2); - assertSame(e.resourceWrapper.getType(), EntryType.IN); + assertSame(e.resourceWrapper.getEntryType(), EntryType.IN); e.exit(2); } @@ -131,7 +131,7 @@ public void testMethodEntryCountType() throws BlockException, NoSuchMethodExcept Method method = SphUTest.class.getMethod("testMethodEntryNormal"); Entry e = SphU.entry(method, EntryType.IN, 2); - assertSame(e.resourceWrapper.getType(), EntryType.IN); + assertSame(e.resourceWrapper.getEntryType(), EntryType.IN); e.exit(); } @@ -141,7 +141,7 @@ public void testStringEntryAll() throws BlockException { final String arg0 = "foo"; final String arg1 = "baz"; Entry e = SphU.entry("resourceName", EntryType.IN, 2, arg0, arg1); - assertSame(e.resourceWrapper.getType(), EntryType.IN); + assertSame(e.resourceWrapper.getEntryType(), EntryType.IN); e.exit(2, arg0, arg1); } @@ -153,7 +153,7 @@ public void testMethodEntryAll() throws BlockException, NoSuchMethodException, S Method method = SphUTest.class.getMethod("testMethodEntryNormal"); Entry e = SphU.entry(method, EntryType.IN, 2, arg0, arg1); - assertSame(e.resourceWrapper.getType(), EntryType.IN); + assertSame(e.resourceWrapper.getEntryType(), EntryType.IN); e.exit(2, arg0, arg1); } diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/config/SentinelConfigTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/config/SentinelConfigTest.java index 51ecb9a7a4..3108a08dcd 100644 --- a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/config/SentinelConfigTest.java +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/config/SentinelConfigTest.java @@ -1,7 +1,15 @@ package com.alibaba.csp.sentinel.config; +import org.junit.Assert; import org.junit.Test; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +import static com.alibaba.csp.sentinel.config.SentinelConfig.*; +import static com.alibaba.csp.sentinel.util.ConfigUtil.addSeparator; import static org.junit.Assert.assertEquals; /** @@ -20,7 +28,7 @@ public void testDefaultConfig() { assertEquals(SentinelConfig.DEFAULT_STATISTIC_MAX_RT, SentinelConfig.statisticMaxRt()); } -// add JVM parameter + // add JVM parameter // -Dcsp.sentinel.charset=gbk // -Dcsp.sentinel.metric.file.single.size=104857600 // -Dcsp.sentinel.metric.file.total.count=10 @@ -58,4 +66,51 @@ public void testColdFactoryLargerThanOne() { SentinelConfig.setConfig(SentinelConfig.COLD_FACTOR, "4"); assertEquals(4, SentinelConfig.coldFactor()); } + + + //add Jvm parameter + //-Dcsp.sentinel.config.file=sentinel-propertiesTest.properties + //-Dcsp.sentinel.flow.cold.factor=5 + //-Dcsp.sentinel.statistic.max.rt=1000 + //@Test + public void testLoadProperties() throws IOException { + + File file = null; + String fileName = "sentinel-propertiesTest.properties"; + try { + file = new File(addSeparator(System.getProperty("user.dir")) + "target/classes/" + fileName); + if (!file.exists()) { + file.createNewFile(); + } + BufferedWriter out = new BufferedWriter(new FileWriter(file)); + out.write(buildPropertyStr(CHARSET, "utf-8")); + out.write("\n"); + out.write(buildPropertyStr(SINGLE_METRIC_FILE_SIZE, "1000")); + out.write("\n"); + out.write(buildPropertyStr(TOTAL_METRIC_FILE_COUNT, "20")); + out.write("\n"); + out.write(buildPropertyStr(COLD_FACTOR, "123")); + out.write("\n"); + out.write(buildPropertyStr(STATISTIC_MAX_RT, "6000")); + out.flush(); + out.close(); + + Assert.assertTrue(SentinelConfig.getConfig(CHARSET).equals("utf-8")); + Assert.assertTrue(SentinelConfig.getConfig(SINGLE_METRIC_FILE_SIZE).equals("1000")); + Assert.assertTrue(SentinelConfig.getConfig(TOTAL_METRIC_FILE_COUNT).equals("20")); + Assert.assertTrue(SentinelConfig.getConfig(COLD_FACTOR).equals("5")); + Assert.assertTrue(SentinelConfig.getConfig(STATISTIC_MAX_RT).equals("1000")); + + } finally { + if (file != null) { + file.delete(); + } + } + + + } + + private String buildPropertyStr(String key, String value) { + return key + "=" + value; + } } \ No newline at end of file diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/eagleeye/EagleEyeCoreUtilsTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/eagleeye/EagleEyeCoreUtilsTest.java new file mode 100644 index 0000000000..41c8e0329b --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/eagleeye/EagleEyeCoreUtilsTest.java @@ -0,0 +1,188 @@ +/* + * Copyright 1999-2019 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.eagleeye; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class EagleEyeCoreUtilsTest { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Test + public void testIsBlank() { + Assert.assertTrue(EagleEyeCoreUtils.isBlank("")); + Assert.assertTrue(EagleEyeCoreUtils.isBlank(" ")); + Assert.assertTrue(EagleEyeCoreUtils.isBlank(null)); + + Assert.assertFalse(EagleEyeCoreUtils.isBlank("foo")); + } + + @Test + public void testCheckNotNullEmpty() { + Assert.assertEquals("foo", + EagleEyeCoreUtils.checkNotNullEmpty("foo", "bar")); + } + + @Test + public void testCheckNotNullEmptyInputEmptyStringThrowsException() { + thrown.expect(IllegalArgumentException.class); + EagleEyeCoreUtils.checkNotNullEmpty("", "bar"); + // Method is not expected to return due to exception thrown + } + + @Test + public void testCheckNotNullEmptyInputSpaceThrowsException() { + thrown.expect(IllegalArgumentException.class); + EagleEyeCoreUtils.checkNotNullEmpty(" ", "bar"); + // Method is not expected to return due to exception thrown + } + + @Test + public void testCheckNotNullEmptyInputNullThrowsException() { + thrown.expect(IllegalArgumentException.class); + EagleEyeCoreUtils.checkNotNullEmpty(null, "bar"); + // Method is not expected to return due to exception thrown + } + + @Test + public void testCheckNotNull() { + Assert.assertEquals("", EagleEyeCoreUtils.checkNotNull("", "bar")); + Assert.assertEquals(" ", EagleEyeCoreUtils.checkNotNull(" ", "bar")); + Assert.assertEquals("foo", + EagleEyeCoreUtils.checkNotNull("foo", "bar")); + } + + @Test + public void testCheckNotNullThrowsException() { + thrown.expect(IllegalArgumentException.class); + EagleEyeCoreUtils.checkNotNull(null, "bar"); + // Method is not expected to return due to exception thrown + } + + @Test + public void testDefaultIfNull() { + Assert.assertEquals("", EagleEyeCoreUtils.defaultIfNull("", "bar")); + Assert.assertEquals(" ", EagleEyeCoreUtils.defaultIfNull(" ", "bar")); + Assert.assertEquals("bar", + EagleEyeCoreUtils.defaultIfNull(null, "bar")); + Assert.assertEquals("foo", + EagleEyeCoreUtils.defaultIfNull("foo", "bar")); + } + + @Test + public void testIsNotBlank() { + Assert.assertFalse(EagleEyeCoreUtils.isNotBlank("")); + Assert.assertFalse(EagleEyeCoreUtils.isNotBlank(" ")); + Assert.assertFalse(EagleEyeCoreUtils.isNotBlank(null)); + + Assert.assertTrue(EagleEyeCoreUtils.isNotBlank("foo")); + } + + @Test + public void testIsNotEmpty() { + Assert.assertFalse(EagleEyeCoreUtils.isNotEmpty("")); + Assert.assertFalse(EagleEyeCoreUtils.isNotEmpty(null)); + + Assert.assertTrue(EagleEyeCoreUtils.isNotEmpty(" ")); + Assert.assertTrue(EagleEyeCoreUtils.isNotEmpty("foo")); + } + + @Test + public void testTrim() { + Assert.assertNull(EagleEyeCoreUtils.trim(null)); + + Assert.assertEquals("", EagleEyeCoreUtils.trim("")); + Assert.assertEquals("", EagleEyeCoreUtils.trim(" ")); + Assert.assertEquals("foo", EagleEyeCoreUtils.trim("foo")); + Assert.assertEquals("foo", EagleEyeCoreUtils.trim("foo ")); + Assert.assertEquals("foo", EagleEyeCoreUtils.trim(" foo")); + Assert.assertEquals("foo", EagleEyeCoreUtils.trim(" foo ")); + Assert.assertEquals("f o o", EagleEyeCoreUtils.trim(" f o o ")); + } + + @Test + public void testSplit() { + Assert.assertNull(EagleEyeCoreUtils.split(null, 'a')); + + Assert.assertArrayEquals(new String[0], + EagleEyeCoreUtils.split("", ',')); + Assert.assertArrayEquals(new String[]{"foo", "bar", "baz"}, + EagleEyeCoreUtils.split("foo,bar,baz", ',')); + } + + @Test + public void testAppendWithBlankCheck() { + Assert.assertEquals("bar", EagleEyeCoreUtils.appendWithBlankCheck( + null, "bar", new StringBuilder()).toString()); + Assert.assertEquals("foo", EagleEyeCoreUtils.appendWithBlankCheck( + "foo", "bar", new StringBuilder()).toString()); + } + + @Test + public void testAppendWithNullCheck() { + Assert.assertEquals("bar", EagleEyeCoreUtils.appendWithNullCheck( + null, "bar", new StringBuilder()).toString()); + Assert.assertEquals("foo", EagleEyeCoreUtils.appendWithNullCheck( + "foo", "bar", new StringBuilder()).toString()); + } + + @Test + public void testAppendLog() { + Assert.assertEquals("foo bar baz foo", EagleEyeCoreUtils.appendLog( + "foo\nbar\rbaz,foo", new StringBuilder(), ',').toString()); + Assert.assertEquals("", EagleEyeCoreUtils.appendLog( + null, new StringBuilder(), ',').toString()); + } + + @Test + public void testFormatTime() { + Assert.assertEquals("2019-06-15 20:13:14.000", + EagleEyeCoreUtils.formatTime(1560600794000L)); + } + + @Test + public void testGetSystemProperty() { + Assert.assertNull(EagleEyeCoreUtils.getSystemProperty(null)); + Assert.assertNull(EagleEyeCoreUtils.getSystemProperty("foo")); + } + + @Test + public void testGetSystemPropertyForLong() { + Assert.assertEquals(2L, + EagleEyeCoreUtils.getSystemPropertyForLong(null, 2L)); + } + + @Test + public void testIsHexNumeric() { + Assert.assertTrue(EagleEyeCoreUtils.isHexNumeric('2')); + Assert.assertTrue(EagleEyeCoreUtils.isHexNumeric('b')); + + Assert.assertFalse(EagleEyeCoreUtils.isHexNumeric('z')); + Assert.assertFalse(EagleEyeCoreUtils.isHexNumeric('%')); + } + + @Test + public void testIsNumeric() { + Assert.assertTrue(EagleEyeCoreUtils.isNumeric('2')); + + Assert.assertFalse(EagleEyeCoreUtils.isNumeric('b')); + Assert.assertFalse(EagleEyeCoreUtils.isNumeric('%')); + } +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/log/ConsoleHandlerTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/log/ConsoleHandlerTest.java new file mode 100644 index 0000000000..57413603ca --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/log/ConsoleHandlerTest.java @@ -0,0 +1,81 @@ +/* + * 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.log; + +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import static org.junit.Assert.*; + +/** + * Test cases for {@link ConsoleHandler}. + * + * @author cdfive + */ +public class ConsoleHandlerTest { + + @Test + public void testPublish() { + ByteArrayOutputStream baosOut = new ByteArrayOutputStream(); + System.setOut(new PrintStream(baosOut)); + + ByteArrayOutputStream baosErr = new ByteArrayOutputStream(); + System.setErr(new PrintStream(baosErr)); + + CspFormatter cspFormatter = new CspFormatter(); + ConsoleHandler consoleHandler = new ConsoleHandler(); + + LogRecord logRecord; + + // Test INFO level, should log to stdout + logRecord = new LogRecord(Level.INFO, "test info message"); + consoleHandler.publish(logRecord); + assertEquals(cspFormatter.format(logRecord), baosOut.toString()); + assertEquals("", baosErr.toString()); + baosOut.reset(); + baosErr.reset(); + + // Test INFO level, should log to stderr + logRecord = new LogRecord(Level.WARNING, "test warning message"); + consoleHandler.publish(logRecord); + assertEquals(cspFormatter.format(logRecord), baosErr.toString()); + assertEquals("", baosOut.toString()); + baosOut.reset(); + baosErr.reset(); + + // Test FINE level, no log by default + // Default log level is INFO, to log FINE message to stdout, add following config in $JAVA_HOME/jre/lib/logging.properties + // java.util.logging.StreamHandler.level=FINE + logRecord = new LogRecord(Level.FINE, "test fine message"); + consoleHandler.publish(logRecord); + assertEquals("", baosOut.toString()); + assertEquals("", baosErr.toString()); + baosOut.reset(); + baosErr.reset(); + + // Test SEVERE level, should log to stderr + logRecord = new LogRecord(Level.SEVERE, "test severe message"); + consoleHandler.publish(logRecord); + assertEquals(cspFormatter.format(logRecord), baosErr.toString()); + assertEquals("", baosOut.toString()); + baosOut.reset(); + baosErr.reset(); + } +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/log/LogBaseTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/log/LogBaseTest.java new file mode 100644 index 0000000000..5a380956bc --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/log/LogBaseTest.java @@ -0,0 +1,79 @@ +/* + * 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.log; + +import org.junit.Assert; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +import static com.alibaba.csp.sentinel.log.LogBase.*; +import static com.alibaba.csp.sentinel.util.ConfigUtil.addSeparator; + +/** + * @author lianglin + * @since 1.7.0 + */ +public class LogBaseTest { + + + + //add Jvm parameter + //-Dcsp.sentinel.config.file=log-propertiesTest.properties + //-Dcsp.sentinel.log.charset="utf-8" + //-Dcsp.sentinel.log.output.type="file" + //@Test + public void testLoadProperties() throws IOException { + + File file = null; + String fileName = "log-propertiesTest.properties"; + try { + file = new File(addSeparator(System.getProperty("user.dir")) + "target/classes/" + fileName); + if (!file.exists()) { + file.createNewFile(); + } + BufferedWriter out = new BufferedWriter(new FileWriter(file)); + out.write(buildPropertyStr(LOG_DIR, "/data/logs/")); + out.write("\n"); + out.write(buildPropertyStr(LOG_NAME_USE_PID, "true")); + out.write("\n"); + out.write(buildPropertyStr(LOG_OUTPUT_TYPE, "console")); + out.write("\n"); + out.write(buildPropertyStr(LOG_CHARSET, "gbk")); + out.flush(); + out.close(); + + //test will make dir + //Assert.assertTrue(LogBase.getLogBaseDir().equals("/data/logs/")); + Assert.assertTrue(LogBase.isLogNameUsePid()); + Assert.assertTrue(LogBase.getLogOutputType().equals("file")); + Assert.assertTrue(LogBase.getLogCharset().equals("utf-8")); + } finally { + if (file != null) { + file.delete(); + } + } + + + } + + + private String buildPropertyStr(String key, String value) { + return key + "=" + value; + } +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/metric/extension/callback/MetricExitCallbackTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/metric/extension/callback/MetricExitCallbackTest.java index 8bf2b89520..604c94c29e 100644 --- a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/metric/extension/callback/MetricExitCallbackTest.java +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/metric/extension/callback/MetricExitCallbackTest.java @@ -1,3 +1,18 @@ +/* + * 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.metric.extension.callback; import com.alibaba.csp.sentinel.Entry; @@ -5,18 +20,18 @@ import com.alibaba.csp.sentinel.context.Context; import com.alibaba.csp.sentinel.metric.extension.MetricExtensionProvider; import com.alibaba.csp.sentinel.slotchain.StringResourceWrapper; -import com.alibaba.csp.sentinel.util.TimeUtil; +import com.alibaba.csp.sentinel.test.AbstractTimeBasedTest; import org.junit.Assert; import org.junit.Test; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.when; /** * @author Carpenter Lee */ -public class MetricExitCallbackTest { +public class MetricExitCallbackTest extends AbstractTimeBasedTest { @Test public void onExit() { @@ -27,17 +42,24 @@ public void onExit() { StringResourceWrapper resourceWrapper = new StringResourceWrapper("resource", EntryType.OUT); int count = 2; Object[] args = {"args1", "args2"}; - extension.rt = 20; + long prevRt = 20; + extension.rt = prevRt; extension.success = 6; extension.thread = 10; Context context = mock(Context.class); Entry entry = mock(Entry.class); + + // Mock current time + long curMillis = System.currentTimeMillis(); + setCurrentMillis(curMillis); + + int deltaMs = 100; when(entry.getError()).thenReturn(null); - when(entry.getCreateTime()).thenReturn(TimeUtil.currentTimeMillis() - 100); + when(entry.getCreateTime()).thenReturn(curMillis - deltaMs); when(context.getCurEntry()).thenReturn(entry); exitCallback.onExit(context, resourceWrapper, count, args); - Assert.assertEquals(120, extension.rt, 10); + Assert.assertEquals(prevRt + deltaMs, extension.rt); Assert.assertEquals(extension.success, 6 + count); Assert.assertEquals(extension.thread, 10 - 1); } -} \ No newline at end of file +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/node/ClusterNodeTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/node/ClusterNodeTest.java index e900cea9f2..9c5710573c 100644 --- a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/node/ClusterNodeTest.java +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/node/ClusterNodeTest.java @@ -41,7 +41,7 @@ public class ClusterNodeTest { @Test public void testGetOrCreateOriginNodeSingleThread() { - ClusterNode clusterNode = new ClusterNode(); + ClusterNode clusterNode = new ClusterNode("test"); String origin1 = "origin1"; Node originNode1 = clusterNode.getOrCreateOriginNode(origin1); @@ -74,7 +74,7 @@ public void testGetOrCreateOriginNodeMultiThread() { final int testTimes = 10; for (int times = 0; times < testTimes; times++) { - final ClusterNode clusterNode = new ClusterNode(); + final ClusterNode clusterNode = new ClusterNode("test"); // Store all distinct nodes by calling ClusterNode#getOrCreateOriginNode. // Here we need a thread-safe concurrent set (created from ConcurrentHashMap). @@ -130,7 +130,7 @@ public Object call() throws Exception { @Test public void testTraceException() { - ClusterNode clusterNode = new ClusterNode(); + ClusterNode clusterNode = new ClusterNode("test"); Exception exception = new RuntimeException("test"); diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/node/StatisticNodeTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/node/StatisticNodeTest.java index c34b8bf919..d128e6864a 100644 --- a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/node/StatisticNodeTest.java +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/node/StatisticNodeTest.java @@ -15,7 +15,7 @@ */ package com.alibaba.csp.sentinel.node; -import com.alibaba.csp.sentinel.Constants; +import com.alibaba.csp.sentinel.config.SentinelConfig; import com.alibaba.csp.sentinel.util.TimeUtil; import org.junit.Assert; import org.junit.Test; @@ -111,7 +111,7 @@ public void testStatisticThreadNumAndQps() { assertEquals(totalRequest, node.totalSuccess()); // now there are no data in time span, so the minRT should be equals to TIME_DROP_VALVE - assertEquals(node.minRt(), Constants.TIME_DROP_VALVE, 0.01); + assertEquals(node.minRt(), SentinelConfig.statisticMaxRt(), 0.01); log("===================================================="); log("testStatisticThreadNumAndQps done, cost " + (TimeUtil.currentTimeMillis() - testStartTime) + "ms"); diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/node/metric/MetricNodeTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/node/metric/MetricNodeTest.java new file mode 100644 index 0000000000..9331f0b682 --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/node/metric/MetricNodeTest.java @@ -0,0 +1,37 @@ +/* + * 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.node.metric; + +import com.alibaba.csp.sentinel.ResourceTypeConstants; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author Eric Zhao + */ +public class MetricNodeTest { + + @Test + public void testFromFatString() { + String line = "1564382218000|2019-07-29 14:36:58|/foo/*|1|0|1|0|0|0|2|1"; + MetricNode node = MetricNode.fromFatString(line); + assertEquals(ResourceTypeConstants.COMMON_WEB, node.getClassification()); + assertEquals(2, node.getConcurrency()); + assertEquals(1, node.getSuccessQps()); + } +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/DefaultSlotChainBuilderTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/DefaultSlotChainBuilderTest.java index e90f2206fe..cbe7a951b8 100644 --- a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/DefaultSlotChainBuilderTest.java +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/DefaultSlotChainBuilderTest.java @@ -16,6 +16,7 @@ package com.alibaba.csp.sentinel.slots; import com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot; +import com.alibaba.csp.sentinel.slotchain.ProcessorSlot; import com.alibaba.csp.sentinel.slotchain.ProcessorSlotChain; import com.alibaba.csp.sentinel.slots.block.authority.AuthoritySlot; import com.alibaba.csp.sentinel.slots.block.degrade.DegradeSlot; @@ -27,6 +28,8 @@ import com.alibaba.csp.sentinel.slots.system.SystemSlot; import org.junit.Test; +import java.util.ServiceLoader; + import static org.junit.Assert.*; /** diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeRuleManagerTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeRuleManagerTest.java index 3884773c06..0d73610bd5 100644 --- a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeRuleManagerTest.java +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeRuleManagerTest.java @@ -43,11 +43,23 @@ public void testIsValidRule() { .setCount(-3) .setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT) .setTimeWindow(2); + DegradeRule rule5 = new DegradeRule("Sentinel") + .setCount(97) + .setGrade(RuleConstant.DEGRADE_GRADE_RT) + .setTimeWindow(15) + .setRtSlowRequestAmount(0); + DegradeRule rule6 = new DegradeRule("Sentinel") + .setCount(0.93d) + .setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO) + .setTimeWindow(20) + .setMinRequestAmount(0); assertFalse(DegradeRuleManager.isValidRule(rule1)); assertFalse(DegradeRuleManager.isValidRule(rule2)); assertFalse(DegradeRuleManager.isValidRule(rule3)); assertTrue(DegradeRuleManager.isValidRule(rule3.setCount(1.0d))); assertTrue(DegradeRuleManager.isValidRule(rule3.setCount(0.0d))); assertFalse(DegradeRuleManager.isValidRule(rule4)); + assertFalse(DegradeRuleManager.isValidRule(rule5)); + assertFalse(DegradeRuleManager.isValidRule(rule6)); } } \ No newline at end of file diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeTest.java index b2518a8016..75668c0be9 100755 --- a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeTest.java +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeTest.java @@ -15,14 +15,6 @@ */ package com.alibaba.csp.sentinel.slots.block.degrade; -import static org.junit.Assert.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.concurrent.TimeUnit; - -import org.junit.Test; - import com.alibaba.csp.sentinel.EntryType; import com.alibaba.csp.sentinel.context.Context; import com.alibaba.csp.sentinel.node.ClusterNode; @@ -30,6 +22,14 @@ import com.alibaba.csp.sentinel.slotchain.StringResourceWrapper; import com.alibaba.csp.sentinel.slots.block.RuleConstant; import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * @author jialiang.linjl @@ -47,12 +47,16 @@ public void testAverageRtDegrade() throws InterruptedException { when(node.getClusterNode()).thenReturn(cn); when(cn.avgRt()).thenReturn(2d); + int rtSlowRequestAmount = 10; DegradeRule rule = new DegradeRule(); rule.setCount(1); rule.setResource(key); rule.setTimeWindow(2); + rule.setGrade(RuleConstant.DEGRADE_GRADE_RT); + rule.setRtSlowRequestAmount(rtSlowRequestAmount); - for (int i = 0; i < 4; i++) { + //Will true + for (int i = 0; i < rtSlowRequestAmount - 1; i++) { assertTrue(rule.passCheck(context, node, 1)); } @@ -69,9 +73,6 @@ public void testAverageRtDegrade() throws InterruptedException { public void testExceptionRatioModeDegrade() throws Throwable { String key = "test_degrade_exception_ratio"; ClusterNode cn = mock(ClusterNode.class); - when(cn.exceptionQps()).thenReturn(2d); - // Indicates that there are QPS more than min threshold. - when(cn.totalQps()).thenReturn(12d); ClusterBuilderSlot.getClusterNodeMap().put(new StringResourceWrapper(key, EntryType.IN), cn); Context context = mock(Context.class); @@ -83,17 +84,39 @@ public void testExceptionRatioModeDegrade() throws Throwable { rule.setResource(key); rule.setTimeWindow(2); rule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO); + rule.setMinRequestAmount(20); - when(cn.successQps()).thenReturn(8d); - // Will fail. + // Will true. While totalQps < minRequestAmount + when(cn.totalQps()).thenReturn(8d); + assertTrue(rule.passCheck(context, node, 1)); + + // Will true. + when(cn.totalQps()).thenReturn(21d); + when(cn.successQps()).thenReturn(9d); + when(cn.exceptionQps()).thenReturn(9d); + assertTrue(rule.passCheck(context, node, 1)); + + + // Will true. While totalQps > minRequestAmount and exceptionRation < count + when(cn.totalQps()).thenReturn(100d); + when(cn.successQps()).thenReturn(90d); + when(cn.exceptionQps()).thenReturn(10d); + assertTrue(rule.passCheck(context, node, 1)); + + // Will fail. While totalQps > minRequestAmount and exceptionRation > count + rule.setMinRequestAmount(5); + when(cn.totalQps()).thenReturn(12d); + when(cn.successQps()).thenReturn(8d); + when(cn.exceptionQps()).thenReturn(6d); assertFalse(rule.passCheck(context, node, 1)); // Restore from the degrade timeout. TimeUnit.MILLISECONDS.sleep(2200); - when(cn.successQps()).thenReturn(20d); // Will pass. + when(cn.totalQps()).thenReturn(106d); + when(cn.successQps()).thenReturn(100d); assertTrue(rule.passCheck(context, node, 1)); } @@ -127,4 +150,33 @@ public void testExceptionCountModeDegrade() throws Throwable { assertTrue(rule.passCheck(context, node, 1)); } + @Test + public void testEquals() { + DegradeRule degradeRule1 = new DegradeRule(); + DegradeRule degradeRule2 = new DegradeRule(); + assertTrue(degradeRule1.equals(degradeRule2)); + + int rtSlowRequestAmount = 10; + int minRequestAmount = 20; + double count = 1.0; + int timeWindow = 2; + degradeRule1.setRtSlowRequestAmount(rtSlowRequestAmount); + degradeRule1.setMinRequestAmount(minRequestAmount); + degradeRule1.setCount(count); + degradeRule1.setTimeWindow(timeWindow); + degradeRule1.setGrade(RuleConstant.DEGRADE_GRADE_RT); + + degradeRule2.setRtSlowRequestAmount(rtSlowRequestAmount); + degradeRule2.setMinRequestAmount(minRequestAmount); + degradeRule2.setCount(count); + degradeRule2.setGrade(RuleConstant.DEGRADE_GRADE_RT); + degradeRule2.setTimeWindow(timeWindow); + assertTrue(degradeRule1.equals(degradeRule2)); + + degradeRule2.setMinRequestAmount(100); + assertFalse(degradeRule1.equals(degradeRule2)); + + + } + } diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/logger/EagleEyeLogUtilTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/logger/EagleEyeLogUtilTest.java index fd9e9f4318..f9e7dd7029 100644 --- a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/logger/EagleEyeLogUtilTest.java +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/logger/EagleEyeLogUtilTest.java @@ -1,19 +1,16 @@ package com.alibaba.csp.sentinel.slots.logger; -import java.io.File; -import java.util.concurrent.Callable; -import java.util.concurrent.TimeUnit; - import com.alibaba.csp.sentinel.log.LogBase; import com.alibaba.csp.sentinel.log.RecordLog; - -import org.hamcrest.CoreMatchers; -import org.hamcrest.Matchers; import org.hamcrest.io.FileMatchers; +import org.junit.Assert; import org.junit.Test; +import java.io.File; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + import static org.awaitility.Awaitility.await; -import static org.junit.Assert.*; /** * @author Carpenter Lee @@ -34,7 +31,9 @@ public File call() throws Exception { }, FileMatchers.anExistingFile()); } - @Test + //Change LogBase It is not not work when integration Testing + //Because LogBase.LOG_DIR can be just static init for once and it will not be changed + //@Test public void testChangeLogBase() throws Exception { String userHome = System.getProperty("user.home"); String newLogBase = userHome + File.separator + "tmpLogDir" + System.currentTimeMillis(); @@ -42,13 +41,27 @@ public void testChangeLogBase() throws Exception { EagleEyeLogUtil.log("resourceName", "BlockException", "app1", "origin", 1); + final File file = new File(RecordLog.getLogBaseDir() + EagleEyeLogUtil.FILE_NAME); await().timeout(2, TimeUnit.SECONDS) - .until(new Callable() { - @Override - public File call() throws Exception { - return file; + .until(new Callable() { + @Override + public File call() throws Exception { + return file; + } + }, FileMatchers.anExistingFile()); + Assert.assertTrue(file.getAbsolutePath().startsWith(newLogBase)); + deleteLogDir(new File(RecordLog.getLogBaseDir())); + } + + private void deleteLogDir(File logDirFile) { + if (logDirFile != null && logDirFile.isDirectory()) { + if (logDirFile.listFiles() != null) { + for (File file : logDirFile.listFiles()) { + file.delete(); } - }, FileMatchers.anExistingFile()); + } + logDirFile.delete(); + } } } \ No newline at end of file diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/statistic/metric/ArrayMetricTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/statistic/metric/ArrayMetricTest.java index 80293d4874..2097670e0a 100644 --- a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/statistic/metric/ArrayMetricTest.java +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/statistic/metric/ArrayMetricTest.java @@ -16,9 +16,14 @@ package com.alibaba.csp.sentinel.slots.statistic.metric; import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import com.alibaba.csp.sentinel.node.metric.MetricNode; +import com.alibaba.csp.sentinel.slots.statistic.MetricEvent; import com.alibaba.csp.sentinel.slots.statistic.base.WindowWrap; import com.alibaba.csp.sentinel.slots.statistic.data.MetricBucket; +import com.alibaba.csp.sentinel.util.function.Predicate; import org.junit.Test; @@ -38,7 +43,8 @@ public class ArrayMetricTest { @Test public void testOperateArrayMetric() { BucketLeapArray leapArray = mock(BucketLeapArray.class); - final WindowWrap windowWrap = new WindowWrap(windowLengthInMs, 0, new MetricBucket()); + final WindowWrap windowWrap = new WindowWrap(windowLengthInMs, 0, + new MetricBucket()); when(leapArray.currentWindow()).thenReturn(windowWrap); when(leapArray.values()).thenReturn(new ArrayList() {{ add(windowWrap.value()); }}); @@ -70,4 +76,45 @@ public void testOperateArrayMetric() { assertEquals(expectedException, metric.exception()); assertEquals(expectedRt, metric.rt()); } -} \ No newline at end of file + + @Test + public void testGetMetricDetailsOnCondition() { + BucketLeapArray leapArray = mock(BucketLeapArray.class); + // Mock interval=2s, sampleCount=2 + final WindowWrap w1 = new WindowWrap<>(windowLengthInMs, 500, + new MetricBucket().add(MetricEvent.PASS, 1)); + final WindowWrap w2 = new WindowWrap<>(windowLengthInMs, 1000, + new MetricBucket().add(MetricEvent.PASS, 2)); + final WindowWrap w3 = new WindowWrap<>(windowLengthInMs, 1500, + new MetricBucket().add(MetricEvent.PASS, 3)); + final WindowWrap w4 = new WindowWrap<>(windowLengthInMs, 2000, + new MetricBucket().add(MetricEvent.PASS, 4)); + List> buckets = Arrays.asList(w1, w2, w3, w4); + when(leapArray.currentWindow()).thenReturn(w4); + when(leapArray.list()).thenReturn(buckets); + + ArrayMetric metric = new ArrayMetric(leapArray); + + // Empty condition -> retrieve all + assertEquals(4, metric.detailsOnCondition(null).size()); + // Normal condition + List metricNodes = metric.detailsOnCondition(new Predicate() { + @Override + public boolean test(Long t) { + return t >= 1500; + } + }); + assertEquals(2, metricNodes.size()); + assertEquals(3, metricNodes.get(0).getPassQps()); + assertEquals(4, metricNodes.get(1).getPassQps()); + + // Future condition + metricNodes = metric.detailsOnCondition(new Predicate() { + @Override + public boolean test(Long t) { + return t >= 2500; + } + }); + assertEquals(0, metricNodes.size()); + } +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/system/SystemRuleManagerTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/system/SystemRuleManagerTest.java new file mode 100644 index 0000000000..fcd1f66512 --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/system/SystemRuleManagerTest.java @@ -0,0 +1,111 @@ +/* + * 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.slots.system; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.slotchain.StringResourceWrapper; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author Eric Zhao + */ +public class SystemRuleManagerTest { + + @Test + public void testLoadInvalidRules() { + SystemRule rule1 = new SystemRule(); + rule1.setHighestSystemLoad(-0.9d); + SystemRule rule2 = new SystemRule(); + rule2.setHighestCpuUsage(2.7d); + SystemRuleManager.loadRules(Arrays.asList(rule1, rule2)); + assertEquals(0, SystemRuleManager.getRules().size()); + } + + @Test + public void testLoadAndGetRules() { + SystemRule rule1 = new SystemRule(); + rule1.setHighestSystemLoad(1.2d); + SystemRule rule2 = new SystemRule(); + rule2.setMaxThread(17); + SystemRule rule3 = new SystemRule(); + rule3.setHighestCpuUsage(0.7d); + SystemRule rule4 = new SystemRule(); + rule4.setQps(1500); + SystemRule rule5 = new SystemRule(); + rule5.setAvgRt(50); + SystemRuleManager.loadRules(Arrays.asList(rule1, rule2, rule3, rule4, rule5)); + assertEquals(1.2d, SystemRuleManager.getSystemLoadThreshold(), 0.01); + assertEquals(17, SystemRuleManager.getMaxThreadThreshold()); + assertEquals(0.7d, SystemRuleManager.getCpuUsageThreshold(), 0.01); + assertEquals(1500, SystemRuleManager.getInboundQpsThreshold(), 0.01); + assertEquals(50, SystemRuleManager.getRtThreshold()); + } + + @Test + public void testLoadDuplicateTypeOfRules() { + SystemRule rule1 = new SystemRule(); + rule1.setHighestSystemLoad(1.2d); + SystemRule rule2 = new SystemRule(); + rule2.setHighestSystemLoad(2.3d); + SystemRule rule3 = new SystemRule(); + rule3.setHighestSystemLoad(3.4d); + SystemRuleManager.loadRules(Arrays.asList(rule1, rule2, rule3)); + + List rules = SystemRuleManager.getRules(); + assertEquals(1, rules.size()); + assertEquals(1.2d, rules.get(0).getHighestSystemLoad(), 0.01); + assertEquals(1.2d, SystemRuleManager.getSystemLoadThreshold(), 0.01); + } + + @Test + public void testCheckMaxCpuUsageNotBBR() throws Exception { + SystemRule rule1 = new SystemRule(); + rule1.setHighestCpuUsage(0d); + SystemRuleManager.loadRules(Collections.singletonList(rule1)); + + // Wait until SystemStatusListener triggered the first CPU usage collecting. + Thread.sleep(1500); + + boolean blocked = false; + try { + SystemRuleManager.checkSystem(new StringResourceWrapper("testCheckMaxCpuUsageNotBBR", EntryType.IN)); + } catch (BlockException ex) { + blocked = true; + } + assertTrue("The entry should be blocked under SystemRule maxCpuUsage=0", blocked); + } + + @Before + public void setUp() throws Exception { + SystemRuleManager.loadRules(new ArrayList()); + } + + @After + public void tearDown() throws Exception { + SystemRuleManager.loadRules(new ArrayList()); + } +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/util/ConfigUtilTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/util/ConfigUtilTest.java new file mode 100644 index 0000000000..43094006a1 --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/util/ConfigUtilTest.java @@ -0,0 +1,125 @@ +/* + * 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.util; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.Properties; + +import static com.alibaba.csp.sentinel.log.LogBase.LOG_DIR; +import static com.alibaba.csp.sentinel.log.LogBase.LOG_OUTPUT_TYPE; +import static com.alibaba.csp.sentinel.util.ConfigUtil.addSeparator; + +/** + * @author lianglin + * @since 1.7.0 + */ +public class ConfigUtilTest { + + @Test + public void testLoadProperties() throws IOException { + + File file = null; + String logOutputType = "utf-8", + dir = "/data/logs/", + fileName = "propertiesTest.properties"; + try { + String userDir = System.getProperty("user.dir"); + file = new File(addSeparator(userDir) + "target/classes/" + fileName); + if (!file.exists()) { + file.createNewFile(); + } + BufferedWriter out = new BufferedWriter(new FileWriter(file)); + out.write(LOG_OUTPUT_TYPE + "=" + logOutputType); + out.write(System.getProperty("line.separator")); + out.write(LOG_DIR + "=" + dir); + out.flush(); + out.close(); + + //Load from absolutePath + Properties properties = ConfigUtil.loadProperties(file.getAbsolutePath()); + Assert.assertTrue(logOutputType.equals(properties.getProperty(LOG_OUTPUT_TYPE))); + Assert.assertTrue(dir.equals(properties.getProperty(LOG_DIR))); + + + //Load from classPath + properties = ConfigUtil.loadProperties(ConfigUtil.CLASSPATH_FILE_FLAG + fileName); + Assert.assertTrue(logOutputType.equals(properties.getProperty(LOG_OUTPUT_TYPE))); + Assert.assertTrue(dir.equals(properties.getProperty(LOG_DIR))); + + + //Load from relativePath + properties = ConfigUtil.loadProperties("target/classes/" + fileName); + Assert.assertTrue(logOutputType.equals(properties.getProperty(LOG_OUTPUT_TYPE))); + Assert.assertTrue(dir.equals(properties.getProperty(LOG_DIR))); + + } finally { + if (file != null) { + file.delete(); + } + } + + } + + //add Jvm parameter + //-Dcsp.sentinel.charset="UTF-16" + //@Test + public void testLoadPropertiesWithCustomizedCharset() throws IOException { + + String charset = "UTF-16"; + + File file = null; + String dir = "/data/logs/", + fileName = "propertiesTest.properties"; + try { + String userDir = System.getProperty("user.dir"); + file = new File(addSeparator(userDir) + "target/classes/" + fileName); + if (!file.exists()) { + file.createNewFile(); + } + OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream(file), charset); + out.write(LOG_DIR + "=" + dir); + out.flush(); + out.close(); + + //Load from absolutePath + Properties properties = ConfigUtil.loadProperties(file.getAbsolutePath()); + Assert.assertTrue(dir.equals(properties.getProperty(LOG_DIR))); + + //Load from classPath + properties = ConfigUtil.loadProperties(ConfigUtil.CLASSPATH_FILE_FLAG + fileName); + Assert.assertTrue(dir.equals(properties.getProperty(LOG_DIR))); + + //Load from relativePath + properties = ConfigUtil.loadProperties("target/classes/" + fileName); + Assert.assertTrue(dir.equals(properties.getProperty(LOG_DIR))); + + } finally { + if (file != null) { + file.delete(); + } + } + + } + +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/util/SpiLoaderTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/util/SpiLoaderTest.java index 35944be4b5..e8127303a0 100644 --- a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/util/SpiLoaderTest.java +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/util/SpiLoaderTest.java @@ -106,9 +106,41 @@ public void testLoadInstanceListSorted() { for (int i = 0; i < sortedSlots.size(); i++) { ProcessorSlot slot = sortedSlots.get(i); ProcessorSlot slot2 = sortedSlots2.get(i); + assertEquals(slot.getClass(), slot2.getClass()); + } + } + + @Test + public void testLoadDifferentInstanceListSorted() { + List sortedSlots = SpiLoader.loadInstanceListSorted(ProcessorSlot.class); + assertNotNull(sortedSlots); + + // Total 8 default slot in sentinel-core + assertEquals(8, sortedSlots.size()); + + // Verify the order of slot + int index = 0; + assertTrue(sortedSlots.get(index++) instanceof NodeSelectorSlot); + assertTrue(sortedSlots.get(index++) instanceof ClusterBuilderSlot); + assertTrue(sortedSlots.get(index++) instanceof LogSlot); + assertTrue(sortedSlots.get(index++) instanceof StatisticSlot); + assertTrue(sortedSlots.get(index++) instanceof SystemSlot); + assertTrue(sortedSlots.get(index++) instanceof AuthoritySlot); + assertTrue(sortedSlots.get(index++) instanceof FlowSlot); + assertTrue(sortedSlots.get(index++) instanceof DegradeSlot); + + // Verify each call return different instances + List sortedSlots2 = SpiLoader.loadDifferentInstanceListSorted(ProcessorSlot.class); + assertNotSame(sortedSlots, sortedSlots2); + assertEquals(sortedSlots.size(), sortedSlots2.size()); + for (int i = 0; i < sortedSlots.size(); i++) { + ProcessorSlot slot = sortedSlots.get(i); + ProcessorSlot slot2 = sortedSlots2.get(i); + assertEquals(slot.getClass(), slot2.getClass()); + + // Verify the instances are different assertNotSame(slot, slot2); assertNotEquals(slot, slot2); - assertEquals(slot.getClass(), slot2.getClass()); } } } diff --git a/sentinel-dashboard/pom.xml b/sentinel-dashboard/pom.xml index b8bafb1090..fcddca45af 100755 --- a/sentinel-dashboard/pom.xml +++ b/sentinel-dashboard/pom.xml @@ -6,7 +6,7 @@ com.alibaba.csp sentinel-parent - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT sentinel-dashboard @@ -38,6 +38,11 @@ sentinel-parameter-flow-control ${project.version} + + com.alibaba.csp + sentinel-api-gateway-adapter-common + ${project.version} + org.springframework.boot @@ -175,6 +180,7 @@ src/main/resources + true diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/AuthAction.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/AuthAction.java new file mode 100644 index 0000000000..f521c04dfa --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/AuthAction.java @@ -0,0 +1,47 @@ +/* + * 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.dashboard.auth; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author lkxiaolou + * @since 1.7.1 + */ +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Target({ElementType.METHOD}) +public @interface AuthAction { + + /** + * @return the privilege type + */ + AuthService.PrivilegeType value(); + + /** + * @return the target name to control + */ + String targetName() default "app"; + + /** + * @return the message when permission is denied + */ + String message() default "Permission denied"; +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/AuthorizationInterceptor.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/AuthorizationInterceptor.java new file mode 100644 index 0000000000..19472521ae --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/AuthorizationInterceptor.java @@ -0,0 +1,72 @@ +/* + * 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.dashboard.auth; + +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.fastjson.JSON; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.lang.reflect.Method; + +/** + * The web interceptor for privilege-based authorization. + * + * @author lkxiaolou + * @since 1.7.1 + */ +@Component +public class AuthorizationInterceptor implements HandlerInterceptor { + + @Autowired + private AuthService authService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + if (handler.getClass().isAssignableFrom(HandlerMethod.class)) { + Method method = ((HandlerMethod) handler).getMethod(); + + AuthAction authAction = method.getAnnotation(AuthAction.class); + if (authAction != null) { + AuthService.AuthUser authUser = authService.getAuthUser(request); + if (authUser == null) { + responseNoPrivilegeMsg(response, authAction.message()); + return false; + } + String target = request.getParameter(authAction.targetName()); + + if (!authUser.authTarget(target, authAction.value())) { + responseNoPrivilegeMsg(response, authAction.message()); + return false; + } + } + } + + return true; + } + + private void responseNoPrivilegeMsg(HttpServletResponse response, String message) throws IOException { + Result result = Result.ofFail(-1, message); + response.addHeader("Content-Type", "application/json;charset=UTF-8"); + response.getOutputStream().write(JSON.toJSONBytes(result)); + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/filter/AuthFilter.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/LoginAuthenticationFilter.java similarity index 74% rename from sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/filter/AuthFilter.java rename to sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/LoginAuthenticationFilter.java index 45972c4f15..f489b48e7f 100644 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/filter/AuthFilter.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/LoginAuthenticationFilter.java @@ -13,9 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.alibaba.csp.sentinel.dashboard.filter; +package com.alibaba.csp.sentinel.dashboard.auth; -import com.alibaba.csp.sentinel.dashboard.auth.AuthService; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -30,39 +29,46 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; + import java.io.IOException; import java.util.List; /** - * Servlet Filter that authenticate requests. - * - * Note: - * Some urls are excluded as they needn't auth, such as: + *

The Servlet filter for authentication.

* - * Index url: / - * Authentication request url: /login,logout - * Used for client: /registry/machine - * Static resources: htm,html,js and so on. + *

Note: some urls are excluded as they needn't auth, such as:

+ *
    + *
  • index url: {@code /}
  • + *
  • authentication request url: {@code /login}, {@code /logout}
  • + *
  • machine registry: {@code /registry/machine}
  • + *
  • static resources
  • + *
* - * The excluded urls and urlSuffixes are configured in application.properties + * The excluded urls and urlSuffixes could be configured in {@code application.properties} file. * * @author cdfive * @since 1.6.0 */ @Component -public class AuthFilter implements Filter { +public class LoginAuthenticationFilter implements Filter { private static final String URL_SUFFIX_DOT = "."; - /**Some urls which needn't auth, such as /auth/login,/registry/machine and so on*/ + /** + * Some urls which needn't auth, such as /auth/login, /registry/machine and so on. + */ @Value("#{'${auth.filter.exclude-urls}'.split(',')}") private List authFilterExcludeUrls; - /**Some urls with suffixes which needn't auth, such as htm,html,js and so on*/ + /** + * Some urls with suffixes which needn't auth, such as htm, html, js and so on. + */ @Value("#{'${auth.filter.exclude-url-suffixes}'.split(',')}") private List authFilterExcludeUrlSuffixes; - /**Authentication using AuthService interface*/ + /** + * Authentication using AuthService interface. + */ @Autowired private AuthService authService; @@ -72,13 +78,14 @@ public void init(FilterConfig filterConfig) throws ServletException { } @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; - String requestURI = httpRequest.getRequestURI(); + String servletPath = httpRequest.getServletPath(); // Exclude the urls which needn't auth - if (authFilterExcludeUrls.contains(requestURI)) { + if (authFilterExcludeUrls.contains(servletPath)) { chain.doFilter(request, response); return; } @@ -94,7 +101,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha authFilterExcludeUrlSuffix = URL_SUFFIX_DOT + authFilterExcludeUrlSuffix; } - if (requestURI.endsWith(authFilterExcludeUrlSuffix)) { + if (servletPath.endsWith(authFilterExcludeUrlSuffix)) { chain.doFilter(request, response); return; } diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/SimpleWebAuthServiceImpl.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/SimpleWebAuthServiceImpl.java index 1cae8a60de..5d9599ee96 100644 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/SimpleWebAuthServiceImpl.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/auth/SimpleWebAuthServiceImpl.java @@ -15,6 +15,7 @@ */ package com.alibaba.csp.sentinel.dashboard.auth; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; @@ -25,16 +26,17 @@ * @author cdfive * @since 1.6.0 */ -@Primary @Component +@Primary +@ConditionalOnProperty(name = "auth.enabled", matchIfMissing = true) public class SimpleWebAuthServiceImpl implements AuthService { - public static final String WEB_SESSTION_KEY = "session_sentinel_admin"; + public static final String WEB_SESSION_KEY = "session_sentinel_admin"; @Override public AuthUser getAuthUser(HttpServletRequest request) { HttpSession session = request.getSession(); - Object sentinelUserObj = session.getAttribute(SimpleWebAuthServiceImpl.WEB_SESSTION_KEY); + Object sentinelUserObj = session.getAttribute(SimpleWebAuthServiceImpl.WEB_SESSION_KEY); if (sentinelUserObj != null && sentinelUserObj instanceof AuthUser) { return (AuthUser) sentinelUserObj; } diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/client/SentinelApiClient.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/client/SentinelApiClient.java index e3aef79ba4..396258e3ce 100755 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/client/SentinelApiClient.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/client/SentinelApiClient.java @@ -30,9 +30,12 @@ import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; +import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule; import com.alibaba.csp.sentinel.command.CommandConstants; import com.alibaba.csp.sentinel.config.SentinelConfig; import com.alibaba.csp.sentinel.command.vo.NodeVo; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity; import com.alibaba.csp.sentinel.dashboard.util.AsyncUtils; import com.alibaba.csp.sentinel.slots.block.Rule; import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule; @@ -59,12 +62,14 @@ import com.alibaba.csp.sentinel.dashboard.domain.cluster.config.ServerTransportConfig; import com.alibaba.csp.sentinel.dashboard.util.VersionUtils; +import org.apache.http.Consts; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.concurrent.FutureCallback; import org.apache.http.entity.ContentType; import org.apache.http.impl.client.DefaultRedirectStrategy; @@ -89,6 +94,8 @@ public class SentinelApiClient { private static Logger logger = LoggerFactory.getLogger(SentinelApiClient.class); private static final Charset DEFAULT_CHARSET = Charset.forName(SentinelConfig.charset()); + private static final String HTTP_HEADER_CONTENT_TYPE = "Content-Type"; + private static final String HTTP_HEADER_CONTENT_TYPE_URLENCODED = ContentType.create(URLEncodedUtils.CONTENT_TYPE).toString(); private static final String RESOURCE_URL_PATH = "jsonTree"; private static final String CLUSTER_NODE_PATH = "clusterNode"; @@ -102,13 +109,18 @@ public class SentinelApiClient { private static final String FETCH_CLUSTER_CLIENT_CONFIG_PATH = "cluster/client/fetchConfig"; private static final String MODIFY_CLUSTER_CLIENT_CONFIG_PATH = "cluster/client/modifyConfig"; - private static final String FETCH_CLUSTER_SERVER_ALL_CONFIG_PATH = "cluster/server/fetchConfig"; private static final String FETCH_CLUSTER_SERVER_BASIC_INFO_PATH = "cluster/server/info"; private static final String MODIFY_CLUSTER_SERVER_TRANSPORT_CONFIG_PATH = "cluster/server/modifyTransportConfig"; private static final String MODIFY_CLUSTER_SERVER_FLOW_CONFIG_PATH = "cluster/server/modifyFlowConfig"; private static final String MODIFY_CLUSTER_SERVER_NAMESPACE_SET_PATH = "cluster/server/modifyNamespaceSet"; + private static final String FETCH_GATEWAY_API_PATH = "gateway/getApiDefinitions"; + private static final String MODIFY_GATEWAY_API_PATH = "gateway/updateApiDefinitions"; + + private static final String FETCH_GATEWAY_FLOW_RULE_PATH = "gateway/getRules"; + private static final String MODIFY_GATEWAY_FLOW_RULE_PATH = "gateway/updateRules"; + private static final String FLOW_RULE_TYPE = "flow"; private static final String DEGRADE_RULE_TYPE = "degrade"; private static final String SYSTEM_RULE_TYPE = "system"; @@ -117,6 +129,7 @@ public class SentinelApiClient { private CloseableHttpAsyncClient httpClient; private static final SentinelVersion version160 = new SentinelVersion(1, 6, 0); + private static final SentinelVersion version171 = new SentinelVersion(1, 7, 1); @Autowired private AppManagement appManagement; @@ -141,6 +154,30 @@ private boolean isCommandNotFound(int statusCode, String body) { return statusCode == 400 && StringUtil.isNotEmpty(body) && body.contains(CommandConstants.MSG_UNKNOWN_COMMAND_PREFIX); } + protected boolean isSupportPost(String app, String ip, int port) { + return StringUtil.isNotEmpty(app) && Optional.ofNullable(appManagement.getDetailApp(app)) + .flatMap(e -> e.getMachine(ip, port)) + .flatMap(m -> VersionUtils.parseVersion(m.getVersion()) + .map(v -> v.greaterOrEqual(version160))) + .orElse(false); + } + + /** + * Check wheter target instance (identified by tuple of app-ip:port) + * supports the form of "xxxxx; xx=xx" in "Content-Type" header. + * + * @param app target app name + * @param ip target node's address + * @param port target node's port + */ + protected boolean isSupportEnhancedContentType(String app, String ip, int port) { + return StringUtil.isNotEmpty(app) && Optional.ofNullable(appManagement.getDetailApp(app)) + .flatMap(e -> e.getMachine(ip, port)) + .flatMap(m -> VersionUtils.parseVersion(m.getVersion()) + .map(v -> v.greaterOrEqual(version171))) + .orElse(false); + } + private StringBuilder queryString(Map params) { StringBuilder queryStringBuilder = new StringBuilder(); for (Entry entry : params.entrySet()) { @@ -159,18 +196,24 @@ private StringBuilder queryString(Map params) { return queryStringBuilder; } - private HttpUriRequest postRequest(String url, Map params) { + /** + * Build an `HttpUriRequest` in POST way. + * + * @param url + * @param params + * @param supportEnhancedContentType see {@link #isSupportEnhancedContentType(String, String, int)} + * @return + */ + protected static HttpUriRequest postRequest(String url, Map params, boolean supportEnhancedContentType) { HttpPost httpPost = new HttpPost(url); if (params != null && params.size() > 0) { List list = new ArrayList<>(params.size()); for (Entry entry : params.entrySet()) { list.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); } - try { - httpPost.setEntity(new UrlEncodedFormEntity(list)); - } catch (UnsupportedEncodingException e) { - logger.warn("httpPostContent encode entity error: {}", params, e); - return null; + httpPost.setEntity(new UrlEncodedFormEntity(list, Consts.UTF_8)); + if (!supportEnhancedContentType) { + httpPost.setHeader(HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_CONTENT_TYPE_URLENCODED); } } return httpPost; @@ -188,7 +231,7 @@ private String urlEncode(String str) { private String getBody(HttpResponse response) throws Exception { Charset charset = null; try { - String contentTypeStr = response.getFirstHeader("Content-type").getValue(); + String contentTypeStr = response.getFirstHeader(HTTP_HEADER_CONTENT_TYPE).getValue(); if (StringUtil.isNotEmpty(contentTypeStr)) { ContentType contentType = ContentType.parse(contentTypeStr); charset = contentType.getCharset(); @@ -245,12 +288,7 @@ private CompletableFuture executeCommand(String app, String ip, int port if (params == null) { params = Collections.emptyMap(); } - boolean supportPost = StringUtil.isNotEmpty(app) && Optional.ofNullable(appManagement.getDetailApp(app)) - .flatMap(e -> e.getMachine(ip, port)) - .flatMap(m -> VersionUtils.parseVersion(m.getVersion()) - .map(v -> v.greaterOrEqual(version160))) - .orElse(false); - if (!useHttpPost || !supportPost) { + if (!useHttpPost || !isSupportPost(app, ip, port)) { // Using GET in older versions, append parameters after url if (!params.isEmpty()) { if (urlBuilder.indexOf("?") == -1) { @@ -263,7 +301,8 @@ private CompletableFuture executeCommand(String app, String ip, int port return executeCommand(new HttpGet(urlBuilder.toString())); } else { // Using POST - return executeCommand(postRequest(urlBuilder.toString(), params)); + return executeCommand( + postRequest(urlBuilder.toString(), params, isSupportEnhancedContentType(app, ip, port))); } } @@ -360,17 +399,44 @@ private boolean setRules(String app, String ip, int port, String type, List setRulesAsync(String app, String ip, int port, String type, List entities) { + try { + AssertUtil.notNull(entities, "rules cannot be null"); + AssertUtil.notEmpty(app, "Bad app name"); + AssertUtil.notEmpty(ip, "Bad machine IP"); + AssertUtil.isTrue(port > 0, "Bad machine port"); + String data = JSON.toJSONString( + entities.stream().map(r -> r.toRule()).collect(Collectors.toList())); + Map params = new HashMap<>(2); + params.put("type", type); + params.put("data", data); + return executeCommand(app, ip, port, SET_RULES_PATH, params, true) + .thenCompose(r -> { + if ("success".equalsIgnoreCase(r.trim())) { + return CompletableFuture.completedFuture(null); + } + return AsyncUtils.newFailedFuture(new CommandFailedException(r)); + }); + } catch (Exception e) { + logger.error("setRulesAsync API failed, type={}", type, e); + return AsyncUtils.newFailedFuture(e); + } + } + public List fetchResourceOfMachine(String ip, int port, String type) { return fetchItems(ip, port, RESOURCE_URL_PATH, type, NodeVo.class); } @@ -384,7 +450,7 @@ public List fetchResourceOfMachine(String ip, int port, String type) { * @return */ public List fetchClusterNodeOfMachine(String ip, int port, boolean includeZero) { - String type = "noZero"; + String type = "notZero"; if (includeZero) { type = "zero"; } @@ -482,6 +548,10 @@ public boolean setFlowRuleOfMachine(String app, String ip, int port, List setFlowRuleOfMachineAsync(String app, String ip, int port, List rules) { + return setRulesAsync(app, ip, port, FLOW_RULE_TYPE, rules); + } + /** * set rules of the machine. rules == null will return immediately; * rules.isEmpty() means setting the rules to empty. @@ -693,4 +763,90 @@ public CompletableFuture fetchClusterServerBasicInfo(Strin return AsyncUtils.newFailedFuture(ex); } } + + public CompletableFuture> fetchApis(String app, String ip, int port) { + if (StringUtil.isBlank(ip) || port <= 0) { + return AsyncUtils.newFailedFuture(new IllegalArgumentException("Invalid parameter")); + } + + try { + return executeCommand(ip, port, FETCH_GATEWAY_API_PATH, false) + .thenApply(r -> { + List entities = JSON.parseArray(r, ApiDefinitionEntity.class); + if (entities != null) { + for (ApiDefinitionEntity entity : entities) { + entity.setApp(app); + entity.setIp(ip); + entity.setPort(port); + } + } + return entities; + }); + } catch (Exception ex) { + logger.warn("Error when fetching gateway apis", ex); + return AsyncUtils.newFailedFuture(ex); + } + } + + public boolean modifyApis(String app, String ip, int port, List apis) { + if (apis == null) { + return true; + } + + try { + AssertUtil.notEmpty(app, "Bad app name"); + AssertUtil.notEmpty(ip, "Bad machine IP"); + AssertUtil.isTrue(port > 0, "Bad machine port"); + String data = JSON.toJSONString( + apis.stream().map(r -> r.toApiDefinition()).collect(Collectors.toList())); + Map params = new HashMap<>(2); + params.put("data", data); + String result = executeCommand(app, ip, port, MODIFY_GATEWAY_API_PATH, params, true).get(); + logger.info("Modify gateway apis: {}", result); + return true; + } catch (Exception e) { + logger.warn("Error when modifying gateway apis", e); + return false; + } + } + + public CompletableFuture> fetchGatewayFlowRules(String app, String ip, int port) { + if (StringUtil.isBlank(ip) || port <= 0) { + return AsyncUtils.newFailedFuture(new IllegalArgumentException("Invalid parameter")); + } + + try { + return executeCommand(ip, port, FETCH_GATEWAY_FLOW_RULE_PATH, false) + .thenApply(r -> { + List gatewayFlowRules = JSON.parseArray(r, GatewayFlowRule.class); + List entities = gatewayFlowRules.stream().map(rule -> GatewayFlowRuleEntity.fromGatewayFlowRule(app, ip, port, rule)).collect(Collectors.toList()); + return entities; + }); + } catch (Exception ex) { + logger.warn("Error when fetching gateway flow rules", ex); + return AsyncUtils.newFailedFuture(ex); + } + } + + public boolean modifyGatewayFlowRules(String app, String ip, int port, List rules) { + if (rules == null) { + return true; + } + + try { + AssertUtil.notEmpty(app, "Bad app name"); + AssertUtil.notEmpty(ip, "Bad machine IP"); + AssertUtil.isTrue(port > 0, "Bad machine port"); + String data = JSON.toJSONString( + rules.stream().map(r -> r.toGatewayFlowRule()).collect(Collectors.toList())); + Map params = new HashMap<>(2); + params.put("data", data); + String result = executeCommand(app, ip, port, MODIFY_GATEWAY_FLOW_RULE_PATH, params, true).get(); + logger.info("Modify gateway flow rules: {}", result); + return true; + } catch (Exception e) { + logger.warn("Error when modifying gateway apis", e); + return false; + } + } } diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/config/WebConfig.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/config/WebConfig.java index 3307fc0c02..92e51e54ae 100755 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/config/WebConfig.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/config/WebConfig.java @@ -15,18 +15,28 @@ */ package com.alibaba.csp.sentinel.dashboard.config; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + import com.alibaba.csp.sentinel.adapter.servlet.CommonFilter; -import com.alibaba.csp.sentinel.dashboard.filter.AuthFilter; +import com.alibaba.csp.sentinel.adapter.servlet.callback.WebCallbackManager; +import com.alibaba.csp.sentinel.dashboard.auth.AuthorizationInterceptor; +import com.alibaba.csp.sentinel.dashboard.auth.LoginAuthenticationFilter; +import com.alibaba.csp.sentinel.util.StringUtil; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import javax.annotation.PostConstruct; import javax.servlet.Filter; /** @@ -38,7 +48,15 @@ public class WebConfig implements WebMvcConfigurer { private final Logger logger = LoggerFactory.getLogger(WebConfig.class); @Autowired - private AuthFilter authFilter; + private LoginAuthenticationFilter loginAuthenticationFilter; + + @Autowired + private AuthorizationInterceptor authorizationInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(authorizationInterceptor).addPathPatterns("/**"); + } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { @@ -61,16 +79,35 @@ public FilterRegistrationBean sentinelFilterRegistration() { registration.addUrlPatterns("/*"); registration.setName("sentinelFilter"); registration.setOrder(1); + // If this is enabled, the entrance of all Web URL resources will be unified as a single context name. + // In most scenarios that's enough, and it could reduce the memory footprint. + registration.addInitParameter(CommonFilter.WEB_CONTEXT_UNIFY, "true"); logger.info("Sentinel servlet CommonFilter registered"); return registration; } + @PostConstruct + public void doInit() { + Set suffixSet = new HashSet<>(Arrays.asList(".js", ".css", ".html", ".ico", ".txt", + ".woff", ".woff2")); + // Example: register a UrlCleaner to exclude URLs of common static resources. + WebCallbackManager.setUrlCleaner(url -> { + if (StringUtil.isEmpty(url)) { + return url; + } + if (suffixSet.stream().anyMatch(url::endsWith)) { + return null; + } + return url; + }); + } + @Bean public FilterRegistrationBean authenticationFilterRegistration() { FilterRegistrationBean registration = new FilterRegistrationBean<>(); - registration.setFilter(authFilter); + registration.setFilter(loginAuthenticationFilter); registration.addUrlPatterns("/*"); registration.setName("authenticationFilter"); registration.setOrder(0); diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AppController.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AppController.java index 0e98bef909..635489661e 100755 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AppController.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AppController.java @@ -63,17 +63,7 @@ public Result> getMachinesByApp(@PathVariable("app") String return Result.ofSuccess(null); } List list = new ArrayList<>(appInfo.getMachines()); - Collections.sort(list, (o1, o2) -> { - int t = o1.getApp().compareTo(o2.getApp()); - if (t != 0) { - return t; - } - t = o1.getIp().compareTo(o2.getIp()); - if (t != 0) { - return t; - } - return o1.getPort().compareTo(o2.getPort()); - }); + Collections.sort(list, Comparator.comparing(MachineInfo::getApp).thenComparing(MachineInfo::getIp).thenComparingInt(MachineInfo::getPort)); return Result.ofSuccess(MachineInfoVo.fromMachineInfoList(list)); } diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AuthController.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AuthController.java index 58be6f6560..8b01aed232 100644 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AuthController.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AuthController.java @@ -22,10 +22,10 @@ import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; @@ -38,7 +38,7 @@ @RequestMapping("/auth") public class AuthController { - private static Logger LOGGER = LoggerFactory.getLogger(AuthController.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AuthController.class); @Value("${auth.username:sentinel}") private String authUsername; @@ -46,8 +46,11 @@ public class AuthController { @Value("${auth.password:sentinel}") private String authPassword; + @Autowired + private AuthService authService; + @PostMapping("/login") - public Result login(HttpServletRequest request, String username, String password) { + public Result login(HttpServletRequest request, String username, String password) { if (StringUtils.isNotBlank(DashboardConfig.getAuthUsername())) { authUsername = DashboardConfig.getAuthUsername(); } @@ -63,18 +66,27 @@ public Result login(HttpServletRequest request, String username, String password */ if (StringUtils.isNotBlank(authUsername) && !authUsername.equals(username) || StringUtils.isNotBlank(authPassword) && !authPassword.equals(password)) { - LOGGER.error("Login failed: Invalid username or password, username=" + username + ", password=" + password); + LOGGER.error("Login failed: Invalid username or password, username=" + username); return Result.ofFail(-1, "Invalid username or password"); } AuthService.AuthUser authUser = new SimpleWebAuthServiceImpl.SimpleWebAuthUserImpl(username); - request.getSession().setAttribute(SimpleWebAuthServiceImpl.WEB_SESSTION_KEY, authUser); + request.getSession().setAttribute(SimpleWebAuthServiceImpl.WEB_SESSION_KEY, authUser); return Result.ofSuccess(authUser); } - @RequestMapping(value = "/logout", method = RequestMethod.POST) - public Result logout(HttpServletRequest request) { + @PostMapping(value = "/logout") + public Result logout(HttpServletRequest request) { request.getSession().invalidate(); return Result.ofSuccess(null); } + + @PostMapping(value = "/check") + public Result check(HttpServletRequest request) { + AuthService.AuthUser authUser = authService.getAuthUser(request); + if (authUser == null) { + return Result.ofFail(-1, "Not logged in"); + } + return Result.ofSuccess(authUser); + } } diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AuthorityRuleController.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AuthorityRuleController.java index 2b285eff80..294455f02a 100644 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AuthorityRuleController.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AuthorityRuleController.java @@ -18,12 +18,9 @@ import java.util.Date; import java.util.List; -import javax.servlet.http.HttpServletRequest; - +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; -import com.alibaba.csp.sentinel.dashboard.auth.AuthService; -import com.alibaba.csp.sentinel.dashboard.auth.AuthService.AuthUser; import com.alibaba.csp.sentinel.dashboard.auth.AuthService.PrivilegeType; import com.alibaba.csp.sentinel.slots.block.RuleConstant; import com.alibaba.csp.sentinel.util.StringUtil; @@ -60,16 +57,11 @@ public class AuthorityRuleController { @Autowired private RuleRepository repository; - @Autowired - private AuthService authService; - @GetMapping("/rules") - public Result> apiQueryAllRulesForMachine(HttpServletRequest request, - @RequestParam String app, + @AuthAction(PrivilegeType.READ_RULE) + public Result> apiQueryAllRulesForMachine(@RequestParam String app, @RequestParam String ip, @RequestParam Integer port) { - AuthUser authUser = authService.getAuthUser(request); - authUser.authTarget(app, PrivilegeType.READ_RULE); if (StringUtil.isEmpty(app)) { return Result.ofFail(-1, "app cannot be null or empty"); } @@ -119,10 +111,8 @@ private Result checkEntityInternal(AuthorityRuleEntity entity) { } @PostMapping("/rule") - public Result apiAddAuthorityRule(HttpServletRequest request, - @RequestBody AuthorityRuleEntity entity) { - AuthUser authUser = authService.getAuthUser(request); - authUser.authTarget(entity.getApp(), PrivilegeType.WRITE_RULE); + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiAddAuthorityRule(@RequestBody AuthorityRuleEntity entity) { Result checkResult = checkEntityInternal(entity); if (checkResult != null) { return checkResult; @@ -144,11 +134,9 @@ public Result apiAddAuthorityRule(HttpServletRequest reques } @PutMapping("/rule/{id}") - public Result apiUpdateParamFlowRule(HttpServletRequest request, - @PathVariable("id") Long id, + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiUpdateParamFlowRule(@PathVariable("id") Long id, @RequestBody AuthorityRuleEntity entity) { - AuthUser authUser = authService.getAuthUser(request); - authUser.authTarget(entity.getApp(), PrivilegeType.WRITE_RULE); if (id == null || id <= 0) { return Result.ofFail(-1, "Invalid id"); } @@ -176,8 +164,8 @@ public Result apiUpdateParamFlowRule(HttpServletRequest req } @DeleteMapping("/rule/{id}") - public Result apiDeleteRule(HttpServletRequest request, @PathVariable("id") Long id) { - AuthUser authUser = authService.getAuthUser(request); + @AuthAction(PrivilegeType.DELETE_RULE) + public Result apiDeleteRule(@PathVariable("id") Long id) { if (id == null) { return Result.ofFail(-1, "id cannot be null"); } @@ -185,7 +173,6 @@ public Result apiDeleteRule(HttpServletRequest request, @PathVariable("id" if (oldEntity == null) { return Result.ofSuccess(null); } - authUser.authTarget(oldEntity.getApp(), PrivilegeType.DELETE_RULE); try { repository.delete(id); } catch (Exception e) { diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/DegradeController.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/DegradeController.java index 7733129a00..61aaee68aa 100755 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/DegradeController.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/DegradeController.java @@ -18,12 +18,9 @@ import java.util.Date; import java.util.List; -import javax.servlet.http.HttpServletRequest; - +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; -import com.alibaba.csp.sentinel.dashboard.auth.AuthService; -import com.alibaba.csp.sentinel.dashboard.auth.AuthService.AuthUser; import com.alibaba.csp.sentinel.dashboard.auth.AuthService.PrivilegeType; import com.alibaba.csp.sentinel.slots.block.RuleConstant; import com.alibaba.csp.sentinel.util.StringUtil; @@ -54,14 +51,10 @@ public class DegradeController { @Autowired private SentinelApiClient sentinelApiClient; - @Autowired - private AuthService authService; - @ResponseBody @RequestMapping("/rules.json") - public Result> queryMachineRules(HttpServletRequest request, String app, String ip, Integer port) { - AuthUser authUser = authService.getAuthUser(request); - authUser.authTarget(app, PrivilegeType.READ_RULE); + @AuthAction(PrivilegeType.READ_RULE) + public Result> queryMachineRules(String app, String ip, Integer port) { if (StringUtil.isEmpty(app)) { return Result.ofFail(-1, "app can't be null or empty"); @@ -84,12 +77,9 @@ public Result> queryMachineRules(HttpServletRequest requ @ResponseBody @RequestMapping("/new.json") - public Result add(HttpServletRequest request, - String app, String ip, Integer port, String limitApp, String resource, + @AuthAction(PrivilegeType.WRITE_RULE) + public Result add(String app, String ip, Integer port, String limitApp, String resource, Double count, Integer timeWindow, Integer grade) { - AuthUser authUser = authService.getAuthUser(request); - authUser.authTarget(app, PrivilegeType.WRITE_RULE); - if (StringUtil.isBlank(app)) { return Result.ofFail(-1, "app can't be null or empty"); } @@ -143,10 +133,9 @@ public Result add(HttpServletRequest request, @ResponseBody @RequestMapping("/save.json") - public Result updateIfNotNull(HttpServletRequest request, - Long id, String app, String limitApp, String resource, + @AuthAction(PrivilegeType.WRITE_RULE) + public Result updateIfNotNull(Long id, String app, String limitApp, String resource, Double count, Integer timeWindow, Integer grade) { - AuthUser authUser = authService.getAuthUser(request); if (id == null) { return Result.ofFail(-1, "id can't be null"); } @@ -159,7 +148,7 @@ public Result updateIfNotNull(HttpServletRequest request, if (entity == null) { return Result.ofFail(-1, "id " + id + " dose not exist"); } - authUser.authTarget(entity.getApp(), PrivilegeType.WRITE_RULE); + if (StringUtil.isNotBlank(app)) { entity.setApp(app.trim()); } @@ -195,8 +184,8 @@ public Result updateIfNotNull(HttpServletRequest request, @ResponseBody @RequestMapping("/delete.json") - public Result delete(HttpServletRequest request, Long id) { - AuthUser authUser = authService.getAuthUser(request); + @AuthAction(PrivilegeType.DELETE_RULE) + public Result delete(Long id) { if (id == null) { return Result.ofFail(-1, "id can't be null"); } @@ -205,7 +194,7 @@ public Result delete(HttpServletRequest request, Long id) { if (oldEntity == null) { return Result.ofSuccess(null); } - authUser.authTarget(oldEntity.getApp(), PrivilegeType.DELETE_RULE); + try { repository.delete(id); } catch (Throwable throwable) { diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/FlowControllerV1.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/FlowControllerV1.java index 71e5b70122..50c4e32f66 100755 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/FlowControllerV1.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/FlowControllerV1.java @@ -17,11 +17,11 @@ import java.util.Date; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; -import javax.servlet.http.HttpServletRequest; - -import com.alibaba.csp.sentinel.dashboard.auth.AuthService; -import com.alibaba.csp.sentinel.dashboard.auth.AuthService.AuthUser; +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; import com.alibaba.csp.sentinel.dashboard.auth.AuthService.PrivilegeType; import com.alibaba.csp.sentinel.util.StringUtil; @@ -57,19 +57,15 @@ public class FlowControllerV1 { @Autowired private InMemoryRuleRepositoryAdapter repository; - @Autowired - private AuthService authService; @Autowired private SentinelApiClient sentinelApiClient; @GetMapping("/rules") - public Result> apiQueryMachineRules(HttpServletRequest request, - @RequestParam String app, + @AuthAction(PrivilegeType.READ_RULE) + public Result> apiQueryMachineRules(@RequestParam String app, @RequestParam String ip, @RequestParam Integer port) { - AuthUser authUser = authService.getAuthUser(request); - authUser.authTarget(app, PrivilegeType.READ_RULE); if (StringUtil.isEmpty(app)) { return Result.ofFail(-1, "app can't be null or empty"); @@ -138,10 +134,8 @@ private Result checkEntityInternal(FlowRuleEntity entity) { } @PostMapping("/rule") - public Result apiAddFlowRule(HttpServletRequest request, @RequestBody FlowRuleEntity entity) { - AuthUser authUser = authService.getAuthUser(request); - authUser.authTarget(entity.getApp(), PrivilegeType.WRITE_RULE); - + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiAddFlowRule(@RequestBody FlowRuleEntity entity) { Result checkResult = checkEntityInternal(entity); if (checkResult != null) { return checkResult; @@ -154,25 +148,23 @@ public Result apiAddFlowRule(HttpServletRequest request, @Reques entity.setResource(entity.getResource().trim()); try { entity = repository.save(entity); - } catch (Throwable throwable) { - logger.error("Failed to add flow rule", throwable); - return Result.ofThrowable(-1, throwable); - } - if (!publishRules(entity.getApp(), entity.getIp(), entity.getPort())) { - logger.error("Publish flow rules failed after rule add"); + + publishRules(entity.getApp(), entity.getIp(), entity.getPort()).get(5000, TimeUnit.MILLISECONDS); + return Result.ofSuccess(entity); + } catch (Throwable t) { + Throwable e = t instanceof ExecutionException ? t.getCause() : t; + logger.error("Failed to add new flow rule, app={}, ip={}", entity.getApp(), entity.getIp(), e); + return Result.ofFail(-1, e.getMessage()); } - return Result.ofSuccess(entity); } @PutMapping("/save.json") - public Result updateIfNotNull(HttpServletRequest request, Long id, String app, + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiUpdateFlowRule(Long id, String app, String limitApp, String resource, Integer grade, Double count, Integer strategy, String refResource, Integer controlBehavior, Integer warmUpPeriodSec, Integer maxQueueingTimeMs) { - AuthUser authUser = authService.getAuthUser(request); - authUser.authTarget(app, PrivilegeType.WRITE_RULE); - if (id == null) { return Result.ofFail(-1, "id can't be null"); } @@ -233,21 +225,23 @@ public Result updateIfNotNull(HttpServletRequest request, Long i try { entity = repository.save(entity); if (entity == null) { - return Result.ofFail(-1, "save entity fail"); + return Result.ofFail(-1, "save entity fail: null"); } - } catch (Throwable throwable) { - logger.error("save error:", throwable); - return Result.ofThrowable(-1, throwable); - } - if (!publishRules(entity.getApp(), entity.getIp(), entity.getPort())) { - logger.info("publish flow rules fail after rule update"); + + publishRules(entity.getApp(), entity.getIp(), entity.getPort()).get(5000, TimeUnit.MILLISECONDS); + return Result.ofSuccess(entity); + } catch (Throwable t) { + Throwable e = t instanceof ExecutionException ? t.getCause() : t; + logger.error("Error when updating flow rules, app={}, ip={}, ruleId={}", entity.getApp(), + entity.getIp(), id, e); + return Result.ofFail(-1, e.getMessage()); } - return Result.ofSuccess(entity); } @DeleteMapping("/delete.json") - public Result delete(HttpServletRequest request, Long id) { - AuthUser authUser = authService.getAuthUser(request); + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiDeleteFlowRule(Long id) { + if (id == null) { return Result.ofFail(-1, "id can't be null"); } @@ -255,20 +249,25 @@ public Result delete(HttpServletRequest request, Long id) { if (oldEntity == null) { return Result.ofSuccess(null); } - authUser.authTarget(oldEntity.getApp(), PrivilegeType.DELETE_RULE); + try { repository.delete(id); } catch (Exception e) { return Result.ofFail(-1, e.getMessage()); } - if (!publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort())) { - logger.info("publish flow rules fail after rule delete"); + try { + publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort()).get(5000, TimeUnit.MILLISECONDS); + return Result.ofSuccess(id); + } catch (Throwable t) { + Throwable e = t instanceof ExecutionException ? t.getCause() : t; + logger.error("Error when deleting flow rules, app={}, ip={}, id={}", oldEntity.getApp(), + oldEntity.getIp(), id, e); + return Result.ofFail(-1, e.getMessage()); } - return Result.ofSuccess(id); } - private boolean publishRules(String app, String ip, Integer port) { + private CompletableFuture publishRules(String app, String ip, Integer port) { List rules = repository.findAllByMachine(MachineInfo.of(app, ip, port)); - return sentinelApiClient.setFlowRuleOfMachine(app, ip, port, rules); + return sentinelApiClient.setFlowRuleOfMachineAsync(app, ip, port, rules); } } diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/MachineRegistryController.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/MachineRegistryController.java index f2e7fe2a14..9a9a7f76a4 100755 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/MachineRegistryController.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/MachineRegistryController.java @@ -27,6 +27,7 @@ import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; @Controller @@ -40,7 +41,7 @@ public class MachineRegistryController { @ResponseBody @RequestMapping("/machine") - public Result receiveHeartBeat(String app, Long version, String v, String hostname, String ip, Integer port) { + public Result receiveHeartBeat(String app, @RequestParam(value = "app_type", required = false, defaultValue = "0") Integer appType, Long version, String v, String hostname, String ip, Integer port) { if (app == null) { app = MachineDiscovery.UNKNOWN_APP_NAME; } @@ -59,6 +60,7 @@ public Result receiveHeartBeat(String app, Long version, String v, String hos try { MachineInfo machineInfo = new MachineInfo(); machineInfo.setApp(app); + machineInfo.setAppType(appType); machineInfo.setHostname(hostname); machineInfo.setIp(ip); machineInfo.setPort(port); diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/ParamFlowRuleController.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/ParamFlowRuleController.java index d9529783fe..4039ca6746 100644 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/ParamFlowRuleController.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/ParamFlowRuleController.java @@ -21,18 +21,15 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import javax.servlet.http.HttpServletRequest; - +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; import com.alibaba.csp.sentinel.dashboard.client.CommandNotFoundException; import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; import com.alibaba.csp.sentinel.dashboard.discovery.AppManagement; import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; import com.alibaba.csp.sentinel.dashboard.auth.AuthService; -import com.alibaba.csp.sentinel.dashboard.auth.AuthService.AuthUser; import com.alibaba.csp.sentinel.dashboard.auth.AuthService.PrivilegeType; import com.alibaba.csp.sentinel.slots.block.RuleConstant; import com.alibaba.csp.sentinel.util.StringUtil; - import com.alibaba.csp.sentinel.dashboard.datasource.entity.SentinelVersion; import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.ParamFlowRuleEntity; import com.alibaba.csp.sentinel.dashboard.domain.Result; @@ -69,9 +66,6 @@ public class ParamFlowRuleController { @Autowired private RuleRepository repository; - @Autowired - private AuthService authService; - private boolean checkIfSupported(String app, String ip, int port) { try { return Optional.ofNullable(appManagement.getDetailApp(app)) @@ -86,12 +80,10 @@ private boolean checkIfSupported(String app, String ip, int port) { } @GetMapping("/rules") - public Result> apiQueryAllRulesForMachine(HttpServletRequest request, - @RequestParam String app, + @AuthAction(PrivilegeType.READ_RULE) + public Result> apiQueryAllRulesForMachine(@RequestParam String app, @RequestParam String ip, @RequestParam Integer port) { - AuthUser authUser = authService.getAuthUser(request); - authUser.authTarget(app, PrivilegeType.READ_RULE); if (StringUtil.isEmpty(app)) { return Result.ofFail(-1, "app cannot be null or empty"); } @@ -127,10 +119,8 @@ private boolean isNotSupported(Throwable ex) { } @PostMapping("/rule") - public Result apiAddParamFlowRule(HttpServletRequest request, - @RequestBody ParamFlowRuleEntity entity) { - AuthUser authUser = authService.getAuthUser(request); - authUser.authTarget(entity.getApp(), PrivilegeType.WRITE_RULE); + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + public Result apiAddParamFlowRule(@RequestBody ParamFlowRuleEntity entity) { Result checkResult = checkEntityInternal(entity); if (checkResult != null) { return checkResult; @@ -188,14 +178,19 @@ private Result checkEntityInternal(ParamFlowRuleEntity entity) { if (entity.getParamIdx() == null || entity.getParamIdx() < 0) { return Result.ofFail(-1, "paramIdx should be valid"); } + if (entity.getDurationInSec() <= 0) { + return Result.ofFail(-1, "durationInSec should be valid"); + } + if (entity.getControlBehavior() < 0) { + return Result.ofFail(-1, "controlBehavior should be valid"); + } return null; } @PutMapping("/rule/{id}") - public Result apiUpdateParamFlowRule(HttpServletRequest request, - @PathVariable("id") Long id, + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + public Result apiUpdateParamFlowRule(@PathVariable("id") Long id, @RequestBody ParamFlowRuleEntity entity) { - AuthUser authUser = authService.getAuthUser(request); if (id == null || id <= 0) { return Result.ofFail(-1, "Invalid id"); } @@ -203,7 +198,7 @@ public Result apiUpdateParamFlowRule(HttpServletRequest req if (oldEntity == null) { return Result.ofFail(-1, "id " + id + " does not exist"); } - authUser.authTarget(oldEntity.getApp(), PrivilegeType.WRITE_RULE); + Result checkResult = checkEntityInternal(entity); if (checkResult != null) { return checkResult; @@ -233,8 +228,8 @@ public Result apiUpdateParamFlowRule(HttpServletRequest req } @DeleteMapping("/rule/{id}") - public Result apiDeleteRule(HttpServletRequest request, @PathVariable("id") Long id) { - AuthUser authUser = authService.getAuthUser(request); + @AuthAction(PrivilegeType.DELETE_RULE) + public Result apiDeleteRule(@PathVariable("id") Long id) { if (id == null) { return Result.ofFail(-1, "id cannot be null"); } @@ -242,7 +237,7 @@ public Result apiDeleteRule(HttpServletRequest request, @PathVariable("id" if (oldEntity == null) { return Result.ofSuccess(null); } - authUser.authTarget(oldEntity.getApp(), PrivilegeType.DELETE_RULE); + try { repository.delete(id); publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort()).get(); diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/SystemController.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/SystemController.java index 8e99718bc3..daa0b98b5a 100755 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/SystemController.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/SystemController.java @@ -18,47 +18,38 @@ import java.util.Date; import java.util.List; -import javax.servlet.http.HttpServletRequest; - -import com.alibaba.csp.sentinel.dashboard.auth.AuthService; -import com.alibaba.csp.sentinel.dashboard.auth.AuthService.AuthUser; +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; import com.alibaba.csp.sentinel.dashboard.auth.AuthService.PrivilegeType; +import com.alibaba.csp.sentinel.dashboard.repository.rule.RuleRepository; import com.alibaba.csp.sentinel.util.StringUtil; import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.SystemRuleEntity; import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; import com.alibaba.csp.sentinel.dashboard.domain.Result; -import com.alibaba.csp.sentinel.dashboard.repository.rule.InMemSystemRuleStore; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; -import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; /** * @author leyou(lihao) */ -@Controller -@RequestMapping(value = "/system", produces = MediaType.APPLICATION_JSON_VALUE) +@RestController +@RequestMapping("/system") public class SystemController { - private static Logger logger = LoggerFactory.getLogger(SystemController.class); + + private final Logger logger = LoggerFactory.getLogger(SystemController.class); @Autowired - private InMemSystemRuleStore repository; + private RuleRepository repository; @Autowired private SentinelApiClient sentinelApiClient; - @Autowired - private AuthService authService; - @ResponseBody - @RequestMapping("/rules.json") - Result> queryMachineRules(HttpServletRequest request, String app, String ip, Integer port) { - AuthUser authUser = authService.getAuthUser(request); - authUser.authTarget(app, PrivilegeType.READ_RULE); + private Result checkBasicParams(String app, String ip, Integer port) { if (StringUtil.isEmpty(app)) { return Result.ofFail(-1, "app can't be null or empty"); } @@ -68,12 +59,26 @@ Result> queryMachineRules(HttpServletRequest request, Str if (port == null) { return Result.ofFail(-1, "port can't be null"); } + if (port <= 0 || port > 65535) { + return Result.ofFail(-1, "port should be in (0, 65535)"); + } + return null; + } + + @GetMapping("/rules.json") + @AuthAction(PrivilegeType.READ_RULE) + public Result> apiQueryMachineRules(String app, String ip, + Integer port) { + Result> checkResult = checkBasicParams(app, ip, port); + if (checkResult != null) { + return checkResult; + } try { List rules = sentinelApiClient.fetchSystemRuleOfMachine(app, ip, port); rules = repository.saveAll(rules); return Result.ofSuccess(rules); } catch (Throwable throwable) { - logger.error("queryApps error:", throwable); + logger.error("Query machine system rules error", throwable); return Result.ofThrowable(-1, throwable); } } @@ -88,36 +93,42 @@ private int countNotNullAndNotNegative(Number... values) { return notNullCount; } - @ResponseBody @RequestMapping("/new.json") - Result add(HttpServletRequest request, - String app, String ip, Integer port, Double avgLoad, Long avgRt, Long maxThread, Double qps) { - AuthUser authUser = authService.getAuthUser(request); - authUser.authTarget(app, PrivilegeType.WRITE_RULE); - if (StringUtil.isBlank(app)) { - return Result.ofFail(-1, "app can't be null or empty"); - } - if (StringUtil.isBlank(ip)) { - return Result.ofFail(-1, "ip can't be null or empty"); - } - if (port == null) { - return Result.ofFail(-1, "port can't be null"); + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiAdd(String app, String ip, Integer port, + Double highestSystemLoad, Double highestCpuUsage, Long avgRt, + Long maxThread, Double qps) { + + Result checkResult = checkBasicParams(app, ip, port); + if (checkResult != null) { + return checkResult; } - int notNullCount = countNotNullAndNotNegative(avgLoad, avgRt, maxThread, qps); + + int notNullCount = countNotNullAndNotNegative(highestSystemLoad, avgRt, maxThread, qps, highestCpuUsage); if (notNullCount != 1) { - return Result.ofFail(-1, "only one of [avgLoad, avgRt, maxThread, qps] " - + "value must be set >= 0, but " + notNullCount + " values get"); + return Result.ofFail(-1, "only one of [highestSystemLoad, avgRt, maxThread, qps,highestCpuUsage] " + + "value must be set > 0, but " + notNullCount + " values get"); + } + if (null != highestCpuUsage && highestCpuUsage > 1) { + return Result.ofFail(-1, "highestCpuUsage must between [0.0, 1.0]"); } SystemRuleEntity entity = new SystemRuleEntity(); entity.setApp(app.trim()); entity.setIp(ip.trim()); entity.setPort(port); // -1 is a fake value - if (avgLoad != null) { - entity.setAvgLoad(avgLoad); + if (null != highestSystemLoad) { + entity.setHighestSystemLoad(highestSystemLoad); + } else { + entity.setHighestSystemLoad(-1D); + } + + if (null != highestCpuUsage) { + entity.setHighestCpuUsage(highestCpuUsage); } else { - entity.setAvgLoad(-1D); + entity.setHighestCpuUsage(-1D); } + if (avgRt != null) { entity.setAvgRt(avgRt); } else { @@ -139,20 +150,19 @@ Result add(HttpServletRequest request, try { entity = repository.save(entity); } catch (Throwable throwable) { - logger.error("add error:", throwable); + logger.error("Add SystemRule error", throwable); return Result.ofThrowable(-1, throwable); } if (!publishRules(app, ip, port)) { - logger.info("publish system rules fail after rule add"); + logger.warn("Publish system rules fail after rule add"); } return Result.ofSuccess(entity); } - @ResponseBody - @RequestMapping("/save.json") - Result updateIfNotNull(HttpServletRequest request, - Long id, String app, Double avgLoad, Long avgRt, Long maxThread, Double qps) { - AuthUser authUser = authService.getAuthUser(request); + @GetMapping("/save.json") + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiUpdateIfNotNull(Long id, String app, Double highestSystemLoad, + Double highestCpuUsage, Long avgRt, Long maxThread, Double qps) { if (id == null) { return Result.ofFail(-1, "id can't be null"); } @@ -160,15 +170,24 @@ Result updateIfNotNull(HttpServletRequest request, if (entity == null) { return Result.ofFail(-1, "id " + id + " dose not exist"); } - authUser.authTarget(entity.getApp(), PrivilegeType.WRITE_RULE); + if (StringUtil.isNotBlank(app)) { entity.setApp(app.trim()); } - if (avgLoad != null) { - if (avgLoad < 0) { - return Result.ofFail(-1, "avgLoad must >= 0"); + if (highestSystemLoad != null) { + if (highestSystemLoad < 0) { + return Result.ofFail(-1, "highestSystemLoad must >= 0"); + } + entity.setHighestSystemLoad(highestSystemLoad); + } + if (highestCpuUsage != null) { + if (highestCpuUsage < 0) { + return Result.ofFail(-1, "highestCpuUsage must >= 0"); + } + if (highestCpuUsage > 1) { + return Result.ofFail(-1, "highestCpuUsage must <= 1"); } - entity.setAvgLoad(avgLoad); + entity.setHighestCpuUsage(highestCpuUsage); } if (avgRt != null) { if (avgRt < 0) { @@ -202,10 +221,9 @@ Result updateIfNotNull(HttpServletRequest request, return Result.ofSuccess(entity); } - @ResponseBody @RequestMapping("/delete.json") - Result delete(HttpServletRequest request, Long id) { - AuthUser authUser = authService.getAuthUser(request); + @AuthAction(PrivilegeType.DELETE_RULE) + public Result delete(Long id) { if (id == null) { return Result.ofFail(-1, "id can't be null"); } @@ -213,7 +231,6 @@ Result delete(HttpServletRequest request, Long id) { if (oldEntity == null) { return Result.ofSuccess(null); } - authUser.authTarget(oldEntity.getApp(), PrivilegeType.DELETE_RULE); try { repository.delete(id); } catch (Throwable throwable) { diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/VersionController.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/VersionController.java new file mode 100644 index 0000000000..69d66ea134 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/VersionController.java @@ -0,0 +1,49 @@ +/* + * 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.dashboard.controller; + +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.util.StringUtil; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author hisenyuan + * @since 1.7.0 + */ +@RestController +public class VersionController { + + private static final String VERSION_PATTERN = "-"; + + @Value("${sentinel.dashboard.version:}") + private String sentinelDashboardVersion; + + @GetMapping("/version") + public Result apiGetVersion() { + if (StringUtil.isNotBlank(sentinelDashboardVersion)) { + String res = sentinelDashboardVersion; + if (sentinelDashboardVersion.contains(VERSION_PATTERN)) { + res = sentinelDashboardVersion.substring(0, sentinelDashboardVersion.indexOf(VERSION_PATTERN)); + } + return Result.ofSuccess(res); + } else { + return Result.ofFail(1, "getVersion failed: empty version"); + } + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayApiController.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayApiController.java new file mode 100644 index 0000000000..c7a405d9c4 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayApiController.java @@ -0,0 +1,260 @@ +/* + * 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.dashboard.controller.gateway; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService; +import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiPredicateItemEntity; +import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api.AddApiReqVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api.ApiPredicateItemVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api.UpdateApiReqVo; +import com.alibaba.csp.sentinel.dashboard.repository.gateway.InMemApiDefinitionStore; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.CollectionUtils; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.util.*; + +import static com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants.*; + +/** + * Gateway api Controller for manage gateway api definitions. + * + * @author cdfive + * @since 1.7.0 + */ +@RestController +@RequestMapping(value = "/gateway/api") +public class GatewayApiController { + + private final Logger logger = LoggerFactory.getLogger(GatewayApiController.class); + + @Autowired + private InMemApiDefinitionStore repository; + + @Autowired + private SentinelApiClient sentinelApiClient; + + @GetMapping("/list.json") + @AuthAction(AuthService.PrivilegeType.READ_RULE) + public Result> queryApis(String app, String ip, Integer port) { + + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isEmpty(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + + try { + List apis = sentinelApiClient.fetchApis(app, ip, port).get(); + repository.saveAll(apis); + return Result.ofSuccess(apis); + } catch (Throwable throwable) { + logger.error("queryApis error:", throwable); + return Result.ofThrowable(-1, throwable); + } + } + + @PostMapping("/new.json") + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + public Result addApi(HttpServletRequest request, @RequestBody AddApiReqVo reqVo) { + + String app = reqVo.getApp(); + if (StringUtil.isBlank(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + + ApiDefinitionEntity entity = new ApiDefinitionEntity(); + entity.setApp(app.trim()); + + String ip = reqVo.getIp(); + if (StringUtil.isBlank(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + entity.setIp(ip.trim()); + + Integer port = reqVo.getPort(); + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + entity.setPort(port); + + // API名称 + String apiName = reqVo.getApiName(); + if (StringUtil.isBlank(apiName)) { + return Result.ofFail(-1, "apiName can't be null or empty"); + } + entity.setApiName(apiName.trim()); + + // 匹配规则列表 + List predicateItems = reqVo.getPredicateItems(); + if (CollectionUtils.isEmpty(predicateItems)) { + return Result.ofFail(-1, "predicateItems can't empty"); + } + + List predicateItemEntities = new ArrayList<>(); + for (ApiPredicateItemVo predicateItem : predicateItems) { + ApiPredicateItemEntity predicateItemEntity = new ApiPredicateItemEntity(); + + // 匹配模式 + Integer matchStrategy = predicateItem.getMatchStrategy(); + if (!Arrays.asList(URL_MATCH_STRATEGY_EXACT, URL_MATCH_STRATEGY_PREFIX, URL_MATCH_STRATEGY_REGEX).contains(matchStrategy)) { + return Result.ofFail(-1, "invalid matchStrategy: " + matchStrategy); + } + predicateItemEntity.setMatchStrategy(matchStrategy); + + // 匹配串 + String pattern = predicateItem.getPattern(); + if (StringUtil.isBlank(pattern)) { + return Result.ofFail(-1, "pattern can't be null or empty"); + } + predicateItemEntity.setPattern(pattern); + + predicateItemEntities.add(predicateItemEntity); + } + entity.setPredicateItems(new LinkedHashSet<>(predicateItemEntities)); + + // 检查API名称不能重复 + List allApis = repository.findAllByMachine(MachineInfo.of(app.trim(), ip.trim(), port)); + if (allApis.stream().map(o -> o.getApiName()).anyMatch(o -> o.equals(apiName.trim()))) { + return Result.ofFail(-1, "apiName exists: " + apiName); + } + + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + + try { + entity = repository.save(entity); + } catch (Throwable throwable) { + logger.error("add gateway api error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishApis(app, ip, port)) { + logger.warn("publish gateway apis fail after add"); + } + + return Result.ofSuccess(entity); + } + + @PostMapping("/save.json") + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + public Result updateApi(@RequestBody UpdateApiReqVo reqVo) { + String app = reqVo.getApp(); + if (StringUtil.isBlank(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + + Long id = reqVo.getId(); + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + ApiDefinitionEntity entity = repository.findById(id); + if (entity == null) { + return Result.ofFail(-1, "api does not exist, id=" + id); + } + + // 匹配规则列表 + List predicateItems = reqVo.getPredicateItems(); + if (CollectionUtils.isEmpty(predicateItems)) { + return Result.ofFail(-1, "predicateItems can't empty"); + } + + List predicateItemEntities = new ArrayList<>(); + for (ApiPredicateItemVo predicateItem : predicateItems) { + ApiPredicateItemEntity predicateItemEntity = new ApiPredicateItemEntity(); + + // 匹配模式 + int matchStrategy = predicateItem.getMatchStrategy(); + if (!Arrays.asList(URL_MATCH_STRATEGY_EXACT, URL_MATCH_STRATEGY_PREFIX, URL_MATCH_STRATEGY_REGEX).contains(matchStrategy)) { + return Result.ofFail(-1, "Invalid matchStrategy: " + matchStrategy); + } + predicateItemEntity.setMatchStrategy(matchStrategy); + + // 匹配串 + String pattern = predicateItem.getPattern(); + if (StringUtil.isBlank(pattern)) { + return Result.ofFail(-1, "pattern can't be null or empty"); + } + predicateItemEntity.setPattern(pattern); + + predicateItemEntities.add(predicateItemEntity); + } + entity.setPredicateItems(new LinkedHashSet<>(predicateItemEntities)); + + Date date = new Date(); + entity.setGmtModified(date); + + try { + entity = repository.save(entity); + } catch (Throwable throwable) { + logger.error("update gateway api error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishApis(app, entity.getIp(), entity.getPort())) { + logger.warn("publish gateway apis fail after update"); + } + + return Result.ofSuccess(entity); + } + + @PostMapping("/delete.json") + @AuthAction(AuthService.PrivilegeType.DELETE_RULE) + + public Result deleteApi(Long id) { + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + ApiDefinitionEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + + try { + repository.delete(id); + } catch (Throwable throwable) { + logger.error("delete gateway api error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishApis(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort())) { + logger.warn("publish gateway apis fail after delete"); + } + + return Result.ofSuccess(id); + } + + private boolean publishApis(String app, String ip, Integer port) { + List apis = repository.findAllByMachine(MachineInfo.of(app, ip, port)); + return sentinelApiClient.modifyApis(app, ip, port, apis); + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayFlowRuleController.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayFlowRuleController.java new file mode 100644 index 0000000000..0189163781 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayFlowRuleController.java @@ -0,0 +1,433 @@ +/* + * 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.dashboard.controller.gateway; + + +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService; +import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayParamFlowItemEntity; +import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule.AddFlowRuleReqVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule.GatewayParamFlowItemVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule.UpdateFlowRuleReqVo; +import com.alibaba.csp.sentinel.dashboard.repository.gateway.InMemGatewayFlowRuleStore; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import static com.alibaba.csp.sentinel.slots.block.RuleConstant.*; +import static com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants.*; +import static com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity.*; + +/** + * Gateway flow rule Controller for manage gateway flow rules. + * + * @author cdfive + * @since 1.7.0 + */ +@RestController +@RequestMapping(value = "/gateway/flow") +public class GatewayFlowRuleController { + + private final Logger logger = LoggerFactory.getLogger(GatewayFlowRuleController.class); + + @Autowired + private InMemGatewayFlowRuleStore repository; + + @Autowired + private SentinelApiClient sentinelApiClient; + + @GetMapping("/list.json") + @AuthAction(AuthService.PrivilegeType.READ_RULE) + public Result> queryFlowRules(String app, String ip, Integer port) { + + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isEmpty(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + + try { + List rules = sentinelApiClient.fetchGatewayFlowRules(app, ip, port).get(); + repository.saveAll(rules); + return Result.ofSuccess(rules); + } catch (Throwable throwable) { + logger.error("query gateway flow rules error:", throwable); + return Result.ofThrowable(-1, throwable); + } + } + + @PostMapping("/new.json") + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + public Result addFlowRule(@RequestBody AddFlowRuleReqVo reqVo) { + + String app = reqVo.getApp(); + if (StringUtil.isBlank(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + + GatewayFlowRuleEntity entity = new GatewayFlowRuleEntity(); + entity.setApp(app.trim()); + + String ip = reqVo.getIp(); + if (StringUtil.isBlank(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + entity.setIp(ip.trim()); + + Integer port = reqVo.getPort(); + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + entity.setPort(port); + + // API类型, Route ID或API分组 + Integer resourceMode = reqVo.getResourceMode(); + if (resourceMode == null) { + return Result.ofFail(-1, "resourceMode can't be null"); + } + if (!Arrays.asList(RESOURCE_MODE_ROUTE_ID, RESOURCE_MODE_CUSTOM_API_NAME).contains(resourceMode)) { + return Result.ofFail(-1, "invalid resourceMode: " + resourceMode); + } + entity.setResourceMode(resourceMode); + + // API名称 + String resource = reqVo.getResource(); + if (StringUtil.isBlank(resource)) { + return Result.ofFail(-1, "resource can't be null or empty"); + } + entity.setResource(resource.trim()); + + // 针对请求属性 + GatewayParamFlowItemVo paramItem = reqVo.getParamItem(); + if (paramItem != null) { + GatewayParamFlowItemEntity itemEntity = new GatewayParamFlowItemEntity(); + entity.setParamItem(itemEntity); + + // 参数属性 0-ClientIP 1-Remote Host 2-Header 3-URL参数 4-Cookie + Integer parseStrategy = paramItem.getParseStrategy(); + if (!Arrays.asList(PARAM_PARSE_STRATEGY_CLIENT_IP, PARAM_PARSE_STRATEGY_HOST, PARAM_PARSE_STRATEGY_HEADER + , PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) { + return Result.ofFail(-1, "invalid parseStrategy: " + parseStrategy); + } + itemEntity.setParseStrategy(paramItem.getParseStrategy()); + + // 当参数属性为2-Header 3-URL参数 4-Cookie时,参数名称必填 + if (Arrays.asList(PARAM_PARSE_STRATEGY_HEADER, PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) { + // 参数名称 + String fieldName = paramItem.getFieldName(); + if (StringUtil.isBlank(fieldName)) { + return Result.ofFail(-1, "fieldName can't be null or empty"); + } + itemEntity.setFieldName(paramItem.getFieldName()); + + String pattern = paramItem.getPattern(); + // 如果匹配串不为空,验证匹配模式 + if (StringUtil.isNotEmpty(pattern)) { + itemEntity.setPattern(pattern); + + Integer matchStrategy = paramItem.getMatchStrategy(); + if (!Arrays.asList(PARAM_MATCH_STRATEGY_EXACT, PARAM_MATCH_STRATEGY_CONTAINS, PARAM_MATCH_STRATEGY_REGEX).contains(matchStrategy)) { + return Result.ofFail(-1, "invalid matchStrategy: " + matchStrategy); + } + itemEntity.setMatchStrategy(matchStrategy); + } + } + } + + // 阈值类型 0-线程数 1-QPS + Integer grade = reqVo.getGrade(); + if (grade == null) { + return Result.ofFail(-1, "grade can't be null"); + } + if (!Arrays.asList(FLOW_GRADE_THREAD, FLOW_GRADE_QPS).contains(grade)) { + return Result.ofFail(-1, "invalid grade: " + grade); + } + entity.setGrade(grade); + + // QPS阈值 + Double count = reqVo.getCount(); + if (count == null) { + return Result.ofFail(-1, "count can't be null"); + } + if (count < 0) { + return Result.ofFail(-1, "count should be at lease zero"); + } + entity.setCount(count); + + // 间隔 + Long interval = reqVo.getInterval(); + if (interval == null) { + return Result.ofFail(-1, "interval can't be null"); + } + if (interval <= 0) { + return Result.ofFail(-1, "interval should be greater than zero"); + } + entity.setInterval(interval); + + // 间隔单位 + Integer intervalUnit = reqVo.getIntervalUnit(); + if (intervalUnit == null) { + return Result.ofFail(-1, "intervalUnit can't be null"); + } + if (!Arrays.asList(INTERVAL_UNIT_SECOND, INTERVAL_UNIT_MINUTE, INTERVAL_UNIT_HOUR, INTERVAL_UNIT_DAY).contains(intervalUnit)) { + return Result.ofFail(-1, "Invalid intervalUnit: " + intervalUnit); + } + entity.setIntervalUnit(intervalUnit); + + // 流控方式 0-快速失败 2-匀速排队 + Integer controlBehavior = reqVo.getControlBehavior(); + if (controlBehavior == null) { + return Result.ofFail(-1, "controlBehavior can't be null"); + } + if (!Arrays.asList(CONTROL_BEHAVIOR_DEFAULT, CONTROL_BEHAVIOR_RATE_LIMITER).contains(controlBehavior)) { + return Result.ofFail(-1, "invalid controlBehavior: " + controlBehavior); + } + entity.setControlBehavior(controlBehavior); + + if (CONTROL_BEHAVIOR_DEFAULT == controlBehavior) { + // 0-快速失败, 则Burst size必填 + Integer burst = reqVo.getBurst(); + if (burst == null) { + return Result.ofFail(-1, "burst can't be null"); + } + if (burst < 0) { + return Result.ofFail(-1, "invalid burst: " + burst); + } + entity.setBurst(burst); + } else if (CONTROL_BEHAVIOR_RATE_LIMITER == controlBehavior) { + // 1-匀速排队, 则超时时间必填 + Integer maxQueueingTimeoutMs = reqVo.getMaxQueueingTimeoutMs(); + if (maxQueueingTimeoutMs == null) { + return Result.ofFail(-1, "maxQueueingTimeoutMs can't be null"); + } + if (maxQueueingTimeoutMs < 0) { + return Result.ofFail(-1, "invalid maxQueueingTimeoutMs: " + maxQueueingTimeoutMs); + } + entity.setMaxQueueingTimeoutMs(maxQueueingTimeoutMs); + } + + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + + try { + entity = repository.save(entity); + } catch (Throwable throwable) { + logger.error("add gateway flow rule error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishRules(app, ip, port)) { + logger.warn("publish gateway flow rules fail after add"); + } + + return Result.ofSuccess(entity); + } + + @PostMapping("/save.json") + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + public Result updateFlowRule(@RequestBody UpdateFlowRuleReqVo reqVo) { + + String app = reqVo.getApp(); + if (StringUtil.isBlank(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + + Long id = reqVo.getId(); + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + GatewayFlowRuleEntity entity = repository.findById(id); + if (entity == null) { + return Result.ofFail(-1, "gateway flow rule does not exist, id=" + id); + } + + // 针对请求属性 + GatewayParamFlowItemVo paramItem = reqVo.getParamItem(); + if (paramItem != null) { + GatewayParamFlowItemEntity itemEntity = new GatewayParamFlowItemEntity(); + entity.setParamItem(itemEntity); + + // 参数属性 0-ClientIP 1-Remote Host 2-Header 3-URL参数 4-Cookie + Integer parseStrategy = paramItem.getParseStrategy(); + if (!Arrays.asList(PARAM_PARSE_STRATEGY_CLIENT_IP, PARAM_PARSE_STRATEGY_HOST, PARAM_PARSE_STRATEGY_HEADER + , PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) { + return Result.ofFail(-1, "invalid parseStrategy: " + parseStrategy); + } + itemEntity.setParseStrategy(paramItem.getParseStrategy()); + + // 当参数属性为2-Header 3-URL参数 4-Cookie时,参数名称必填 + if (Arrays.asList(PARAM_PARSE_STRATEGY_HEADER, PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) { + // 参数名称 + String fieldName = paramItem.getFieldName(); + if (StringUtil.isBlank(fieldName)) { + return Result.ofFail(-1, "fieldName can't be null or empty"); + } + itemEntity.setFieldName(paramItem.getFieldName()); + + String pattern = paramItem.getPattern(); + // 如果匹配串不为空,验证匹配模式 + if (StringUtil.isNotEmpty(pattern)) { + itemEntity.setPattern(pattern); + + Integer matchStrategy = paramItem.getMatchStrategy(); + if (!Arrays.asList(PARAM_MATCH_STRATEGY_EXACT, PARAM_MATCH_STRATEGY_CONTAINS, PARAM_MATCH_STRATEGY_REGEX).contains(matchStrategy)) { + return Result.ofFail(-1, "invalid matchStrategy: " + matchStrategy); + } + itemEntity.setMatchStrategy(matchStrategy); + } + } + } else { + entity.setParamItem(null); + } + + // 阈值类型 0-线程数 1-QPS + Integer grade = reqVo.getGrade(); + if (grade == null) { + return Result.ofFail(-1, "grade can't be null"); + } + if (!Arrays.asList(FLOW_GRADE_THREAD, FLOW_GRADE_QPS).contains(grade)) { + return Result.ofFail(-1, "invalid grade: " + grade); + } + entity.setGrade(grade); + + // QPS阈值 + Double count = reqVo.getCount(); + if (count == null) { + return Result.ofFail(-1, "count can't be null"); + } + if (count < 0) { + return Result.ofFail(-1, "count should be at lease zero"); + } + entity.setCount(count); + + // 间隔 + Long interval = reqVo.getInterval(); + if (interval == null) { + return Result.ofFail(-1, "interval can't be null"); + } + if (interval <= 0) { + return Result.ofFail(-1, "interval should be greater than zero"); + } + entity.setInterval(interval); + + // 间隔单位 + Integer intervalUnit = reqVo.getIntervalUnit(); + if (intervalUnit == null) { + return Result.ofFail(-1, "intervalUnit can't be null"); + } + if (!Arrays.asList(INTERVAL_UNIT_SECOND, INTERVAL_UNIT_MINUTE, INTERVAL_UNIT_HOUR, INTERVAL_UNIT_DAY).contains(intervalUnit)) { + return Result.ofFail(-1, "Invalid intervalUnit: " + intervalUnit); + } + entity.setIntervalUnit(intervalUnit); + + // 流控方式 0-快速失败 2-匀速排队 + Integer controlBehavior = reqVo.getControlBehavior(); + if (controlBehavior == null) { + return Result.ofFail(-1, "controlBehavior can't be null"); + } + if (!Arrays.asList(CONTROL_BEHAVIOR_DEFAULT, CONTROL_BEHAVIOR_RATE_LIMITER).contains(controlBehavior)) { + return Result.ofFail(-1, "invalid controlBehavior: " + controlBehavior); + } + entity.setControlBehavior(controlBehavior); + + if (CONTROL_BEHAVIOR_DEFAULT == controlBehavior) { + // 0-快速失败, 则Burst size必填 + Integer burst = reqVo.getBurst(); + if (burst == null) { + return Result.ofFail(-1, "burst can't be null"); + } + if (burst < 0) { + return Result.ofFail(-1, "invalid burst: " + burst); + } + entity.setBurst(burst); + } else if (CONTROL_BEHAVIOR_RATE_LIMITER == controlBehavior) { + // 2-匀速排队, 则超时时间必填 + Integer maxQueueingTimeoutMs = reqVo.getMaxQueueingTimeoutMs(); + if (maxQueueingTimeoutMs == null) { + return Result.ofFail(-1, "maxQueueingTimeoutMs can't be null"); + } + if (maxQueueingTimeoutMs < 0) { + return Result.ofFail(-1, "invalid maxQueueingTimeoutMs: " + maxQueueingTimeoutMs); + } + entity.setMaxQueueingTimeoutMs(maxQueueingTimeoutMs); + } + + Date date = new Date(); + entity.setGmtModified(date); + + try { + entity = repository.save(entity); + } catch (Throwable throwable) { + logger.error("update gateway flow rule error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishRules(app, entity.getIp(), entity.getPort())) { + logger.warn("publish gateway flow rules fail after update"); + } + + return Result.ofSuccess(entity); + } + + + @PostMapping("/delete.json") + @AuthAction(AuthService.PrivilegeType.DELETE_RULE) + public Result deleteFlowRule(Long id) { + + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + GatewayFlowRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + + try { + repository.delete(id); + } catch (Throwable throwable) { + logger.error("delete gateway flow rule error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort())) { + logger.warn("publish gateway flow rules fail after delete"); + } + + return Result.ofSuccess(id); + } + + private boolean publishRules(String app, String ip, Integer port) { + List rules = repository.findAllByMachine(MachineInfo.of(app, ip, port)); + return sentinelApiClient.modifyGatewayFlowRules(app, ip, port, rules); + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/FlowControllerV2.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/FlowControllerV2.java index 41aea755be..96bea1d741 100755 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/FlowControllerV2.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/FlowControllerV2.java @@ -18,10 +18,8 @@ import java.util.Date; import java.util.List; -import javax.servlet.http.HttpServletRequest; - +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; import com.alibaba.csp.sentinel.dashboard.auth.AuthService; -import com.alibaba.csp.sentinel.dashboard.auth.AuthService.AuthUser; import com.alibaba.csp.sentinel.dashboard.auth.AuthService.PrivilegeType; import com.alibaba.csp.sentinel.util.StringUtil; @@ -67,13 +65,9 @@ public class FlowControllerV2 { @Qualifier("flowRuleDefaultPublisher") private DynamicRulePublisher> rulePublisher; - @Autowired - private AuthService authService; - @GetMapping("/rules") - public Result> apiQueryMachineRules(HttpServletRequest request, @RequestParam String app) { - AuthUser authUser = authService.getAuthUser(request); - authUser.authTarget(app, PrivilegeType.READ_RULE); + @AuthAction(PrivilegeType.READ_RULE) + public Result> apiQueryMachineRules(@RequestParam String app) { if (StringUtil.isEmpty(app)) { return Result.ofFail(-1, "app can't be null or empty"); @@ -141,9 +135,8 @@ private Result checkEntityInternal(FlowRuleEntity entity) { } @PostMapping("/rule") - public Result apiAddFlowRule(HttpServletRequest request, @RequestBody FlowRuleEntity entity) { - AuthUser authUser = authService.getAuthUser(request); - authUser.authTarget(entity.getApp(), PrivilegeType.WRITE_RULE); + @AuthAction(value = AuthService.PrivilegeType.WRITE_RULE) + public Result apiAddFlowRule(@RequestBody FlowRuleEntity entity) { Result checkResult = checkEntityInternal(entity); if (checkResult != null) { @@ -166,10 +159,10 @@ public Result apiAddFlowRule(HttpServletRequest request, @Reques } @PutMapping("/rule/{id}") - public Result apiUpdateFlowRule(HttpServletRequest request, - @PathVariable("id") Long id, + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + + public Result apiUpdateFlowRule(@PathVariable("id") Long id, @RequestBody FlowRuleEntity entity) { - AuthUser authUser = authService.getAuthUser(request); if (id == null || id <= 0) { return Result.ofFail(-1, "Invalid id"); } @@ -180,7 +173,6 @@ public Result apiUpdateFlowRule(HttpServletRequest request, if (entity == null) { return Result.ofFail(-1, "invalid body"); } - authUser.authTarget(oldEntity.getApp(), PrivilegeType.WRITE_RULE); entity.setApp(oldEntity.getApp()); entity.setIp(oldEntity.getIp()); @@ -208,8 +200,8 @@ public Result apiUpdateFlowRule(HttpServletRequest request, } @DeleteMapping("/rule/{id}") - public Result apiDeleteRule(HttpServletRequest request, @PathVariable("id") Long id) { - AuthUser authUser = authService.getAuthUser(request); + @AuthAction(PrivilegeType.DELETE_RULE) + public Result apiDeleteRule(@PathVariable("id") Long id) { if (id == null || id <= 0) { return Result.ofFail(-1, "Invalid id"); } @@ -217,7 +209,7 @@ public Result apiDeleteRule(HttpServletRequest request, @PathVariable("id" if (oldEntity == null) { return Result.ofSuccess(null); } - authUser.authTarget(oldEntity.getApp(), PrivilegeType.DELETE_RULE); + try { repository.delete(id); publishRules(oldEntity.getApp()); diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/ApplicationEntity.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/ApplicationEntity.java index 51dc229edd..44c10daaf9 100755 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/ApplicationEntity.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/ApplicationEntity.java @@ -28,6 +28,7 @@ public class ApplicationEntity { private Date gmtCreate; private Date gmtModified; private String app; + private Integer appType; private String activeConsole; private Date lastFetch; @@ -63,6 +64,14 @@ public void setApp(String app) { this.app = app; } + public Integer getAppType() { + return appType; + } + + public void setAppType(Integer appType) { + this.appType = appType; + } + public String getActiveConsole() { return activeConsole; } @@ -80,7 +89,7 @@ public void setActiveConsole(String activeConsole) { } public AppInfo toAppInfo() { - return new AppInfo(app); + return new AppInfo(app, appType); } @Override diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/ApiDefinitionEntity.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/ApiDefinitionEntity.java new file mode 100644 index 0000000000..b042e0ada4 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/ApiDefinitionEntity.java @@ -0,0 +1,208 @@ +/* + * 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.dashboard.datasource.entity.gateway; + +import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition; +import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem; +import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.RuleEntity; +import com.alibaba.csp.sentinel.slots.block.Rule; + +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Entity for {@link ApiDefinition}. + * + * @author cdfive + * @since 1.7.0 + */ +public class ApiDefinitionEntity implements RuleEntity { + + private Long id; + private String app; + private String ip; + private Integer port; + + private Date gmtCreate; + private Date gmtModified; + + private String apiName; + private Set predicateItems; + + public static ApiDefinitionEntity fromApiDefinition(String app, String ip, Integer port, ApiDefinition apiDefinition) { + ApiDefinitionEntity entity = new ApiDefinitionEntity(); + entity.setApp(app); + entity.setIp(ip); + entity.setPort(port); + entity.setApiName(apiDefinition.getApiName()); + + Set predicateItems = new LinkedHashSet<>(); + entity.setPredicateItems(predicateItems); + + Set apiPredicateItems = apiDefinition.getPredicateItems(); + if (apiPredicateItems != null) { + for (ApiPredicateItem apiPredicateItem : apiPredicateItems) { + ApiPredicateItemEntity itemEntity = new ApiPredicateItemEntity(); + predicateItems.add(itemEntity); + ApiPathPredicateItem pathPredicateItem = (ApiPathPredicateItem) apiPredicateItem; + itemEntity.setPattern(pathPredicateItem.getPattern()); + itemEntity.setMatchStrategy(pathPredicateItem.getMatchStrategy()); + } + } + + return entity; + } + + public ApiDefinition toApiDefinition() { + ApiDefinition apiDefinition = new ApiDefinition(); + apiDefinition.setApiName(apiName); + + Set apiPredicateItems = new LinkedHashSet<>(); + apiDefinition.setPredicateItems(apiPredicateItems); + + if (predicateItems != null) { + for (ApiPredicateItemEntity predicateItem : predicateItems) { + ApiPathPredicateItem apiPredicateItem = new ApiPathPredicateItem(); + apiPredicateItems.add(apiPredicateItem); + apiPredicateItem.setMatchStrategy(predicateItem.getMatchStrategy()); + apiPredicateItem.setPattern(predicateItem.getPattern()); + } + } + + return apiDefinition; + } + + public ApiDefinitionEntity() { + + } + + public ApiDefinitionEntity(String apiName, Set predicateItems) { + this.apiName = apiName; + this.predicateItems = predicateItems; + } + + public String getApiName() { + return apiName; + } + + public void setApiName(String apiName) { + this.apiName = apiName; + } + + public Set getPredicateItems() { + return predicateItems; + } + + public void setPredicateItems(Set predicateItems) { + this.predicateItems = predicateItems; + } + + @Override + public Long getId() { + return id; + } + + @Override + public void setId(Long id) { + this.id = id; + } + + @Override + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + @Override + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + @Override + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + @Override + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + public Date getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + } + + @Override + public Rule toRule() { + return null; + } + + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + ApiDefinitionEntity entity = (ApiDefinitionEntity) o; + return Objects.equals(id, entity.id) && + Objects.equals(app, entity.app) && + Objects.equals(ip, entity.ip) && + Objects.equals(port, entity.port) && + Objects.equals(gmtCreate, entity.gmtCreate) && + Objects.equals(gmtModified, entity.gmtModified) && + Objects.equals(apiName, entity.apiName) && + Objects.equals(predicateItems, entity.predicateItems); + } + + @Override + public int hashCode() { + return Objects.hash(id, app, ip, port, gmtCreate, gmtModified, apiName, predicateItems); + } + + @Override + public String toString() { + return "ApiDefinitionEntity{" + + "id=" + id + + ", app='" + app + '\'' + + ", ip='" + ip + '\'' + + ", port=" + port + + ", gmtCreate=" + gmtCreate + + ", gmtModified=" + gmtModified + + ", apiName='" + apiName + '\'' + + ", predicateItems=" + predicateItems + + '}'; + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/ApiPredicateItemEntity.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/ApiPredicateItemEntity.java new file mode 100644 index 0000000000..1d6058cd01 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/ApiPredicateItemEntity.java @@ -0,0 +1,79 @@ +/* + * 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.dashboard.datasource.entity.gateway; + +import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem; + +import java.util.Objects; + +/** + * Entity for {@link ApiPredicateItem}. + * + * @author cdfive + * @since 1.7.0 + */ +public class ApiPredicateItemEntity { + + private String pattern; + + private Integer matchStrategy; + + public ApiPredicateItemEntity() { + } + + public ApiPredicateItemEntity(String pattern, int matchStrategy) { + this.pattern = pattern; + this.matchStrategy = matchStrategy; + } + + public String getPattern() { + return pattern; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public Integer getMatchStrategy() { + return matchStrategy; + } + + public void setMatchStrategy(Integer matchStrategy) { + this.matchStrategy = matchStrategy; + } + + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + ApiPredicateItemEntity that = (ApiPredicateItemEntity) o; + return Objects.equals(pattern, that.pattern) && + Objects.equals(matchStrategy, that.matchStrategy); + } + + @Override + public int hashCode() { + return Objects.hash(pattern, matchStrategy); + } + + @Override + public String toString() { + return "ApiPredicateItemEntity{" + + "pattern='" + pattern + '\'' + + ", matchStrategy=" + matchStrategy + + '}'; + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/GatewayFlowRuleEntity.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/GatewayFlowRuleEntity.java new file mode 100644 index 0000000000..391ea41f14 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/GatewayFlowRuleEntity.java @@ -0,0 +1,354 @@ +/* + * 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.dashboard.datasource.entity.gateway; + +import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule; +import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayParamFlowItem; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.RuleEntity; +import com.alibaba.csp.sentinel.slots.block.Rule; + +import java.util.Date; +import java.util.Objects; + +/** + * Entity for {@link GatewayFlowRule}. + * + * @author cdfive + * @since 1.7.0 + */ +public class GatewayFlowRuleEntity implements RuleEntity { + + /**间隔单位*/ + /**0-秒*/ + public static final int INTERVAL_UNIT_SECOND = 0; + /**1-分*/ + public static final int INTERVAL_UNIT_MINUTE = 1; + /**2-时*/ + public static final int INTERVAL_UNIT_HOUR = 2; + /**3-天*/ + public static final int INTERVAL_UNIT_DAY = 3; + + private Long id; + private String app; + private String ip; + private Integer port; + + private Date gmtCreate; + private Date gmtModified; + + private String resource; + private Integer resourceMode; + + private Integer grade; + private Double count; + private Long interval; + private Integer intervalUnit; + + private Integer controlBehavior; + private Integer burst; + + private Integer maxQueueingTimeoutMs; + + private GatewayParamFlowItemEntity paramItem; + + public static Long calIntervalSec(Long interval, Integer intervalUnit) { + switch (intervalUnit) { + case INTERVAL_UNIT_SECOND: + return interval; + case INTERVAL_UNIT_MINUTE: + return interval * 60; + case INTERVAL_UNIT_HOUR: + return interval * 60 * 60; + case INTERVAL_UNIT_DAY: + return interval * 60 * 60 * 24; + default: + break; + } + + throw new IllegalArgumentException("Invalid intervalUnit: " + intervalUnit); + } + + public static Object[] parseIntervalSec(Long intervalSec) { + if (intervalSec % (60 * 60 * 24) == 0) { + return new Object[] {intervalSec / (60 * 60 * 24), INTERVAL_UNIT_DAY}; + } + + if (intervalSec % (60 * 60 ) == 0) { + return new Object[] {intervalSec / (60 * 60), INTERVAL_UNIT_HOUR}; + } + + if (intervalSec % 60 == 0) { + return new Object[] {intervalSec / 60, INTERVAL_UNIT_MINUTE}; + } + + return new Object[] {intervalSec, INTERVAL_UNIT_SECOND}; + } + + public GatewayFlowRule toGatewayFlowRule() { + GatewayFlowRule rule = new GatewayFlowRule(); + rule.setResource(resource); + rule.setResourceMode(resourceMode); + + rule.setGrade(grade); + rule.setCount(count); + rule.setIntervalSec(calIntervalSec(interval, intervalUnit)); + + rule.setControlBehavior(controlBehavior); + + if (burst != null) { + rule.setBurst(burst); + } + + if (maxQueueingTimeoutMs != null) { + rule.setMaxQueueingTimeoutMs(maxQueueingTimeoutMs); + } + + if (paramItem != null) { + GatewayParamFlowItem ruleItem = new GatewayParamFlowItem(); + rule.setParamItem(ruleItem); + ruleItem.setParseStrategy(paramItem.getParseStrategy()); + ruleItem.setFieldName(paramItem.getFieldName()); + ruleItem.setPattern(paramItem.getPattern()); + + if (paramItem.getMatchStrategy() != null) { + ruleItem.setMatchStrategy(paramItem.getMatchStrategy()); + } + } + + return rule; + } + + public static GatewayFlowRuleEntity fromGatewayFlowRule(String app, String ip, Integer port, GatewayFlowRule rule) { + GatewayFlowRuleEntity entity = new GatewayFlowRuleEntity(); + entity.setApp(app); + entity.setIp(ip); + entity.setPort(port); + + entity.setResource(rule.getResource()); + entity.setResourceMode(rule.getResourceMode()); + + entity.setGrade(rule.getGrade()); + entity.setCount(rule.getCount()); + Object[] intervalSecResult = parseIntervalSec(rule.getIntervalSec()); + entity.setInterval((Long) intervalSecResult[0]); + entity.setIntervalUnit((Integer) intervalSecResult[1]); + + entity.setControlBehavior(rule.getControlBehavior()); + entity.setBurst(rule.getBurst()); + entity.setMaxQueueingTimeoutMs(rule.getMaxQueueingTimeoutMs()); + + GatewayParamFlowItem paramItem = rule.getParamItem(); + if (paramItem != null) { + GatewayParamFlowItemEntity itemEntity = new GatewayParamFlowItemEntity(); + entity.setParamItem(itemEntity); + itemEntity.setParseStrategy(paramItem.getParseStrategy()); + itemEntity.setFieldName(paramItem.getFieldName()); + itemEntity.setPattern(paramItem.getPattern()); + itemEntity.setMatchStrategy(paramItem.getMatchStrategy()); + } + + return entity; + } + + @Override + public Long getId() { + return id; + } + + @Override + public void setId(Long id) { + this.id = id; + } + + @Override + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + @Override + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + @Override + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + @Override + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + @Override + public Rule toRule() { + return null; + } + + public Date getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + } + + public GatewayParamFlowItemEntity getParamItem() { + return paramItem; + } + + public void setParamItem(GatewayParamFlowItemEntity paramItem) { + this.paramItem = paramItem; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public Integer getResourceMode() { + return resourceMode; + } + + public void setResourceMode(Integer resourceMode) { + this.resourceMode = resourceMode; + } + + public Integer getGrade() { + return grade; + } + + public void setGrade(Integer grade) { + this.grade = grade; + } + + public Double getCount() { + return count; + } + + public void setCount(Double count) { + this.count = count; + } + + public Long getInterval() { + return interval; + } + + public void setInterval(Long interval) { + this.interval = interval; + } + + public Integer getIntervalUnit() { + return intervalUnit; + } + + public void setIntervalUnit(Integer intervalUnit) { + this.intervalUnit = intervalUnit; + } + + public Integer getControlBehavior() { + return controlBehavior; + } + + public void setControlBehavior(Integer controlBehavior) { + this.controlBehavior = controlBehavior; + } + + public Integer getBurst() { + return burst; + } + + public void setBurst(Integer burst) { + this.burst = burst; + } + + public Integer getMaxQueueingTimeoutMs() { + return maxQueueingTimeoutMs; + } + + public void setMaxQueueingTimeoutMs(Integer maxQueueingTimeoutMs) { + this.maxQueueingTimeoutMs = maxQueueingTimeoutMs; + } + + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + GatewayFlowRuleEntity that = (GatewayFlowRuleEntity) o; + return Objects.equals(id, that.id) && + Objects.equals(app, that.app) && + Objects.equals(ip, that.ip) && + Objects.equals(port, that.port) && + Objects.equals(gmtCreate, that.gmtCreate) && + Objects.equals(gmtModified, that.gmtModified) && + Objects.equals(resource, that.resource) && + Objects.equals(resourceMode, that.resourceMode) && + Objects.equals(grade, that.grade) && + Objects.equals(count, that.count) && + Objects.equals(interval, that.interval) && + Objects.equals(intervalUnit, that.intervalUnit) && + Objects.equals(controlBehavior, that.controlBehavior) && + Objects.equals(burst, that.burst) && + Objects.equals(maxQueueingTimeoutMs, that.maxQueueingTimeoutMs) && + Objects.equals(paramItem, that.paramItem); + } + + @Override + public int hashCode() { + return Objects.hash(id, app, ip, port, gmtCreate, gmtModified, resource, resourceMode, grade, count, interval, intervalUnit, controlBehavior, burst, maxQueueingTimeoutMs, paramItem); + } + + @Override + public String toString() { + return "GatewayFlowRuleEntity{" + + "id=" + id + + ", app='" + app + '\'' + + ", ip='" + ip + '\'' + + ", port=" + port + + ", gmtCreate=" + gmtCreate + + ", gmtModified=" + gmtModified + + ", resource='" + resource + '\'' + + ", resourceMode=" + resourceMode + + ", grade=" + grade + + ", count=" + count + + ", interval=" + interval + + ", intervalUnit=" + intervalUnit + + ", controlBehavior=" + controlBehavior + + ", burst=" + burst + + ", maxQueueingTimeoutMs=" + maxQueueingTimeoutMs + + ", paramItem=" + paramItem + + '}'; + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/GatewayParamFlowItemEntity.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/GatewayParamFlowItemEntity.java new file mode 100644 index 0000000000..4da71c8ecd --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/GatewayParamFlowItemEntity.java @@ -0,0 +1,95 @@ +/* + * 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.dashboard.datasource.entity.gateway; + +import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayParamFlowItem; + +import java.util.Objects; + +/** + * Entity for {@link GatewayParamFlowItem}. + * + * @author cdfive + * @since 1.7.0 + */ +public class GatewayParamFlowItemEntity { + + private Integer parseStrategy; + + private String fieldName; + + private String pattern; + + private Integer matchStrategy; + + public Integer getParseStrategy() { + return parseStrategy; + } + + public void setParseStrategy(Integer parseStrategy) { + this.parseStrategy = parseStrategy; + } + + public String getFieldName() { + return fieldName; + } + + public void setFieldName(String fieldName) { + this.fieldName = fieldName; + } + + public String getPattern() { + return pattern; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public Integer getMatchStrategy() { + return matchStrategy; + } + + public void setMatchStrategy(Integer matchStrategy) { + this.matchStrategy = matchStrategy; + } + + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + GatewayParamFlowItemEntity that = (GatewayParamFlowItemEntity) o; + return Objects.equals(parseStrategy, that.parseStrategy) && + Objects.equals(fieldName, that.fieldName) && + Objects.equals(pattern, that.pattern) && + Objects.equals(matchStrategy, that.matchStrategy); + } + + @Override + public int hashCode() { + return Objects.hash(parseStrategy, fieldName, pattern, matchStrategy); + } + + @Override + public String toString() { + return "GatewayParamFlowItemEntity{" + + "parseStrategy=" + parseStrategy + + ", fieldName='" + fieldName + '\'' + + ", pattern='" + pattern + '\'' + + ", matchStrategy=" + matchStrategy + + '}'; + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/AbstractRuleEntity.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/AbstractRuleEntity.java index 1f5eaa23dd..6f60647dc4 100644 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/AbstractRuleEntity.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/AbstractRuleEntity.java @@ -18,6 +18,7 @@ import java.util.Date; import com.alibaba.csp.sentinel.slots.block.AbstractRule; +import com.alibaba.csp.sentinel.slots.block.Rule; /** * @author Eric Zhao @@ -103,4 +104,9 @@ public AbstractRuleEntity setGmtModified(Date gmtModified) { this.gmtModified = gmtModified; return this; } + + @Override + public T toRule() { + return rule; + } } diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/AuthorityRuleEntity.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/AuthorityRuleEntity.java index 2a77d32fe4..a085d898b8 100644 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/AuthorityRuleEntity.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/AuthorityRuleEntity.java @@ -15,10 +15,9 @@ */ package com.alibaba.csp.sentinel.dashboard.datasource.entity.rule; -import com.alibaba.csp.sentinel.slots.block.Rule; import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule; import com.alibaba.csp.sentinel.util.AssertUtil; - +import com.alibaba.fastjson.annotation.JSONField; import com.fasterxml.jackson.annotation.JsonIgnore; /** @@ -27,7 +26,8 @@ */ public class AuthorityRuleEntity extends AbstractRuleEntity { - public AuthorityRuleEntity() {} + public AuthorityRuleEntity() { + } public AuthorityRuleEntity(AuthorityRule authorityRule) { AssertUtil.notNull(authorityRule, "Authority rule should not be null"); @@ -43,22 +43,20 @@ public static AuthorityRuleEntity fromAuthorityRule(String app, String ip, Integ } @JsonIgnore + @JSONField(serialize = false) public String getLimitApp() { return rule.getLimitApp(); } @JsonIgnore + @JSONField(serialize = false) public String getResource() { return rule.getResource(); } @JsonIgnore + @JSONField(serialize = false) public int getStrategy() { return rule.getStrategy(); } - - @Override - public Rule toRule() { - return rule; - } } diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/ParamFlowRuleEntity.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/ParamFlowRuleEntity.java index e2bbb03b6c..56bd773c0a 100644 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/ParamFlowRuleEntity.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/ParamFlowRuleEntity.java @@ -15,23 +15,23 @@ */ package com.alibaba.csp.sentinel.dashboard.datasource.entity.rule; -import java.util.List; - -import com.alibaba.csp.sentinel.slots.block.Rule; import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowClusterConfig; import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowItem; import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule; import com.alibaba.csp.sentinel.util.AssertUtil; - +import com.alibaba.fastjson.annotation.JSONField; import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.List; + /** * @author Eric Zhao * @since 0.2.1 */ public class ParamFlowRuleEntity extends AbstractRuleEntity { - public ParamFlowRuleEntity() {} + public ParamFlowRuleEntity() { + } public ParamFlowRuleEntity(ParamFlowRule rule) { AssertUtil.notNull(rule, "Authority rule should not be null"); @@ -47,47 +47,74 @@ public static ParamFlowRuleEntity fromAuthorityRule(String app, String ip, Integ } @JsonIgnore + @JSONField(serialize = false) public String getLimitApp() { return rule.getLimitApp(); } @JsonIgnore + @JSONField(serialize = false) public String getResource() { return rule.getResource(); } @JsonIgnore + @JSONField(serialize = false) public int getGrade() { return rule.getGrade(); } @JsonIgnore + @JSONField(serialize = false) public Integer getParamIdx() { return rule.getParamIdx(); } @JsonIgnore + @JSONField(serialize = false) public double getCount() { return rule.getCount(); } @JsonIgnore + @JSONField(serialize = false) public List getParamFlowItemList() { return rule.getParamFlowItemList(); } @JsonIgnore + @JSONField(serialize = false) + public int getControlBehavior() { + return rule.getControlBehavior(); + } + + @JsonIgnore + @JSONField(serialize = false) + public int getMaxQueueingTimeMs() { + return rule.getMaxQueueingTimeMs(); + } + + @JsonIgnore + @JSONField(serialize = false) + public int getBurstCount() { + return rule.getBurstCount(); + } + + @JsonIgnore + @JSONField(serialize = false) + public long getDurationInSec() { + return rule.getDurationInSec(); + } + + @JsonIgnore + @JSONField(serialize = false) public boolean isClusterMode() { return rule.isClusterMode(); } @JsonIgnore + @JSONField(serialize = false) public ParamFlowClusterConfig getClusterConfig() { return rule.getClusterConfig(); } - - @Override - public Rule toRule() { - return rule; - } } diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/SystemRuleEntity.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/SystemRuleEntity.java index 08041b0a76..483ebcb52f 100755 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/SystemRuleEntity.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/rule/SystemRuleEntity.java @@ -15,10 +15,10 @@ */ package com.alibaba.csp.sentinel.dashboard.datasource.entity.rule; -import java.util.Date; - import com.alibaba.csp.sentinel.slots.system.SystemRule; +import java.util.Date; + /** * @author leyou */ @@ -29,10 +29,11 @@ public class SystemRuleEntity implements RuleEntity { private String app; private String ip; private Integer port; - private Double avgLoad; + private Double highestSystemLoad; private Long avgRt; private Long maxThread; private Double qps; + private Double highestCpuUsage; private Date gmtCreate; private Date gmtModified; @@ -42,7 +43,8 @@ public static SystemRuleEntity fromSystemRule(String app, String ip, Integer por entity.setApp(app); entity.setIp(ip); entity.setPort(port); - entity.setAvgLoad(rule.getHighestSystemLoad()); + entity.setHighestSystemLoad(rule.getHighestSystemLoad()); + entity.setHighestCpuUsage(rule.getHighestCpuUsage()); entity.setAvgRt(rule.getAvgRt()); entity.setMaxThread(rule.getMaxThread()); entity.setQps(rule.getQps()); @@ -86,12 +88,12 @@ public void setApp(String app) { this.app = app; } - public Double getAvgLoad() { - return avgLoad; + public Double getHighestSystemLoad() { + return highestSystemLoad; } - public void setAvgLoad(Double avgLoad) { - this.avgLoad = avgLoad; + public void setHighestSystemLoad(Double highestSystemLoad) { + this.highestSystemLoad = highestSystemLoad; } public Long getAvgRt() { @@ -118,6 +120,14 @@ public void setQps(Double qps) { this.qps = qps; } + public Double getHighestCpuUsage() { + return highestCpuUsage; + } + + public void setHighestCpuUsage(Double highestCpuUsage) { + this.highestCpuUsage = highestCpuUsage; + } + @Override public Date getGmtCreate() { return gmtCreate; @@ -138,10 +148,11 @@ public void setGmtModified(Date gmtModified) { @Override public SystemRule toRule() { SystemRule rule = new SystemRule(); - rule.setHighestSystemLoad(avgLoad); + rule.setHighestSystemLoad(highestSystemLoad); rule.setAvgRt(avgRt); rule.setMaxThread(maxThread); rule.setQps(qps); + rule.setHighestCpuUsage(highestCpuUsage); return rule; } } diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/AppInfo.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/AppInfo.java index 01d3183fbb..f7697698cd 100755 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/AppInfo.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/AppInfo.java @@ -28,6 +28,8 @@ public class AppInfo { private String app = ""; + private Integer appType = 0; + private Set machines = ConcurrentHashMap.newKeySet(); public AppInfo() {} @@ -36,6 +38,11 @@ public AppInfo(String app) { this.app = app; } + public AppInfo(String app, Integer appType) { + this.app = app; + this.appType = appType; + } + public String getApp() { return app; } @@ -44,6 +51,14 @@ public void setApp(String app) { this.app = app; } + public Integer getAppType() { + return appType; + } + + public void setAppType(Integer appType) { + this.appType = appType; + } + /** * Get the current machines. * diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/MachineInfo.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/MachineInfo.java index 9c65f36500..afcc812430 100755 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/MachineInfo.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/MachineInfo.java @@ -23,6 +23,7 @@ public class MachineInfo implements Comparable { private String app = ""; + private Integer appType = 0; private String hostname = ""; private String ip = ""; private Integer port = -1; @@ -62,6 +63,14 @@ public void setApp(String app) { this.app = app; } + public Integer getAppType() { + return appType; + } + + public void setAppType(Integer appType) { + this.appType = appType; + } + public String getHostname() { return hostname; } @@ -139,6 +148,7 @@ public int compareTo(MachineInfo o) { public String toString() { return new StringBuilder("MachineInfo {") .append("app='").append(app).append('\'') + .append(",appType='").append(appType).append('\'') .append(", hostname='").append(hostname).append('\'') .append(", ip='").append(ip).append('\'') .append(", port=").append(port) diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/SimpleMachineDiscovery.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/SimpleMachineDiscovery.java index 23ce943d91..a4ab83acd9 100755 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/SimpleMachineDiscovery.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/SimpleMachineDiscovery.java @@ -37,7 +37,7 @@ public class SimpleMachineDiscovery implements MachineDiscovery { @Override public long addMachine(MachineInfo machineInfo) { AssertUtil.notNull(machineInfo, "machineInfo cannot be null"); - AppInfo appInfo = apps.computeIfAbsent(machineInfo.getApp(), AppInfo::new); + AppInfo appInfo = apps.computeIfAbsent(machineInfo.getApp(), o -> new AppInfo(machineInfo.getApp(), machineInfo.getAppType())); appInfo.addMachine(machineInfo); return 1; } diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/ResourceTreeNode.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/ResourceTreeNode.java index fa2e7ca9b1..be576160c1 100755 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/ResourceTreeNode.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/ResourceTreeNode.java @@ -56,7 +56,7 @@ public static ResourceTreeNode fromNodeVoList(List nodeVos) { ResourceTreeNode node = fromNodeVo(vo); map.put(node.id, node); // real root - if (node.parentId == null) { + if (node.parentId == null || node.parentId.isEmpty()) { root = node; } else if (map.containsKey(node.parentId)) { map.get(node.parentId).children.add(node); diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/AddApiReqVo.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/AddApiReqVo.java new file mode 100644 index 0000000000..445f445cb2 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/AddApiReqVo.java @@ -0,0 +1,78 @@ +/* + * 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.dashboard.domain.vo.gateway.api; + +import java.util.List; + +/** + * Value Object for add gateway api. + * + * @author cdfive + * @since 1.7.0 + */ +public class AddApiReqVo { + + private String app; + + private String ip; + + private Integer port; + + private String apiName; + + private List predicateItems; + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public String getApiName() { + return apiName; + } + + public void setApiName(String apiName) { + this.apiName = apiName; + } + + public List getPredicateItems() { + return predicateItems; + } + + public void setPredicateItems(List predicateItems) { + this.predicateItems = predicateItems; + } +} + diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/ApiPredicateItemVo.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/ApiPredicateItemVo.java new file mode 100644 index 0000000000..d621ddf58a --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/ApiPredicateItemVo.java @@ -0,0 +1,45 @@ +/* + * 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.dashboard.domain.vo.gateway.api; + +/** + * Value Object for add or update gateway api. + * + * @author cdfive + * @since 1.7.0 + */ +public class ApiPredicateItemVo { + + private String pattern; + + private Integer matchStrategy; + + public String getPattern() { + return pattern; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public Integer getMatchStrategy() { + return matchStrategy; + } + + public void setMatchStrategy(Integer matchStrategy) { + this.matchStrategy = matchStrategy; + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/UpdateApiReqVo.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/UpdateApiReqVo.java new file mode 100644 index 0000000000..3dfd96a286 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/UpdateApiReqVo.java @@ -0,0 +1,57 @@ +/* + * 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.dashboard.domain.vo.gateway.api; + +import java.util.List; + +/** + * Value Object for update gateway api. + * + * @author cdfive + * @since 1.7.0 + */ +public class UpdateApiReqVo { + + private Long id; + + private String app; + + private List predicateItems; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public List getPredicateItems() { + return predicateItems; + } + + public void setPredicateItems(List predicateItems) { + this.predicateItems = predicateItems; + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/AddFlowRuleReqVo.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/AddFlowRuleReqVo.java new file mode 100644 index 0000000000..57cc9e2a6e --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/AddFlowRuleReqVo.java @@ -0,0 +1,155 @@ +/* + * 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.dashboard.domain.vo.gateway.rule; + +/** + * Value Object for add gateway flow rule. + * + * @author cdfive + * @since 1.7.0 + */ +public class AddFlowRuleReqVo { + + private String app; + + private String ip; + + private Integer port; + + private String resource; + + private Integer resourceMode; + + private Integer grade; + + private Double count; + + private Long interval; + + private Integer intervalUnit; + + private Integer controlBehavior; + + private Integer burst; + + private Integer maxQueueingTimeoutMs; + + private GatewayParamFlowItemVo paramItem; + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public Integer getResourceMode() { + return resourceMode; + } + + public void setResourceMode(Integer resourceMode) { + this.resourceMode = resourceMode; + } + + public Integer getGrade() { + return grade; + } + + public void setGrade(Integer grade) { + this.grade = grade; + } + + public Double getCount() { + return count; + } + + public void setCount(Double count) { + this.count = count; + } + + public Long getInterval() { + return interval; + } + + public void setInterval(Long interval) { + this.interval = interval; + } + + public Integer getIntervalUnit() { + return intervalUnit; + } + + public void setIntervalUnit(Integer intervalUnit) { + this.intervalUnit = intervalUnit; + } + + public Integer getControlBehavior() { + return controlBehavior; + } + + public void setControlBehavior(Integer controlBehavior) { + this.controlBehavior = controlBehavior; + } + + public Integer getBurst() { + return burst; + } + + public void setBurst(Integer burst) { + this.burst = burst; + } + + public Integer getMaxQueueingTimeoutMs() { + return maxQueueingTimeoutMs; + } + + public void setMaxQueueingTimeoutMs(Integer maxQueueingTimeoutMs) { + this.maxQueueingTimeoutMs = maxQueueingTimeoutMs; + } + + public GatewayParamFlowItemVo getParamItem() { + return paramItem; + } + + public void setParamItem(GatewayParamFlowItemVo paramItem) { + this.paramItem = paramItem; + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/GatewayParamFlowItemVo.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/GatewayParamFlowItemVo.java new file mode 100644 index 0000000000..af24fed96e --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/GatewayParamFlowItemVo.java @@ -0,0 +1,65 @@ +/* + * 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.dashboard.domain.vo.gateway.rule; + +/** + * Value Object for add or update gateway flow rule. + * + * @author cdfive + * @since 1.7.0 + */ +public class GatewayParamFlowItemVo { + + private Integer parseStrategy; + + private String fieldName; + + private String pattern; + + private Integer matchStrategy; + + public Integer getParseStrategy() { + return parseStrategy; + } + + public void setParseStrategy(Integer parseStrategy) { + this.parseStrategy = parseStrategy; + } + + public String getFieldName() { + return fieldName; + } + + public void setFieldName(String fieldName) { + this.fieldName = fieldName; + } + + public String getPattern() { + return pattern; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public Integer getMatchStrategy() { + return matchStrategy; + } + + public void setMatchStrategy(Integer matchStrategy) { + this.matchStrategy = matchStrategy; + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/UpdateFlowRuleReqVo.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/UpdateFlowRuleReqVo.java new file mode 100644 index 0000000000..8d1988febf --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/UpdateFlowRuleReqVo.java @@ -0,0 +1,125 @@ +/* + * 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.dashboard.domain.vo.gateway.rule; + +/** + * Value Object for update gateway flow rule. + * + * @author cdfive + * @since 1.7.0 + */ +public class UpdateFlowRuleReqVo { + + private Long id; + + private String app; + + private Integer grade; + + private Double count; + + private Long interval; + + private Integer intervalUnit; + + private Integer controlBehavior; + + private Integer burst; + + private Integer maxQueueingTimeoutMs; + + private GatewayParamFlowItemVo paramItem; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public Integer getGrade() { + return grade; + } + + public void setGrade(Integer grade) { + this.grade = grade; + } + + public Double getCount() { + return count; + } + + public void setCount(Double count) { + this.count = count; + } + + public Long getInterval() { + return interval; + } + + public void setInterval(Long interval) { + this.interval = interval; + } + + public Integer getIntervalUnit() { + return intervalUnit; + } + + public void setIntervalUnit(Integer intervalUnit) { + this.intervalUnit = intervalUnit; + } + + public Integer getControlBehavior() { + return controlBehavior; + } + + public void setControlBehavior(Integer controlBehavior) { + this.controlBehavior = controlBehavior; + } + + public Integer getBurst() { + return burst; + } + + public void setBurst(Integer burst) { + this.burst = burst; + } + + public Integer getMaxQueueingTimeoutMs() { + return maxQueueingTimeoutMs; + } + + public void setMaxQueueingTimeoutMs(Integer maxQueueingTimeoutMs) { + this.maxQueueingTimeoutMs = maxQueueingTimeoutMs; + } + + public GatewayParamFlowItemVo getParamItem() { + return paramItem; + } + + public void setParamItem(GatewayParamFlowItemVo paramItem) { + this.paramItem = paramItem; + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/gateway/InMemApiDefinitionStore.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/gateway/InMemApiDefinitionStore.java new file mode 100644 index 0000000000..844acbd51f --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/gateway/InMemApiDefinitionStore.java @@ -0,0 +1,39 @@ +/* + * 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.dashboard.repository.gateway; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity; +import com.alibaba.csp.sentinel.dashboard.repository.rule.InMemoryRuleRepositoryAdapter; +import org.springframework.stereotype.Component; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * Store {@link ApiDefinitionEntity} in memory. + * + * @author cdfive + * @since 1.7.0 + */ +@Component +public class InMemApiDefinitionStore extends InMemoryRuleRepositoryAdapter { + + private static AtomicLong ids = new AtomicLong(0); + + @Override + protected long nextId() { + return ids.incrementAndGet(); + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/gateway/InMemGatewayFlowRuleStore.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/gateway/InMemGatewayFlowRuleStore.java new file mode 100644 index 0000000000..f1b63c4227 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/gateway/InMemGatewayFlowRuleStore.java @@ -0,0 +1,39 @@ +/* + * 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.dashboard.repository.gateway; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.repository.rule.InMemoryRuleRepositoryAdapter; +import org.springframework.stereotype.Component; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * Store {@link GatewayFlowRuleEntity} in memory. + * + * @author cdfive + * @since 1.7.0 + */ +@Component +public class InMemGatewayFlowRuleStore extends InMemoryRuleRepositoryAdapter { + + private static AtomicLong ids = new AtomicLong(0); + + @Override + protected long nextId() { + return ids.incrementAndGet(); + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemoryRuleRepositoryAdapter.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemoryRuleRepositoryAdapter.java index c6fdeed3a1..08d623cc32 100755 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemoryRuleRepositoryAdapter.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemoryRuleRepositoryAdapter.java @@ -110,6 +110,12 @@ public List findAllByApp(String appName) { return new ArrayList<>(entities.values()); } + public void clearAll() { + allRules.clear(); + machineRules.clear(); + appRules.clear(); + } + protected T preProcess(T entity) { return entity; } diff --git a/sentinel-dashboard/src/main/resources/application.properties b/sentinel-dashboard/src/main/resources/application.properties index 609cd90e1b..a2f84dec89 100755 --- a/sentinel-dashboard/src/main/resources/application.properties +++ b/sentinel-dashboard/src/main/resources/application.properties @@ -10,7 +10,12 @@ logging.pattern.file= %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - % #logging.pattern.console= %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n #auth settings -auth.filter.exclude-urls=/,/auth/login,/auth/logout,/registry/machine +auth.filter.exclude-urls=/,/auth/login,/auth/logout,/registry/machine,/version auth.filter.exclude-url-suffixes=htm,html,js,css,map,ico,ttf,woff,png +# If auth.enabled=false, Sentinel console disable login auth.username=sentinel -auth.password=sentinel \ No newline at end of file +auth.password=sentinel + +# Inject the dashboard version. It's required to enable +# filtering in pom.xml for this resource file. +sentinel.dashboard.version=${project.version} \ No newline at end of file diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/app.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/app.js index fcc61610ef..bc3747af77 100755 --- a/sentinel-dashboard/src/main/webapp/resources/app/scripts/app.js +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/app.js @@ -26,9 +26,9 @@ angular .factory('AuthInterceptor', ['$window', '$state', function ($window, $state) { var authInterceptor = { 'responseError' : function(response) { - if (response.status == 401) { + if (response.status === 401) { // If not auth, clear session in localStorage and jump to the login page - $window.localStorage.removeItem("session_sentinel_admin"); + $window.localStorage.removeItem('session_sentinel_admin'); $state.go('login'); } @@ -123,21 +123,21 @@ angular } }) - .state('dashboard.flow', { - templateUrl: 'app/views/flow_v2.html', - url: '/v2/flow/:app', - controller: 'FlowControllerV2', - resolve: { - loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { - return $ocLazyLoad.load({ - name: 'sentinelDashboardApp', - files: [ - 'app/scripts/controllers/flow_v2.js', - ] - }); - }] - } - }) + .state('dashboard.flow', { + templateUrl: 'app/views/flow_v2.html', + url: '/v2/flow/:app', + controller: 'FlowControllerV2', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/flow_v2.js', + ] + }); + }] + } + }) .state('dashboard.paramFlow', { templateUrl: 'app/views/param_flow.html', @@ -155,69 +155,69 @@ angular } }) - .state('dashboard.clusterAppAssignManage', { - templateUrl: 'app/views/cluster_app_assign_manage.html', - url: '/cluster/assign_manage/:app', - controller: 'SentinelClusterAppAssignManageController', - resolve: { - loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { - return $ocLazyLoad.load({ - name: 'sentinelDashboardApp', - files: [ - 'app/scripts/controllers/cluster_app_assign_manage.js', - ] - }); - }] - } - }) + .state('dashboard.clusterAppAssignManage', { + templateUrl: 'app/views/cluster_app_assign_manage.html', + url: '/cluster/assign_manage/:app', + controller: 'SentinelClusterAppAssignManageController', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/cluster_app_assign_manage.js', + ] + }); + }] + } + }) - .state('dashboard.clusterAppServerList', { - templateUrl: 'app/views/cluster_app_server_list.html', - url: '/cluster/server/:app', - controller: 'SentinelClusterAppServerListController', - resolve: { - loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { - return $ocLazyLoad.load({ - name: 'sentinelDashboardApp', - files: [ - 'app/scripts/controllers/cluster_app_server_list.js', - ] - }); - }] - } - }) + .state('dashboard.clusterAppServerList', { + templateUrl: 'app/views/cluster_app_server_list.html', + url: '/cluster/server/:app', + controller: 'SentinelClusterAppServerListController', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/cluster_app_server_list.js', + ] + }); + }] + } + }) - .state('dashboard.clusterAppClientList', { - templateUrl: 'app/views/cluster_app_client_list.html', - url: '/cluster/client/:app', - controller: 'SentinelClusterAppTokenClientListController', - resolve: { - loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { - return $ocLazyLoad.load({ - name: 'sentinelDashboardApp', - files: [ - 'app/scripts/controllers/cluster_app_token_client_list.js', - ] - }); - }] - } - }) + .state('dashboard.clusterAppClientList', { + templateUrl: 'app/views/cluster_app_client_list.html', + url: '/cluster/client/:app', + controller: 'SentinelClusterAppTokenClientListController', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/cluster_app_token_client_list.js', + ] + }); + }] + } + }) - .state('dashboard.clusterSingle', { - templateUrl: 'app/views/cluster_single_config.html', - url: '/cluster/single/:app', - controller: 'SentinelClusterSingleController', - resolve: { - loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { - return $ocLazyLoad.load({ - name: 'sentinelDashboardApp', - files: [ - 'app/scripts/controllers/cluster_single.js', - ] - }); - }] - } - }) + .state('dashboard.clusterSingle', { + templateUrl: 'app/views/cluster_single_config.html', + url: '/cluster/single/:app', + controller: 'SentinelClusterSingleController', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/cluster_single.js', + ] + }); + }] + } + }) .state('dashboard.authority', { templateUrl: 'app/views/authority.html', @@ -298,6 +298,23 @@ angular }] } }) + + .state('dashboard.gatewayIdentity', { + templateUrl: 'app/views/gateway/identity.html', + url: '/gateway/identity/:app', + controller: 'GatewayIdentityCtl', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/gateway/identity.js', + ] + }); + }] + } + }) + .state('dashboard.metric', { templateUrl: 'app/views/metric.html', url: '/metric/:app', @@ -312,5 +329,37 @@ angular }); }] } + }) + + .state('dashboard.gatewayApi', { + templateUrl: 'app/views/gateway/api.html', + url: '/gateway/api/:app', + controller: 'GatewayApiCtl', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/gateway/api.js', + ] + }); + }] + } + }) + + .state('dashboard.gatewayFlow', { + templateUrl: 'app/views/gateway/flow.html', + url: '/gateway/flow/:app', + controller: 'GatewayFlowCtl', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/gateway/flow.js', + ] + }); + }] + } }); }]); \ No newline at end of file diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/flow_v1.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/flow_v1.js index 8e4be2ab4d..3c6449372c 100755 --- a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/flow_v1.js +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/flow_v1.js @@ -148,18 +148,18 @@ app.controller('FlowControllerV1', ['$scope', '$stateParams', 'FlowServiceV1', ' getMachineRules(); confirmDialog.close(); } else { - alert('失败!'); + alert('失败:' + data.msg); } }); }; function addNewRule(rule) { FlowService.newRule(rule).success(function (data) { - if (data.code == 0) { + if (data.code === 0) { getMachineRules(); flowRuleDialog.close(); } else { - alert('失败!'); + alert('失败:' + data.msg); } }); }; @@ -173,7 +173,7 @@ app.controller('FlowControllerV1', ['$scope', '$stateParams', 'FlowServiceV1', ' function saveRule(rule, edit) { FlowService.saveRule(rule).success(function (data) { - if (data.code == 0) { + if (data.code === 0) { getMachineRules(); if (edit) { flowRuleDialog.close(); @@ -181,7 +181,7 @@ app.controller('FlowControllerV1', ['$scope', '$stateParams', 'FlowServiceV1', ' confirmDialog.close(); } } else { - alert('失败!'); + alert('失败:' + data.msg); } }); } diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/gateway/api.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/gateway/api.js new file mode 100644 index 0000000000..ccf2497cad --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/gateway/api.js @@ -0,0 +1,245 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('GatewayApiCtl', ['$scope', '$stateParams', 'GatewayApiService', 'ngDialog', 'MachineService', + function ($scope, $stateParams, GatewayApiService, ngDialog, MachineService) { + $scope.app = $stateParams.app; + + $scope.apisPageConfig = { + pageSize: 10, + currentPageIndex: 1, + totalPage: 1, + totalCount: 0, + }; + + $scope.macsInputConfig = { + searchField: ['text', 'value'], + persist: true, + create: false, + maxItems: 1, + render: { + item: function (data, escape) { + return '
' + escape(data.text) + '
'; + } + }, + onChange: function (value, oldValue) { + $scope.macInputModel = value; + } + }; + + getApis(); + function getApis() { + if (!$scope.macInputModel) { + return; + } + + var mac = $scope.macInputModel.split(':'); + GatewayApiService.queryApis($scope.app, mac[0], mac[1]).success( + function (data) { + if (data.code == 0 && data.data) { + // To merge rows for api who has more than one predicateItems, here we build data manually + $scope.apis = []; + + data.data.forEach(function(api) { + api["predicateItems"].forEach(function (item, index) { + var newItem = {}; + newItem["id"] = api["id"]; + newItem["app"] = api["app"]; + newItem["ip"] = api["ip"]; + newItem["port"] = api["port"]; + newItem["apiName"] = api["apiName"]; + newItem["pattern"] = item["pattern"]; + newItem["matchStrategy"] = item["matchStrategy"]; + // The itemSize indicates how many rows to merge, by using rowspan="{{api.itemSize}}" in tag + newItem["itemSize"] = api["predicateItems"].length; + // Mark the flag of first item to zero, indicates the start row to merge + newItem["firstFlag"] = index == 0 ? 0 : 1; + // Still hold the data of predicateItems, in order to bind data in edit dialog html + newItem["predicateItems"] = api["predicateItems"]; + $scope.apis.push(newItem); + }); + }); + + $scope.apisPageConfig.totalCount = data.data.length; + } else { + $scope.apis = []; + $scope.apisPageConfig.totalCount = 0; + } + }); + }; + $scope.getApis = getApis; + + var gatewayApiDialog; + $scope.editApi = function (api) { + $scope.currentApi = angular.copy(api); + $scope.gatewayApiDialog = { + title: '编辑自定义 API', + type: 'edit', + confirmBtnText: '保存' + }; + gatewayApiDialog = ngDialog.open({ + template: '/app/views/dialog/gateway/api-dialog.html', + width: 900, + overlay: true, + scope: $scope + }); + }; + + $scope.addNewApi = function () { + var mac = $scope.macInputModel.split(':'); + $scope.currentApi = { + grade: 0, + app: $scope.app, + ip: mac[0], + port: mac[1], + predicateItems: [{matchStrategy: 0, pattern: ''}] + }; + $scope.gatewayApiDialog = { + title: '新增自定义 API', + type: 'add', + confirmBtnText: '新增' + }; + gatewayApiDialog = ngDialog.open({ + template: '/app/views/dialog/gateway/api-dialog.html', + width: 900, + overlay: true, + scope: $scope + }); + }; + + $scope.saveApi = function () { + var apiNames = []; + if ($scope.gatewayApiDialog.type === 'add') { + apiNames = $scope.apis.map(function (item, index, array) { + return item["apiName"]; + }).filter(function (item, index, array) { + return array.indexOf(item) === index; + }); + } + + if (!GatewayApiService.checkApiValid($scope.currentApi, apiNames)) { + return; + } + + if ($scope.gatewayApiDialog.type === 'add') { + addNewApi($scope.currentApi); + } else if ($scope.gatewayApiDialog.type === 'edit') { + saveApi($scope.currentApi, true); + } + }; + + function addNewApi(api) { + GatewayApiService.newApi(api).success(function (data) { + if (data.code == 0) { + getApis(); + gatewayApiDialog.close(); + } else { + alert('新增自定义API失败!' + data.msg); + } + }); + }; + + function saveApi(api, edit) { + GatewayApiService.saveApi(api).success(function (data) { + if (data.code == 0) { + getApis(); + if (edit) { + gatewayApiDialog.close(); + } else { + confirmDialog.close(); + } + } else { + alert('修改自定义API失败!' + data.msg); + } + }); + }; + + var confirmDialog; + $scope.deleteApi = function (api) { + $scope.currentApi = api; + $scope.confirmDialog = { + title: '删除自定义API', + type: 'delete_api', + attentionTitle: '请确认是否删除如下自定义API', + attention: 'API名称: ' + api.apiName, + confirmBtnText: '删除', + }; + confirmDialog = ngDialog.open({ + template: '/app/views/dialog/confirm-dialog.html', + scope: $scope, + overlay: true + }); + }; + + $scope.confirm = function () { + if ($scope.confirmDialog.type == 'delete_api') { + deleteApi($scope.currentApi); + } else { + console.error('error'); + } + }; + + function deleteApi(api) { + GatewayApiService.deleteApi(api).success(function (data) { + if (data.code == 0) { + getApis(); + confirmDialog.close(); + } else { + alert('删除自定义API失败!' + data.msg); + } + }); + }; + + $scope.addNewMatchPattern = function() { + var total; + if ($scope.currentApi.predicateItems == null) { + $scope.currentApi.predicateItems = []; + total = 0; + } else { + total = $scope.currentApi.predicateItems.length; + } + $scope.currentApi.predicateItems.splice(total + 1, 0, {matchStrategy: 0, pattern: ''}); + }; + + $scope.removeMatchPattern = function($index) { + if ($scope.currentApi.predicateItems.length <= 1) { + // Should never happen since no remove button will display when only one predicateItem. + alert('至少有一个匹配规则'); + return; + } + $scope.currentApi.predicateItems.splice($index, 1); + }; + + queryAppMachines(); + function queryAppMachines() { + MachineService.getAppMachines($scope.app).success( + function (data) { + if (data.code == 0) { + // $scope.machines = data.data; + if (data.data) { + $scope.machines = []; + $scope.macsInputOptions = []; + data.data.forEach(function (item) { + if (item.healthy) { + $scope.macsInputOptions.push({ + text: item.ip + ':' + item.port, + value: item.ip + ':' + item.port + }); + } + }); + } + if ($scope.macsInputOptions.length > 0) { + $scope.macInputModel = $scope.macsInputOptions[0].value; + } + } else { + $scope.macsInputOptions = []; + } + } + ); + }; + $scope.$watch('macInputModel', function () { + if ($scope.macInputModel) { + getApis(); + } + }); + }] +); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/gateway/flow.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/gateway/flow.js new file mode 100644 index 0000000000..c492cf9c79 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/gateway/flow.js @@ -0,0 +1,251 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('GatewayFlowCtl', ['$scope', '$stateParams', 'GatewayFlowService', 'GatewayApiService', 'ngDialog', 'MachineService', + function ($scope, $stateParams, GatewayFlowService, GatewayApiService, ngDialog, MachineService) { + $scope.app = $stateParams.app; + + $scope.rulesPageConfig = { + pageSize: 10, + currentPageIndex: 1, + totalPage: 1, + totalCount: 0, + }; + + $scope.macsInputConfig = { + searchField: ['text', 'value'], + persist: true, + create: false, + maxItems: 1, + render: { + item: function (data, escape) { + return '
' + escape(data.text) + '
'; + } + }, + onChange: function (value, oldValue) { + $scope.macInputModel = value; + } + }; + + getMachineRules(); + function getMachineRules() { + if (!$scope.macInputModel) { + return; + } + + var mac = $scope.macInputModel.split(':'); + GatewayFlowService.queryRules($scope.app, mac[0], mac[1]).success( + function (data) { + if (data.code == 0 && data.data) { + $scope.rules = data.data; + $scope.rulesPageConfig.totalCount = $scope.rules.length; + } else { + $scope.rules = []; + $scope.rulesPageConfig.totalCount = 0; + } + }); + }; + $scope.getMachineRules = getMachineRules; + + getApiNames(); + function getApiNames() { + if (!$scope.macInputModel) { + return; + } + + var mac = $scope.macInputModel.split(':'); + GatewayApiService.queryApis($scope.app, mac[0], mac[1]).success( + function (data) { + if (data.code == 0 && data.data) { + $scope.apiNames = []; + + data.data.forEach(function (api) { + $scope.apiNames.push(api["apiName"]); + }); + } + }); + } + + $scope.intervalUnits = [{val: 0, desc: '秒'}, {val: 1, desc: '分'}, {val: 2, desc: '时'}, {val: 3, desc: '天'}]; + + var gatewayFlowRuleDialog; + $scope.editRule = function (rule) { + $scope.currentRule = angular.copy(rule); + $scope.gatewayFlowRuleDialog = { + title: '编辑网关流控规则', + type: 'edit', + confirmBtnText: '保存' + }; + gatewayFlowRuleDialog = ngDialog.open({ + template: '/app/views/dialog/gateway/flow-rule-dialog.html', + width: 780, + overlay: true, + scope: $scope + }); + }; + + $scope.addNewRule = function () { + var mac = $scope.macInputModel.split(':'); + $scope.currentRule = { + grade: 1, + app: $scope.app, + ip: mac[0], + port: mac[1], + resourceMode: 0, + interval: 1, + intervalUnit: 0, + controlBehavior: 0, + burst: 0, + maxQueueingTimeoutMs: 0 + }; + + $scope.gatewayFlowRuleDialog = { + title: '新增网关流控规则', + type: 'add', + confirmBtnText: '新增' + }; + + gatewayFlowRuleDialog = ngDialog.open({ + template: '/app/views/dialog/gateway/flow-rule-dialog.html', + width: 780, + overlay: true, + scope: $scope + }); + }; + + $scope.saveRule = function () { + if (!GatewayFlowService.checkRuleValid($scope.currentRule)) { + return; + } + if ($scope.gatewayFlowRuleDialog.type === 'add') { + addNewRule($scope.currentRule); + } else if ($scope.gatewayFlowRuleDialog.type === 'edit') { + saveRule($scope.currentRule, true); + } + }; + + $scope.useRouteID = function() { + $scope.currentRule.resource = ''; + }; + + $scope.useCustormAPI = function() { + $scope.currentRule.resource = ''; + }; + + $scope.useParamItem = function () { + $scope.currentRule.paramItem = { + parseStrategy: 0, + matchStrategy: 0 + }; + }; + + $scope.notUseParamItem = function () { + $scope.currentRule.paramItem = null; + }; + + $scope.useParamItemVal = function() { + $scope.currentRule.paramItem.pattern = ""; + $scope.currentRule.paramItem.matchStrategy = 0; + }; + + $scope.notUseParamItemVal = function() { + $scope.currentRule.paramItem.pattern = null; + $scope.currentRule.paramItem.matchStrategy = null; + }; + + function addNewRule(rule) { + GatewayFlowService.newRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + gatewayFlowRuleDialog.close(); + } else { + alert('新增网关流控规则失败!' + data.msg); + } + }); + }; + + function saveRule(rule, edit) { + GatewayFlowService.saveRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + if (edit) { + gatewayFlowRuleDialog.close(); + } else { + confirmDialog.close(); + } + } else { + alert('修改网关流控规则失败!' + data.msg); + } + }); + }; + + var confirmDialog; + $scope.deleteRule = function (rule) { + $scope.currentRule = rule; + $scope.confirmDialog = { + title: '删除网关流控规则', + type: 'delete_rule', + attentionTitle: '请确认是否删除如下规则', + attention: 'API名称: ' + rule.resource + ', ' + (rule.grade == 1 ? 'QPS阈值' : '线程数') + ': ' + rule.count, + confirmBtnText: '删除', + }; + confirmDialog = ngDialog.open({ + template: '/app/views/dialog/confirm-dialog.html', + scope: $scope, + overlay: true + }); + }; + + $scope.confirm = function () { + if ($scope.confirmDialog.type == 'delete_rule') { + deleteRule($scope.currentRule); + } else { + console.error('error'); + } + }; + + function deleteRule(rule) { + GatewayFlowService.deleteRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + confirmDialog.close(); + } else { + alert('删除网关流控规则失败!' + data.msg); + } + }); + }; + + queryAppMachines(); + + function queryAppMachines() { + MachineService.getAppMachines($scope.app).success( + function (data) { + if (data.code == 0) { + if (data.data) { + $scope.machines = []; + $scope.macsInputOptions = []; + data.data.forEach(function (item) { + if (item.healthy) { + $scope.macsInputOptions.push({ + text: item.ip + ':' + item.port, + value: item.ip + ':' + item.port + }); + } + }); + } + if ($scope.macsInputOptions.length > 0) { + $scope.macInputModel = $scope.macsInputOptions[0].value; + } + } else { + $scope.macsInputOptions = []; + } + } + ); + }; + $scope.$watch('macInputModel', function () { + if ($scope.macInputModel) { + getMachineRules(); + getApiNames(); + } + }); + }] +); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/gateway/identity.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/gateway/identity.js new file mode 100644 index 0000000000..52871b4a5f --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/gateway/identity.js @@ -0,0 +1,299 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('GatewayIdentityCtl', ['$scope', '$stateParams', 'IdentityService', + 'ngDialog', 'GatewayFlowService', 'GatewayApiService', 'DegradeService', 'MachineService', + '$interval', '$location', '$timeout', + function ($scope, $stateParams, IdentityService, ngDialog, + GatewayFlowService, GatewayApiService, DegradeService, MachineService, $interval, $location, $timeout) { + + $scope.app = $stateParams.app; + + $scope.currentPage = 1; + $scope.pageSize = 16; + $scope.totalPage = 1; + $scope.totalCount = 0; + $scope.identities = []; + + $scope.searchKey = ''; + + $scope.macsInputConfig = { + searchField: ['text', 'value'], + persist: true, + create: false, + maxItems: 1, + render: { + item: function (data, escape) { + return '
' + escape(data.text) + '
'; + } + }, + onChange: function (value, oldValue) { + $scope.macInputModel = value; + } + }; + $scope.table = null; + + getApiNames(); + function getApiNames() { + if (!$scope.macInputModel) { + return; + } + + var mac = $scope.macInputModel.split(':'); + GatewayApiService.queryApis($scope.app, mac[0], mac[1]).success( + function (data) { + if (data.code == 0 && data.data) { + $scope.apiNames = []; + + data.data.forEach(function (api) { + $scope.apiNames.push(api["apiName"]); + }); + } + }); + } + + var gatewayFlowRuleDialog; + var gatewayFlowRuleDialogScope; + $scope.addNewGatewayFlowRule = function (resource) { + if (!$scope.macInputModel) { + return; + } + var mac = $scope.macInputModel.split(':'); + gatewayFlowRuleDialogScope = $scope.$new(true); + + gatewayFlowRuleDialogScope.apiNames = $scope.apiNames; + + gatewayFlowRuleDialogScope.intervalUnits = [{val: 0, desc: '秒'}, {val: 1, desc: '分'}, {val: 2, desc: '时'}, {val: 3, desc: '天'}]; + + gatewayFlowRuleDialogScope.currentRule = { + grade: 1, + app: $scope.app, + ip: mac[0], + port: mac[1], + resourceMode: gatewayFlowRuleDialogScope.apiNames.indexOf(resource) == -1 ? 0 : 1, + resource: resource, + interval: 1, + intervalUnit: 0, + controlBehavior: 0, + burst: 0, + maxQueueingTimeoutMs: 0 + }; + + gatewayFlowRuleDialogScope.gatewayFlowRuleDialog = { + title: '新增网关流控规则', + type: 'add', + confirmBtnText: '新增', + saveAndContinueBtnText: '新增并继续添加', + showAdvanceButton: true + }; + + gatewayFlowRuleDialogScope.useRouteID = function() { + gatewayFlowRuleDialogScope.currentRule.resource = ''; + }; + + gatewayFlowRuleDialogScope.useCustormAPI = function() { + gatewayFlowRuleDialogScope.currentRule.resource = ''; + }; + + gatewayFlowRuleDialogScope.useParamItem = function () { + gatewayFlowRuleDialogScope.currentRule.paramItem = { + parseStrategy: 0, + matchStrategy: 0 + }; + }; + + gatewayFlowRuleDialogScope.notUseParamItem = function () { + gatewayFlowRuleDialogScope.currentRule.paramItem = null; + }; + + gatewayFlowRuleDialogScope.useParamItemVal = function() { + gatewayFlowRuleDialogScope.currentRule.paramItem.pattern = ""; + }; + + gatewayFlowRuleDialogScope.notUseParamItemVal = function() { + gatewayFlowRuleDialogScope.currentRule.paramItem.pattern = null; + }; + + gatewayFlowRuleDialogScope.saveRule = saveGatewayFlowRule; + gatewayFlowRuleDialogScope.saveRuleAndContinue = saveGatewayFlowRuleAndContinue; + gatewayFlowRuleDialogScope.onOpenAdvanceClick = function () { + gatewayFlowRuleDialogScope.gatewayFlowRuleDialog.showAdvanceButton = false; + }; + gatewayFlowRuleDialogScope.onCloseAdvanceClick = function () { + gatewayFlowRuleDialogScope.gatewayFlowRuleDialog.showAdvanceButton = true; + }; + + gatewayFlowRuleDialog = ngDialog.open({ + template: '/app/views/dialog/gateway/flow-rule-dialog.html', + width: 780, + overlay: true, + scope: gatewayFlowRuleDialogScope + }); + }; + + function saveGatewayFlowRule() { + if (!GatewayFlowService.checkRuleValid(gatewayFlowRuleDialogScope.currentRule)) { + return; + } + GatewayFlowService.newRule(gatewayFlowRuleDialogScope.currentRule).success(function (data) { + if (data.code === 0) { + gatewayFlowRuleDialog.close(); + let url = '/dashboard/gateway/flow/' + $scope.app; + $location.path(url); + } else { + alert('失败!'); + } + }).error((data, header, config, status) => { + alert('未知错误'); + }); + } + + function saveGatewayFlowRuleAndContinue() { + if (!GatewayFlowService.checkRuleValid(gatewayFlowRuleDialogScope.currentRule)) { + return; + } + GatewayFlowService.newRule(gatewayFlowRuleDialogScope.currentRule).success(function (data) { + if (data.code == 0) { + gatewayFlowRuleDialog.close(); + } else { + alert('失败!'); + } + }); + } + + var degradeRuleDialog; + $scope.addNewDegradeRule = function (resource) { + if (!$scope.macInputModel) { + return; + } + var mac = $scope.macInputModel.split(':'); + degradeRuleDialogScope = $scope.$new(true); + degradeRuleDialogScope.currentRule = { + enable: false, + grade: 0, + strategy: 0, + resource: resource, + limitApp: 'default', + app: $scope.app, + ip: mac[0], + port: mac[1] + }; + + degradeRuleDialogScope.degradeRuleDialog = { + title: '新增降级规则', + type: 'add', + confirmBtnText: '新增', + saveAndContinueBtnText: '新增并继续添加' + }; + degradeRuleDialogScope.saveRule = saveDegradeRule; + degradeRuleDialogScope.saveRuleAndContinue = saveDegradeRuleAndContinue; + + degradeRuleDialog = ngDialog.open({ + template: '/app/views/dialog/degrade-rule-dialog.html', + width: 680, + overlay: true, + scope: degradeRuleDialogScope + }); + }; + + function saveDegradeRule() { + if (!DegradeService.checkRuleValid(degradeRuleDialogScope.currentRule)) { + return; + } + DegradeService.newRule(degradeRuleDialogScope.currentRule).success(function (data) { + if (data.code == 0) { + degradeRuleDialog.close(); + var url = '/dashboard/degrade/' + $scope.app; + $location.path(url); + } else { + alert('失败!'); + } + }); + } + + function saveDegradeRuleAndContinue() { + if (!DegradeService.checkRuleValid(degradeRuleDialogScope.currentRule)) { + return; + } + DegradeService.newRule(degradeRuleDialogScope.currentRule).success(function (data) { + if (data.code == 0) { + degradeRuleDialog.close(); + } else { + alert('失败!'); + } + }); + } + + var searchHandler; + $scope.searchChange = function (searchKey) { + $timeout.cancel(searchHandler); + searchHandler = $timeout(function () { + $scope.searchKey = searchKey; + reInitIdentityDatas(); + }, 600); + }; + + function queryAppMachines() { + MachineService.getAppMachines($scope.app).success( + function (data) { + if (data.code === 0) { + if (data.data) { + $scope.machines = []; + $scope.macsInputOptions = []; + data.data.forEach(function (item) { + if (item.healthy) { + $scope.macsInputOptions.push({ + text: item.ip + ':' + item.port, + value: item.ip + ':' + item.port + }); + } + }); + } + if ($scope.macsInputOptions.length > 0) { + $scope.macInputModel = $scope.macsInputOptions[0].value; + } + } else { + $scope.macsInputOptions = []; + } + } + ); + } + + // Fetch all machines by current app name. + queryAppMachines(); + + $scope.$watch('macInputModel', function () { + if ($scope.macInputModel) { + reInitIdentityDatas(); + } + }); + + $scope.$on('$destroy', function () { + $interval.cancel(intervalId); + }); + + var intervalId; + function reInitIdentityDatas() { + getApiNames(); + queryIdentities(); + }; + + function queryIdentities() { + var mac = $scope.macInputModel.split(':'); + if (mac == null || mac.length < 2) { + return; + } + + IdentityService.fetchClusterNodeOfMachine(mac[0], mac[1], $scope.searchKey).success( + function (data) { + if (data.code == 0 && data.data) { + $scope.identities = data.data; + $scope.totalCount = $scope.identities.length; + } else { + $scope.identities = []; + $scope.totalCount = 0; + } + } + ); + }; + $scope.queryIdentities = queryIdentities; + }]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/identity.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/identity.js index 33a4ec0762..f8116be78e 100755 --- a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/identity.js +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/identity.js @@ -98,7 +98,7 @@ app.controller('IdentityCtl', ['$scope', '$stateParams', 'IdentityService', let url = '/dashboard/flow/' + $scope.app; $location.path(url); } else { - alert('失败!'); + alert('失败:' + data.msg); } }).error((data, header, config, status) => { alert('未知错误'); @@ -110,10 +110,10 @@ app.controller('IdentityCtl', ['$scope', '$stateParams', 'IdentityService', return; } FlowService.newRule(flowRuleDialogScope.currentRule).success(function (data) { - if (data.code == 0) { + if (data.code === 0) { flowRuleDialog.close(); } else { - alert('失败!'); + alert('失败:' + data.msg); } }); } @@ -159,12 +159,12 @@ app.controller('IdentityCtl', ['$scope', '$stateParams', 'IdentityService', return; } DegradeService.newRule(degradeRuleDialogScope.currentRule).success(function (data) { - if (data.code == 0) { + if (data.code === 0) { degradeRuleDialog.close(); var url = '/dashboard/degrade/' + $scope.app; $location.path(url); } else { - alert('失败!'); + alert('失败:' + data.msg); } }); } @@ -174,10 +174,10 @@ app.controller('IdentityCtl', ['$scope', '$stateParams', 'IdentityService', return; } DegradeService.newRule(degradeRuleDialogScope.currentRule).success(function (data) { - if (data.code == 0) { + if (data.code === 0) { degradeRuleDialog.close(); } else { - alert('失败!'); + alert('失败:' + data.msg); } }); } @@ -322,6 +322,15 @@ app.controller('IdentityCtl', ['$scope', '$stateParams', 'IdentityService', paramFlowItemList: [], count: 0, limitApp: 'default', + controlBehavior: 0, + durationInSec: 1, + burstCount: 0, + maxQueueingTimeMs: 0, + clusterMode: false, + clusterConfig: { + thresholdType: 0, + fallbackToLocalWhenFail: true, + } } }; diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/login.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/login.js index 70b6ffd3c8..3d49d3c16f 100644 --- a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/login.js +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/login.js @@ -1,8 +1,8 @@ var app = angular.module('sentinelDashboardApp'); app.controller('LoginCtl', ['$scope', '$state', '$window', 'AuthService', - function ($scope, $state, $window, LoginService) { - // If auth, jump to the index page directly + function ($scope, $state, $window, AuthService) { + // If auth passed, jump to the index page directly if ($window.localStorage.getItem('session_sentinel_admin')) { $state.go('dashboard'); } @@ -20,12 +20,9 @@ app.controller('LoginCtl', ['$scope', '$state', '$window', 'AuthService', var param = {"username": $scope.username, "password": $scope.password}; - LoginService.login(param).success(function (data) { + AuthService.login(param).success(function (data) { if (data.code == 0) { - $window.localStorage.setItem('session_sentinel_admin', { - username: data.data - }); - + $window.localStorage.setItem('session_sentinel_admin', JSON.stringify(data.data)); $state.go('dashboard'); } else { alert(data.msg); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/param_flow.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/param_flow.js index 03a2054f3f..65d868a8f9 100644 --- a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/param_flow.js +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/param_flow.js @@ -130,6 +130,9 @@ angular.module('sentinelDashboardApp').controller('ParamFlowController', ['$scop $scope.editRule = function (rule) { $scope.currentRule = angular.copy(rule); + if ($scope.currentRule.rule && $scope.currentRule.rule.durationInSec === undefined) { + $scope.currentRule.rule.durationInSec = 1; + } $scope.paramFlowRuleDialog = { title: '编辑热点规则', type: 'edit', @@ -157,9 +160,14 @@ angular.module('sentinelDashboardApp').controller('ParamFlowController', ['$scop paramFlowItemList: [], count: 0, limitApp: 'default', + controlBehavior: 0, + durationInSec: 1, + burstCount: 0, + maxQueueingTimeMs: 0, clusterMode: false, clusterConfig: { - thresholdType: 0 + thresholdType: 0, + fallbackToLocalWhenFail: true, } } }; @@ -167,6 +175,7 @@ angular.module('sentinelDashboardApp').controller('ParamFlowController', ['$scop title: '新增热点规则', type: 'add', confirmBtnText: '新增', + supportAdvanced: true, showAdvanceButton: true, }; paramFlowRuleDialog = ngDialog.open({ diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/system.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/system.js index ff754727e8..5b3107ffb3 100755 --- a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/system.js +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/system.js @@ -31,14 +31,13 @@ app.controller('SystemCtl', ['$scope', '$stateParams', 'SystemService', 'ngDialo if (!$scope.macInputModel) { return; } - var mac = $scope.macInputModel.split(':'); + let mac = $scope.macInputModel.split(':'); SystemService.queryMachineRules($scope.app, mac[0], mac[1]).success( function (data) { - if (data.code == 0 && data.data) { + if (data.code === 0 && data.data) { $scope.rules = data.data; $.each($scope.rules, function (idx, rule) { - // rule.orginEnable = rule.enable; - if (rule.avgLoad >= 0) { + if (rule.highestSystemLoad >= 0) { rule.grade = 0; } else if (rule.avgRt >= 0) { rule.grade = 1; @@ -46,6 +45,8 @@ app.controller('SystemCtl', ['$scope', '$stateParams', 'SystemService', 'ngDialo rule.grade = 2; } else if (rule.qps >= 0) { rule.grade = 3; + } else if (rule.highestCpuUsage >= 0) { + rule.grade = 4; } }); $scope.rulesPageConfig.totalCount = $scope.rules.length; @@ -54,7 +55,8 @@ app.controller('SystemCtl', ['$scope', '$stateParams', 'SystemService', 'ngDialo $scope.rulesPageConfig.totalCount = 0; } }); - }; + } + $scope.getMachineRules = getMachineRules; var systemRuleDialog; $scope.editRule = function (rule) { @@ -94,22 +96,21 @@ app.controller('SystemCtl', ['$scope', '$stateParams', 'SystemService', 'ngDialo }; $scope.saveRule = function () { - if ($scope.systemRuleDialog.type == 'add') { + if ($scope.systemRuleDialog.type === 'add') { addNewRule($scope.currentRule); - } else if ($scope.systemRuleDialog.type == 'edit') { + } else if ($scope.systemRuleDialog.type === 'edit') { saveRule($scope.currentRule, true); } - } - + }; var confirmDialog; $scope.deleteRule = function (rule) { $scope.currentRule = rule; var ruleTypeDesc = ''; var ruleTypeCount = null; - if (rule.avgLoad != -1) { + if (rule.highestSystemLoad != -1) { ruleTypeDesc = 'LOAD'; - ruleTypeCount = rule.avgLoad; + ruleTypeCount = rule.highestSystemLoad; } else if (rule.avgRt != -1) { ruleTypeDesc = 'RT'; ruleTypeCount = rule.avgRt; @@ -119,6 +120,9 @@ app.controller('SystemCtl', ['$scope', '$stateParams', 'SystemService', 'ngDialo } else if (rule.qps != -1) { ruleTypeDesc = 'QPS'; ruleTypeCount = rule.qps; + }else if (rule.highestCpuUsage != -1) { + ruleTypeDesc = 'CPU 使用率'; + ruleTypeCount = rule.highestCpuUsage; } $scope.confirmDialog = { @@ -137,7 +141,7 @@ app.controller('SystemCtl', ['$scope', '$stateParams', 'SystemService', 'ngDialo $scope.confirm = function () { - if ($scope.confirmDialog.type == 'delete_rule') { + if ($scope.confirmDialog.type === 'delete_rule') { deleteRule($scope.currentRule); // } else if ($scope.confirmDialog.type == 'enable_rule') { // $scope.currentRule.enable = true; @@ -156,42 +160,47 @@ app.controller('SystemCtl', ['$scope', '$stateParams', 'SystemService', 'ngDialo function deleteRule(rule) { SystemService.deleteRule(rule).success(function (data) { - if (data.code == 0) { + if (data.code === 0) { getMachineRules(); confirmDialog.close(); + } else if (data.msg != null) { + alert('失败:' + data.msg); } else { - alert + alert('失败:未知错误'); } }); - }; + } function addNewRule(rule) { + if (rule.grade == 4 && (rule.highestCpuUsage < 0 || rule.highestCpuUsage > 1)) { + alert('CPU 使用率模式的取值范围应为 [0.0, 1.0],对应 0% - 100%'); + return; + } SystemService.newRule(rule).success(function (data) { - if (data.code == 0) { + if (data.code === 0) { getMachineRules(); systemRuleDialog.close(); + } else if (data.msg != null) { + alert('失败:' + data.msg); } else { - alert('失败!'); + alert('失败:未知错误'); } }); - }; + } function saveRule(rule, edit) { SystemService.saveRule(rule).success(function (data) { - if (data.code == 0) { - // if (rule.enable) { - // rule.orginEnable = true; - // } else { - // rule.orginEnable = false; - // } + if (data.code === 0) { getMachineRules(); if (edit) { systemRuleDialog.close(); } else { confirmDialog.close(); } + } else if (data.msg != null) { + alert('失败:' + data.msg); } else { - alert('失败!'); + alert('失败:未知错误'); } }); } @@ -199,7 +208,7 @@ app.controller('SystemCtl', ['$scope', '$stateParams', 'SystemService', 'ngDialo function queryAppMachines() { MachineService.getAppMachines($scope.app).success( function (data) { - if (data.code == 0) { + if (data.code === 0) { // $scope.machines = data.data; if (data.data) { $scope.machines = []; diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/header/header.html b/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/header/header.html index 744e731805..4584e89bab 100755 --- a/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/header/header.html +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/header/header.html @@ -1,10 +1,10 @@
- +
-
@@ -72,9 +72,9 @@
-  如果 Token Server 不可用是否退化到单机限流
diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/gateway/api-dialog.html b/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/gateway/api-dialog.html new file mode 100644 index 0000000000..8c8d4615e3 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/gateway/api-dialog.html @@ -0,0 +1,49 @@ +
+ {{gatewayApiDialog.title}} +
+
+
+
+ +
+ + +
+
+ +
+ +
+
+  精确   +  前缀   +  正则   +
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+
+
+ + + +
+
+
+
diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/gateway/flow-rule-dialog.html b/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/gateway/flow-rule-dialog.html new file mode 100644 index 0000000000..7dca16d719 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/gateway/flow-rule-dialog.html @@ -0,0 +1,172 @@ +
+ {{gatewayFlowRuleDialog.title}} +
+
+
+
+
+ +
+
+  Route ID   +  Route ID   +  API 分组   +  API 分组   +
+
+
+ +
+ +
+ + + + + +
+
+ +
+ +
+ +
+
+ +
+ +
+
+  Client IP   +  Remote Host   +  Header   +  URL 参数   +  Cookie   +
+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+  精确   +  子串   +  正则   +
+
+ +
+ +
+
+ +
+ +
+
+  QPS   +  线程数   +
+
+
+ +
+
+ +
+ +
+
+
+ +
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+
+  快速失败   +  匀速排队   +
+
+
+ +
+
+ +
+ +
+
+
+ +
+
+ +
+ +
+
+
+
+
+
+
+ + + +
+
+
+
diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/param-flow-rule-dialog.html b/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/param-flow-rule-dialog.html index eae5d08ad3..02f00b0891 100644 --- a/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/param-flow-rule-dialog.html +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/param-flow-rule-dialog.html @@ -27,9 +27,16 @@
-
+
+ +
+ + +
@@ -61,6 +68,16 @@
+
+ +
+
+ +  若选择,则 Token Server 不可用时将退化到单机限流 +
+
+
diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/system-rule-dialog.html b/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/system-rule-dialog.html index 728909d541..3dd9cd974a 100755 --- a/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/system-rule-dialog.html +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/system-rule-dialog.html @@ -9,40 +9,41 @@
- -  LOAD   + +  LOAD    RT    线程数   -  入口 QPS +  入口 QPS   + +  CPU 使用率   +
- +  LOAD    RT    线程数   -  入口 QPS +  入口 QPS   + +  CPU 使用率   +
- - - - + + + + +
diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/gateway/api.html b/sentinel-dashboard/src/main/webapp/resources/app/views/gateway/api.html new file mode 100644 index 0000000000..b4e101c915 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/gateway/api.html @@ -0,0 +1,87 @@ +
+
+ {{app}} +
+
+ +
+
+ +
+ +
+
+
+
+
+ API 分组管理 + + +
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + +
+ API 名称 + + 匹配模式 + + 匹配串 + + 操作 +
{{api.apiName}} + 精确 + 前缀 + 正则 + {{api.pattern}} + + +
+
+ + + +
+ +
+ +
+ +
+ diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/gateway/flow.html b/sentinel-dashboard/src/main/webapp/resources/app/views/gateway/flow.html new file mode 100644 index 0000000000..62708c4143 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/gateway/flow.html @@ -0,0 +1,94 @@ +
+
+ {{app}} +
+
+ +
+
+ +
+ +
+
+
+
+
+ 网关流控规则 + + +
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + +
+ API 名称 + + API 类型 + + 阈值类型 + + 单机阈值 + + 操作 +
{{rule.resource}} + Route ID + API 分组 + + QPS + 线程数 + {{rule.count}} + + +
+
+ + + +
+ +
+ +
+ +
+ diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/gateway/identity.html b/sentinel-dashboard/src/main/webapp/resources/app/views/gateway/identity.html new file mode 100644 index 0000000000..0736adc273 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/gateway/identity.html @@ -0,0 +1,98 @@ +
+
+ {{app}} +
+
+ +
+ +
+
+
+
+
+ 请求链路 + + +
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ API 名称 + + API 类型 + 通过 QPS拒绝 QPS线程数平均 RT分钟通过分钟拒绝操作
+ {{resource.resource}} + + Route ID + 自定义 API + {{resource.passQps}}{{resource.blockQps}}{{resource.threadNum}}{{resource.averageRt}}{{resource.oneMinutePass}} + {{resource.oneMinuteBlock}} {{resource.oneMinuteBlock}} +
+ + +
+
+
+ + + +
+ +
+ +
+ +
+ \ No newline at end of file diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/system.html b/sentinel-dashboard/src/main/webapp/resources/app/views/system.html index 80fa9b29f0..6f4e4b84cc 100755 --- a/sentinel-dashboard/src/main/webapp/resources/app/views/system.html +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/system.html @@ -45,16 +45,18 @@ - LOAD - RT - 线程数 - QPS + 系统 load + 平均 RT + 并发数 + 入口 QPS + CPU 使用率 - {{rule.avgLoad}} + {{rule.highestSystemLoad}} {{rule.avgRt}} {{rule.maxThread}} {{rule.qps}} + {{rule.highestCpuUsage}} diff --git a/sentinel-dashboard/src/main/webapp/resources/dist/js/app.js b/sentinel-dashboard/src/main/webapp/resources/dist/js/app.js old mode 100755 new mode 100644 index 6016ef3e7d..32e69e24fc --- a/sentinel-dashboard/src/main/webapp/resources/dist/js/app.js +++ b/sentinel-dashboard/src/main/webapp/resources/dist/js/app.js @@ -1 +1 @@ -"use strict";var app;angular.module("sentinelDashboardApp",["oc.lazyLoad","ui.router","ui.bootstrap","angular-loading-bar","ngDialog","ui.bootstrap.datetimepicker","ui-notification","rzTable","angular-clipboard","selectize","angularUtils.directives.dirPagination"]).factory("AuthInterceptor",["$window","$state",function(r,t){return{responseError:function(e){return 401==e.status&&(r.localStorage.removeItem("session_sentinel_admin"),t.go("login")),e},response:function(e){return e},request:function(e){return e},requestError:function(e){return e}}}]).config(["$stateProvider","$urlRouterProvider","$ocLazyLoadProvider","$httpProvider",function(e,r,t,a){a.interceptors.push("AuthInterceptor"),t.config({debug:!1,events:!0}),r.otherwise("/dashboard/home"),e.state("login",{url:"/login",templateUrl:"app/views/login.html",controller:"LoginCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/login.js"]})}]}}).state("dashboard",{url:"/dashboard",templateUrl:"app/views/dashboard/main.html",resolve:{loadMyDirectives:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/directives/header/header.js","app/scripts/directives/sidebar/sidebar.js","app/scripts/directives/sidebar/sidebar-search/sidebar-search.js"]})}]}}).state("dashboard.home",{url:"/home",templateUrl:"app/views/dashboard/home.html",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/main.js"]})}]}}).state("dashboard.flowV1",{templateUrl:"app/views/flow_v1.html",url:"/flow/:app",controller:"FlowControllerV1",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/flow_v1.js"]})}]}}).state("dashboard.flow",{templateUrl:"app/views/flow_v2.html",url:"/v2/flow/:app",controller:"FlowControllerV2",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/flow_v2.js"]})}]}}).state("dashboard.paramFlow",{templateUrl:"app/views/param_flow.html",url:"/paramFlow/:app",controller:"ParamFlowController",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/param_flow.js"]})}]}}).state("dashboard.clusterAppAssignManage",{templateUrl:"app/views/cluster_app_assign_manage.html",url:"/cluster/assign_manage/:app",controller:"SentinelClusterAppAssignManageController",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/cluster_app_assign_manage.js"]})}]}}).state("dashboard.clusterAppServerList",{templateUrl:"app/views/cluster_app_server_list.html",url:"/cluster/server/:app",controller:"SentinelClusterAppServerListController",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/cluster_app_server_list.js"]})}]}}).state("dashboard.clusterAppClientList",{templateUrl:"app/views/cluster_app_client_list.html",url:"/cluster/client/:app",controller:"SentinelClusterAppTokenClientListController",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/cluster_app_token_client_list.js"]})}]}}).state("dashboard.clusterSingle",{templateUrl:"app/views/cluster_single_config.html",url:"/cluster/single/:app",controller:"SentinelClusterSingleController",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/cluster_single.js"]})}]}}).state("dashboard.authority",{templateUrl:"app/views/authority.html",url:"/authority/:app",controller:"AuthorityRuleController",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/authority.js"]})}]}}).state("dashboard.degrade",{templateUrl:"app/views/degrade.html",url:"/degrade/:app",controller:"DegradeCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/degrade.js"]})}]}}).state("dashboard.system",{templateUrl:"app/views/system.html",url:"/system/:app",controller:"SystemCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/system.js"]})}]}}).state("dashboard.machine",{templateUrl:"app/views/machine.html",url:"/app/:app",controller:"MachineCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/machine.js"]})}]}}).state("dashboard.identity",{templateUrl:"app/views/identity.html",url:"/identity/:app",controller:"IdentityCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/identity.js"]})}]}}).state("dashboard.metric",{templateUrl:"app/views/metric.html",url:"/metric/:app",controller:"MetricCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/metric.js"]})}]}})}]),(app=angular.module("sentinelDashboardApp")).filter("range",[function(){return function(e,r){if(isNaN(r)||r<=0)return[];e=[];for(var t=1;t<=r;t++)e.push(t);return e}}]),(app=angular.module("sentinelDashboardApp")).service("AuthService",["$http",function(r){this.login=function(e){return r({url:"/auth/login",params:e,method:"POST"})},this.logout=function(){return r({url:"/auth/logout",method:"POST"})}}]),(app=angular.module("sentinelDashboardApp")).service("AppService",["$http",function(e){this.getApps=function(){return e({url:"app/briefinfos.json",method:"GET"})}}]),(app=angular.module("sentinelDashboardApp")).service("FlowServiceV1",["$http",function(a){function r(e){return void 0===e||""===e||isNaN(e)||e<=0}this.queryMachineRules=function(e,r,t){return a({url:"/v1/flow/rules",params:{app:e,ip:r,port:t},method:"GET"})},this.newRule=function(e){e.resource,e.limitApp,e.grade,e.count,e.strategy,e.refResource,e.controlBehavior,e.warmUpPeriodSec,e.maxQueueingTimeMs,e.app,e.ip,e.port;return a({url:"/v1/flow/rule",data:e,method:"POST"})},this.saveRule=function(e){var r={id:e.id,resource:e.resource,limitApp:e.limitApp,grade:e.grade,count:e.count,strategy:e.strategy,refResource:e.refResource,controlBehavior:e.controlBehavior,warmUpPeriodSec:e.warmUpPeriodSec,maxQueueingTimeMs:e.maxQueueingTimeMs};return a({url:"/v1/flow/save.json",params:r,method:"PUT"})},this.deleteRule=function(e){var r={id:e.id,app:e.app};return a({url:"/v1/flow/delete.json",params:r,method:"DELETE"})},this.checkRuleValid=function(e){return void 0===e.resource||""===e.resource?(alert("资源名称不能为空"),!1):void 0===e.count||e.count<0?(alert("限流阈值必须大于等于 0"),!1):void 0===e.strategy||e.strategy<0?(alert("无效的流控模式"),!1):1!=e.strategy&&2!=e.strategy||void 0!==e.refResource&&""!=e.refResource?void 0===e.controlBehavior||e.controlBehavior<0?(alert("无效的流控整形方式"),!1):1==e.controlBehavior&&r(e.warmUpPeriodSec)?(alert("预热时长必须大于 0"),!1):2==e.controlBehavior&&r(e.maxQueueingTimeMs)?(alert("排队超时时间必须大于 0"),!1):!e.clusterMode||void 0!==e.clusterConfig&&void 0!==e.clusterConfig.thresholdType||(alert("集群限流配置不正确"),!1):(alert("请填写关联资源或入口"),!1)}}]),(app=angular.module("sentinelDashboardApp")).service("FlowServiceV2",["$http",function(a){function r(e){return void 0===e||""===e||isNaN(e)||e<=0}this.queryMachineRules=function(e,r,t){return a({url:"/v2/flow/rules",params:{app:e,ip:r,port:t},method:"GET"})},this.newRule=function(e){return a({url:"/v2/flow/rule",data:e,method:"POST"})},this.saveRule=function(e){return a({url:"/v2/flow/rule/"+e.id,data:e,method:"PUT"})},this.deleteRule=function(e){return a({url:"/v2/flow/rule/"+e.id,method:"DELETE"})},this.checkRuleValid=function(e){return void 0===e.resource||""===e.resource?(alert("资源名称不能为空"),!1):void 0===e.count||e.count<0?(alert("限流阈值必须大于等于 0"),!1):void 0===e.strategy||e.strategy<0?(alert("无效的流控模式"),!1):1!=e.strategy&&2!=e.strategy||void 0!==e.refResource&&""!=e.refResource?void 0===e.controlBehavior||e.controlBehavior<0?(alert("无效的流控整形方式"),!1):1==e.controlBehavior&&r(e.warmUpPeriodSec)?(alert("预热时长必须大于 0"),!1):2==e.controlBehavior&&r(e.maxQueueingTimeMs)?(alert("排队超时时间必须大于 0"),!1):!e.clusterMode||void 0!==e.clusterConfig&&void 0!==e.clusterConfig.thresholdType||(alert("集群限流配置不正确"),!1):(alert("请填写关联资源或入口"),!1)}}]),(app=angular.module("sentinelDashboardApp")).service("DegradeService",["$http",function(a){this.queryMachineRules=function(e,r,t){return a({url:"degrade/rules.json",params:{app:e,ip:r,port:t},method:"GET"})},this.newRule=function(e){var r={id:e.id,resource:e.resource,limitApp:e.limitApp,count:e.count,timeWindow:e.timeWindow,grade:e.grade,app:e.app,ip:e.ip,port:e.port};return a({url:"/degrade/new.json",params:r,method:"GET"})},this.saveRule=function(e){var r={id:e.id,resource:e.resource,limitApp:e.limitApp,grade:e.grade,count:e.count,timeWindow:e.timeWindow};return a({url:"/degrade/save.json",params:r,method:"GET"})},this.deleteRule=function(e){var r={id:e.id,app:e.app};return a({url:"/degrade/delete.json",params:r,method:"GET"})},this.checkRuleValid=function(e){return void 0===e.resource||""===e.resource?(alert("资源名称不能为空"),!1):void 0===e.grade||e.grade<0?(alert("未知的降级策略"),!1):void 0===e.count||""===e.count||e.count<0?(alert("降级阈值不能为空或小于 0"),!1):void 0===e.timeWindow||""===e.timeWindow||e.timeWindow<=0?(alert("降级时间窗口必须大于 0"),!1):!(1==e.grade&&1=r?n.apply(null,t):function(){return e(t.concat([].slice.apply(arguments)))}}(e)}function n(){var n=arguments,r=n.length-1;return function(){for(var e=r,t=n[r].apply(this,arguments);e--;)t=n[e].call(this,t);return t}}function u(){for(var e=[],t=0;tthis._limit&&this.evict(),e},e.prototype.evict=function(){var t=this._items.shift();return this._evictListeners.forEach(function(e){return e(t)}),t},e.prototype.dequeue=function(){if(this.size())return this._items.splice(0,1)[0]},e.prototype.clear=function(){var e=this._items;return this._items=[],e},e.prototype.size=function(){return this._items.length},e.prototype.remove=function(e){var t=this._items.indexOf(e);return-1 "+qe(e))},e.prototype.traceTransitionIgnored=function(e){this.enabled(g.Category.TRANSITION)&&console.log(st(e)+": Ignored <> "+qe(e))},e.prototype.traceHookInvocation=function(e,t,n){if(this.enabled(g.Category.HOOK)){var r=C("traceData.hookType")(n)||"internal",i=C("traceData.context.state.name")(n)||C("traceData.context")(n)||"unknown",o=He(e.registeredHook.callback);console.log(st(t)+": Hook -> "+r+" context: "+i+", "+Le(200,o))}},e.prototype.traceHookResult=function(e,t,n){this.enabled(g.Category.HOOK)&&console.log(st(t)+": <- Hook returned: "+Le(200,qe(e)))},e.prototype.traceResolvePath=function(e,t,n){this.enabled(g.Category.RESOLVE)&&console.log(st(n)+": Resolving "+e+" ("+t+")")},e.prototype.traceResolvableResolved=function(e,t){this.enabled(g.Category.RESOLVE)&&console.log(st(t)+": <- Resolved "+e+" to: "+Le(200,qe(e.data)))},e.prototype.traceError=function(e,t){this.enabled(g.Category.TRANSITION)&&console.log(st(t)+": <- Rejected "+qe(t)+", reason: "+e)},e.prototype.traceSuccess=function(e,t){this.enabled(g.Category.TRANSITION)&&console.log(st(t)+": <- Success "+qe(t)+", final state: "+e.name)},e.prototype.traceUIViewEvent=function(e,t,n){void 0===n&&(n=""),this.enabled(g.Category.UIVIEW)&&console.log("ui-view: "+Fe(30,e)+" "+et(t)+n)},e.prototype.traceUIViewConfigUpdated=function(e,t){this.enabled(g.Category.UIVIEW)&&this.traceUIViewEvent("Updating",e," with ViewConfig from context='"+t+"'")},e.prototype.traceUIViewFill=function(e,t){this.enabled(g.Category.UIVIEW)&&this.traceUIViewEvent("Fill",e," with: "+Le(200,t))},e.prototype.traceViewSync=function(e){if(this.enabled(g.Category.VIEWCONFIG)){var a="uiview component fqn",t=e.map(function(e){var t,n=e.uiView,r=e.viewConfig,i=n&&n.fqn,o=r&&r.viewDecl.$context.name+": ("+r.viewDecl.$name+")";return(t={})[a]=i,t["view config state (view name)"]=o,t}).sort(function(e,t){return(e[a]||"").localeCompare(t[a]||"")});it(t)}},e.prototype.traceViewServiceEvent=function(e,t){this.enabled(g.Category.VIEWCONFIG)&&console.log("VIEWCONFIG: "+e+" "+function(e){var t=e.viewDecl,n=t.$context.name||"(root)";return"[View#"+e.$id+" from '"+n+"' state]: target ui-view: '"+t.$uiViewName+"@"+t.$uiViewContextAnchor+"'"}(t))},e.prototype.traceViewServiceUIViewEvent=function(e,t){this.enabled(g.Category.VIEWCONFIG)&&console.log("VIEWCONFIG: "+e+" "+et(t))},e}(),lt=new ut,ct=function(){function e(e){this.pattern=/.*/,this.inherit=!0,Y(this,e)}return e.prototype.is=function(e,t){return!0},e.prototype.encode=function(e,t){return e},e.prototype.decode=function(e,t){return e},e.prototype.equals=function(e,t){return e==t},e.prototype.$subPattern=function(){var e=this.pattern.toString();return e.substr(1,e.length-2)},e.prototype.toString=function(){return"{ParamType:"+this.name+"}"},e.prototype.$normalize=function(e){return this.is(e)?e:this.decode(e)},e.prototype.$asArray=function(e,t){if(!e)return this;if("auto"===e&&!t)throw new Error("'auto' array mode is for query parameters only");return new dt(this,e)},e}();function dt(r,i){var o=this;function a(e){return A(e)?e:k(e)?[e]:[]}function s(n,r){return function(e){if(A(e)&&0===e.length)return e;var t=ce(a(e),n);return!0===r?0===se(t,function(e){return!e}).length:function(e){switch(e.length){case 0:return;case 1:return"auto"===i?e[0]:e;default:return e}}(t)}}function u(o){return function(e,t){var n=a(e),r=a(t);if(n.length!==r.length)return!1;for(var i=0;i=n.invokeLimit&&n.deregister()}}},o.prototype.handleHookResult=function(e){var t=this,n=this.getNotCurrentRejection();return n||(R(e)?e.then(function(e){return t.handleHookResult(e)}):(lt.traceHookResult(e,this.transition,this.options),!1===e?Ve.aborted("Hook aborted transition").toPromise():c($t)(e)?Ve.redirected(e).toPromise():void 0))},o.prototype.getNotCurrentRejection=function(){var e=this.transition.router;return e._disposed?Ve.aborted("UIRouter instance #"+e.$id+" has been stopped (disposed)").toPromise():this.transition._aborted?Ve.aborted().toPromise():this.isSuperseded()?Ve.superseded(this.options.current()).toPromise():void 0},o.prototype.toString=function(){var e=this.options,t=this.registeredHook;return(C("traceData.hookType")(e)||"internal")+" context: "+(C("traceData.context.state.name")(e)||C("traceData.context")(e)||"unknown")+", "+Le(200,Ne(t.callback))},o.HANDLE_RESULT=function(t){return function(e){return t.handleHookResult(e)}},o.LOG_REJECTED_RESULT=function(t){return function(e){R(e)&&e.catch(function(e){return t.logError(Ve.normalize(e))})}},o.LOG_ERROR=function(t){return function(e){return t.logError(e)}},o.REJECT_ERROR=function(e){return function(e){return Pe(e)}},o.THROW_ERROR=function(e){return function(e){throw e}},o}();function Gt(e,t,n){var i=O(t)?[t]:t;return!!(D(i)?i:function(e){for(var t=i,n=0;n "+(this.valid()?"":"(X) ")+"'"+(T(t)?t.name:t)+"'"+qe(n(this.params()))+" )"},t.diToken=t}();function en(e,t){var n=["",""],r=e.replace(/[\\\[\]\^$*+?.()|{}]/g,"\\$&");if(!t)return r;switch(t.squash){case!1:n=["(",")"+(t.isOptional?"?":"")];break;case!0:r=r.replace(/\/$/,""),n=["(?:/(",")|/)?"];break;default:n=["("+t.squash+"|",")?"]}return r+n[0]+t.type.pattern.source+n[1]}var tn=Xe("/"),nn={state:{params:{}},strict:!0,caseInsensitive:!0},rn=function(){function m(o,a,e,t){var s=this;this._cache={path:[this]},this._children=[],this._params=[],this._segments=[],this._compiled=[],this.config=t=te(t,nn),this.pattern=o;for(var n,r,i,u=/([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,l=/([:]?)([\w\[\].-]+)|\{([\w\[\].-]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,c=[],d=0,p=function(e){if(!m.nameValidator.test(e))throw new Error("Invalid parameter name '"+e+"' in pattern '"+o+"'");if(ue(s._params,y("id",e)))throw new Error("Duplicate parameter name '"+e+"' in pattern '"+o+"'")},h=function(e,t){var n,r=e[2]||e[3],i=t?e[4]:e[4]||("*"===e[1]?"[\\s\\S]*":null);return{id:r,regexp:i,segment:o.substring(d,e.index),type:i?a.type(i)||(n=i,W(a.type(t?"query":"path"),{pattern:new RegExp(n,s.config.caseInsensitive?"i":void 0)})):null}};(n=u.exec(o))&&!(0<=(r=h(n,!1)).segment.indexOf("?"));)p(r.id),this._params.push(e.fromPath(r.id,r.type,t.state)),this._segments.push(r.segment),c.push([r.segment,De(this._params)]),d=u.lastIndex;var f=(i=o.substring(d)).indexOf("?");if(0<=f){var g=i.substring(f);if(i=i.substring(0,f),0 Registering",e),this._viewConfigs.push(e)},u.prototype.sync=function(){var n=this,r=this._uiViews.map(function(e){return[e.fqn,e]}).reduce(ke,{});function i(e){for(var t=e.viewDecl.$context,n=0;++n&&t.parent;)t=t.parent;return n}var o=l(function(e,t,n,r){return t*(e(n)-e(r))}),e=this._uiViews.sort(o(function(e){var t=function(e){return e&&e.parent?t(e.parent)+1:1};return 1e4*e.fqn.split(".").length+t(e.creationContext)},1)).map(function(e){var t=n._viewConfigs.filter(u.matches(r,e));return 1 Registering",t);var e=this._uiViews;return e.filter(function(e){return e.fqn===t.fqn&&e.$type===t.$type}).length&<.traceViewServiceUIViewEvent("!!!! duplicate uiView named:",t),e.push(t),this.sync(),function(){-1!==e.indexOf(t)?(lt.traceViewServiceUIViewEvent("<- Deregistering",t),Q(e)(t)):lt.traceViewServiceUIViewEvent("Tried removing non-registered uiView",t)}},u.prototype.available=function(){return this._uiViews.map(v("fqn"))},u.prototype.active=function(){return this._uiViews.filter(v("$config")).map(v("name"))},u.matches=function(s,u){return function(e){if(u.$type!==e.viewDecl.$type)return!1;var t=e.viewDecl,n=t.$uiViewName.split("."),r=u.fqn.split(".");if(!U(n,r.slice(0-n.length)))return!1;var i=1-n.length||void 0,o=r.slice(0,i).join("."),a=s[o].creationContext;return t.$uiViewContextAnchor===(a&&a.name)}},u}(),hn=function(){function e(){this.params=new wt,this.lastStartedTransitionId=-1,this.transitionHistory=new Re([],1),this.successfulTransitions=new Re([],1)}return e.prototype.dispose=function(){this.transitionHistory.clear(),this.successfulTransitions.clear(),this.transition=null},e}();function fn(e){if(!(D(e)||O(e)||c($t)(e)||$t.isDef(e)))throw new Error("'handler' must be a string, function, TargetState, or have a state: 'newtarget' property");return D(e)?e:p(e)}cn=function(e,t){var n=function(e,t){return(t.priority||0)-(e.priority||0)}(e,t);return 0!==n?n:0!==(n=function(e,t){var n={STATE:4,URLMATCHER:4,REGEXP:3,RAW:2,OTHER:1};return(n[e.type]||0)-(n[t.type]||0)}(e,t))?n:0!==(n=function(e,t){return e.urlMatcher&&t.urlMatcher?rn.compare(e.urlMatcher,t.urlMatcher):0}(e,t))?n:function(e,t){var n={STATE:!0,URLMATCHER:!0};return n[e.type]&&n[t.type]?0:(e.$id||0)-(t.$id||0)}(e,t)};var gn=function(){function e(e){this.router=e,this._sortFn=cn,this._rules=[],this._id=0,this.urlRuleFactory=new un(e)}return e.prototype.dispose=function(e){this._rules=[],delete this._otherwiseFn},e.prototype.initial=function(e){var t=fn(e);this.rule(this.urlRuleFactory.create(function(e,t){return 0===t.globals.transitionHistory.size()&&!!/^\/?$/.exec(e.path)},t))},e.prototype.otherwise=function(e){var t=fn(e);this._otherwiseFn=this.urlRuleFactory.create(p(!0),t),this._sorted=!1},e.prototype.removeRule=function(e){Q(this._rules,e)},e.prototype.rule=function(e){var t=this;if(!un.isUrlRule(e))throw new Error("invalid rule");return e.$id=this._id++,e.priority=e.priority||0,this._rules.push(e),this._sorted=!1,function(){return t.removeRule(e)}},e.prototype.rules=function(){return this.ensureSorted(),this._rules.concat(this._otherwiseFn?[this._otherwiseFn]:[])},e.prototype.sort=function(e){for(var t=this.stableSort(this._rules,this._sortFn=e||this._sortFn),n=0,r=0;rn.weight?s:n}return n},e}(),yn=0,wn=I("LocationServices",["url","path","search","hash","onChange"]),bn=I("LocationConfig",["port","protocol","host","baseHref","html5Mode","hashPrefix"]),$n=function(){function e(e,t){void 0===e&&(e=wn),void 0===t&&(t=bn),this.locationService=e,this.locationConfig=t,this.$id=yn++,this._disposed=!1,this._disposables=[],this.trace=lt,this.viewService=new pn(this),this.globals=new hn,this.transitionService=new Un(this),this.urlMatcherFactory=new sn(this),this.urlRouter=new dn(this),this.urlService=new vn(this),this.stateRegistry=new zt(this),this.stateService=new qn(this),this._plugins={},this.viewService._pluginapi._rootViewContext(this.stateRegistry.root()),this.globals.$current=this.stateRegistry.root(),this.globals.current=this.globals.$current.self,this.disposable(this.globals),this.disposable(this.stateService),this.disposable(this.stateRegistry),this.disposable(this.transitionService),this.disposable(this.urlService),this.disposable(e),this.disposable(t)}return e.prototype.disposable=function(e){this._disposables.push(e)},e.prototype.dispose=function(e){var t=this;e&&D(e.dispose)?e.dispose(this):(this._disposed=!0,this._disposables.slice().forEach(function(e){try{"function"==typeof e.dispose&&e.dispose(t),Q(t._disposables,e)}catch(e){}}))},e.prototype.plugin=function(e,t){void 0===t&&(t={});var n=new e(this,t);if(!n.name)throw new Error("Required property `name` missing on plugin: "+n);return this._disposables.push(n),this._plugins[n.name]=n},e.prototype.getPlugin=function(e){return e?this._plugins[e]:de(this._plugins)},e}();function _n(t){t.addResolvable(kt.fromData($n,t.router),""),t.addResolvable(kt.fromData(Jt,t),""),t.addResolvable(kt.fromData("$transition$",t),""),t.addResolvable(kt.fromData("$stateParams",t.params()),""),t.entering().forEach(function(e){t.addResolvable(kt.fromData("$state$",e),e)})}var Sn=G(["$transition$",Jt]),Cn=function(e){var t=de(e.treeChanges()).reduce(fe,[]).reduce(ve,[]),n=function(e){return Sn(e.token)?kt.fromData(e.token,null):e};t.forEach(function(e){e.resolvables=e.resolvables.map(n)})},kn=function(t){var e=t.to().redirectTo;if(e){var n=t.router.stateService;return D(e)?V.$q.when(e(t)).then(r):r(e)}function r(e){if(e)return e instanceof $t?e:O(e)?n.target(e,t.params(),t.options()):e.state||e.params?n.target(e.state||t.to(),e.params||t.params(),t.options()):void 0}};function Dn(n){return function(e,t){return(0,t.$$state()[n])(e,t)}}var xn=Dn("onExit"),On=Dn("onRetain"),Tn=Dn("onEnter"),An=function(e){return new At(e.treeChanges().to).resolvePath("EAGER",e).then(z)},En=function(e,t){return new At(e.treeChanges().to).subContext(t.$$state()).resolvePath("LAZY",e).then(z)},Pn=function(e){return new At(e.treeChanges().to).resolvePath("LAZY",e).then(z)},Mn=function(e){var t=V.$q,n=e.views("entering");if(n.length)return t.all(n.map(function(e){return t.when(e.load())})).then(z)},Rn=function(e){var t=e.views("entering"),n=e.views("exiting");if(t.length||n.length){var r=e.router.viewService;n.forEach(function(e){return r.deactivateViewConfig(e)}),t.forEach(function(e){return r.activateViewConfig(e)}),r.sync()}},In=function(e){var t=e.router.globals,n=function(){t.transition===e&&(t.transition=null)};e.onSuccess({},function(){t.successfulTransitions.enqueue(e),t.$current=e.$to(),t.current=t.$current.self,xe(e.params(),t.params)},{priority:1e4}),e.promise.then(n,n)},Vn=function(e){var t=e.options(),n=e.router.stateService,r=e.router.urlRouter;if("url"!==t.source&&t.location&&n.$current.navigable){var i={replace:"replace"===t.location};r.push(n.$current.navigable.url,n.params,i)}r.update(!0)},Ln=function(a){var s=a.router;var e=a.entering().filter(function(e){return!!e.$$state().lazyLoad}).map(function(e){return Fn(a,e)});return V.$q.all(e).then(function(){if("url"!==a.originalTransition().options().source){var e=a.targetState();return s.stateService.target(e.identifier(),e.params(),e.options())}var t=s.urlService,n=t.match(t.parts()),r=n&&n.rule;if(r&&"STATE"===r.type){var i=r.state,o=n.match;return s.stateService.target(i,o,a.options())}s.urlService.sync()})};function Fn(t,n){var r=n.$$state().lazyLoad,e=r._promise;if(!e){e=r._promise=V.$q.when(r(t,n)).then(function(e){e&&Array.isArray(e.states)&&e.states.forEach(function(e){return t.router.stateRegistry.register(e)});return e}).then(function(e){return delete n.lazyLoad,delete n.$$state().lazyLoad,delete r._promise,e},function(e){return delete r._promise,V.$q.reject(e)})}return e}var jn=function(e,t,n,r,i,o,a,s){void 0===i&&(i=!1),void 0===o&&(o=Wt.HANDLE_RESULT),void 0===a&&(a=Wt.REJECT_ERROR),void 0===s&&(s=!1),this.name=e,this.hookPhase=t,this.hookOrder=n,this.criteriaMatchPath=r,this.reverseSort=i,this.getResultHandler=o,this.getErrorHandler=a,this.synchronous=s};function Hn(e){var t=e._ignoredReason();if(t){lt.traceTransitionIgnored(e);var n=e.router.globals.transition;return"SameAsCurrent"===t&&n&&n.abort(),Ve.ignored().toPromise()}}function Nn(e){if(!e.valid())throw new Error(e.error().toString())}var Yn={location:!0,relative:null,inherit:!1,notify:!0,reload:!1,custom:{},current:function(){return null},source:"unknown"},Un=function(){function e(e){this._transitionCount=0,this._eventTypes=[],this._registeredHooks={},this._criteriaPaths={},this._router=e,this.$view=e.viewService,this._deregisterHookFns={},this._pluginapi=B(p(this),{},p(this),["_definePathType","_defineEvent","_getPathTypes","_getEvents","getHooks"]),this._defineCorePaths(),this._defineCoreEvents(),this._registerCoreTransitionHooks(),e.globals.successfulTransitions.onEvict(Cn)}return e.prototype.onCreate=function(e,t,n){},e.prototype.onBefore=function(e,t,n){},e.prototype.onStart=function(e,t,n){},e.prototype.onExit=function(e,t,n){},e.prototype.onRetain=function(e,t,n){},e.prototype.onEnter=function(e,t,n){},e.prototype.onFinish=function(e,t,n){},e.prototype.onSuccess=function(e,t,n){},e.prototype.onError=function(e,t,n){},e.prototype.dispose=function(e){de(this._registeredHooks).forEach(function(t){return t.forEach(function(e){e._deregistered=!0,Q(t,e)})})},e.prototype.create=function(e,t){return new Jt(e,t,this._router)},e.prototype._defineCoreEvents=function(){var e=g.TransitionHookPhase,t=Wt,n=this._criteriaPaths;this._defineEvent("onCreate",e.CREATE,0,n.to,!1,t.LOG_REJECTED_RESULT,t.THROW_ERROR,!0),this._defineEvent("onBefore",e.BEFORE,0,n.to),this._defineEvent("onStart",e.RUN,0,n.to),this._defineEvent("onExit",e.RUN,100,n.exiting,!0),this._defineEvent("onRetain",e.RUN,200,n.retained),this._defineEvent("onEnter",e.RUN,300,n.entering),this._defineEvent("onFinish",e.RUN,400,n.to),this._defineEvent("onSuccess",e.SUCCESS,0,n.to,!1,t.LOG_REJECTED_RESULT,t.LOG_ERROR,!0),this._defineEvent("onError",e.ERROR,0,n.to,!1,t.LOG_REJECTED_RESULT,t.LOG_ERROR,!0)},e.prototype._defineCorePaths=function(){var e=g.TransitionHookScope.STATE,t=g.TransitionHookScope.TRANSITION;this._definePathType("to",t),this._definePathType("from",t),this._definePathType("exiting",e),this._definePathType("retained",e),this._definePathType("entering",e)},e.prototype._defineEvent=function(e,t,n,r,i,o,a,s){void 0===i&&(i=!1),void 0===o&&(o=Wt.HANDLE_RESULT),void 0===a&&(a=Wt.REJECT_ERROR),void 0===s&&(s=!1);var u=new jn(e,t,n,r,i,o,a,s);this._eventTypes.push(u),Qt(this,this,u)},e.prototype._getEvents=function(t){return(k(t)?this._eventTypes.filter(function(e){return e.hookPhase===t}):this._eventTypes.slice()).sort(function(e,t){var n=e.hookPhase-t.hookPhase;return 0==n?e.hookOrder-t.hookOrder:n})},e.prototype._definePathType=function(e,t){this._criteriaPaths[e]={name:e,scope:t}},e.prototype._getPathTypes=function(){return this._criteriaPaths},e.prototype.getHooks=function(e){return this._registeredHooks[e]},e.prototype._registerCoreTransitionHooks=function(){var e=this._deregisterHookFns;e.addCoreResolves=function(e){return e.onCreate({},_n)}(this),e.ignored=function(e){return e.onBefore({},Hn,{priority:-9999})}(this),e.invalid=function(e){return e.onBefore({},Nn,{priority:-1e4})}(this),e.redirectTo=function(e){return e.onStart({to:function(e){return!!e.redirectTo}},kn)}(this),e.onExit=function(e){return e.onExit({exiting:function(e){return!!e.onExit}},xn)}(this),e.onRetain=function(e){return e.onRetain({retained:function(e){return!!e.onRetain}},On)}(this),e.onEnter=function(e){return e.onEnter({entering:function(e){return!!e.onEnter}},Tn)}(this),e.eagerResolve=function(e){return e.onStart({},An,{priority:1e3})}(this),e.lazyResolve=function(e){return e.onEnter({entering:p(!0)},En,{priority:1e3})}(this),e.resolveAll=function(e){return e.onFinish({},Pn,{priority:1e3})}(this),e.loadViews=function(e){return e.onFinish({},Mn)}(this),e.activateViews=function(e){return e.onSuccess({},Rn)}(this),e.updateGlobals=function(e){return e.onCreate({},In)}(this),e.updateUrl=function(e){return e.onSuccess({},Vn,{priority:9999})}(this),e.lazyLoad=function(e){return e.onBefore({entering:function(e){return!!e.lazyLoad}},Ln)}(this)},e}(),qn=function(){function n(e){this.router=e,this.invalidCallbacks=[],this._defaultErrorHandler=function(e){e instanceof Error&&e.stack?(console.error(e),console.error(e.stack)):e instanceof Ve?(console.error(e.toString()),e.detail&&e.detail.stack&&console.error(e.detail.stack)):console.error(e)};var t=Object.keys(n.prototype).filter(d(G(["current","$current","params","transition"])));B(p(n.prototype),this,p(this),t)}return Object.defineProperty(n.prototype,"transition",{get:function(){return this.router.globals.transition},enumerable:!0,configurable:!0}),Object.defineProperty(n.prototype,"params",{get:function(){return this.router.globals.params},enumerable:!0,configurable:!0}),Object.defineProperty(n.prototype,"current",{get:function(){return this.router.globals.current},enumerable:!0,configurable:!0}),Object.defineProperty(n.prototype,"$current",{get:function(){return this.router.globals.$current},enumerable:!0,configurable:!0}),n.prototype.dispose=function(){this.defaultErrorHandler(z),this.invalidCallbacks=[]},n.prototype._handleInvalidTargetState=function(e,n){var r=this,i=_t.makeTargetState(this.router.stateRegistry,e),t=this.router.globals,o=function(){return t.transitionHistory.peekTail()},a=o(),s=new Re(this.invalidCallbacks.slice()),u=new At(e).injector(),l=function(e){if(e instanceof $t){var t=e;return(t=r.target(t.identifier(),t.params(),t.options())).valid()?o()!==a?Ve.superseded().toPromise():r.transitionTo(t.identifier(),t.params(),t.options()):Ve.invalid(t.error()).toPromise()}};return function t(){var e=s.dequeue();return void 0===e?Ve.invalid(n.error()).toPromise():V.$q.when(e(n,i,u)).then(l).then(function(e){return e||t()})}()},n.prototype.onInvalid=function(e){return this.invalidCallbacks.push(e),function(){Q(this.invalidCallbacks)(e)}.bind(this)},n.prototype.reload=function(e){return this.transitionTo(this.current,this.params,{reload:!k(e)||e,inherit:!1,notify:!1})},n.prototype.go=function(e,t,n){var r=te(n,{relative:this.$current,inherit:!0},Yn);return this.transitionTo(e,t,r)},n.prototype.target=function(e,t,n){if(void 0===n&&(n={}),T(n.reload)&&!n.reload.name)throw new Error("Invalid reload state object");var r=this.router.stateRegistry;if(n.reloadState=!0===n.reload?r.root():r.matcher.find(n.reload,n.relative),n.reload&&!n.reloadState)throw new Error("No such reload state '"+(O(n.reload)?n.reload:n.reload.name)+"'");return new $t(this.router.stateRegistry,e,t,n)},n.prototype.getCurrentPath=function(){var e=this,t=this.router.globals.successfulTransitions.peekTail();return t?t.treeChanges().to:[new bt(e.router.stateRegistry.root())]},n.prototype.transitionTo=function(e,t,n){var o=this;void 0===t&&(t={}),void 0===n&&(n={});var a=this.router,s=a.globals;n=te(n,Yn);n=Y(n,{current:function(){return s.transition}});var r=this.target(e,t,n),i=this.getCurrentPath();if(!r.exists())return this._handleInvalidTargetState(i,r);if(!r.valid())return Pe(r.error());var u=function(i){return function(e){if(e instanceof Ve){var t=a.globals.lastStartedTransitionId<=i.$id;if(e.type===g.RejectType.IGNORED)return t&&a.urlRouter.update(),V.$q.when(s.current);var n=e.detail;if(e.type===g.RejectType.SUPERSEDED&&e.redirected&&n instanceof $t){var r=i.redirect(n);return r.run().catch(u(r))}if(e.type===g.RejectType.ABORTED)return t&&a.urlRouter.update(),V.$q.reject(e)}return o.defaultErrorHandler()(e),V.$q.reject(e)}},l=this.router.transitionService.create(i,r),c=l.run().catch(u(l));return Ee(c),Y(c,{transition:l})},n.prototype.is=function(e,t,n){n=te(n,{relative:this.$current});var r=this.router.stateRegistry.matcher.find(e,n.relative);if(k(r)){if(this.$current!==r)return!1;if(!t)return!0;var i=r.parameters({inherit:!0,matchingKeys:t});return vt.equals(i,vt.values(i,t),this.params)}},n.prototype.includes=function(e,t,n){n=te(n,{relative:this.$current});var r=O(e)&&Me.fromString(e);if(r){if(!r.matches(this.$current.name))return!1;e=this.$current.name}var i=this.router.stateRegistry.matcher.find(e,n.relative),o=this.$current.includes;if(k(i)){if(!k(o[i.name]))return!1;if(!t)return!0;var a=i.parameters({inherit:!0,matchingKeys:t});return vt.equals(a,vt.values(a,t),this.params)}},n.prototype.href=function(e,t,n){n=te(n,{lossy:!0,inherit:!0,absolute:!1,relative:this.$current}),t=t||{};var r=this.router.stateRegistry.matcher.find(e,n.relative);if(!k(r))return null;n.inherit&&(t=this.params.$inherit(t,this.$current,r));var i=r&&n.lossy?r.navigable:r;return i&&void 0!==i.url&&null!==i.url?this.router.urlRouter.href(i.url,t,{absolute:n.absolute}):null},n.prototype.defaultErrorHandler=function(e){return this._defaultErrorHandler=e||this._defaultErrorHandler},n.prototype.get=function(e,t){var n=this.router.stateRegistry;return 0===arguments.length?n.get():n.get(e,t||this.$current)},n.prototype.lazyLoad=function(e,t){var n=this.get(e);if(!n||!n.lazyLoad)throw new Error("Can not lazy load "+e);var r=this.getCurrentPath(),i=_t.makeTargetState(this.router.stateRegistry,r);return Fn(t=t||this.router.transitionService.create(r,i),n)},n}(),zn={when:function(n){return new Promise(function(e,t){return e(n)})},reject:function(n){return new Promise(function(e,t){t(n)})},defer:function(){var n={};return n.promise=new Promise(function(e,t){n.resolve=e,n.reject=t}),n},all:function(e){if(A(e))return Promise.all(e);if(T(e)){var t=Object.keys(e).map(function(t){return e[t].then(function(e){return{key:t,val:e}})});return zn.all(t).then(function(e){return e.reduce(function(e,t){return e[t.key]=t.val,e},{})})}}},Bn={},Wn=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/gm,Gn=/([^\s,]+)/g,Kn={get:function(e){return Bn[e]},has:function(e){return null!=Kn.get(e)},invoke:function(e,t,n){var r=Y({},Bn,n||{}),i=Kn.annotate(e),o=be(function(e){return r.hasOwnProperty(e)},function(e){return"DI can't find injectable: '"+e+"'"}),a=i.filter(o).map(function(e){return r[e]});return D(e)?e.apply(t,a):e.slice(-1)[0].apply(t,a)},annotate:function(e){if(!M(e))throw new Error("Not an injectable function: "+e);if(e&&e.$inject)return e.$inject;if(A(e))return e.slice(0,-1);var t=e.toString().replace(Wn,"");return t.slice(t.indexOf("(")+1,t.indexOf(")")).match(Gn)||[]}},Qn=function(e,t){var n=t[0],r=t[1];return e.hasOwnProperty(n)?A(e[n])?e[n].push(r):e[n]=[e[n],r]:e[n]=r,e},Zn=function(e){return e.split("&").filter(q).map(Qe).reduce(Qn,{})};function Xn(e){var t=function(e){return e||""},n=Ge(e).map(t),r=n[0],i=n[1],o=Ke(r).map(t);return{path:o[0],search:o[1],hash:i,url:e}}var Jn=function(e){var t=e.path(),n=e.search(),r=e.hash(),i=Object.keys(n).map(function(t){var e=n[t];return(A(e)?e:[e]).map(function(e){return t+"="+e})}).reduce(fe,[]).join("&");return t+(i?"?"+i:"")+(r?"#"+r:"")};function er(r,i,o,a){return function(e){var t=e.locationService=new o(e),n=e.locationConfig=new a(e,i);return{name:r,service:t,configuration:n,dispose:function(e){e.dispose(t),e.dispose(n)}}}}var tr,nr,rr,ir=function(){function e(e,t){var n=this;this.fireAfterUpdate=t,this._listeners=[],this._listener=function(t){return n._listeners.forEach(function(e){return e(t)})},this.hash=function(){return Xn(n._get()).hash},this.path=function(){return Xn(n._get()).path},this.search=function(){return Zn(Xn(n._get()).search)},this._location=L.location,this._history=L.history}return e.prototype.url=function(t,e){return void 0===e&&(e=!0),k(t)&&t!==this._get()&&(this._set(null,null,t,e),this.fireAfterUpdate&&this._listeners.forEach(function(e){return e({url:t})})),Jn(this)},e.prototype.onChange=function(e){var t=this;return this._listeners.push(e),function(){return Q(t._listeners,e)}},e.prototype.dispose=function(e){ee(this._listeners)},e}(),or=(tr=function(e,t){return(tr=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])})(e,t)},function(e,t){function n(){this.constructor=e}tr(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),ar=function(n){function e(e){var t=n.call(this,e,!1)||this;return L.addEventListener("hashchange",t._listener,!1),t}return or(e,n),e.prototype._get=function(){return Ze(this._location.hash)},e.prototype._set=function(e,t,n,r){this._location.hash=n},e.prototype.dispose=function(e){n.prototype.dispose.call(this,e),L.removeEventListener("hashchange",this._listener)},e}(ir),sr=(nr=function(e,t){return(nr=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])})(e,t)},function(e,t){function n(){this.constructor=e}nr(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),ur=function(t){function e(e){return t.call(this,e,!0)||this}return sr(e,t),e.prototype._get=function(){return this._url},e.prototype._set=function(e,t,n,r){this._url=n},e}(ir),lr=(rr=function(e,t){return(rr=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])})(e,t)},function(e,t){function n(){this.constructor=e}rr(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),cr=function(n){function e(e){var t=n.call(this,e,!0)||this;return t._config=e.urlService.config,L.addEventListener("popstate",t._listener,!1),t}return lr(e,n),e.prototype._getBasePrefix=function(){return We(this._config.baseHref())},e.prototype._get=function(){var e=this._location,t=e.pathname,n=e.hash,r=e.search;r=Ke(r)[1],n=Ge(n)[1];var i=this._getBasePrefix(),o=t===this._config.baseHref(),a=t.substr(0,i.length)===i;return(t=o?"/":a?t.substring(i.length):t)+(r?"?"+r:"")+(n?"#"+n:"")},e.prototype._set=function(e,t,n,r){var i=this._getBasePrefix(),o=n&&"/"!==n[0]?"/":"",a=""===n||"/"===n?this._config.baseHref():i+o+n;r?this._history.replaceState(e,t,a):this._history.pushState(e,t,a)},e.prototype.dispose=function(e){n.prototype.dispose.call(this,e),L.removeEventListener("popstate",this._listener)},e}(ir),dr=function(){var t=this;this.dispose=z,this._baseHref="",this._port=80,this._protocol="http",this._host="localhost",this._hashPrefix="",this.port=function(){return t._port},this.protocol=function(){return t._protocol},this.host=function(){return t._host},this.baseHref=function(){return t._baseHref},this.html5Mode=function(){return!1},this.hashPrefix=function(e){return k(e)?t._hashPrefix=e:t._hashPrefix}},pr=function(){function e(e,t){void 0===t&&(t=!1),this._isHtml5=t,this._baseHref=void 0,this._hashPrefix=""}return e.prototype.port=function(){return location.port?Number(location.port):"https"===this.protocol()?443:80},e.prototype.protocol=function(){return location.protocol.replace(/:/g,"")},e.prototype.host=function(){return location.hostname},e.prototype.html5Mode=function(){return this._isHtml5},e.prototype.hashPrefix=function(e){return k(e)?this._hashPrefix=e:this._hashPrefix},e.prototype.baseHref=function(e){return k(e)&&(this._baseHref=e),b(this._baseHref)&&(this._baseHref=this.getBaseHref()),this._baseHref},e.prototype.getBaseHref=function(){var e=document.getElementsByTagName("base")[0];return e&&e.href?e.href.replace(/^([^/:]*:)?\/\/[^/]*/,""):this._isHtml5?"/":location.pathname||"/"},e.prototype.dispose=function(){},e}();function hr(e){return V.$injector=Kn,{name:"vanilla.services",$q:V.$q=zn,$injector:Kn,dispose:function(){return null}}}var fr=er("vanilla.hashBangLocation",!1,ar,pr),gr=er("vanilla.pushStateLocation",!0,cr,pr),mr=er("vanilla.memoryLocation",!1,ur,dr),vr=function(){function e(){}return e.prototype.dispose=function(e){},e}(),yr=Object.freeze({root:L,fromJson:j,toJson:H,forEach:N,extend:Y,equals:U,identity:q,noop:z,createProxyFunctions:B,inherit:W,inArray:G,_inArray:K,removeFrom:Q,_removeFrom:Z,pushTo:X,_pushTo:J,deregAll:ee,defaults:te,mergeR:ne,ancestors:re,pick:ie,omit:oe,pluck:ae,filter:se,find:ue,mapObj:le,map:ce,values:de,allTrueR:pe,anyTrueR:he,unnestR:fe,flattenR:ge,pushR:me,uniqR:ve,unnest:ye,flatten:we,assertPredicate:be,assertMap:$e,assertFn:_e,pairs:Se,arrayTuples:Ce,applyPairs:ke,tail:De,copy:xe,_extend:Oe,silenceUncaughtInPromise:Ee,silentRejection:Pe,makeStub:I,services:V,Glob:Me,curry:l,compose:n,pipe:u,prop:v,propEq:y,parse:C,not:d,and:r,or:i,all:a,any:s,is:c,eq:o,val:p,invoke:h,pattern:f,isUndefined:b,isDefined:k,isNull:$,isNullOrUndefined:_,isFunction:D,isNumber:x,isString:O,isObject:T,isArray:A,isDate:E,isRegExp:P,isInjectable:M,isPromise:R,Queue:Re,maxLength:Le,padString:Fe,kebobString:je,functionToString:He,fnToString:Ne,stringify:qe,beforeAfterSubstr:ze,hostRegex:Be,stripLastPathElement:We,splitHash:Ge,splitQuery:Ke,splitEqual:Qe,trimHashVal:Ze,splitOnDelim:Xe,joinNeighborsR:Je,get Category(){return g.Category},Trace:ut,trace:lt,get DefType(){return g.DefType},Param:vt,ParamTypes:yt,StateParams:wt,ParamType:ct,PathNode:bt,PathUtils:_t,resolvePolicies:St,defaultResolvePolicy:Ct,Resolvable:kt,NATIVE_INJECTOR_TOKEN:Tt,ResolveContext:At,resolvablesBuilder:Ft,StateBuilder:Nt,StateObject:Yt,StateMatcher:Ut,StateQueueManager:qt,StateRegistry:zt,StateService:qn,TargetState:$t,get TransitionHookPhase(){return g.TransitionHookPhase},get TransitionHookScope(){return g.TransitionHookScope},HookBuilder:Zt,matchState:Gt,RegisteredHook:Kt,makeEvent:Qt,get RejectType(){return g.RejectType},Rejection:Ve,Transition:Jt,TransitionHook:Wt,TransitionEventType:jn,defaultTransOpts:Yn,TransitionService:Un,UrlRules:gn,UrlConfig:mn,UrlMatcher:rn,ParamFactory:an,UrlMatcherFactory:sn,UrlRouter:dn,UrlRuleFactory:un,BaseUrlRule:ln,UrlService:vn,ViewService:pn,UIRouterGlobals:hn,UIRouter:$n,$q:zn,$injector:Kn,BaseLocationServices:ir,HashLocationService:ar,MemoryLocationService:ur,PushStateLocationService:cr,MemoryLocationConfig:dr,BrowserLocationConfig:pr,keyValsToObjectR:Qn,getParams:Zn,parseUrl:Xn,buildUrl:Jn,locationPluginFactory:er,servicesPlugin:hr,hashLocationPlugin:fr,pushStateLocationPlugin:gr,memoryLocationPlugin:mr,UIRouterPluginBase:vr});function wr(){var n=null;return function(e,t){return n=n||V.$injector.get("$templateFactory"),[new Sr(e,t,n)]}}var br=function(e,n){return e.reduce(function(e,t){return e||k(n[t])},!1)};function $r(r){if(!r.parent)return{};var i=["component","bindings","componentProvider"],o=["templateProvider","templateUrl","template","notify","async"].concat(["controller","controllerProvider","controllerAs","resolveAs"]),e=i.concat(o);if(k(r.views)&&br(e,r))throw new Error("State '"+r.name+"' has a 'views' object. It cannot also have \"view properties\" at the state level. Move the following properties into a view (in the 'views' object): "+e.filter(function(e){return k(r[e])}).join(", "));var a={},t=r.views||{$default:ie(r,e)};return N(t,function(e,t){if(t=t||"$default",O(e)&&(e={component:e}),e=Y({},e),br(i,e)&&br(o,e))throw new Error("Cannot combine: "+i.join("|")+" with: "+o.join("|")+" in stateview: '"+t+"@"+r.name+"'");e.resolveAs=e.resolveAs||"$resolve",e.$type="ng1",e.$context=r,e.$name=t;var n=pn.normalizeUIViewTarget(e.$context,e.$name);e.$uiViewName=n.uiViewName,e.$uiViewContextAnchor=n.uiViewContextAnchor,a[t]=e}),a}var _r=0,Sr=function(){function e(e,t,n){var r=this;this.path=e,this.viewDecl=t,this.factory=n,this.$id=_r++,this.loaded=!1,this.getTemplate=function(e,t){return r.component?r.factory.makeComponentTemplate(e,t,r.component,r.viewDecl.bindings):r.template}}return e.prototype.load=function(){var t=this,e=V.$q,n=new At(this.path),r=this.path.reduce(function(e,t){return Y(e,t.paramValues)},{}),i={template:e.when(this.factory.fromConfig(this.viewDecl,r,n)),controller:e.when(this.getController(n))};return e.all(i).then(function(e){return lt.traceViewServiceEvent("Loaded",t),t.controller=e.controller,Y(t,e.template),t})},e.prototype.getController=function(e){var t=this.viewDecl.controllerProvider;if(!M(t))return this.viewDecl.controller;var n=V.$injector.annotate(t),r=A(t)?De(t):t;return new kt("",r,n).get(e)},e}(),Cr=function(){function e(){var r=this;this._useHttp=S.version.minor<3,this.$get=["$http","$templateCache","$injector",function(e,t,n){return r.$templateRequest=n.has&&n.has("$templateRequest")&&n.get("$templateRequest"),r.$http=e,r.$templateCache=t,r}]}return e.prototype.useHttpService=function(e){this._useHttp=e},e.prototype.fromConfig=function(e,t,n){var r=function(e){return V.$q.when(e).then(function(e){return{template:e}})},i=function(e){return V.$q.when(e).then(function(e){return{component:e}})};return k(e.template)?r(this.fromString(e.template,t)):k(e.templateUrl)?r(this.fromUrl(e.templateUrl,t)):k(e.templateProvider)?r(this.fromProvider(e.templateProvider,t,n)):k(e.component)?i(e.component):k(e.componentProvider)?i(this.fromComponentProvider(e.componentProvider,t,n)):r("")},e.prototype.fromString=function(e,t){return D(e)?e(t):e},e.prototype.fromUrl=function(e,t){return D(e)&&(e=e(t)),null==e?null:this._useHttp?this.$http.get(e,{cache:this.$templateCache,headers:{Accept:"text/html"}}).then(function(e){return e.data}):this.$templateRequest(e)},e.prototype.fromProvider=function(e,t,n){var r=V.$injector.annotate(e),i=A(e)?De(e):e;return new kt("",i,r).get(n)},e.prototype.fromComponentProvider=function(e,t,n){var r=V.$injector.annotate(e),i=A(e)?De(e):e;return new kt("",i,r).get(n)},e.prototype.makeComponentTemplate=function(u,l,e,c){c=c||{};var d=3<=S.version.minor?"::":"",p=function(e){var t=je(e);return/^(x|data)-/.exec(t)?"x-"+t:t},t=function(e){var t=V.$injector.get(e+"Directive");if(!t||!t.length)throw new Error("Unable to find component named '"+e+"'");return t.map(kr).reduce(fe,[])}(e).map(function(e){var t=e.name,n=e.type,r=p(t);if(u.attr(r)&&!c[t])return r+"='"+u.attr(r)+"'";var i=c[t]||t;if("@"===n)return r+"='{{"+d+"$resolve."+i+"}}'";if("&"!==n)return r+"='"+d+"$resolve."+i+"'";var o=l.getResolvable(i),a=o&&o.data,s=a&&V.$injector.annotate(a)||[];return r+"='$resolve."+i+(A(a)?"["+(a.length-1)+"]":"")+"("+s.join(",")+")'"}).join(" "),n=p(e);return"<"+n+" "+t+">"},e}();var kr=function(e){return T(e.bindToController)?Dr(e.bindToController):Dr(e.scope)},Dr=function(t){return Object.keys(t||{}).map(function(e){return[e,/^([=<@&])[?]?(.*)/.exec(t[e])]}).filter(function(e){return k(e)&&A(e[1])}).map(function(e){return{name:e[1][2]||e[0],type:e[1][1]}})},xr=function(){function n(e,t){this.stateRegistry=e,this.stateService=t,B(p(n.prototype),this,p(this))}return n.prototype.decorator=function(e,t){return this.stateRegistry.decorator(e,t)||this},n.prototype.state=function(e,t){return T(e)?t=e:t.name=e,this.stateRegistry.register(t),this},n.prototype.onInvalid=function(e){return this.stateService.onInvalid(e)},n}(),Or=function(n){return function(e,t){var i=e[n],o="onExit"===n?"from":"to";return i?function(e,t){var n=new At(e.treeChanges(o)).subContext(t.$$state()),r=Y(zr(n),{$state$:t,$transition$:e});return V.$injector.invoke(i,this,r)}:void 0}},Tr=function(){function e(e){this._urlListeners=[],this.$locationProvider=e;var t=p(e);B(t,this,t,["hashPrefix"])}return e.monkeyPatchPathParameterType=function(e){var t=e.urlMatcherFactory.type("path");t.encode=function(e){return null!=e?e.toString().replace(/(~|\/)/g,function(e){return{"~":"~~","/":"~2F"}[e]}):e},t.decode=function(e){return null!=e?e.toString().replace(/(~~|~2F)/g,function(e){return{"~~":"~","~2F":"/"}[e]}):e}},e.prototype.dispose=function(){},e.prototype.onChange=function(e){var t=this;return this._urlListeners.push(e),function(){return Q(t._urlListeners)(e)}},e.prototype.html5Mode=function(){var e=this.$locationProvider.html5Mode();return(e=T(e)?e.enabled:e)&&this.$sniffer.history},e.prototype.baseHref=function(){return this._baseHref||(this._baseHref=this.$browser.baseHref()||this.$window.location.pathname)},e.prototype.url=function(e,t,n){return void 0===t&&(t=!1),k(e)&&this.$location.url(e),t&&this.$location.replace(),n&&this.$location.state(n),this.$location.url()},e.prototype._runtimeServices=function(e,t,n,r,i){var o=this;this.$location=t,this.$sniffer=n,this.$browser=r,this.$window=i,e.$on("$locationChangeSuccess",function(t){return o._urlListeners.forEach(function(e){return e(t)})});var a=p(t);B(a,this,a,["replace","path","search","hash"]),B(a,this,a,["port","protocol","host"])},e}(),Ar=function(){function n(e){this.router=e}return n.injectableHandler=function(t,n){return function(e){return V.$injector.invoke(n,null,{$match:e,$stateParams:t.globals.params})}},n.prototype.$get=function(){var e=this.router.urlService;return this.router.urlRouter.update(!0),e.interceptDeferred||e.listen(),this.router.urlRouter},n.prototype.rule=function(e){var t=this;if(!D(e))throw new Error("'rule' must be a function");var n=new ln(function(){return e(V.$injector,t.router.locationService)},q);return this.router.urlService.rules.rule(n),this},n.prototype.otherwise=function(e){var t=this,n=this.router.urlService.rules;if(O(e))n.otherwise(e);else{if(!D(e))throw new Error("'rule' must be a string or function");n.otherwise(function(){return e(V.$injector,t.router.locationService)})}return this},n.prototype.when=function(e,t){return(A(t)||D(t))&&(t=n.injectableHandler(this.router,t)),this.router.urlService.rules.when(e,t),this},n.prototype.deferIntercept=function(e){this.router.urlService.deferIntercept(e)},n}();S.module("ui.router.angular1",[]);var Er=S.module("ui.router.init",["ng"]),Pr=S.module("ui.router.util",["ui.router.init"]),Mr=S.module("ui.router.router",["ui.router.util"]),Rr=S.module("ui.router.state",["ui.router.router","ui.router.util","ui.router.angular1"]),Ir=S.module("ui.router",["ui.router.init","ui.router.state","ui.router.angular1"]),Vr=(S.module("ui.router.compat",["ui.router"]),null);function Lr(e){(Vr=this.router=new $n).stateProvider=new xr(Vr.stateRegistry,Vr.stateService),Vr.stateRegistry.decorator("views",$r),Vr.stateRegistry.decorator("onExit",Or("onExit")),Vr.stateRegistry.decorator("onRetain",Or("onRetain")),Vr.stateRegistry.decorator("onEnter",Or("onEnter")),Vr.viewService._pluginapi._viewConfigFactory("ng1",wr());var s=Vr.locationService=Vr.locationConfig=new Tr(e);function t(e,t,n,r,i,o,a){return s._runtimeServices(i,e,r,t,n),delete Vr.router,delete Vr.$get,Vr}return Tr.monkeyPatchPathParameterType(Vr),((Vr.router=Vr).$get=t).$inject=["$location","$browser","$window","$sniffer","$rootScope","$http","$templateCache"],Vr}Lr.$inject=["$locationProvider"];var Fr=function(n){return["$uiRouterProvider",function(e){var t=e.router[n];return t.$get=function(){return t},t}]};function jr(t,e,n){if(V.$injector=t,V.$q=e,!t.hasOwnProperty("strictDi"))try{t.invoke(function(e){})}catch(e){t.strictDi=!!/strict mode/.exec(e&&e.toString())}n.stateRegistry.get().map(function(e){return e.$$state().resolvables}).reduce(fe,[]).filter(function(e){return"deferred"===e.deps}).forEach(function(e){return e.deps=t.annotate(e.resolveFn,t.strictDi)})}jr.$inject=["$injector","$q","$uiRouter"];function Hr(e){e.$watch(function(){lt.approximateDigests++})}Hr.$inject=["$rootScope"],Er.provider("$uiRouter",Lr),Mr.provider("$urlRouter",["$uiRouterProvider",function(e){return e.urlRouterProvider=new Ar(e)}]),Pr.provider("$urlService",Fr("urlService")),Pr.provider("$urlMatcherFactory",["$uiRouterProvider",function(){return Vr.urlMatcherFactory}]),Pr.provider("$templateFactory",function(){return new Cr}),Rr.provider("$stateRegistry",Fr("stateRegistry")),Rr.provider("$uiRouterGlobals",Fr("globals")),Rr.provider("$transitions",Fr("transitionService")),Rr.provider("$state",["$uiRouterProvider",function(){return Y(Vr.stateProvider,{$get:function(){return Vr.stateService}})}]),Rr.factory("$stateParams",["$uiRouter",function(e){return e.globals.params}]),Ir.factory("$view",function(){return Vr.viewService}),Ir.service("$trace",function(){return lt}),Ir.run(Hr),Pr.run(["$urlMatcherFactory",function(e){}]),Rr.run(["$state",function(e){}]),Mr.run(["$urlRouter",function(e){}]),Er.run(jr);var Nr,Yr,Ur,qr,zr=function(n){return n.getTokens().filter(O).map(function(e){var t=n.getResolvable(e);return[e,"NOWAIT"===n.getPolicy(t).async?t.promise:t.data]}).reduce(ke,{})};function Br(e){var t,n=e.match(/^\s*({[^}]*})\s*$/);if(n&&(e="("+n[1]+")"),!(t=e.replace(/\n/g," ").match(/^\s*([^(]*?)\s*(\((.*)\))?\s*$/))||4!==t.length)throw new Error("Invalid state ref '"+e+"'");return{state:t[1]||null,paramExpr:t[3]||null}}function Wr(e){var t=e.parent().inheritedData("$uiView"),n=C("$cfg.path")(t);return n?De(n).state.name:void 0}function Gr(e,t,n){var r=n.uiState||e.current.name,i=Y(function(e,t){return{relative:Wr(e)||t.$current,inherit:!0,source:"sref"}}(t,e),n.uiStateOpts||{}),o=e.href(r,n.uiStateParams,i);return{uiState:r,uiStateParams:n.uiStateParams,uiStateOpts:i,href:o}}function Kr(e){var t="[object SVGAnimatedString]"===Object.prototype.toString.call(e.prop("href")),n="FORM"===e[0].nodeName;return{attr:n?"action":t?"xlink:href":"href",isAnchor:"A"===e.prop("tagName").toUpperCase(),clickable:!n}}function Qr(o,a,s,u,l){return function(e){var t=e.which||e.button,n=l();if(!(1>>0;if(0==i)return-1;var o=+t||0;if(Math.abs(o)===1/0&&(o=0),i<=o)return-1;for(n=Math.max(0<=o?o:i-Math.abs(o),0);n
',this.loadingBarTemplate='
',this.$get=["$injector","$document","$timeout","$rootScope",function(i,o,a,s){function u(e){if(m){var t=100*e+"%";f.css("width",t),v=e,y&&(a.cancel(c),c=a(function(){n()},250))}}function n(){if(!(1<=r())){var e,t=r();e=0<=t&&t<.25?(3*Math.random()+3)/100:.25<=t&&t<.65?3*Math.random()/100:.65<=t&&t<.9?2*Math.random()/100:.9<=t&&t<.99?.005:0,u(r()+e)}}function r(){return v}function t(){v=0,m=!1}var l,c,d,p=this.parentSelector,h=angular.element(this.loadingBarTemplate),f=h.find("div").eq(0),g=angular.element(this.spinnerTemplate),m=!1,v=0,y=this.autoIncrement,w=this.includeSpinner,b=this.includeBar,$=this.startSize;return{start:function(){if(l||(l=i.get("$animate")),a.cancel(d),!m){var e=o[0],t=e.querySelector?e.querySelector(p):o.find(p)[0];t||(t=e.getElementsByTagName("body")[0]);var n=angular.element(t),r=t.lastChild&&angular.element(t.lastChild);s.$broadcast("cfpLoadingBar:started"),m=!0,b&&l.enter(h,n,r),w&&l.enter(g,n,h),u($)}},set:u,status:r,inc:n,complete:function(){l||(l=i.get("$animate")),s.$broadcast("cfpLoadingBar:completed"),u(1),a.cancel(d),d=a(function(){var e=l.leave(h,t);e&&e.then&&e.then(t),l.leave(g)},500)},autoIncrement:this.autoIncrement,includeSpinner:this.includeSpinner,latencyThreshold:this.latencyThreshold,parentSelector:this.parentSelector,startSize:this.startSize}}]})}(),angular.module("ui.bootstrap",["ui.bootstrap.tpls","ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]),angular.module("ui.bootstrap.tpls",["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/datepicker/day.html","template/datepicker/month.html","template/datepicker/popup.html","template/datepicker/year.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/timepicker/timepicker.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]),angular.module("ui.bootstrap.transition",[]).factory("$transition",["$q","$timeout","$rootScope",function(a,s,u){function e(e){for(var t in e)if(void 0!==n.style[t])return e[t]}var l=function(e,t,n){n=n||{};var r=a.defer(),i=l[n.animation?"animationEndEventName":"transitionEndEventName"],o=function(){u.$apply(function(){e.unbind(i,o),r.resolve(e)})};return i&&e.bind(i,o),s(function(){angular.isString(t)?e.addClass(t):angular.isFunction(t)?t(e):angular.isObject(t)&&e.css(t),i||r.resolve(e)}),r.promise.cancel=function(){i&&e.unbind(i,o),r.reject("Transition cancelled")},r.promise},n=document.createElement("trans");return l.transitionEndEventName=e({WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",transition:"transitionend"}),l.animationEndEventName=e({WebkitTransition:"webkitAnimationEnd",MozTransition:"animationend",OTransition:"oAnimationEnd",transition:"animationend"}),l}]),angular.module("ui.bootstrap.collapse",["ui.bootstrap.transition"]).directive("collapse",["$transition",function(u){return{link:function(e,r,t){function n(e){function t(){a===n&&(a=void 0)}var n=u(r,e);return a&&a.cancel(),(a=n).then(t,t),n}function i(){r.removeClass("collapsing"),r.addClass("collapse in"),r.css({height:"auto"})}function o(){r.removeClass("collapsing"),r.addClass("collapse")}var a,s=!0;e.$watch(t.collapse,function(e){e?s?(s=!1,o(),r.css({height:0})):(r.css({height:r[0].scrollHeight+"px"}),r[0].offsetWidth,r.removeClass("collapse in").addClass("collapsing"),n({height:0}).then(o)):s?(s=!1,i()):(r.removeClass("collapse").addClass("collapsing"),n({height:r[0].scrollHeight+"px"}).then(i))})}}}]),angular.module("ui.bootstrap.accordion",["ui.bootstrap.collapse"]).constant("accordionConfig",{closeOthers:!0}).controller("AccordionController",["$scope","$attrs","accordionConfig",function(e,n,r){this.groups=[],this.closeOthers=function(t){(angular.isDefined(n.closeOthers)?e.$eval(n.closeOthers):r.closeOthers)&&angular.forEach(this.groups,function(e){e!==t&&(e.isOpen=!1)})},this.addGroup=function(e){var t=this;this.groups.push(e),e.$on("$destroy",function(){t.removeGroup(e)})},this.removeGroup=function(e){var t=this.groups.indexOf(e);-1!==t&&this.groups.splice(t,1)}}]).directive("accordion",function(){return{restrict:"EA",controller:"AccordionController",transclude:!0,replace:!1,templateUrl:"template/accordion/accordion.html"}}).directive("accordionGroup",function(){return{require:"^accordion",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/accordion/accordion-group.html",scope:{heading:"@",isOpen:"=?",isDisabled:"=?"},controller:function(){this.setHeading=function(e){this.heading=e}},link:function(t,e,n,r){r.addGroup(t),t.$watch("isOpen",function(e){e&&r.closeOthers(t)}),t.toggleOpen=function(){t.isDisabled||(t.isOpen=!t.isOpen)}}}}).directive("accordionHeading",function(){return{restrict:"EA",transclude:!0,template:"",replace:!0,require:"^accordionGroup",link:function(e,t,n,r,i){r.setHeading(i(e,function(){}))}}}).directive("accordionTransclude",function(){return{require:"^accordionGroup",link:function(e,t,n,r){e.$watch(function(){return r[n.accordionTransclude]},function(e){e&&(t.html(""),t.append(e))})}}}),angular.module("ui.bootstrap.alert",[]).controller("AlertController",["$scope","$attrs",function(e,t){e.closeable="close"in t,this.close=e.close}]).directive("alert",function(){return{restrict:"EA",controller:"AlertController",templateUrl:"template/alert/alert.html",transclude:!0,replace:!0,scope:{type:"@",close:"&"}}}).directive("dismissOnTimeout",["$timeout",function(i){return{require:"alert",link:function(e,t,n,r){i(function(){r.close()},parseInt(n.dismissOnTimeout,10))}}}]),angular.module("ui.bootstrap.bindHtml",[]).directive("bindHtmlUnsafe",function(){return function(e,t,n){t.addClass("ng-binding").data("$binding",n.bindHtmlUnsafe),e.$watch(n.bindHtmlUnsafe,function(e){t.html(e||"")})}}),angular.module("ui.bootstrap.buttons",[]).constant("buttonConfig",{activeClass:"active",toggleEvent:"click"}).controller("ButtonsController",["buttonConfig",function(e){this.activeClass=e.activeClass||"active",this.toggleEvent=e.toggleEvent||"click"}]).directive("btnRadio",function(){return{require:["btnRadio","ngModel"],controller:"ButtonsController",link:function(t,n,r,e){var i=e[0],o=e[1];o.$render=function(){n.toggleClass(i.activeClass,angular.equals(o.$modelValue,t.$eval(r.btnRadio)))},n.bind(i.toggleEvent,function(){var e=n.hasClass(i.activeClass);(!e||angular.isDefined(r.uncheckable))&&t.$apply(function(){o.$setViewValue(e?null:t.$eval(r.btnRadio)),o.$render()})})}}}).directive("btnCheckbox",function(){return{require:["btnCheckbox","ngModel"],controller:"ButtonsController",link:function(r,e,t,n){function i(){return o(t.btnCheckboxTrue,!0)}function o(e,t){var n=r.$eval(e);return angular.isDefined(n)?n:t}var a=n[0],s=n[1];s.$render=function(){e.toggleClass(a.activeClass,angular.equals(s.$modelValue,i()))},e.bind(a.toggleEvent,function(){r.$apply(function(){s.$setViewValue(e.hasClass(a.activeClass)?o(t.btnCheckboxFalse,!1):i()),s.$render()})})}}}),angular.module("ui.bootstrap.carousel",["ui.bootstrap.transition"]).controller("CarouselController",["$scope","$timeout","$interval","$transition",function(a,t,n,s){function u(){r();var e=+a.interval;!isNaN(e)&&0=d.length?d[t-1]:d[t]):t
");e.attr({"ng-model":"date","ng-change":"dateSelection()"});var c=angular.element(e.children()[0]);i.datepickerOptions&&angular.forEach(r.$parent.$eval(i.datepickerOptions),function(e,t){c.attr(o(t),e)}),r.watchData={},angular.forEach(["minDate","maxDate","datepickerMode"],function(t){if(i[t]){var e=g(i[t]);if(r.$parent.$watch(e,function(e){r.watchData[t]=e}),c.attr(o(t),"watchData."+t),"datepickerMode"===t){var n=e.assign;r.$watch("watchData."+t,function(e,t){e!==t&&n(r.$parent,e)})}}}),i.dateDisabled&&c.attr("date-disabled","dateDisabled({ date: date, mode: mode })"),n.$parsers.unshift(a),r.dateSelection=function(e){angular.isDefined(e)&&(r.date=e),n.$setViewValue(r.date),n.$render(),u&&(r.isOpen=!1,t[0].focus())},t.bind("input change keyup",function(){r.$apply(function(){r.date=n.$modelValue})}),n.$render=function(){var e=n.$viewValue?y(n.$viewValue,s):"";t.val(e),r.date=a(n.$modelValue)};var d=function(e){r.isOpen&&e.target!==t[0]&&r.$apply(function(){r.isOpen=!1})},p=function(e){r.keydown(e)};t.bind("keydown",p),r.keydown=function(e){27===e.which?(e.preventDefault(),e.stopPropagation(),r.close()):40!==e.which||r.isOpen||(r.isOpen=!0)},r.$watch("isOpen",function(e){e?(r.$broadcast("datepicker.focus"),r.position=l?v.offset(t):v.position(t),r.position.top=r.position.top+t.prop("offsetHeight"),m.bind("click",d)):m.unbind("click",d)}),r.select=function(e){if("today"===e){var t=new Date;angular.isDate(n.$modelValue)?(e=new Date(n.$modelValue)).setFullYear(t.getFullYear(),t.getMonth(),t.getDate()):e=new Date(t.setHours(0,0,0,0))}r.dateSelection(e)},r.close=function(){r.isOpen=!1,t[0].focus()};var h=f(e)(r);e.remove(),l?m.find("body").append(h):t.after(h),r.$on("$destroy",function(){h.remove(),t.unbind("keydown",p),m.unbind("click",d)})}}}]).directive("datepickerPopupWrap",function(){return{restrict:"EA",replace:!0,transclude:!0,templateUrl:"template/datepicker/popup.html",link:function(e,t){t.bind("click",function(e){e.preventDefault(),e.stopPropagation()})}}}),angular.module("ui.bootstrap.dropdown",[]).constant("dropdownConfig",{openClass:"open"}).service("dropdownService",["$document",function(t){var n=null;this.open=function(e){n||(t.bind("click",r),t.bind("keydown",i)),n&&n!==e&&(n.isOpen=!1),n=e},this.close=function(e){n===e&&(n=null,t.unbind("click",r),t.unbind("keydown",i))};var r=function(e){if(n){var t=n.getToggleElement();e&&t&&t[0].contains(e.target)||n.$apply(function(){n.isOpen=!1})}},i=function(e){27===e.which&&(n.focusToggleElement(),r())}}]).controller("DropdownController",["$scope","$attrs","$parse","dropdownConfig","dropdownService","$animate",function(n,t,r,e,i,o){var a,s=this,u=n.$new(),l=e.openClass,c=angular.noop,d=t.onToggle?r(t.onToggle):angular.noop;this.init=function(e){s.$element=e,t.isOpen&&(a=r(t.isOpen),c=a.assign,n.$watch(a,function(e){u.isOpen=!!e}))},this.toggle=function(e){return u.isOpen=arguments.length?!!e:!u.isOpen},this.isOpen=function(){return u.isOpen},u.getToggleElement=function(){return s.toggleElement},u.focusToggleElement=function(){s.toggleElement&&s.toggleElement[0].focus()},u.$watch("isOpen",function(e,t){o[e?"addClass":"removeClass"](s.$element,l),e?(u.focusToggleElement(),i.open(u)):i.close(u),c(n,e),angular.isDefined(e)&&e!==t&&d(n,{open:!!e})}),n.$on("$locationChangeSuccess",function(){u.isOpen=!1}),n.$on("$destroy",function(){u.$destroy()})}]).directive("dropdown",function(){return{controller:"DropdownController",link:function(e,t,n,r){r.init(t)}}}).directive("dropdownToggle",function(){return{require:"?^dropdown",link:function(t,n,r,i){if(i){i.toggleElement=n;var e=function(e){e.preventDefault(),n.hasClass("disabled")||r.disabled||t.$apply(function(){i.toggle()})};n.bind("click",e),n.attr({"aria-haspopup":!0,"aria-expanded":!1}),t.$watch(i.isOpen,function(e){n.attr("aria-expanded",!!e)}),t.$on("$destroy",function(){n.unbind("click",e)})}}}}),angular.module("ui.bootstrap.modal",["ui.bootstrap.transition"]).factory("$$stackedMap",function(){return{createNew:function(){var r=[];return{add:function(e,t){r.push({key:e,value:t})},get:function(e){for(var t=0;t");i.attr("backdrop-class",t.backdropClass),h=c(i)(f),n.append(h)}var o=angular.element("
");o.attr({"template-url":t.windowTemplateUrl,"window-class":t.windowClass,size:t.size,index:m.length()-1,animate:"animate"}).html(t.content);var a=c(o)(t.scope);m.top().value.modalDomEl=a,n.append(a),n.addClass(g)},n.close=function(e,t){var n=m.get(e);n&&(n.value.deferred.resolve(t),r(e))},n.dismiss=function(e,t){var n=m.get(e);n&&(n.value.deferred.reject(t),r(e))},n.dismissAll=function(e){for(var t=this.getTop();t;)this.dismiss(t.key,e),t=this.getTop()},n.getTop=function(){return m.top()},n}]).provider("$modal",function(){var p={options:{backdrop:!0,keyboard:!0},$get:["$injector","$rootScope","$q","$http","$templateCache","$controller","$modalStack",function(n,u,r,i,l,c,d){var e={};return e.open=function(o){var a=r.defer(),e=r.defer(),s={result:a.promise,opened:e.promise,close:function(e){d.close(s,e)},dismiss:function(e){d.dismiss(s,e)}};if((o=angular.extend({},p.options,o)).resolve=o.resolve||{},!o.template&&!o.templateUrl)throw new Error("One of template or templateUrl options is required.");var t=r.all([function(e){return e.template?r.when(e.template):i.get(angular.isFunction(e.templateUrl)?e.templateUrl():e.templateUrl,{cache:l}).then(function(e){return e.data})}(o)].concat(function(e){var t=[];return angular.forEach(e,function(e){(angular.isFunction(e)||angular.isArray(e))&&t.push(r.when(n.invoke(e)))}),t}(o.resolve)));return t.then(function(n){var e=(o.scope||u).$new();e.$close=s.close,e.$dismiss=s.dismiss;var t,r={},i=1;o.controller&&(r.$scope=e,r.$modalInstance=s,angular.forEach(o.resolve,function(e,t){r[t]=n[i++]}),t=c(o.controller,r),o.controllerAs&&(e[o.controllerAs]=t)),d.open(s,{scope:e,deferred:a,content:n[0],backdrop:o.backdrop,keyboard:o.keyboard,backdropClass:o.backdropClass,windowClass:o.windowClass,windowTemplateUrl:o.windowTemplateUrl,size:o.size})},function(e){a.reject(e)}),t.then(function(){e.resolve(!0)},function(){e.reject(!1)}),s},e}]};return p}),angular.module("ui.bootstrap.pagination",[]).controller("PaginationController",["$scope","$attrs","$parse",function(n,r,i){var o=this,a={$setViewValue:angular.noop},t=r.numPages?i(r.numPages).assign:angular.noop;this.init=function(e,t){a=e,this.config=t,a.$render=function(){o.render()},r.itemsPerPage?n.$parent.$watch(i(r.itemsPerPage),function(e){o.itemsPerPage=parseInt(e,10),n.totalPages=o.calculateTotalPages()}):this.itemsPerPage=t.itemsPerPage},this.calculateTotalPages=function(){var e=this.itemsPerPage<1?1:Math.ceil(n.totalItems/this.itemsPerPage);return Math.max(e||0,1)},this.render=function(){n.page=parseInt(a.$viewValue,10)||1},n.selectPage=function(e){n.page!==e&&0e?n.selectPage(e):a.$render()})}]).constant("paginationConfig",{itemsPerPage:10,boundaryLinks:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0}).directive("pagination",["$parse","paginationConfig",function(s,u){return{restrict:"EA",scope:{totalItems:"=",firstText:"@",previousText:"@",nextText:"@",lastText:"@"},require:["pagination","?ngModel"],controller:"PaginationController",templateUrl:"template/pagination/pagination.html",replace:!0,link:function(e,t,n,r){function c(e,t,n){return{number:e,text:t,active:n}}var i=r[0],o=r[1];if(o){var d=angular.isDefined(n.maxSize)?e.$parent.$eval(n.maxSize):u.maxSize,p=angular.isDefined(n.rotate)?e.$parent.$eval(n.rotate):u.rotate;e.boundaryLinks=angular.isDefined(n.boundaryLinks)?e.$parent.$eval(n.boundaryLinks):u.boundaryLinks,e.directionLinks=angular.isDefined(n.directionLinks)?e.$parent.$eval(n.directionLinks):u.directionLinks,i.init(o,u),n.maxSize&&e.$parent.$watch(s(n.maxSize),function(e){d=parseInt(e,10),i.render()});var a=i.render;i.render=function(){a(),0';return{restrict:"EA",compile:function(){var _=o(i);return function(e,t,n){function r(){m.isOpen?o():i()}function i(){(!g||e.$eval(n[C+"Enable"]))&&(function(){var e=n[C+"Placement"];m.placement=angular.isDefined(e)?e:D.placement}(),function(){var e=n[C+"PopupDelay"],t=parseInt(e,10);m.popupDelay=isNaN(t)?D.popupDelay:t}(),m.popupDelay?p||(p=x(a,m.popupDelay,!1)).then(function(e){e()}):a()())}function o(){e.$apply(function(){s()})}function a(){return p=null,d&&(x.cancel(d),d=null),m.content?(l&&u(),c=m.$new(),(l=_(c,function(e){h?O.find("body").append(e):t.after(e)})).css({top:0,left:0,display:"block"}),m.$digest(),v(),m.isOpen=!0,m.$digest(),v):angular.noop}function s(){m.isOpen=!1,x.cancel(p),p=null,m.animation?d||(d=x(u,500)):u()}function u(){d=null,l&&(l.remove(),l=null),c&&(c.$destroy(),c=null)}var l,c,d,p,h=!!angular.isDefined(D.appendToBody)&&D.appendToBody,f=k(void 0),g=angular.isDefined(n[C+"Enable"]),m=e.$new(!0),v=function(){var e=T.positionElements(t,l,m.placement,h);e.top+="px",e.left+="px",l.css(e)};m.isOpen=!1,n.$observe(S,function(e){!(m.content=e)&&m.isOpen&&s()}),n.$observe(C+"Title",function(e){m.title=e});var y,w=function(){t.unbind(f.show,i),t.unbind(f.hide,o)};y=n[C+"Trigger"],w(),(f=k(y)).show===f.hide?t.bind(f.show,r):(t.bind(f.show,i),t.bind(f.hide,o));var b=e.$eval(n[C+"Animation"]);m.animation=angular.isDefined(b)?!!b:D.animation;var $=e.$eval(n[C+"AppendToBody"]);(h=angular.isDefined($)?$:h)&&e.$on("$locationChangeSuccess",function(){m.isOpen&&s()}),e.$on("$destroy",function(){x.cancel(d),x.cancel(p),w(),u(),m=null})}}}}}]}).directive("tooltipPopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-popup.html"}}).directive("tooltip",["$tooltip",function(e){return e("tooltip","tooltip","mouseenter")}]).directive("tooltipHtmlUnsafePopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-unsafe-popup.html"}}).directive("tooltipHtmlUnsafe",["$tooltip",function(e){return e("tooltipHtmlUnsafe","tooltip","mouseenter")}]),angular.module("ui.bootstrap.popover",["ui.bootstrap.tooltip"]).directive("popoverPopup",function(){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover.html"}}).directive("popover",["$tooltip",function(e){return e("popover","popover","click")}]),angular.module("ui.bootstrap.progressbar",[]).constant("progressConfig",{animate:!0,max:100}).controller("ProgressController",["$scope","$attrs","progressConfig",function(n,e,t){var r=this,i=angular.isDefined(e.animate)?n.$parent.$eval(e.animate):t.animate;this.bars=[],n.max=angular.isDefined(e.max)?n.$parent.$eval(e.max):t.max,this.addBar=function(t,e){i||e.css({transition:"none"}),this.bars.push(t),t.$watch("value",function(e){t.percent=+(100*e/n.max).toFixed(2)}),t.$on("$destroy",function(){e=null,r.removeBar(t)})},this.removeBar=function(e){this.bars.splice(this.bars.indexOf(e),1)}}]).directive("progress",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",require:"progress",scope:{},templateUrl:"template/progressbar/progress.html"}}).directive("bar",function(){return{restrict:"EA",replace:!0,transclude:!0,require:"^progress",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/bar.html",link:function(e,t,n,r){r.addBar(e,t)}}}).directive("progressbar",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/progressbar.html",link:function(e,t,n,r){r.addBar(e,angular.element(t.children()[0]))}}}),angular.module("ui.bootstrap.rating",[]).constant("ratingConfig",{max:5,stateOn:null,stateOff:null}).controller("RatingController",["$scope","$attrs","ratingConfig",function(n,r,i){var o={$setViewValue:angular.noop};this.init=function(e){(o=e).$render=this.render,this.stateOn=angular.isDefined(r.stateOn)?n.$parent.$eval(r.stateOn):i.stateOn,this.stateOff=angular.isDefined(r.stateOff)?n.$parent.$eval(r.stateOff):i.stateOff;var t=angular.isDefined(r.ratingStates)?n.$parent.$eval(r.ratingStates):new Array(angular.isDefined(r.max)?n.$parent.$eval(r.max):i.max);n.range=this.buildTemplateObjects(t)},this.buildTemplateObjects=function(e){for(var t=0,n=e.length;t");v.attr({id:m,matches:"matches",active:"activeIdx",select:"select(activeIdx)",query:"query",position:"position"}),angular.isDefined(e.typeaheadTemplateUrl)&&v.attr("template-url",e.typeaheadTemplateUrl);var y=function(){g.matches=[],g.activeIdx=-1,a.attr("aria-expanded",!1)},w=function(e){return m+"-option-"+e};g.$watch("activeIdx",function(e){e<0?a.removeAttr("aria-activedescendant"):a.attr("aria-activedescendant",w(e))});var b=function(r){var i={$viewValue:r};l(o,!0),x.when(f.source(o,i)).then(function(e){var t=r===s.$viewValue;if(t&&u)if(0=t?0$&"):e}}),angular.module("template/accordion/accordion-group.html",[]).run(["$templateCache",function(e){e.put("template/accordion/accordion-group.html",'
\n
\n

\n {{heading}}\n

\n
\n
\n\t
\n
\n
\n')}]),angular.module("template/accordion/accordion.html",[]).run(["$templateCache",function(e){e.put("template/accordion/accordion.html",'
')}]),angular.module("template/alert/alert.html",[]).run(["$templateCache",function(e){e.put("template/alert/alert.html",'\n')}]),angular.module("template/carousel/carousel.html",[]).run(["$templateCache",function(e){e.put("template/carousel/carousel.html",'\n')}]),angular.module("template/carousel/slide.html",[]).run(["$templateCache",function(e){e.put("template/carousel/slide.html","
\n")}]),angular.module("template/datepicker/datepicker.html",[]).run(["$templateCache",function(e){e.put("template/datepicker/datepicker.html",'
\n \n \n \n
')}]),angular.module("template/datepicker/day.html",[]).run(["$templateCache",function(e){e.put("template/datepicker/day.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
{{label.abbr}}
{{ weekNumbers[$index] }}\n \n
\n')}]),angular.module("template/datepicker/month.html",[]).run(["$templateCache",function(e){e.put("template/datepicker/month.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n
\n \n
\n')}]),angular.module("template/datepicker/popup.html",[]).run(["$templateCache",function(e){e.put("template/datepicker/popup.html",'\n')}]),angular.module("template/datepicker/year.html",[]).run(["$templateCache",function(e){e.put("template/datepicker/year.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n
\n \n
\n')}]),angular.module("template/modal/backdrop.html",[]).run(["$templateCache",function(e){e.put("template/modal/backdrop.html",'\n')}]),angular.module("template/modal/window.html",[]).run(["$templateCache",function(e){e.put("template/modal/window.html",'')}]),angular.module("template/pagination/pager.html",[]).run(["$templateCache",function(e){e.put("template/pagination/pager.html",'')}]),angular.module("template/pagination/pagination.html",[]).run(["$templateCache",function(e){e.put("template/pagination/pagination.html",'')}]),angular.module("template/tooltip/tooltip-html-unsafe-popup.html",[]).run(["$templateCache",function(e){e.put("template/tooltip/tooltip-html-unsafe-popup.html",'
\n
\n
\n
\n')}]),angular.module("template/tooltip/tooltip-popup.html",[]).run(["$templateCache",function(e){e.put("template/tooltip/tooltip-popup.html",'
\n
\n
\n
\n')}]),angular.module("template/popover/popover.html",[]).run(["$templateCache",function(e){e.put("template/popover/popover.html",'
\n
\n\n
\n

\n
\n
\n
\n')}]),angular.module("template/progressbar/bar.html",[]).run(["$templateCache",function(e){e.put("template/progressbar/bar.html",'
')}]),angular.module("template/progressbar/progress.html",[]).run(["$templateCache",function(e){e.put("template/progressbar/progress.html",'
')}]),angular.module("template/progressbar/progressbar.html",[]).run(["$templateCache",function(e){e.put("template/progressbar/progressbar.html",'
\n
\n
')}]),angular.module("template/rating/rating.html",[]).run(["$templateCache",function(e){e.put("template/rating/rating.html",'\n \n ({{ $index < value ? \'*\' : \' \' }})\n \n')}]),angular.module("template/tabs/tab.html",[]).run(["$templateCache",function(e){e.put("template/tabs/tab.html",'
  • \n {{heading}}\n
  • \n')}]),angular.module("template/tabs/tabset.html",[]).run(["$templateCache",function(e){e.put("template/tabs/tabset.html",'
    \n \n
    \n
    \n
    \n
    \n
    \n')}]),angular.module("template/timepicker/timepicker.html",[]).run(["$templateCache",function(e){e.put("template/timepicker/timepicker.html",'\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n
     
    \n\t\t\t\t\n\t\t\t:\n\t\t\t\t\n\t\t\t
     
    \n')}]),angular.module("template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(e){e.put("template/typeahead/typeahead-match.html",'')}]),angular.module("template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(e){e.put("template/typeahead/typeahead-popup.html",'\n')}]),function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.moment=t()}(this,function(){"use strict";var e,i;function p(){return e.apply(null,arguments)}function a(e){return e instanceof Array||"[object Array]"===Object.prototype.toString.call(e)}function s(e){return null!=e&&"[object Object]"===Object.prototype.toString.call(e)}function o(e){return void 0===e}function u(e){return"number"==typeof e||"[object Number]"===Object.prototype.toString.call(e)}function l(e){return e instanceof Date||"[object Date]"===Object.prototype.toString.call(e)}function r(e,t){var n,r=[];for(n=0;n>>0,r=0;rSe(e)?(o=e+1,s-Se(e)):(o=e,s),{year:o,dayOfYear:a}}function Ye(e,t,n){var r,i,o=He(e.year(),t,n),a=Math.floor((e.dayOfYear()-o-1)/7)+1;return a<1?r=a+Ue(i=e.year()-1,t,n):a>Ue(e.year(),t,n)?(r=a-Ue(e.year(),t,n),i=e.year()+1):(i=e.year(),r=a),{week:r,year:i}}function Ue(e,t,n){var r=He(e,t,n),i=He(e+1,t,n);return(Se(e)-r+i)/7}Y("w",["ww",2],"wo","week"),Y("W",["WW",2],"Wo","isoWeek"),P("week","w"),P("isoWeek","W"),V("week",5),V("isoWeek",5),ue("w",Q),ue("ww",Q,B),ue("W",Q),ue("WW",Q,B),he(["w","ww","W","WW"],function(e,t,n,r){t[r.substr(0,1)]=_(e)});function qe(e,t){return e.slice(t,7).concat(e.slice(0,t))}Y("d",0,"do","day"),Y("dd",0,0,function(e){return this.localeData().weekdaysMin(this,e)}),Y("ddd",0,0,function(e){return this.localeData().weekdaysShort(this,e)}),Y("dddd",0,0,function(e){return this.localeData().weekdays(this,e)}),Y("e",0,0,"weekday"),Y("E",0,0,"isoWeekday"),P("day","d"),P("weekday","e"),P("isoWeekday","E"),V("day",11),V("weekday",11),V("isoWeekday",11),ue("d",Q),ue("e",Q),ue("E",Q),ue("dd",function(e,t){return t.weekdaysMinRegex(e)}),ue("ddd",function(e,t){return t.weekdaysShortRegex(e)}),ue("dddd",function(e,t){return t.weekdaysRegex(e)}),he(["dd","ddd","dddd"],function(e,t,n,r){var i=n._locale.weekdaysParse(e,r,n._strict);null!=i?t.d=i:f(n).invalidWeekday=e}),he(["d","e","E"],function(e,t,n,r){t[r]=_(e)});var ze="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_");var Be="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_");var We="Su_Mo_Tu_We_Th_Fr_Sa".split("_");var Ge=ae;var Ke=ae;var Qe=ae;function Ze(){function e(e,t){return t.length-e.length}var t,n,r,i,o,a=[],s=[],u=[],l=[];for(t=0;t<7;t++)n=d([2e3,1]).day(t),r=this.weekdaysMin(n,""),i=this.weekdaysShort(n,""),o=this.weekdays(n,""),a.push(r),s.push(i),u.push(o),l.push(r),l.push(i),l.push(o);for(a.sort(e),s.sort(e),u.sort(e),l.sort(e),t=0;t<7;t++)s[t]=ce(s[t]),u[t]=ce(u[t]),l[t]=ce(l[t]);this._weekdaysRegex=new RegExp("^("+l.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+u.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+s.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+a.join("|")+")","i")}function Xe(){return this.hours()%12||12}function Je(e,t){Y(e,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),t)})}function et(e,t){return t._meridiemParse}Y("H",["HH",2],0,"hour"),Y("h",["hh",2],0,Xe),Y("k",["kk",2],0,function(){return this.hours()||24}),Y("hmm",0,0,function(){return""+Xe.apply(this)+L(this.minutes(),2)}),Y("hmmss",0,0,function(){return""+Xe.apply(this)+L(this.minutes(),2)+L(this.seconds(),2)}),Y("Hmm",0,0,function(){return""+this.hours()+L(this.minutes(),2)}),Y("Hmmss",0,0,function(){return""+this.hours()+L(this.minutes(),2)+L(this.seconds(),2)}),Je("a",!0),Je("A",!1),P("hour","h"),V("hour",13),ue("a",et),ue("A",et),ue("H",Q),ue("h",Q),ue("k",Q),ue("HH",Q,B),ue("hh",Q,B),ue("kk",Q,B),ue("hmm",Z),ue("hmmss",X),ue("Hmm",Z),ue("Hmmss",X),pe(["H","HH"],ve),pe(["k","kk"],function(e,t,n){var r=_(e);t[ve]=24===r?0:r}),pe(["a","A"],function(e,t,n){n._isPm=n._locale.isPM(e),n._meridiem=e}),pe(["h","hh"],function(e,t,n){t[ve]=_(e),f(n).bigHour=!0}),pe("hmm",function(e,t,n){var r=e.length-2;t[ve]=_(e.substr(0,r)),t[ye]=_(e.substr(r)),f(n).bigHour=!0}),pe("hmmss",function(e,t,n){var r=e.length-4,i=e.length-2;t[ve]=_(e.substr(0,r)),t[ye]=_(e.substr(r,2)),t[we]=_(e.substr(i)),f(n).bigHour=!0}),pe("Hmm",function(e,t,n){var r=e.length-2;t[ve]=_(e.substr(0,r)),t[ye]=_(e.substr(r))}),pe("Hmmss",function(e,t,n){var r=e.length-4,i=e.length-2;t[ve]=_(e.substr(0,r)),t[ye]=_(e.substr(r,2)),t[we]=_(e.substr(i))});var tt,nt=xe("Hours",!0),rt={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Pe,monthsShort:Me,week:{dow:0,doy:6},weekdays:ze,weekdaysMin:We,weekdaysShort:Be,meridiemParse:/[ap]\.?m?\.?/i},it={},ot={};function at(e){return e?e.toLowerCase().replace("_","-"):e}function st(e){var t=null;if(!it[e]&&"undefined"!=typeof module&&module&&module.exports)try{t=tt._abbr,require("./locale/"+e),ut(t)}catch(e){}return it[e]}function ut(e,t){var n;return e&&((n=o(t)?ct(e):lt(e,t))?tt=n:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+e+" not found. Did you forget to load it?")),tt._abbr}function lt(e,t){if(null===t)return delete it[e],null;var n,r=rt;if(t.abbr=e,null!=it[e])x("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),r=it[e]._config;else if(null!=t.parentLocale)if(null!=it[t.parentLocale])r=it[t.parentLocale]._config;else{if(null==(n=st(t.parentLocale)))return ot[t.parentLocale]||(ot[t.parentLocale]=[]),ot[t.parentLocale].push({name:e,config:t}),null;r=n._config}return it[e]=new A(T(r,t)),ot[e]&&ot[e].forEach(function(e){lt(e.name,e.config)}),ut(e),it[e]}function ct(e){var t;if(e&&e._locale&&e._locale._abbr&&(e=e._locale._abbr),!e)return tt;if(!a(e)){if(t=st(e))return t;e=[e]}return function(e){for(var t,n,r,i,o=0;o=t&&S(i,n,!0)>=t-1)break;t--}o++}return tt}(e)}function dt(e){var t,n=e._a;return n&&-2===f(e).overflow&&(t=n[ge]<0||11Ae(n[fe],n[ge])?me:n[ve]<0||24Ue(n,o,a)?f(e)._overflowWeeks=!0:null!=u?f(e)._overflowWeekday=!0:(s=Ne(n,r,i,o,a),e._a[fe]=s.year,e._dayOfYear=s.dayOfYear)}(e),null!=e._dayOfYear&&(o=pt(e._a[fe],r[fe]),(e._dayOfYear>Se(o)||0===e._dayOfYear)&&(f(e)._overflowDayOfYear=!0),n=je(o,0,e._dayOfYear),e._a[ge]=n.getUTCMonth(),e._a[me]=n.getUTCDate()),t=0;t<3&&null==e._a[t];++t)e._a[t]=a[t]=r[t];for(;t<7;t++)e._a[t]=a[t]=null==e._a[t]?2===t?1:0:e._a[t];24===e._a[ve]&&0===e._a[ye]&&0===e._a[we]&&0===e._a[be]&&(e._nextDay=!0,e._a[ve]=0),e._d=(e._useUTC?je:function(e,t,n,r,i,o,a){var s;return e<100&&0<=e?(s=new Date(e+400,t,n,r,i,o,a),isFinite(s.getFullYear())&&s.setFullYear(e)):s=new Date(e,t,n,r,i,o,a),s}).apply(null,a),i=e._useUTC?e._d.getUTCDay():e._d.getDay(),null!=e._tzm&&e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),e._nextDay&&(e._a[ve]=24),e._w&&void 0!==e._w.d&&e._w.d!==i&&(f(e).weekdayMismatch=!0)}}var ft=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,gt=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,mt=/Z|[+-]\d\d(?::?\d\d)?/,vt=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],yt=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],wt=/^\/?Date\((\-?\d+)/i;function bt(e){var t,n,r,i,o,a,s=e._i,u=ft.exec(s)||gt.exec(s);if(u){for(f(e).iso=!0,t=0,n=vt.length;tn.valueOf():n.valueOf()this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},fn.isLocal=function(){return!!this.isValid()&&!this._isUTC},fn.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},fn.isUtc=Nt,fn.isUTC=Nt,fn.zoneAbbr=function(){return this._isUTC?"UTC":""},fn.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},fn.dates=n("dates accessor is deprecated. Use date instead.",un),fn.months=n("months accessor is deprecated. Use month instead",Ie),fn.years=n("years accessor is deprecated. Use year instead",De),fn.zone=n("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function(e,t){return null!=e?("string"!=typeof e&&(e=-e),this.utcOffset(e,t),this):-this.utcOffset()}),fn.isDSTShifted=n("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function(){if(!o(this._isDSTShifted))return this._isDSTShifted;var e={};if(y(e,this),(e=Dt(e))._a){var t=e._isUTC?d(e._a):Ot(e._a);this._isDSTShifted=this.isValid()&&0-1/0&&t.selectable&&c[e]){var r=c[e](t.utcDateValue),i=[];if(r.weeks)for(var o=0;o9+n,past:l.year()r.indexOf(e.startView))throw new Error("startView must be greater than minView");if(!a.isNumber(e.minuteStep))throw new Error("minuteStep must be numeric");if(e.minuteStep<=0||60<=e.minuteStep)throw new Error("minuteStep must be greater than zero and less than 60");if(null!==e.configureOn&&!a.isString(e.configureOn))throw new Error("configureOn must be a string");if(null!==e.configureOn&&e.configureOn.length<1)throw new Error("configureOn must not be an empty string");if(null!==e.renderOn&&!a.isString(e.renderOn))throw new Error("renderOn must be a string");if(null!==e.renderOn&&e.renderOn.length<1)throw new Error("renderOn must not be an empty string");if(null!==e.modelType&&!a.isString(e.modelType))throw new Error("modelType must be a string");if(null!==e.modelType&&e.modelType.length<1)throw new Error("modelType must not be an empty string");"Date"!==e.modelType&&"moment"!==e.modelType&&"milliseconds"!==e.modelType&&(e.parseFormat=e.modelType);if(null!==e.dropdownSelector&&!a.isString(e.dropdownSelector))throw new Error("dropdownSelector must be a string");null===e.dropdownSelector||"undefined"!=typeof jQuery&&"function"==typeof jQuery().dropdown||(i.error("Please DO NOT specify the dropdownSelector option unless you are using jQuery AND Bootstrap.js. Please include jQuery AND Bootstrap.js, or write code to close the dropdown in the on-set-time callback. \n\nThe dropdownSelector configuration option is being removed because it will not function properly."),delete e.dropdownSelector)}}}a.module("ui.bootstrap.datetimepicker",[]).service("dateTimePickerConfig",function(){var e={bg:{previous:"предишна",next:"следваща"},ca:{previous:"anterior",next:"següent"},da:{previous:"forrige",next:"næste"},de:{previous:"vorige",next:"weiter"},"en-au":{previous:"previous",next:"next"},"en-gb":{previous:"previous",next:"next"},en:{previous:"previous",next:"next"},"es-us":{previous:"atrás",next:"siguiente"},es:{previous:"atrás",next:"siguiente"},fi:{previous:"edellinen",next:"seuraava"},fr:{previous:"précédent",next:"suivant"},hu:{previous:"előző",next:"következő"},it:{previous:"precedente",next:"successivo"},ja:{previous:"前へ",next:"次へ"},ml:{previous:"മുൻപുള്ളത്",next:"അടുത്തത്"},nl:{previous:"vorige",next:"volgende"},pl:{previous:"poprzednia",next:"następna"},"pt-br":{previous:"anteriores",next:"próximos"},pt:{previous:"anterior",next:"próximo"},ro:{previous:"anterior",next:"următor"},ru:{previous:"предыдущая",next:"следующая"},sk:{previous:"predošlá",next:"ďalšia"},sv:{previous:"föregående",next:"nästa"},tr:{previous:"önceki",next:"sonraki"},uk:{previous:"назад",next:"далі"},"zh-cn":{previous:"上一页",next:"下一页"},"zh-tw":{previous:"上一頁",next:"下一頁"}}[b.locale().toLowerCase()];return a.extend({},{configureOn:null,dropdownSelector:null,minuteStep:5,minView:"minute",modelType:"Date",parseFormat:"YYYY-MM-DDTHH:mm:ss.SSSZZ",renderOn:null,startView:"day"},{screenReader:e})}).service("dateTimePickerValidator",t).directive("datetimepicker",e),e.$inject=["dateTimePickerConfig","dateTimePickerValidator"],t.$inject=["$log"]}),angular.module("rzTable",[]),angular.module("rzTable").directive("rzTable",["resizeStorage","$injector","$parse",function(s,i,o){function e(e){}function r(n,r,i){return function(e,t){!0!==i.busy&&void 0!==t&&t!==e&&(a(n),u(n,r,i))}}function a(e){_=!0,v.map(function(e){e.remove()}),v=[]}function u(e,t,n){if(!n.busy){h=$(e).find("th"),l=n.mode,d=!angular.isDefined(n.saveTableSizes)||n.saveTableSizes,p=n.profile;var r=function(t,e){try{var n=e.rzMode?t.mode:"BasicResizer",r=i.get(n);return r}catch(e){return console.error("The resizer "+t.mode+" was not found"),null}}(n,t);r&&(b=new r(e,h,w),d&&(S=s.loadTableSizes(e,n.mode,n.profile)),g=b.handles(h),f=b.ctrlColumns,b.setup(),function(o){o&&($(y).width("auto"),f.each(function(e,t){var n=angular.element(t).scope(),r=n.rzCol||$(t).attr("id"),i=o[r];$(t).css({width:i})}),b.onTableReady())}(S),g.each(function(e,t){!function(e,t,n){var r=$("
    ",{class:e.options.handleClass||"rz-handle"});$(n).prepend(r),v.push(r);var i=b.handleMiddleware(r,n);!function(i,e,o,a){$(o).mousedown(function(e){_&&(b.onFirstDrag(a,o),b.onTableReady(),_=!1),i.options.onResizeStarted&&i.options.onResizeStarted(a);var t={};b.intervene&&(((t=b.intervene.selector(a)).column=t).orgWidth=$(t).width()),e.preventDefault(),$(o).addClass(i.options.handleClassActive||"rz-handle-active");var n=e.clientX,r=$(a).width();m=function(o,a,s,u,l){return function(e){var t=e.clientX,n=t-s,r=b.calculate(u,n);if(!(rt)||(this.fixedColumn.width()<=this.getMinWidth(this.fixedColumn)?(this.bound=t,$(this.fixedColumn).width(this.minWidth),!0):void 0)},e.prototype.onEndDrag=function(){this.bound=!1},e.prototype.calculate=function(e,t){return e-t},e}]),angular.module("rzTable").factory("OverflowResizer",["ResizerModel",function(r){function e(e,t,n){r.call(this,e,t,n)}return(e.prototype=Object.create(r.prototype)).setup=function(){$(this.container).css({overflow:"auto"})},e.prototype.onTableReady=function(){$(this.table).width(1)},e}]),function(e,t){"function"==typeof define&&define.amd?define(["angular"],t):"object"==typeof module&&module.exports?module.exports=t(require("angular")):e.angularClipboard=t(e.angular)}(this,function(i){return i.module("angular-clipboard",[]).factory("clipboard",["$document","$window",function(o,a){return{copyText:function(e,t){var n=a.pageXOffset||o[0].documentElement.scrollLeft,r=a.pageYOffset||o[0].documentElement.scrollTop,i=function(e,t){var n=o[0].createElement("textarea");return n.style.position="absolute",n.style.fontSize="12pt",n.style.border="0",n.style.padding="0",n.style.margin="0",n.style.left="-10000px",n.style.top=(a.pageYOffset||o[0].documentElement.scrollTop)+"px",n.textContent=e,n}(e);o[0].body.appendChild(i),function(e){try{o[0].body.style.webkitUserSelect="initial";var t=o[0].getSelection();t.removeAllRanges();var n=document.createRange();n.selectNodeContents(e),t.addRange(n),e.select(),e.setSelectionRange(0,999999);try{if(!o[0].execCommand("copy"))throw"failure copy"}finally{t.removeAllRanges()}}finally{o[0].body.style.webkitUserSelect=""}}(i),a.scrollTo(n,r),o[0].body.removeChild(i)},supported:"queryCommandSupported"in o[0]&&o[0].queryCommandSupported("copy")}}]).directive("clipboard",["clipboard",function(r){return{restrict:"A",scope:{onCopied:"&",onError:"&",text:"=",supported:"=?"},link:function(t,n){t.supported=r.supported,n.on("click",function(e){try{r.copyText(t.text,n[0]),i.isFunction(t.onCopied)&&t.$evalAsync(t.onCopied())}catch(e){i.isFunction(t.onError)&&t.$evalAsync(t.onError({err:e}))}})}}}])}),function(e,t){"function"==typeof define&&define.amd?define("sifter",t):"object"==typeof exports?module.exports=t():e.Sifter=t()}(this,function(){var e=function(e,t){this.items=e,this.settings=t||{diacritics:!0}};e.prototype.tokenize=function(e){if(!(e=s(String(e||"").toLowerCase()))||!e.length)return[];var t,n,r,i,o=[],a=e.split(/ +/);for(t=0,n=a.length;t/g,">").replace(/"/g,""")},t={before:function(e,t,n){var r=e[t];e[t]=function(){return n.apply(e,arguments),r.apply(e,arguments)}},after:function(t,e,n){var r=t[e];t[e]=function(){var e=r.apply(t,arguments);return n.apply(t,arguments),e}}},n=function(t,n,e){var r,i=t.trigger,o={};for(r in t.trigger=function(){var e=arguments[0];if(-1===n.indexOf(e))return i.apply(t,arguments);o[e]=arguments},e.apply(t,[]),t.trigger=i,o)o.hasOwnProperty(r)&&i.apply(t,o[r])},p=function(e){var t={};if("selectionStart"in e)t.start=e.selectionStart,t.length=e.selectionEnd-t.start;else if(document.selection){e.focus();var n=document.selection.createRange(),r=document.selection.createRange().text.length;n.moveStart("character",-e.value.length),t.start=n.text.length-r,t.length=r}return t},$=function(c){var d=null,e=function(e,t){var n,r,i,o,a,s,u,l;t=t||{},(e=e||window.event||{}).metaKey||e.altKey||(t.force||!1!==c.data("grow"))&&(n=c.val(),e.type&&"keydown"===e.type.toLowerCase()&&(i=48<=(r=e.keyCode)&&r<=57||65<=r&&r<=90||96<=r&&r<=111||186<=r&&r<=222||32===r,46===r||8===r?(l=p(c[0])).length?n=n.substring(0,l.start)+n.substring(l.start+l.length):8===r&&l.start?n=n.substring(0,l.start-1)+n.substring(l.start+1):46===r&&void 0!==l.start&&(n=n.substring(0,l.start)+n.substring(l.start+1)):i&&(s=e.shiftKey,u=String.fromCharCode(e.keyCode),n+=u=s?u.toUpperCase():u.toLowerCase())),o=c.attr("placeholder"),!n&&o&&(n=o),(a=function(e,t){return e?(_.$testInput||(_.$testInput=C("").css({position:"absolute",top:-99999,left:-99999,width:"auto",padding:0,whiteSpace:"pre"}).appendTo("body")),_.$testInput.text(e),function(e,t,n){var r,i,o={};if(n)for(r=0,i=n.length;r").addClass(d.wrapperClass).addClass(s).addClass(a),t=C("
    ").addClass(d.inputClass).addClass("items").appendTo(e),n=C('').appendTo(t).attr("tabindex",g.is(":disabled")?"-1":c.tabIndex),o=C(d.dropdownParent||e),r=C("
    ").addClass(d.dropdownClass).addClass(a).hide().appendTo(o),i=C("
    ").addClass(d.dropdownContentClass).appendTo(r),(l=g.attr("id"))&&(n.attr("id",l+"-selectized"),C("label[for='"+l+"']").attr("for",l+"-selectized")),c.settings.copyClassesToDropdown&&r.addClass(s),e.css({width:g[0].style.width}),c.plugins.names.length&&(u="plugin-"+c.plugins.names.join(" plugin-"),e.addClass(u),r.addClass(u)),(null===d.maxItems||1[data-selectable]",function(e){e.stopImmediatePropagation()}),r.on("mouseenter","[data-selectable]",function(){return c.onOptionHover.apply(c,arguments)}),r.on("mousedown click","[data-selectable]",function(){return c.onOptionSelect.apply(c,arguments)}),function(n,e,t,r){n.on(e,t,function(e){for(var t=e.target;t&&t.parentNode!==n[0];)t=t.parentNode;return e.currentTarget=t,r.apply(this,[e])})}(t,"mousedown","*:not(input)",function(){return c.onItemSelect.apply(c,arguments)}),$(n),t.on({mousedown:function(){return c.onMouseDown.apply(c,arguments)},click:function(){return c.onClick.apply(c,arguments)}}),n.on({mousedown:function(e){e.stopPropagation()},keydown:function(){return c.onKeyDown.apply(c,arguments)},keyup:function(){return c.onKeyUp.apply(c,arguments)},keypress:function(){return c.onKeyPress.apply(c,arguments)},resize:function(){c.positionDropdown.apply(c,[])},blur:function(){return c.onBlur.apply(c,arguments)},focus:function(){return c.ignoreBlur=!1,c.onFocus.apply(c,arguments)},paste:function(){return c.onPaste.apply(c,arguments)}}),f.on("keydown"+p,function(e){c.isCmdDown=e[v?"metaKey":"ctrlKey"],c.isCtrlDown=e[v?"altKey":"ctrlKey"],c.isShiftDown=e.shiftKey}),f.on("keyup"+p,function(e){e.keyCode===w&&(c.isCtrlDown=!1),16===e.keyCode&&(c.isShiftDown=!1),e.keyCode===y&&(c.isCmdDown=!1)}),f.on("mousedown"+p,function(e){if(c.isFocused){if(e.target===c.$dropdown[0]||e.target.parentNode===c.$dropdown[0])return!1;c.$control.has(e.target).length||e.target===c.$control[0]||c.blur(e.target)}}),h.on(["scroll"+p,"resize"+p].join(" "),function(){c.isOpen&&c.positionDropdown.apply(c,arguments)}),h.on("mousemove"+p,function(){c.ignoreHover=!1}),this.revertSettings={$children:g.children().detach(),tabindex:g.attr("tabindex")},g.attr("tabindex",-1).hide().after(c.$wrapper),C.isArray(d.items)&&(c.setValue(d.items),delete d.items),b&&g.on("invalid"+p,function(e){e.preventDefault(),c.isInvalid=!0,c.refreshState()}),c.updateOriginalInput(),c.refreshItems(),c.refreshState(),c.updatePlaceholder(),c.isSetup=!0,g.is(":disabled")&&c.disable(),c.on("change",this.onChange),g.data("selectize",c),g.addClass("selectized"),c.trigger("initialize"),!0===d.preload&&c.onSearchChange("")},setupTemplates:function(){var n=this.settings.labelField,r=this.settings.optgroupLabelField,e={optgroup:function(e){return'
    '+e.html+"
    "},optgroup_header:function(e,t){return'
    '+t(e[r])+"
    "},option:function(e,t){return'
    '+t(e[n])+"
    "},item:function(e,t){return'
    '+t(e[n])+"
    "},option_create:function(e,t){return'
    Add '+t(e.input)+"
    "}};this.settings.render=C.extend({},e,this.settings.render)},setupCallbacks:function(){var e,t,n={initialize:"onInitialize",change:"onChange",item_add:"onItemAdd",item_remove:"onItemRemove",clear:"onClear",option_add:"onOptionAdd",option_remove:"onOptionRemove",option_clear:"onOptionClear",optgroup_add:"onOptionGroupAdd",optgroup_remove:"onOptionGroupRemove",optgroup_clear:"onOptionGroupClear",dropdown_open:"onDropdownOpen",dropdown_close:"onDropdownClose",type:"onType",load:"onLoad",focus:"onFocus",blur:"onBlur"};for(e in n)n.hasOwnProperty(e)&&(t=this.settings[n[e]])&&this.on(e,t)},onClick:function(e){this.isFocused&&this.isOpen||(this.focus(),e.preventDefault())},onMouseDown:function(e){var t=this,n=e.isDefaultPrevented();C(e.target);if(t.isFocused){if(e.target!==t.$control_input[0])return"single"===t.settings.mode?t.isOpen?t.close():t.open():n||t.setActiveItem(null),!1}else n||window.setTimeout(function(){t.focus()},0)},onChange:function(){this.$input.trigger("change")},onPaste:function(e){var i=this;i.isFull()||i.isInputHidden||i.isLocked?e.preventDefault():i.settings.splitOn&&setTimeout(function(){var e=i.$control_input.val();if(e.match(i.settings.splitOn))for(var t=C.trim(e).split(i.settings.splitOn),n=0,r=t.length;n=this.settings.maxItems},updateOriginalInput:function(e){var t,n,r,i,o=this;if(e=e||{},1===o.tagType){for(r=[],t=0,n=o.items.length;t'+s(i)+"");r.length||this.$input.attr("multiple")||r.push(''),o.$input.html(r.join(""))}else o.$input.val(o.getValue()),o.$input.attr("value",o.$input.val());o.isSetup&&(e.silent||o.trigger("change",o.$input.val()))},updatePlaceholder:function(){if(this.settings.placeholder){var e=this.$control_input;this.items.length?e.removeAttr("placeholder"):e.attr("placeholder",this.settings.placeholder),e.triggerHandler("update",{force:!0})}},open:function(){var e=this;e.isLocked||e.isOpen||"multi"===e.settings.mode&&e.isFull()||(e.focus(),e.isOpen=!0,e.refreshState(),e.$dropdown.css({visibility:"hidden",display:"block"}),e.positionDropdown(),e.$dropdown.css({visibility:"visible"}),e.trigger("dropdown_open",e.$dropdown))},close:function(){var e=this,t=e.isOpen;"single"===e.settings.mode&&e.items.length&&(e.hideInput(),e.isBlurring||e.$control_input.blur()),e.isOpen=!1,e.$dropdown.hide(),e.setActiveOption(null),e.refreshState(),t&&e.trigger("dropdown_close",e.$dropdown)},positionDropdown:function(){var e=this.$control,t="body"===this.settings.dropdownParent?e.offset():e.position();t.top+=e.outerHeight(!0),this.$dropdown.css({width:e[0].getBoundingClientRect().width,top:t.top,left:t.left})},clear:function(e){var t=this;t.items.length&&(t.$control.children(":not(input)").remove(),t.items=[],t.lastQuery=null,t.setCaret(0),t.setActiveItem(null),t.updatePlaceholder(),t.updateOriginalInput({silent:e}),t.refreshState(),t.showInput(),t.trigger("clear"))},insertAtCaret:function(e){var t=Math.min(this.caretPos,this.items.length),n=e[0],r=this.buffer||this.$control[0];0===t?r.insertBefore(n,r.firstChild):r.insertBefore(n,r.childNodes[t]),this.setCaret(t+1)},deleteSelection:function(e){var t,n,r,i,o,a,s,u,l,c=this;if(r=e&&8===e.keyCode?-1:1,i=p(c.$control_input[0]),c.$activeOption&&!c.settings.hideSelected&&(s=c.getAdjacentOption(c.$activeOption,-1).attr("data-value")),o=[],c.$activeItems.length){for(l=c.$control.children(".active:"+(0
    '+e.title+'×
    '}},e),n.setup=(t=n.setup,function(){t.apply(n,arguments),n.$dropdown_header=C(e.html(e)),n.$dropdown.prepend(n.$dropdown_header)})}),_.define("optgroup_columns",function(s){var o,u=this;s=C.extend({equalizeWidth:!0,equalizeHeight:!0},s),this.getAdjacentOption=function(e,t){var n=e.closest("[data-group]").find("[data-selectable]"),r=n.index(e)+t;return 0<=r&&r
    ',e=e.firstChild,n.body.appendChild(e),t=l.width=e.offsetWidth-e.clientWidth,n.body.removeChild(e)),t},e=function(){var e,t,n,r,i,o,a;if((t=(a=C("[data-group]",u.$dropdown_content)).length)&&u.$dropdown_content.width()){if(s.equalizeHeight){for(e=n=0;e'+r.label+"",n.setup=(i=o.setup,function(){if(r.append){var t=o.settings.render.item;o.settings.render.item=function(e){return function(e,t){var n=e.search(/(<\/[^>]+>\s*)$/);return e.substring(0,n)+t+e.substring(n)}(t.apply(n,arguments),a)}}i.apply(n,arguments),n.$control.on("click","."+r.className,function(e){if(e.preventDefault(),!o.isLocked){var t=C(e.currentTarget).parent();o.setActiveItem(t),o.deleteSelection()&&o.setCaret(o.items.length)}})})):function(n,r){r.className="remove-single";var i,o=n,a=''+r.label+"";n.setup=(i=o.setup,function(){if(r.append){var e=C(o.$input.context).attr("id"),t=(C("#"+e),o.settings.render.item);o.settings.render.item=function(e){return function(e,t){return C("").append(e).append(t)}(t.apply(n,arguments),a)}}i.apply(n,arguments),n.$control.on("click","."+r.className,function(e){e.preventDefault(),o.isLocked||o.clear()})})}(this,e)}),_.define("restore_on_backspace",function(r){var i,e=this;r.text=r.text||function(e){return e[this.settings.labelField]},this.onKeyDown=(i=e.onKeyDown,function(e){var t,n;return 8===e.keyCode&&""===this.$control_input.val()&&!this.$activeItems.length&&0<=(t=this.caretPos-1)&&t",{class:function(){var e=[];return e.push(i.options.state?"on":"off"),i.options.size&&e.push(i.options.size),i.options.disabled&&e.push("disabled"),i.options.readonly&&e.push("readonly"),i.options.indeterminate&&e.push("indeterminate"),i.options.inverse&&e.push("inverse"),i.$element.attr("id")&&e.push("id-"+i.$element.attr("id")),e.map(i._getClass.bind(i)).concat([i.options.baseClass],i._getClasses(i.options.wrapperClass)).join(" ")}}),this.$container=s("
    ",{class:this._getClass("container")}),this.$on=s("",{html:this.options.onText,class:this._getClass("handle-on")+" "+this._getClass(this.options.onColor)}),this.$off=s("",{html:this.options.offText,class:this._getClass("handle-off")+" "+this._getClass(this.options.offColor)}),this.$label=s("",{html:this.options.labelText,class:this._getClass("label")}),this.$element.on("init.bootstrapSwitch",this.options.onInit.bind(this,r)),this.$element.on("switchChange.bootstrapSwitch",function(){for(var e=arguments.length,t=Array(e),n=0;n-n._handleWidth/2;n._dragEnd=!1,n.state(n.options.inverse?!t:t)}else n.state(!n.options.state);n._dragStart=!1}},"mouseleave.bootstrapSwitch":function(){n.$label.trigger("mouseup.bootstrapSwitch")}})}},{key:"_externalLabelHandler",value:function(){var t=this,n=this.$element.closest("label");n.on("click",function(e){e.preventDefault(),e.stopImmediatePropagation(),e.target===n[0]&&t.toggleState()})}},{key:"_formHandler",value:function(){var e=this.$element.closest("form");e.data("bootstrap-switch")||e.on("reset.bootstrapSwitch",function(){window.setTimeout(function(){e.find("input").filter(function(){return s(this).data("bootstrap-switch")}).each(function(){return s(this).bootstrapSwitch("state",this.checked)})},1)}).data("bootstrap-switch",!0)}},{key:"_getClass",value:function(e){return this.options.baseClass+"-"+e}},{key:"_getClasses",value:function(e){return s.isArray(e)?e.map(this._getClass.bind(this)):[this._getClass(e)]}}]),t}();s.fn.bootstrapSwitch=function(o){for(var e=arguments.length,a=Array(1
    ');var r,i=f.overlay?"":" ngdialog-no-overlay";if((d=T('
    ')).html(f.overlay?'
    '+t+"
    ":'
    '+t+"
    "),d.data("$ngDialogOptions",f),c.ngDialogId=l,f.data&&O.isString(f.data)){var o=f.data.replace(/^\s*/,"")[0];c.ngDialogData="{"===o||"["===o?O.fromJson(f.data):new String(f.data),c.ngDialogData.ngDialogId=l}else f.data&&O.isObject(f.data)&&(c.ngDialogData=f.data,c.ngDialogData.ngDialogId=l);(f.className&&d.addClass(f.className),f.appendClassName&&d.addClass(f.appendClassName),f.width&&(h=d[0].querySelector(".ngdialog-content"),O.isString(f.width)?h.style.width=f.width:h.style.width=f.width+"px"),f.height&&(h=d[0].querySelector(".ngdialog-content"),O.isString(f.height)?h.style.height=f.height:h.style.height=f.height+"px"),f.disableAnimation&&d.addClass("ngdialog-disabled-animation"),p=f.appendTo&&O.isString(f.appendTo)?O.element(document.querySelector(f.appendTo)):b.body,$.applyAriaAttributes(d,f),f.preCloseCallback)&&(O.isFunction(f.preCloseCallback)?r=f.preCloseCallback:O.isString(f.preCloseCallback)&&c&&(O.isFunction(c[f.preCloseCallback])?r=c[f.preCloseCallback]:c.$parent&&O.isFunction(c.$parent[f.preCloseCallback])?r=c.$parent[f.preCloseCallback]:m&&O.isFunction(m[f.preCloseCallback])&&(r=m[f.preCloseCallback])),r&&d.data("$ngDialogPreCloseCallback",r));if(c.closeThisDialog=function(e){$.closeDialog(d,e)},f.controller&&(O.isString(f.controller)||O.isArray(f.controller)||O.isFunction(f.controller))){var a;f.controllerAs&&O.isString(f.controllerAs)&&(a=f.controllerAs);var s=w(f.controller,O.extend(n,{$scope:c,$element:d}),!0,a);f.bindToController&&O.extend(s.instance,{ngDialogId:c.ngDialogId,ngDialogData:c.ngDialogData,closeThisDialog:c.closeThisDialog,confirm:c.confirm}),"function"==typeof s?d.data("$ngDialogControllerController",s()):d.data("$ngDialogControllerController",s)}if(v(function(){var e=document.querySelectorAll(".ngdialog");$.deactivateAll(e),g(d)(c);var t=y.innerWidth-b.body.prop("clientWidth");b.html.addClass(f.bodyClassName),b.body.addClass(f.bodyClassName);var n=t-(y.innerWidth-b.body.prop("clientWidth"));0window.innerHeight&&(u=v,t++,e=0);var l=u?0===e?u:u+w:v,c=n+t*(b+s);o.css(o._positionY,l+"px"),"center"==o._positionX?o.css("left",parseInt(window.innerWidth/2-s/2)+"px"):o.css(o._positionX,c+"px"),r[o._positionY+o._positionX]=l+a,0m.maxCount&&0===i&&o.scope().kill(!0),e++}}},i=c(e)(n);i._positionY=h.positionY,i._positionX=h.positionX,i._priority=h.priority,i.addClass(h.type);var o=function(e){("click"===(e=e.originalEvent||e).type||"opacity"===e.propertyName&&1<=e.elapsedTime)&&(n.onClose&&n.$apply(n.onClose(i)),i.remove(),$.splice($.indexOf(i),1),n.$destroy(),r())};h.closeOnClick&&(i.addClass("clickable"),i.bind("click",o)),i.bind("webkitTransitionEnd oTransitionEnd otransitionend transitionend msTransitionEnd",o),angular.isNumber(h.delay)&&l(function(){i.addClass("killed")},h.delay),t("none"),angular.element(document.querySelector(h.container)).append(i);var a=-(parseInt(i[0].offsetHeight)+50);if(i.css(i._positionY,a+"px"),$.push(i),"center"==h.positionX){var s=parseInt(i[0].offsetWidth);i.css("left",parseInt(window.innerWidth/2-s/2)+"px")}l(function(){t("")}),n._templateElement=i,n.kill=function(e){e?(n.onClose&&n.$apply(n.onClose(n._templateElement)),$.splice($.indexOf(n._templateElement),1),n._templateElement.remove(),n.$destroy(),l(r)):n._templateElement.addClass("killed")},l(r),_||(angular.element(g).bind("resize",function(e){l(r)}),_=!0),u.resolve(n)}var u=a.defer();"object"==typeof h&&null!==h||(h={message:h}),h.scope=h.scope?h.scope:o,h.template=h.templateUrl?h.templateUrl:m.templateUrl,h.delay=angular.isUndefined(h.delay)?s:h.delay,h.type=e||h.type||m.type||"",h.positionY=h.positionY?h.positionY:m.positionY,h.positionX=h.positionX?h.positionX:m.positionX,h.replaceMessage=h.replaceMessage?h.replaceMessage:m.replaceMessage,h.onClose=h.onClose?h.onClose:m.onClose,h.closeOnClick=null!==h.closeOnClick&&void 0!==h.closeOnClick?h.closeOnClick:m.closeOnClick,h.container=h.container?h.container:m.container,h.priority=h.priority?h.priority:m.priority;var n=i.get(h.template);return n?t(n):r.get(h.template,{cache:!0}).then(function(e){t(e.data)}).catch(function(e){throw new Error("Template ("+h.template+") could not be loaded. "+e)}),u.promise};return t.primary=function(e){return this(e,"primary")},t.error=function(e){return this(e,"error")},t.success=function(e){return this(e,"success")},t.info=function(e){return this(e,"info")},t.warning=function(e){return this(e,"warning")},t.clearAll=function(){angular.forEach($,function(e){e.addClass("killed")})},t}]}),angular.module("ui-notification").run(["$templateCache",function(e){e.put("angular-ui-notification.html",'

    ')}]),function(){var g="__default";angular.module("angularUtils.directives.dirPagination",[]).directive("dirPaginate",["$compile","$parse","paginationService",function(l,c,d){return{terminal:!0,multiElement:!0,priority:100,compile:function(e,t){var s=t.dirPaginate,n=s.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/),r=/\|\s*itemsPerPage\s*:\s*(.*\(\s*\w*\)|([^\)]*?(?=\s+as\s+))|[^\)]*)/;if(null===n[2].match(r))throw"pagination directive: the 'itemsPerPage' filter must be set.";var i=n[2].replace(r,""),u=c(i);!function(e){angular.forEach(e,function(e){1===e.nodeType&&angular.element(e).attr("dir-paginate-no-compile",!0)})}(e);var o=t.paginationId||g;return d.registerInstance(o),function(e,t,n){var r=c(n.paginationId)(e)||n.paginationId||g;d.registerInstance(r);var i=function(e,t){var n=!!e.match(/(\|\s*itemsPerPage\s*:[^|]*:[^|]*)/);return t===g||n?e:e.replace(/(\|\s*itemsPerPage\s*:\s*[^|\s]*)/,"$1 : '"+t+"'")}(s,r);!function(e,t,n){e[0].hasAttribute("dir-paginate-start")||e[0].hasAttribute("data-dir-paginate-start")?(t.$set("ngRepeatStart",n),e.eq(e.length-1).attr("ng-repeat-end",!0)):t.$set("ngRepeat",n)}(t,n,i),function(e){angular.forEach(e,function(e){1===e.nodeType&&angular.element(e).removeAttr("dir-paginate-no-compile")}),e.eq(0).removeAttr("dir-paginate-start").removeAttr("dir-paginate").removeAttr("data-dir-paginate-start").removeAttr("data-dir-paginate"),e.eq(e.length-1).removeAttr("dir-paginate-end").removeAttr("data-dir-paginate-end")}(t);var o=l(t),a=function(e,t,n){var r;if(t.currentPage)r=c(t.currentPage);else{var i=(n+"__currentPage").replace(/\W/g,"_");e[i]=1,r=c(i)}return r}(e,n,r);d.setCurrentPageParser(r,a,e),void 0!==n.totalItems?(d.setAsyncModeTrue(r),e.$watch(function(){return c(n.totalItems)(e)},function(e){0<=e&&d.setCollectionLength(r,e)})):(d.setAsyncModeFalse(r),e.$watchCollection(function(){return u(e)},function(e){if(e){var t=e instanceof Array?e.length:Object.keys(e).length;d.setCollectionLength(r,t)}})),o(e)}}}}]).directive("dirPaginateNoCompile",function(){return{priority:5e3,terminal:!0}}).directive("dirPaginationControls",["paginationService","paginationTemplate",function(d,n){var p=/^\d+$/,e={restrict:"AE",scope:{maxSize:"=?",onPageChange:"&?",paginationId:"=?",autoHide:"=?"},link:function(r,e,t){var n=t.paginationId||g,i=r.paginationId||t.paginationId||g;if(!d.isRegistered(i)&&!d.isRegistered(n)){var o=i!==g?" (id: "+i+") ":" ";window.console&&console.warn("Pagination directive: the pagination controls"+o+"cannot be used without the corresponding pagination directive, which was not found at link time.")}r.maxSize||(r.maxSize=9);r.autoHide=void 0===r.autoHide||r.autoHide,r.directionLinks=!angular.isDefined(t.directionLinks)||r.$parent.$eval(t.directionLinks),r.boundaryLinks=!!angular.isDefined(t.boundaryLinks)&&r.$parent.$eval(t.boundaryLinks);var a=Math.max(r.maxSize,5);function s(e){if(d.isRegistered(i)&&c(e)){var t=r.pagination.current;r.pages=h(e,d.getCollectionLength(i),d.getItemsPerPage(i),a),r.pagination.current=e,l(),r.onPageChange&&r.onPageChange({newPageNumber:e,oldPageNumber:t})}}function u(){if(d.isRegistered(i)){var e=parseInt(d.getCurrentPage(i))||1;r.pages=h(e,d.getCollectionLength(i),d.getItemsPerPage(i),a),r.pagination.current=e,r.pagination.last=r.pages[r.pages.length-1],r.pagination.last
  • «
  • {{ pageNumber }}
  • »
  • ')}])}();var com_github_culmat_jsTreeTable=function(){function u(e,r,i){return i=i||"children",$.each(e,function(e,t){!function n(e){e[i]&&$.each(e[i],function(e,t){n(t)}),r(e)}(t)}),e}function t(e,n,o,a){var t=e;n=n||"id",o=o||"parent",a=a||"children";var s=[];$.each(t,function(e,t){s[t[n]]=t});var u=[];return $.each(t,function(e,r){var t=r[o];if($.isArray(t)||(t=[t]),0==t.length)u.push(r);else{var i=!1;$.each(t,function(e,t){var n=s[t];n&&(n[a]||(n[a]=[]),$.inArray(r,n[a])<0&&n[a].push(r),i=!0)}),i||u.push(r)}}),u}function l(e,a,s,u,l,t){a=a||"children",s=s||"id",t=t||{};var n=0,r=$("");$.each(t,function(e,t){"class"==e&&"jsTT"!=t?r.addClass(t):r.attr(e,t)});var i=$(""),o=$(""),c=$("");return r.append(i),i.append(o),r.append(c),u?$.each(u,function(e,t){$(o).append($(""))}):($(o).append($("")),$.each(e[0],function(e,t){e!=a&&e!=s&&$(o).append($(""))})),function r(e,i,o,t){n=Math.max(n,o),$.each(e,function(e,n){n["data-tt-level"]=o,function(n,e){var r=$("");$(r).attr("data-tt-id",n[s]),$(r).attr("data-tt-level",n["data-tt-level"]),n[a]&&0!=n[a].length?$(r).attr("data-tt-isnode",!0):$(r).attr("data-tt-isleaf",!0),e&&$(r).attr("data-tt-parent-id",e[s]),l?l($(r),n):u?$.each(u,function(e,t){$(r).append($(""))}):($(r).append($("")),$.each(n,function(e,t){e!=a&&e!=s&&"data-tt-level"!=e&&$(r).append($(""))})),c.append(r)}(n,t),n[i]&&$.each(n[i],function(e,t){r([t],i,o+1,n)})})}(e,a,1),e[0]&&(e[0].maxLevel=n),r}function n(e,t){return $.each(e,function(e,n){$.each(t,function(e,t){n[t]=$(n).attr(t)})}),e}function c(i){i.addClass("jsTT"),i.expandLevel=function(n){$("tr[data-tt-level]",i).each(function(e){var t=parseInt($(this).attr("data-tt-level"));n-1')):r.prepend($('')),r.prepend($('')),t.trExpand=function(e){if(!(this.trChildren.length<1)){e&&(this.trChildrenVisible=!0,$("#state",this).get(0).src=o);var n=e||this.trChildrenVisible;$.each(this.trChildren,function(e,t){n&&$(t).css("display","table-row"),t.trExpand()})}},t.trCollapse=function(e){this.trChildren.length<1||(e&&(this.trChildrenVisible=!1,$("#state",this).get(0).src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAAHlJREFUeNrcU1sNgDAQ6wgmcAM2MICGGlg1gJnNzWQcvwQGy1j4oUl/7tH0mpwzM7SgQyO+EZAUWh2MkkzSWhJwuRAlHYsJwEwyvs1gABDuzqoJcTw5qxaIJN0bgQRgIjnlmn1heSO5PE6Y2YXe+5Cr5+h++gs12AcAS6FS+7YOsj4AAAAASUVORK5CYII="),$.each(this.trChildren,function(e,t){$(t).css("display","none"),t.trCollapse()}))},$(t).click(function(){this.trChildrenVisible?this.trCollapse(!0):this.trExpand(!0)})}),i}return{depthFirst:u,makeTree:t,renderTree:l,attr2attr:n,treeTable:c,appendTreetable:function(e,t){(t=t||{}).idAttr=t.idAttr||"id",t.childrenAttr=t.childrenAttr||"children";var n=t.controls||[];t.mountPoint||(t.mountPoint=$("body")),t.depthFirst&&u(e,t.depthFirst,t.childrenAttr);var r=l(e,t.childrenAttr,t.idAttr,t.renderedAttr,t.renderer,t.tableAttributes);c(r),t.replaceContent&&t.mountPoint.html("");var i,o,a=t.initialExpandLevel?parseInt(t.initialExpandLevel):-1;if(a=Math.min(a,e[0].maxLevel),r.expandLevel(a),t.slider){var s=$('
    ');s.width("200px"),s.slider({min:1,max:e[0].maxLevel,range:"min",value:a,slide:function(e,t){r.expandLevel(t.value)}}),n=[s].concat(t.controls)}return 0"),$.each(i,function(e,t){o.append($('
    "+t+""+s+""+e+"
    "+n[e]+""+n[s]+""+t+"').append(t))}),$('').append(o))),t.mountPoint.append(r),r},jsTreeTable:"1.0",register:function(n){$.each(this,function(e,t){"register"!=e&&(n[e]=t)})}}}(); \ No newline at end of file +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("angular")):"function"==typeof define&&define.amd?define(["exports","angular"],t):t(e["@uirouter/angularjs"]={},e.angular)}(this,function(g,e){"use strict";var t=angular,C=e&&e.module?e:t;function u(n){var e=[].slice.apply(arguments,[1]),r=n.length;return function e(t){return t.length>=r?n.apply(null,t):function(){return e(t.concat([].slice.apply(arguments)))}}(e)}function n(){var n=arguments,r=n.length-1;return function(){for(var e=r,t=n[r].apply(this,arguments);e--;)t=n[e].call(this,t);return t}}function l(){for(var e=[],t=0;tthis._limit&&this.evict(),e},e.prototype.evict=function(){var t=this._items.shift();return this._evictListeners.forEach(function(e){return e(t)}),t},e.prototype.dequeue=function(){if(this.size())return this._items.splice(0,1)[0]},e.prototype.clear=function(){var e=this._items;return this._items=[],e},e.prototype.size=function(){return this._items.length},e.prototype.remove=function(e){var t=this._items.indexOf(e);return-1 "+Ue(e))},e.prototype.traceTransitionIgnored=function(e){this.enabled(g.Category.TRANSITION)&&console.log(st(e)+": Ignored <> "+Ue(e))},e.prototype.traceHookInvocation=function(e,t,n){if(this.enabled(g.Category.HOOK)){var r=S("traceData.hookType")(n)||"internal",i=S("traceData.context.state.name")(n)||S("traceData.context")(n)||"unknown",o=He(e.registeredHook.callback);console.log(st(t)+": Hook -> "+r+" context: "+i+", "+Fe(200,o))}},e.prototype.traceHookResult=function(e,t,n){this.enabled(g.Category.HOOK)&&console.log(st(t)+": <- Hook returned: "+Fe(200,Ue(e)))},e.prototype.traceResolvePath=function(e,t,n){this.enabled(g.Category.RESOLVE)&&console.log(st(n)+": Resolving "+e+" ("+t+")")},e.prototype.traceResolvableResolved=function(e,t){this.enabled(g.Category.RESOLVE)&&console.log(st(t)+": <- Resolved "+e+" to: "+Fe(200,Ue(e.data)))},e.prototype.traceError=function(e,t){this.enabled(g.Category.TRANSITION)&&console.log(st(t)+": <- Rejected "+Ue(t)+", reason: "+e)},e.prototype.traceSuccess=function(e,t){this.enabled(g.Category.TRANSITION)&&console.log(st(t)+": <- Success "+Ue(t)+", final state: "+e.name)},e.prototype.traceUIViewEvent=function(e,t,n){void 0===n&&(n=""),this.enabled(g.Category.UIVIEW)&&console.log("ui-view: "+Le(30,e)+" "+et(t)+n)},e.prototype.traceUIViewConfigUpdated=function(e,t){this.enabled(g.Category.UIVIEW)&&this.traceUIViewEvent("Updating",e," with ViewConfig from context='"+t+"'")},e.prototype.traceUIViewFill=function(e,t){this.enabled(g.Category.UIVIEW)&&this.traceUIViewEvent("Fill",e," with: "+Fe(200,t))},e.prototype.traceViewSync=function(e){if(this.enabled(g.Category.VIEWCONFIG)){var a="uiview component fqn",t=e.map(function(e){var t,n=e.uiView,r=e.viewConfig,i=n&&n.fqn,o=r&&r.viewDecl.$context.name+": ("+r.viewDecl.$name+")";return(t={})[a]=i,t["view config state (view name)"]=o,t}).sort(function(e,t){return(e[a]||"").localeCompare(t[a]||"")});it(t)}},e.prototype.traceViewServiceEvent=function(e,t){var n,r,i;this.enabled(g.Category.VIEWCONFIG)&&console.log("VIEWCONFIG: "+e+" "+(r=(n=t).viewDecl,i=r.$context.name||"(root)","[View#"+n.$id+" from '"+i+"' state]: target ui-view: '"+r.$uiViewName+"@"+r.$uiViewContextAnchor+"'"))},e.prototype.traceViewServiceUIViewEvent=function(e,t){this.enabled(g.Category.VIEWCONFIG)&&console.log("VIEWCONFIG: "+e+" "+et(t))},e}(),ut=new lt,ct=function(){function e(e){this.pattern=/.*/,this.inherit=!0,N(this,e)}return e.prototype.is=function(e,t){return!0},e.prototype.encode=function(e,t){return e},e.prototype.decode=function(e,t){return e},e.prototype.equals=function(e,t){return e==t},e.prototype.$subPattern=function(){var e=this.pattern.toString();return e.substr(1,e.length-2)},e.prototype.toString=function(){return"{ParamType:"+this.name+"}"},e.prototype.$normalize=function(e){return this.is(e)?e:this.decode(e)},e.prototype.$asArray=function(e,t){if(!e)return this;if("auto"===e&&!t)throw new Error("'auto' array mode is for query parameters only");return new dt(this,e)},e}();function dt(r,i){var o=this;function a(e){return E(e)?e:k(e)?[e]:[]}function s(n,r){return function(e){if(E(e)&&0===e.length)return e;var t=ce(a(e),n);return!0===r?0===se(t,function(e){return!e}).length:function(e){switch(e.length){case 0:return;case 1:return"auto"===i?e[0]:e;default:return e}}(t)}}function l(o){return function(e,t){var n=a(e),r=a(t);if(n.length!==r.length)return!1;for(var i=0;i=n.invokeLimit&&n.deregister()}}},o.prototype.handleHookResult=function(e){var t=this,n=this.getNotCurrentRejection();return n||(R(e)?e.then(function(e){return t.handleHookResult(e)}):(ut.traceHookResult(e,this.transition,this.options),!1===e?Ve.aborted("Hook aborted transition").toPromise():h($t)(e)?Ve.redirected(e).toPromise():void 0))},o.prototype.getNotCurrentRejection=function(){var e=this.transition.router;return e._disposed?Ve.aborted("UIRouter instance #"+e.$id+" has been stopped (disposed)").toPromise():this.transition._aborted?Ve.aborted().toPromise():this.isSuperseded()?Ve.superseded(this.options.current()).toPromise():void 0},o.prototype.toString=function(){var e=this.options,t=this.registeredHook;return(S("traceData.hookType")(e)||"internal")+" context: "+(S("traceData.context.state.name")(e)||S("traceData.context")(e)||"unknown")+", "+Fe(200,Ye(t.callback))},o.HANDLE_RESULT=function(t){return function(e){return t.handleHookResult(e)}},o.LOG_REJECTED_RESULT=function(t){return function(e){R(e)&&e.catch(function(e){return t.logError(Ve.normalize(e))})}},o.LOG_ERROR=function(t){return function(e){return t.logError(e)}},o.REJECT_ERROR=function(e){return function(e){return Pe(e)}},o.THROW_ERROR=function(e){return function(e){throw e}},o}();function Gt(e,t){var i=O(t)?[t]:t;return!!(D(i)?i:function(e){for(var t=i,n=0;n "+(this.valid()?"":"(X) ")+"'"+(T(t)?t.name:t)+"'"+Ue(n(this.params()))+" )"},t.diToken=t}();function en(e,t){var n=["",""],r=e.replace(/[\\\[\]\^$*+?.()|{}]/g,"\\$&");if(!t)return r;switch(t.squash){case!1:n=["(",")"+(t.isOptional?"?":"")];break;case!0:r=r.replace(/\/$/,""),n=["(?:/(",")|/)?"];break;default:n=["("+t.squash+"|",")?"]}return r+n[0]+t.type.pattern.source+n[1]}var tn=Xe("/"),nn={state:{params:{}},strict:!0,caseInsensitive:!0},rn=function(){function m(o,a,e,t){var s=this;this._cache={path:[this]},this._children=[],this._params=[],this._segments=[],this._compiled=[],this.config=t=te(t,nn),this.pattern=o;for(var n,r,i,l=/([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,u=/([:]?)([\w\[\].-]+)|\{([\w\[\].-]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,c=[],d=0,p=function(e){if(!m.nameValidator.test(e))throw new Error("Invalid parameter name '"+e+"' in pattern '"+o+"'");if(le(s._params,v("id",e)))throw new Error("Duplicate parameter name '"+e+"' in pattern '"+o+"'")},h=function(e,t){var n,r=e[2]||e[3],i=t?e[4]:e[4]||("*"===e[1]?"[\\s\\S]*":null);return{id:r,regexp:i,segment:o.substring(d,e.index),type:i?a.type(i)||(n=i,W(a.type(t?"query":"path"),{pattern:new RegExp(n,s.config.caseInsensitive?"i":void 0)})):null}};(n=l.exec(o))&&!(0<=(r=h(n,!1)).segment.indexOf("?"));)p(r.id),this._params.push(e.fromPath(r.id,r.type,t.state)),this._segments.push(r.segment),c.push([r.segment,De(this._params)]),d=l.lastIndex;var f=(i=o.substring(d)).indexOf("?");if(0<=f){var g=i.substring(f);if(i=i.substring(0,f),0r.weight?s:r}return r},t.prototype.sync=function(e){if(!e||!e.defaultPrevented){var t=this._router,n=t.urlService,r=t.stateService,i={path:n.path(),search:n.search(),hash:n.hash()},o=this.match(i);m([[O,function(e){return n.url(e,!0)}],[$t.isDef,function(e){return r.go(e.state,e.params,e.options)}],[h($t),function(e){return r.go(e.state(),e.params(),e.options())}]])(o&&o.rule.handler(o.match,i,t))}},t.prototype.listen=function(e){var t=this;if(!1!==e)return this._stopFn=this._stopFn||this._router.urlService.onChange(function(e){return t.sync(e)});this._stopFn&&this._stopFn(),delete this._stopFn},t.prototype.update=function(e){var t=this._router.locationService;e?this.location=t.url():t.url()!==this.location&&t.url(this.location,!0)},t.prototype.push=function(e,t,n){var r=n&&!!n.replace;this._router.urlService.url(e.format(t||{}),r)},t.prototype.href=function(e,t,n){var r=e.format(t);if(null==r)return null;n=n||{absolute:!1};var i,o,a,s,l=this._router.urlService.config,u=l.html5Mode();if(u||null===r||(r="#"+l.hashPrefix()+r),i=r,o=u,a=n.absolute,r="/"===(s=l.baseHref())?i:o?We(s)+i:a?s.slice(1)+i:i,!n.absolute||!r)return r;var c=!u&&r?"/":"",d=l.port(),p=80===d||443===d?"":":"+d;return[l.protocol(),"://",l.host(),p,c,r].join("")},t.prototype.rule=function(e){var t=this;if(!ln.isUrlRule(e))throw new Error("invalid rule");return e.$id=this._id++,e.priority=e.priority||0,this._rules.push(e),this._sorted=!1,function(){return t.removeRule(e)}},t.prototype.removeRule=function(e){Q(this._rules,e)},t.prototype.rules=function(){return this.ensureSorted(),this._rules.slice()},t.prototype.otherwise=function(e){var t=pn(e);this._otherwiseFn=this.urlRuleFactory.create(f(!0),t),this._sorted=!1},t.prototype.initial=function(e){var t=pn(e);this.rule(this.urlRuleFactory.create(function(e,t){return 0===t.globals.transitionHistory.size()&&!!/^\/?$/.exec(e.path)},t))},t.prototype.when=function(e,t,n){var r=this.urlRuleFactory.create(e,t);return k(n&&n.priority)&&(r.priority=n.priority),this.rule(r),r},t.prototype.deferIntercept=function(e){void 0===e&&(e=!0),this.interceptDeferred=e},t}();function pn(e){if(!(D(e)||O(e)||h($t)(e)||$t.isDef(e)))throw new Error("'handler' must be a string, function, TargetState, or have a state: 'newtarget' property");return D(e)?e:f(e)}var hn=function(){function l(e){var n=this;this.router=e,this._uiViews=[],this._viewConfigs=[],this._viewConfigFactories={},this._listeners=[],this._pluginapi={_rootViewContext:this._rootViewContext.bind(this),_viewConfigFactory:this._viewConfigFactory.bind(this),_registeredUIView:function(t){return le(n._uiViews,function(e){return n.router.$id+"."+e.id===t})},_registeredUIViews:function(){return n._uiViews},_activeViewConfigs:function(){return n._viewConfigs},_onSync:function(e){return n._listeners.push(e),function(){return Q(n._listeners,e)}}}}return l.normalizeUIViewTarget=function(e,t){void 0===t&&(t="");var n=t.split("@"),r=n[0]||"$default",i=O(n[1])?n[1]:"^",o=/^(\^(?:\.\^)*)\.(.*$)/.exec(r);o&&(i=o[1],r=o[2]),"!"===r.charAt(0)&&(r=r.substr(1),i="");/^(\^(?:\.\^)*)$/.exec(i)?i=i.split(".").reduce(function(e,t){return e.parent},e).name:"."===i&&(i=e.name);return{uiViewName:r,uiViewContextAnchor:i}},l.prototype._rootViewContext=function(e){return this._rootContext=e||this._rootContext},l.prototype._viewConfigFactory=function(e,t){this._viewConfigFactories[e]=t},l.prototype.createViewConfig=function(e,t){var n=this._viewConfigFactories[t.$type];if(!n)throw new Error("ViewService: No view config factory registered for type "+t.$type);var r=n(e,t);return E(r)?r:[r]},l.prototype.deactivateViewConfig=function(e){ut.traceViewServiceEvent("<- Removing",e),Q(this._viewConfigs,e)},l.prototype.activateViewConfig=function(e){ut.traceViewServiceEvent("-> Registering",e),this._viewConfigs.push(e)},l.prototype.sync=function(){var n=this,r=this._uiViews.map(function(e){return[e.fqn,e]}).reduce(ke,{});function i(e){for(var t=e.viewDecl.$context,n=0;++n&&t.parent;)t=t.parent;return n}var o=u(function(e,t,n,r){return t*(e(n)-e(r))}),e=this._uiViews.sort(o(function(e){var t=function(e){return e&&e.parent?t(e.parent)+1:1};return 1e4*e.fqn.split(".").length+t(e.creationContext)},1)).map(function(e){var t=n._viewConfigs.filter(l.matches(r,e));return 1 Registering",t);var e=this._uiViews;return e.filter(function(e){return e.fqn===t.fqn&&e.$type===t.$type}).length&&ut.traceViewServiceUIViewEvent("!!!! duplicate uiView named:",t),e.push(t),this.sync(),function(){-1!==e.indexOf(t)?(ut.traceViewServiceUIViewEvent("<- Deregistering",t),Q(e)(t)):ut.traceViewServiceUIViewEvent("Tried removing non-registered uiView",t)}},l.prototype.available=function(){return this._uiViews.map(w("fqn"))},l.prototype.active=function(){return this._uiViews.filter(w("$config")).map(w("name"))},l.matches=function(s,l){return function(e){if(l.$type!==e.viewDecl.$type)return!1;var t=e.viewDecl,n=t.$uiViewName.split("."),r=l.fqn.split(".");if(!q(n,r.slice(0-n.length)))return!1;var i=1-n.length||void 0,o=r.slice(0,i).join("."),a=s[o].creationContext;return t.$uiViewContextAnchor===(a&&a.name)}},l}(),fn=function(){function e(){this.params=new wt,this.lastStartedTransitionId=-1,this.transitionHistory=new Re([],1),this.successfulTransitions=new Re([],1)}return e.prototype.dispose=function(){this.transitionHistory.clear(),this.successfulTransitions.clear(),this.transition=null},e}(),gn=function(e){return e.reduce(function(e,t){return e[t]=I(t),e},{dispose:z})},mn=["url","path","search","hash","onChange"],vn=["port","protocol","host","baseHref","html5Mode","hashPrefix"],yn=["type","caseInsensitive","strictMode","defaultSquashPolicy"],wn=["sort","when","initial","otherwise","rules","rule","removeRule"],bn=["deferIntercept","listen","sync","match"],$n=function(){function e(e,t){void 0===t&&(t=!0),this.router=e,this.rules={},this.config={};var n=function(){return e.locationService};B(n,this,n,mn,t);var r=function(){return e.locationConfig};B(r,this.config,r,vn,t);var i=function(){return e.urlMatcherFactory};B(i,this.config,i,yn);var o=function(){return e.urlRouter};B(o,this.rules,o,wn),B(o,this,o,bn)}return e.prototype.url=function(e,t,n){},e.prototype.path=function(){},e.prototype.search=function(){},e.prototype.hash=function(){},e.prototype.onChange=function(e){},e.prototype.parts=function(){return{path:this.path(),search:this.search(),hash:this.hash()}},e.prototype.dispose=function(){},e.prototype.sync=function(e){},e.prototype.listen=function(e){},e.prototype.deferIntercept=function(e){},e.prototype.match=function(e){},e.locationServiceStub=gn(mn),e.locationConfigStub=gn(vn),e}(),_n=0,Cn=function(){function e(e,t){void 0===e&&(e=$n.locationServiceStub),void 0===t&&(t=$n.locationConfigStub),this.locationService=e,this.locationConfig=t,this.$id=_n++,this._disposed=!1,this._disposables=[],this.trace=ut,this.viewService=new hn(this),this.globals=new fn,this.transitionService=new zn(this),this.urlMatcherFactory=new sn,this.urlRouter=new dn(this),this.stateRegistry=new zt(this),this.stateService=new Bn(this),this.urlService=new $n(this),this._plugins={},this.viewService._pluginapi._rootViewContext(this.stateRegistry.root()),this.globals.$current=this.stateRegistry.root(),this.globals.current=this.globals.$current.self,this.disposable(this.globals),this.disposable(this.stateService),this.disposable(this.stateRegistry),this.disposable(this.transitionService),this.disposable(this.urlRouter),this.disposable(e),this.disposable(t)}return e.prototype.disposable=function(e){this._disposables.push(e)},e.prototype.dispose=function(e){var t=this;e&&D(e.dispose)?e.dispose(this):(this._disposed=!0,this._disposables.slice().forEach(function(e){try{"function"==typeof e.dispose&&e.dispose(t),Q(t._disposables,e)}catch(e){}}))},e.prototype.plugin=function(e,t){void 0===t&&(t={});var n=new e(this,t);if(!n.name)throw new Error("Required property `name` missing on plugin: "+n);return this._disposables.push(n),this._plugins[n.name]=n},e.prototype.getPlugin=function(e){return e?this._plugins[e]:de(this._plugins)},e}();function Sn(t){t.addResolvable(kt.fromData(Cn,t.router),""),t.addResolvable(kt.fromData(Jt,t),""),t.addResolvable(kt.fromData("$transition$",t),""),t.addResolvable(kt.fromData("$stateParams",t.params()),""),t.entering().forEach(function(e){t.addResolvable(kt.fromData("$state$",e),e)})}var kn=G(["$transition$",Jt]),Dn=function(e){var t=de(e.treeChanges()).reduce(fe,[]).reduce(ve,[]),n=function(e){return kn(e.token)?kt.fromData(e.token,null):e};t.forEach(function(e){e.resolvables=e.resolvables.map(n)})},xn=function(t){var e=t.to().redirectTo;if(e){var n=t.router.stateService;return D(e)?V.$q.when(e(t)).then(r):r(e)}function r(e){if(e)return e instanceof $t?e:O(e)?n.target(e,t.params(),t.options()):e.state||e.params?n.target(e.state||t.to(),e.params||t.params(),t.options()):void 0}};function On(n){return function(e,t){return(0,t.$$state()[n])(e,t)}}var Tn=On("onExit"),En=On("onRetain"),An=On("onEnter"),Pn=function(e){return new Et(e.treeChanges().to).resolvePath("EAGER",e).then(z)},Mn=function(e,t){return new Et(e.treeChanges().to).subContext(t.$$state()).resolvePath("LAZY",e).then(z)},Rn=function(e){return new Et(e.treeChanges().to).resolvePath("LAZY",e).then(z)},In=function(e){var t=V.$q,n=e.views("entering");if(n.length)return t.all(n.map(function(e){return t.when(e.load())})).then(z)},Vn=function(e){var t=e.views("entering"),n=e.views("exiting");if(t.length||n.length){var r=e.router.viewService;n.forEach(function(e){return r.deactivateViewConfig(e)}),t.forEach(function(e){return r.activateViewConfig(e)}),r.sync()}},Fn=function(e){var t=e.router.globals,n=function(){t.transition===e&&(t.transition=null)};e.onSuccess({},function(){t.successfulTransitions.enqueue(e),t.$current=e.$to(),t.current=t.$current.self,xe(e.params(),t.params)},{priority:1e4}),e.promise.then(n,n)},Ln=function(e){var t=e.options(),n=e.router.stateService,r=e.router.urlRouter;if("url"!==t.source&&t.location&&n.$current.navigable){var i={replace:"replace"===t.location};r.push(n.$current.navigable.url,n.params,i)}r.update(!0)},jn=function(a){var s=a.router;var e=a.entering().filter(function(e){return!!e.$$state().lazyLoad}).map(function(e){return Hn(a,e)});return V.$q.all(e).then(function(){if("url"!==a.originalTransition().options().source){var e=a.targetState();return s.stateService.target(e.identifier(),e.params(),e.options())}var t=s.urlService,n=t.match(t.parts()),r=n&&n.rule;if(r&&"STATE"===r.type){var i=r.state,o=n.match;return s.stateService.target(i,o,a.options())}s.urlService.sync()})};function Hn(t,n){var r=n.$$state().lazyLoad,e=r._promise;if(!e){e=r._promise=V.$q.when(r(t,n)).then(function(e){e&&Array.isArray(e.states)&&e.states.forEach(function(e){return t.router.stateRegistry.register(e)});return e}).then(function(e){return delete n.lazyLoad,delete n.$$state().lazyLoad,delete r._promise,e},function(e){return delete r._promise,V.$q.reject(e)})}return e}var Yn=function(e,t,n,r,i,o,a,s){void 0===i&&(i=!1),void 0===o&&(o=Wt.HANDLE_RESULT),void 0===a&&(a=Wt.REJECT_ERROR),void 0===s&&(s=!1),this.name=e,this.hookPhase=t,this.hookOrder=n,this.criteriaMatchPath=r,this.reverseSort=i,this.getResultHandler=o,this.getErrorHandler=a,this.synchronous=s};function Nn(e){var t=e._ignoredReason();if(t){ut.traceTransitionIgnored(e);var n=e.router.globals.transition;return"SameAsCurrent"===t&&n&&n.abort(),Ve.ignored().toPromise()}}function qn(e){if(!e.valid())throw new Error(e.error().toString())}var Un={location:!0,relative:null,inherit:!1,notify:!0,reload:!1,custom:{},current:function(){return null},source:"unknown"},zn=function(){function e(e){this._transitionCount=0,this._eventTypes=[],this._registeredHooks={},this._criteriaPaths={},this._router=e,this.$view=e.viewService,this._deregisterHookFns={},this._pluginapi=B(f(this),{},f(this),["_definePathType","_defineEvent","_getPathTypes","_getEvents","getHooks"]),this._defineCorePaths(),this._defineCoreEvents(),this._registerCoreTransitionHooks(),e.globals.successfulTransitions.onEvict(Dn)}return e.prototype.onCreate=function(e,t,n){},e.prototype.onBefore=function(e,t,n){},e.prototype.onStart=function(e,t,n){},e.prototype.onExit=function(e,t,n){},e.prototype.onRetain=function(e,t,n){},e.prototype.onEnter=function(e,t,n){},e.prototype.onFinish=function(e,t,n){},e.prototype.onSuccess=function(e,t,n){},e.prototype.onError=function(e,t,n){},e.prototype.dispose=function(e){de(this._registeredHooks).forEach(function(t){return t.forEach(function(e){e._deregistered=!0,Q(t,e)})})},e.prototype.create=function(e,t){return new Jt(e,t,this._router)},e.prototype._defineCoreEvents=function(){var e=g.TransitionHookPhase,t=Wt,n=this._criteriaPaths;this._defineEvent("onCreate",e.CREATE,0,n.to,!1,t.LOG_REJECTED_RESULT,t.THROW_ERROR,!0),this._defineEvent("onBefore",e.BEFORE,0,n.to),this._defineEvent("onStart",e.RUN,0,n.to),this._defineEvent("onExit",e.RUN,100,n.exiting,!0),this._defineEvent("onRetain",e.RUN,200,n.retained),this._defineEvent("onEnter",e.RUN,300,n.entering),this._defineEvent("onFinish",e.RUN,400,n.to),this._defineEvent("onSuccess",e.SUCCESS,0,n.to,!1,t.LOG_REJECTED_RESULT,t.LOG_ERROR,!0),this._defineEvent("onError",e.ERROR,0,n.to,!1,t.LOG_REJECTED_RESULT,t.LOG_ERROR,!0)},e.prototype._defineCorePaths=function(){var e=g.TransitionHookScope.STATE,t=g.TransitionHookScope.TRANSITION;this._definePathType("to",t),this._definePathType("from",t),this._definePathType("exiting",e),this._definePathType("retained",e),this._definePathType("entering",e)},e.prototype._defineEvent=function(e,t,n,r,i,o,a,s){void 0===i&&(i=!1),void 0===o&&(o=Wt.HANDLE_RESULT),void 0===a&&(a=Wt.REJECT_ERROR),void 0===s&&(s=!1);var l=new Yn(e,t,n,r,i,o,a,s);this._eventTypes.push(l),Qt(this,this,l)},e.prototype._getEvents=function(t){return(k(t)?this._eventTypes.filter(function(e){return e.hookPhase===t}):this._eventTypes.slice()).sort(function(e,t){var n=e.hookPhase-t.hookPhase;return 0===n?e.hookOrder-t.hookOrder:n})},e.prototype._definePathType=function(e,t){this._criteriaPaths[e]={name:e,scope:t}},e.prototype._getPathTypes=function(){return this._criteriaPaths},e.prototype.getHooks=function(e){return this._registeredHooks[e]},e.prototype._registerCoreTransitionHooks=function(){var e=this._deregisterHookFns;e.addCoreResolves=this.onCreate({},Sn),e.ignored=this.onBefore({},Nn,{priority:-9999}),e.invalid=this.onBefore({},qn,{priority:-1e4}),e.redirectTo=this.onStart({to:function(e){return!!e.redirectTo}},xn),e.onExit=this.onExit({exiting:function(e){return!!e.onExit}},Tn),e.onRetain=this.onRetain({retained:function(e){return!!e.onRetain}},En),e.onEnter=this.onEnter({entering:function(e){return!!e.onEnter}},An),e.eagerResolve=this.onStart({},Pn,{priority:1e3}),e.lazyResolve=this.onEnter({entering:f(!0)},Mn,{priority:1e3}),e.resolveAll=this.onFinish({},Rn,{priority:1e3}),e.loadViews=this.onFinish({},In),e.activateViews=this.onSuccess({},Vn),e.updateGlobals=this.onCreate({},Fn),e.updateUrl=this.onSuccess({},Ln,{priority:9999}),e.lazyLoad=this.onBefore({entering:function(e){return!!e.lazyLoad}},jn)},e}(),Bn=function(){function n(e){this.router=e,this.invalidCallbacks=[],this._defaultErrorHandler=function(e){e instanceof Error&&e.stack?(console.error(e),console.error(e.stack)):e instanceof Ve?(console.error(e.toString()),e.detail&&e.detail.stack&&console.error(e.detail.stack)):console.error(e)};var t=Object.keys(n.prototype).filter(d(G(["current","$current","params","transition"])));B(f(n.prototype),this,f(this),t)}return Object.defineProperty(n.prototype,"transition",{get:function(){return this.router.globals.transition},enumerable:!0,configurable:!0}),Object.defineProperty(n.prototype,"params",{get:function(){return this.router.globals.params},enumerable:!0,configurable:!0}),Object.defineProperty(n.prototype,"current",{get:function(){return this.router.globals.current},enumerable:!0,configurable:!0}),Object.defineProperty(n.prototype,"$current",{get:function(){return this.router.globals.$current},enumerable:!0,configurable:!0}),n.prototype.dispose=function(){this.defaultErrorHandler(z),this.invalidCallbacks=[]},n.prototype._handleInvalidTargetState=function(e,n){var r=this,i=_t.makeTargetState(this.router.stateRegistry,e),t=this.router.globals,o=function(){return t.transitionHistory.peekTail()},a=o(),s=new Re(this.invalidCallbacks.slice()),l=new Et(e).injector(),u=function(e){if(e instanceof $t){var t=e;return(t=r.target(t.identifier(),t.params(),t.options())).valid()?o()!==a?Ve.superseded().toPromise():r.transitionTo(t.identifier(),t.params(),t.options()):Ve.invalid(t.error()).toPromise()}};return function t(){var e=s.dequeue();return void 0===e?Ve.invalid(n.error()).toPromise():V.$q.when(e(n,i,l)).then(u).then(function(e){return e||t()})}()},n.prototype.onInvalid=function(e){return this.invalidCallbacks.push(e),function(){Q(this.invalidCallbacks)(e)}.bind(this)},n.prototype.reload=function(e){return this.transitionTo(this.current,this.params,{reload:!k(e)||e,inherit:!1,notify:!1})},n.prototype.go=function(e,t,n){var r=te(n,{relative:this.$current,inherit:!0},Un);return this.transitionTo(e,t,r)},n.prototype.target=function(e,t,n){if(void 0===n&&(n={}),T(n.reload)&&!n.reload.name)throw new Error("Invalid reload state object");var r=this.router.stateRegistry;if(n.reloadState=!0===n.reload?r.root():r.matcher.find(n.reload,n.relative),n.reload&&!n.reloadState)throw new Error("No such reload state '"+(O(n.reload)?n.reload:n.reload.name)+"'");return new $t(this.router.stateRegistry,e,t,n)},n.prototype.getCurrentPath=function(){var e=this,t=this.router.globals.successfulTransitions.peekTail();return t?t.treeChanges().to:[new bt(e.router.stateRegistry.root())]},n.prototype.transitionTo=function(e,t,n){var o=this;void 0===t&&(t={}),void 0===n&&(n={});var a=this.router,s=a.globals;n=te(n,Un);n=N(n,{current:function(){return s.transition}});var r=this.target(e,t,n),i=this.getCurrentPath();if(!r.exists())return this._handleInvalidTargetState(i,r);if(!r.valid())return Pe(r.error());var l=function(i){return function(e){if(e instanceof Ve){var t=a.globals.lastStartedTransitionId===i.$id;if(e.type===g.RejectType.IGNORED)return t&&a.urlRouter.update(),V.$q.when(s.current);var n=e.detail;if(e.type===g.RejectType.SUPERSEDED&&e.redirected&&n instanceof $t){var r=i.redirect(n);return r.run().catch(l(r))}if(e.type===g.RejectType.ABORTED)return t&&a.urlRouter.update(),V.$q.reject(e)}return o.defaultErrorHandler()(e),V.$q.reject(e)}},u=this.router.transitionService.create(i,r),c=u.run().catch(l(u));return Ae(c),N(c,{transition:u})},n.prototype.is=function(e,t,n){n=te(n,{relative:this.$current});var r=this.router.stateRegistry.matcher.find(e,n.relative);if(k(r)){if(this.$current!==r)return!1;if(!t)return!0;var i=r.parameters({inherit:!0,matchingKeys:t});return vt.equals(i,vt.values(i,t),this.params)}},n.prototype.includes=function(e,t,n){n=te(n,{relative:this.$current});var r=O(e)&&Me.fromString(e);if(r){if(!r.matches(this.$current.name))return!1;e=this.$current.name}var i=this.router.stateRegistry.matcher.find(e,n.relative),o=this.$current.includes;if(k(i)){if(!k(o[i.name]))return!1;if(!t)return!0;var a=i.parameters({inherit:!0,matchingKeys:t});return vt.equals(a,vt.values(a,t),this.params)}},n.prototype.href=function(e,t,n){n=te(n,{lossy:!0,inherit:!0,absolute:!1,relative:this.$current}),t=t||{};var r=this.router.stateRegistry.matcher.find(e,n.relative);if(!k(r))return null;n.inherit&&(t=this.params.$inherit(t,this.$current,r));var i=r&&n.lossy?r.navigable:r;return i&&void 0!==i.url&&null!==i.url?this.router.urlRouter.href(i.url,t,{absolute:n.absolute}):null},n.prototype.defaultErrorHandler=function(e){return this._defaultErrorHandler=e||this._defaultErrorHandler},n.prototype.get=function(e,t){var n=this.router.stateRegistry;return 0===arguments.length?n.get():n.get(e,t||this.$current)},n.prototype.lazyLoad=function(e,t){var n=this.get(e);if(!n||!n.lazyLoad)throw new Error("Can not lazy load "+e);var r=this.getCurrentPath(),i=_t.makeTargetState(this.router.stateRegistry,r);return Hn(t=t||this.router.transitionService.create(r,i),n)},n}(),Wn={when:function(n){return new Promise(function(e,t){return e(n)})},reject:function(n){return new Promise(function(e,t){t(n)})},defer:function(){var n={};return n.promise=new Promise(function(e,t){n.resolve=e,n.reject=t}),n},all:function(e){if(E(e))return Promise.all(e);if(T(e)){var t=Object.keys(e).map(function(t){return e[t].then(function(e){return{key:t,val:e}})});return Wn.all(t).then(function(e){return e.reduce(function(e,t){return e[t.key]=t.val,e},{})})}}},Gn={},Kn=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/gm,Qn=/([^\s,]+)/g,Zn={get:function(e){return Gn[e]},has:function(e){return null!=Zn.get(e)},invoke:function(e,t,n){var r=N({},Gn,n||{}),i=Zn.annotate(e),o=be(function(e){return r.hasOwnProperty(e)},function(e){return"DI can't find injectable: '"+e+"'"}),a=i.filter(o).map(function(e){return r[e]});return D(e)?e.apply(t,a):e.slice(-1)[0].apply(t,a)},annotate:function(e){if(!M(e))throw new Error("Not an injectable function: "+e);if(e&&e.$inject)return e.$inject;if(E(e))return e.slice(0,-1);var t=e.toString().replace(Kn,"");return t.slice(t.indexOf("(")+1,t.indexOf(")")).match(Qn)||[]}},Xn=function(e,t){var n=t[0],r=t[1];return e.hasOwnProperty(n)?E(e[n])?e[n].push(r):e[n]=[e[n],r]:e[n]=r,e},Jn=function(e){return e.split("&").filter(U).map(Qe).reduce(Xn,{})};function er(e){var t=function(e){return e||""},n=Ge(e).map(t),r=n[0],i=n[1],o=Ke(r).map(t);return{path:o[0],search:o[1],hash:i,url:e}}var tr=function(e){var t=e.path(),n=e.search(),r=e.hash(),i=Object.keys(n).map(function(t){var e=n[t];return(E(e)?e:[e]).map(function(e){return t+"="+e})}).reduce(fe,[]).join("&");return t+(i?"?"+i:"")+(r?"#"+r:"")};function nr(r,i,o,a){return function(e){var t=e.locationService=new o(e),n=e.locationConfig=new a(e,i);return{name:r,service:t,configuration:n,dispose:function(e){e.dispose(t),e.dispose(n)}}}}var rr,ir,or,ar=function(){function e(e,t){var n=this;this.fireAfterUpdate=t,this._listeners=[],this._listener=function(t){return n._listeners.forEach(function(e){return e(t)})},this.hash=function(){return er(n._get()).hash},this.path=function(){return er(n._get()).path},this.search=function(){return Jn(er(n._get()).search)},this._location=F.location,this._history=F.history}return e.prototype.url=function(t,e){return void 0===e&&(e=!0),k(t)&&t!==this._get()&&(this._set(null,null,t,e),this.fireAfterUpdate&&this._listeners.forEach(function(e){return e({url:t})})),tr(this)},e.prototype.onChange=function(e){var t=this;return this._listeners.push(e),function(){return Q(t._listeners,e)}},e.prototype.dispose=function(e){ee(this._listeners)},e}(),sr=(rr=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},function(e,t){function n(){this.constructor=e}rr(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),lr=function(n){function e(e){var t=n.call(this,e,!1)||this;return F.addEventListener("hashchange",t._listener,!1),t}return sr(e,n),e.prototype._get=function(){return Ze(this._location.hash)},e.prototype._set=function(e,t,n,r){this._location.hash=n},e.prototype.dispose=function(e){n.prototype.dispose.call(this,e),F.removeEventListener("hashchange",this._listener)},e}(ar),ur=(ir=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},function(e,t){function n(){this.constructor=e}ir(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),cr=function(t){function e(e){return t.call(this,e,!0)||this}return ur(e,t),e.prototype._get=function(){return this._url},e.prototype._set=function(e,t,n,r){this._url=n},e}(ar),dr=(or=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},function(e,t){function n(){this.constructor=e}or(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),pr=function(n){function e(e){var t=n.call(this,e,!0)||this;return t._config=e.urlService.config,F.addEventListener("popstate",t._listener,!1),t}return dr(e,n),e.prototype._getBasePrefix=function(){return We(this._config.baseHref())},e.prototype._get=function(){var e=this._location,t=e.pathname,n=e.hash,r=e.search;r=Ke(r)[1],n=Ge(n)[1];var i=this._getBasePrefix(),o=t===this._config.baseHref(),a=t.substr(0,i.length)===i;return(t=o?"/":a?t.substring(i.length):t)+(r?"?"+r:"")+(n?"#"+n:"")},e.prototype._set=function(e,t,n,r){var i=this._getBasePrefix(),o=n&&"/"!==n[0]?"/":"",a=""===n||"/"===n?this._config.baseHref():i+o+n;r?this._history.replaceState(e,t,a):this._history.pushState(e,t,a)},e.prototype.dispose=function(e){n.prototype.dispose.call(this,e),F.removeEventListener("popstate",this._listener)},e}(ar),hr=function(){var t=this;this.dispose=z,this._baseHref="",this._port=80,this._protocol="http",this._host="localhost",this._hashPrefix="",this.port=function(){return t._port},this.protocol=function(){return t._protocol},this.host=function(){return t._host},this.baseHref=function(){return t._baseHref},this.html5Mode=function(){return!1},this.hashPrefix=function(e){return k(e)?t._hashPrefix=e:t._hashPrefix}},fr=function(){function e(e,t){void 0===t&&(t=!1),this._isHtml5=t,this._baseHref=void 0,this._hashPrefix=""}return e.prototype.port=function(){return location.port?Number(location.port):"https"===this.protocol()?443:80},e.prototype.protocol=function(){return location.protocol.replace(/:/g,"")},e.prototype.host=function(){return location.hostname},e.prototype.html5Mode=function(){return this._isHtml5},e.prototype.hashPrefix=function(e){return k(e)?this._hashPrefix=e:this._hashPrefix},e.prototype.baseHref=function(e){return k(e)&&(this._baseHref=e),b(this._baseHref)&&(this._baseHref=this.getBaseHref()),this._baseHref},e.prototype.getBaseHref=function(){var e=document.getElementsByTagName("base")[0];return e&&e.href?e.href.replace(/^(https?:)?\/\/[^/]*/,""):location.pathname||"/"},e.prototype.dispose=function(){},e}();function gr(e){return V.$injector=Zn,{name:"vanilla.services",$q:V.$q=Wn,$injector:Zn,dispose:function(){return null}}}var mr=nr("vanilla.hashBangLocation",!1,lr,fr),vr=nr("vanilla.pushStateLocation",!0,pr,fr),yr=nr("vanilla.memoryLocation",!1,cr,hr),wr=function(){function e(){}return e.prototype.dispose=function(e){},e}(),br=Object.freeze({root:F,fromJson:j,toJson:H,forEach:Y,extend:N,equals:q,identity:U,noop:z,createProxyFunctions:B,inherit:W,inArray:G,_inArray:K,removeFrom:Q,_removeFrom:Z,pushTo:X,_pushTo:J,deregAll:ee,defaults:te,mergeR:ne,ancestors:re,pick:ie,omit:oe,pluck:ae,filter:se,find:le,mapObj:ue,map:ce,values:de,allTrueR:pe,anyTrueR:he,unnestR:fe,flattenR:ge,pushR:me,uniqR:ve,unnest:ye,flatten:we,assertPredicate:be,assertMap:$e,assertFn:_e,pairs:Ce,arrayTuples:Se,applyPairs:ke,tail:De,copy:xe,_extend:Oe,silenceUncaughtInPromise:Ae,silentRejection:Pe,notImplemented:I,services:V,Glob:Me,curry:u,compose:n,pipe:l,prop:w,propEq:v,parse:S,not:d,and:r,or:i,all:c,any:p,is:h,eq:o,val:f,invoke:a,pattern:m,isUndefined:b,isDefined:k,isNull:$,isNullOrUndefined:_,isFunction:D,isNumber:x,isString:O,isObject:T,isArray:E,isDate:A,isRegExp:P,isInjectable:M,isPromise:R,Queue:Re,maxLength:Fe,padString:Le,kebobString:je,functionToString:He,fnToString:Ye,stringify:Ue,beforeAfterSubstr:ze,hostRegex:Be,stripLastPathElement:We,splitHash:Ge,splitQuery:Ke,splitEqual:Qe,trimHashVal:Ze,splitOnDelim:Xe,joinNeighborsR:Je,get Category(){return g.Category},Trace:lt,trace:ut,get DefType(){return g.DefType},Param:vt,ParamTypes:yt,StateParams:wt,ParamType:ct,PathNode:bt,PathUtils:_t,resolvePolicies:Ct,defaultResolvePolicy:St,Resolvable:kt,NATIVE_INJECTOR_TOKEN:Tt,ResolveContext:Et,resolvablesBuilder:Lt,StateBuilder:Yt,StateObject:Nt,StateMatcher:qt,StateQueueManager:Ut,StateRegistry:zt,StateService:Bn,TargetState:$t,get TransitionHookPhase(){return g.TransitionHookPhase},get TransitionHookScope(){return g.TransitionHookScope},HookBuilder:Zt,matchState:Gt,RegisteredHook:Kt,makeEvent:Qt,get RejectType(){return g.RejectType},Rejection:Ve,Transition:Jt,TransitionHook:Wt,TransitionEventType:Yn,defaultTransOpts:Un,TransitionService:zn,UrlMatcher:rn,ParamFactory:an,UrlMatcherFactory:sn,UrlRouter:dn,UrlRuleFactory:ln,BaseUrlRule:un,UrlService:$n,ViewService:hn,UIRouterGlobals:fn,UIRouter:Cn,$q:Wn,$injector:Zn,BaseLocationServices:ar,HashLocationService:lr,MemoryLocationService:cr,PushStateLocationService:pr,MemoryLocationConfig:hr,BrowserLocationConfig:fr,keyValsToObjectR:Xn,getParams:Jn,parseUrl:er,buildUrl:tr,locationPluginFactory:nr,servicesPlugin:gr,hashLocationPlugin:mr,pushStateLocationPlugin:vr,memoryLocationPlugin:yr,UIRouterPluginBase:wr});function $r(){var n=null;return function(e,t){return n=n||V.$injector.get("$templateFactory"),[new kr(e,t,n)]}}var _r=function(e,n){return e.reduce(function(e,t){return e||k(n[t])},!1)};function Cr(r){if(!r.parent)return{};var i=["component","bindings","componentProvider"],o=["templateProvider","templateUrl","template","notify","async"].concat(["controller","controllerProvider","controllerAs","resolveAs"]),e=i.concat(o);if(k(r.views)&&_r(e,r))throw new Error("State '"+r.name+"' has a 'views' object. It cannot also have \"view properties\" at the state level. Move the following properties into a view (in the 'views' object): "+e.filter(function(e){return k(r[e])}).join(", "));var a={},t=r.views||{$default:ie(r,e)};return Y(t,function(e,t){if(t=t||"$default",O(e)&&(e={component:e}),e=N({},e),_r(i,e)&&_r(o,e))throw new Error("Cannot combine: "+i.join("|")+" with: "+o.join("|")+" in stateview: '"+t+"@"+r.name+"'");e.resolveAs=e.resolveAs||"$resolve",e.$type="ng1",e.$context=r,e.$name=t;var n=hn.normalizeUIViewTarget(e.$context,e.$name);e.$uiViewName=n.uiViewName,e.$uiViewContextAnchor=n.uiViewContextAnchor,a[t]=e}),a}var Sr=0,kr=function(){function e(e,t,n){var r=this;this.path=e,this.viewDecl=t,this.factory=n,this.$id=Sr++,this.loaded=!1,this.getTemplate=function(e,t){return r.component?r.factory.makeComponentTemplate(e,t,r.component,r.viewDecl.bindings):r.template}}return e.prototype.load=function(){var t=this,e=V.$q,n=new Et(this.path),r=this.path.reduce(function(e,t){return N(e,t.paramValues)},{}),i={template:e.when(this.factory.fromConfig(this.viewDecl,r,n)),controller:e.when(this.getController(n))};return e.all(i).then(function(e){return ut.traceViewServiceEvent("Loaded",t),t.controller=e.controller,N(t,e.template),t})},e.prototype.getController=function(e){var t=this.viewDecl.controllerProvider;if(!M(t))return this.viewDecl.controller;var n=V.$injector.annotate(t),r=E(t)?De(t):t;return new kt("",r,n).get(e)},e}(),Dr=function(){function e(){var r=this;this._useHttp=C.version.minor<3,this.$get=["$http","$templateCache","$injector",function(e,t,n){return r.$templateRequest=n.has&&n.has("$templateRequest")&&n.get("$templateRequest"),r.$http=e,r.$templateCache=t,r}]}return e.prototype.useHttpService=function(e){this._useHttp=e},e.prototype.fromConfig=function(e,t,n){var r=function(e){return V.$q.when(e).then(function(e){return{template:e}})},i=function(e){return V.$q.when(e).then(function(e){return{component:e}})};return k(e.template)?r(this.fromString(e.template,t)):k(e.templateUrl)?r(this.fromUrl(e.templateUrl,t)):k(e.templateProvider)?r(this.fromProvider(e.templateProvider,t,n)):k(e.component)?i(e.component):k(e.componentProvider)?i(this.fromComponentProvider(e.componentProvider,t,n)):r("")},e.prototype.fromString=function(e,t){return D(e)?e(t):e},e.prototype.fromUrl=function(e,t){return D(e)&&(e=e(t)),null==e?null:this._useHttp?this.$http.get(e,{cache:this.$templateCache,headers:{Accept:"text/html"}}).then(function(e){return e.data}):this.$templateRequest(e)},e.prototype.fromProvider=function(e,t,n){var r=V.$injector.annotate(e),i=E(e)?De(e):e;return new kt("",i,r).get(n)},e.prototype.fromComponentProvider=function(e,t,n){var r=V.$injector.annotate(e),i=E(e)?De(e):e;return new kt("",i,r).get(n)},e.prototype.makeComponentTemplate=function(l,u,e,c){c=c||{};var d=3<=C.version.minor?"::":"",p=function(e){var t=je(e);return/^(x|data)-/.exec(t)?"x-"+t:t},t=function(e){var t=V.$injector.get(e+"Directive");if(!t||!t.length)throw new Error("Unable to find component named '"+e+"'");return t.map(xr).reduce(fe,[])}(e).map(function(e){var t=e.name,n=e.type,r=p(t);if(l.attr(r)&&!c[t])return r+"='"+l.attr(r)+"'";var i=c[t]||t;if("@"===n)return r+"='{{"+d+"$resolve."+i+"}}'";if("&"!==n)return r+"='"+d+"$resolve."+i+"'";var o=u.getResolvable(i),a=o&&o.data,s=a&&V.$injector.annotate(a)||[];return r+"='$resolve."+i+(E(a)?"["+(a.length-1)+"]":"")+"("+s.join(",")+")'"}).join(" "),n=p(e);return"<"+n+" "+t+">"},e}();var xr=function(e){return T(e.bindToController)?Or(e.bindToController):Or(e.scope)},Or=function(t){return Object.keys(t||{}).map(function(e){return[e,/^([=<@&])[?]?(.*)/.exec(t[e])]}).filter(function(e){return k(e)&&E(e[1])}).map(function(e){return{name:e[1][2]||e[0],type:e[1][1]}})},Tr=function(){function n(e,t){this.stateRegistry=e,this.stateService=t,B(f(n.prototype),this,f(this))}return n.prototype.decorator=function(e,t){return this.stateRegistry.decorator(e,t)||this},n.prototype.state=function(e,t){return T(e)?t=e:t.name=e,this.stateRegistry.register(t),this},n.prototype.onInvalid=function(e){return this.stateService.onInvalid(e)},n}(),Er=function(n){return function(e,t){var i=e[n],o="onExit"===n?"from":"to";return i?function(e,t){var n=new Et(e.treeChanges(o)).subContext(t.$$state()),r=N(Wr(n),{$state$:t,$transition$:e});return V.$injector.invoke(i,this,r)}:void 0}},Ar=function(){function e(e){this._urlListeners=[],this.$locationProvider=e;var t=f(e);B(t,this,t,["hashPrefix"])}return e.monkeyPatchPathParameterType=function(e){var t=e.urlMatcherFactory.type("path");t.encode=function(e){return null!=e?e.toString().replace(/(~|\/)/g,function(e){return{"~":"~~","/":"~2F"}[e]}):e},t.decode=function(e){return null!=e?e.toString().replace(/(~~|~2F)/g,function(e){return{"~~":"~","~2F":"/"}[e]}):e}},e.prototype.dispose=function(){},e.prototype.onChange=function(e){var t=this;return this._urlListeners.push(e),function(){return Q(t._urlListeners)(e)}},e.prototype.html5Mode=function(){var e=this.$locationProvider.html5Mode();return(e=T(e)?e.enabled:e)&&this.$sniffer.history},e.prototype.baseHref=function(){return this._baseHref||(this._baseHref=this.$browser.baseHref()||this.$window.location.pathname)},e.prototype.url=function(e,t,n){return void 0===t&&(t=!1),k(e)&&this.$location.url(e),t&&this.$location.replace(),n&&this.$location.state(n),this.$location.url()},e.prototype._runtimeServices=function(e,t,n,r,i){var o=this;this.$location=t,this.$sniffer=n,this.$browser=r,this.$window=i,e.$on("$locationChangeSuccess",function(t){return o._urlListeners.forEach(function(e){return e(t)})});var a=f(t);B(a,this,a,["replace","path","search","hash"]),B(a,this,a,["port","protocol","host"])},e}(),Pr=function(){function n(e){this._router=e,this._urlRouter=e.urlRouter}return n.injectableHandler=function(t,n){return function(e){return V.$injector.invoke(n,null,{$match:e,$stateParams:t.globals.params})}},n.prototype.$get=function(){var e=this._urlRouter;return e.update(!0),e.interceptDeferred||e.listen(),e},n.prototype.rule=function(e){var t=this;if(!D(e))throw new Error("'rule' must be a function");var n=new un(function(){return e(V.$injector,t._router.locationService)},U);return this._urlRouter.rule(n),this},n.prototype.otherwise=function(e){var t=this,n=this._urlRouter;if(O(e))n.otherwise(e);else{if(!D(e))throw new Error("'rule' must be a string or function");n.otherwise(function(){return e(V.$injector,t._router.locationService)})}return this},n.prototype.when=function(e,t){return(E(t)||D(t))&&(t=n.injectableHandler(this._router,t)),this._urlRouter.when(e,t),this},n.prototype.deferIntercept=function(e){this._urlRouter.deferIntercept(e)},n}();C.module("ui.router.angular1",[]);var Mr=C.module("ui.router.init",["ng"]),Rr=C.module("ui.router.util",["ui.router.init"]),Ir=C.module("ui.router.router",["ui.router.util"]),Vr=C.module("ui.router.state",["ui.router.router","ui.router.util","ui.router.angular1"]),Fr=C.module("ui.router",["ui.router.init","ui.router.state","ui.router.angular1"]),Lr=(C.module("ui.router.compat",["ui.router"]),null);function jr(e){(Lr=this.router=new Cn).stateProvider=new Tr(Lr.stateRegistry,Lr.stateService),Lr.stateRegistry.decorator("views",Cr),Lr.stateRegistry.decorator("onExit",Er("onExit")),Lr.stateRegistry.decorator("onRetain",Er("onRetain")),Lr.stateRegistry.decorator("onEnter",Er("onEnter")),Lr.viewService._pluginapi._viewConfigFactory("ng1",$r());var s=Lr.locationService=Lr.locationConfig=new Ar(e);function t(e,t,n,r,i,o,a){return s._runtimeServices(i,e,r,t,n),delete Lr.router,delete Lr.$get,Lr}return Ar.monkeyPatchPathParameterType(Lr),((Lr.router=Lr).$get=t).$inject=["$location","$browser","$window","$sniffer","$rootScope","$http","$templateCache"],Lr}jr.$inject=["$locationProvider"];var Hr=function(n){return["$uiRouterProvider",function(e){var t=e.router[n];return t.$get=function(){return t},t}]};function Yr(t,e,n){if(V.$injector=t,V.$q=e,!t.hasOwnProperty("strictDi"))try{t.invoke(function(e){})}catch(e){t.strictDi=!!/strict mode/.exec(e&&e.toString())}n.stateRegistry.get().map(function(e){return e.$$state().resolvables}).reduce(fe,[]).filter(function(e){return"deferred"===e.deps}).forEach(function(e){return e.deps=t.annotate(e.resolveFn,t.strictDi)})}Yr.$inject=["$injector","$q","$uiRouter"];function Nr(e){e.$watch(function(){ut.approximateDigests++})}Nr.$inject=["$rootScope"],Mr.provider("$uiRouter",jr),Ir.provider("$urlRouter",["$uiRouterProvider",function(e){return e.urlRouterProvider=new Pr(e)}]),Rr.provider("$urlService",Hr("urlService")),Rr.provider("$urlMatcherFactory",["$uiRouterProvider",function(){return Lr.urlMatcherFactory}]),Rr.provider("$templateFactory",function(){return new Dr}),Vr.provider("$stateRegistry",Hr("stateRegistry")),Vr.provider("$uiRouterGlobals",Hr("globals")),Vr.provider("$transitions",Hr("transitionService")),Vr.provider("$state",["$uiRouterProvider",function(){return N(Lr.stateProvider,{$get:function(){return Lr.stateService}})}]),Vr.factory("$stateParams",["$uiRouter",function(e){return e.globals.params}]),Fr.factory("$view",function(){return Lr.viewService}),Fr.service("$trace",function(){return ut}),Fr.run(Nr),Rr.run(["$urlMatcherFactory",function(e){}]),Vr.run(["$state",function(e){}]),Ir.run(["$urlRouter",function(e){}]),Mr.run(Yr);var qr,Ur,zr,Br,Wr=function(n){return n.getTokens().filter(O).map(function(e){var t=n.getResolvable(e);return[e,"NOWAIT"===n.getPolicy(t).async?t.promise:t.data]}).reduce(ke,{})};function Gr(e){var t,n=e.match(/^\s*({[^}]*})\s*$/);if(n&&(e="("+n[1]+")"),!(t=e.replace(/\n/g," ").match(/^\s*([^(]*?)\s*(\((.*)\))?\s*$/))||4!==t.length)throw new Error("Invalid state ref '"+e+"'");return{state:t[1]||null,paramExpr:t[3]||null}}function Kr(e){var t=e.parent().inheritedData("$uiView"),n=S("$cfg.path")(t);return n?De(n).state.name:void 0}function Qr(e,t,n){var r,i=n.uiState||e.current.name,o=N((r=e,{relative:Kr(t)||r.$current,inherit:!0,source:"sref"}),n.uiStateOpts||{}),a=e.href(i,n.uiStateParams,o);return{uiState:i,uiStateParams:n.uiStateParams,uiStateOpts:o,href:a}}function Zr(e){var t="[object SVGAnimatedString]"===Object.prototype.toString.call(e.prop("href")),n="FORM"===e[0].nodeName;return{attr:n?"action":t?"xlink:href":"href",isAnchor:"A"===e.prop("tagName").toUpperCase(),clickable:!n}}function Xr(o,a,s,l,u){return function(e){var t=e.which||e.button,n=u();if(!(1>>0;if(0===i)return-1;var o=+t||0;if(Math.abs(o)===1/0&&(o=0),i<=o)return-1;for(n=Math.max(0<=o?o:i-Math.abs(o),0);n
    ',this.loadingBarTemplate='
    ',this.$get=["$injector","$document","$timeout","$rootScope",function(i,o,a,s){function l(e){if(m){var t=100*e+"%";f.css("width",t),v=e,y&&(a.cancel(c),c=a(function(){n()},250))}}function n(){if(!(1<=r())){var e,t=r();e=0<=t&&t<.25?(3*Math.random()+3)/100:.25<=t&&t<.65?3*Math.random()/100:.65<=t&&t<.9?2*Math.random()/100:.9<=t&&t<.99?.005:0,l(r()+e)}}function r(){return v}function t(){v=0,m=!1}var u,c,d,p=this.parentSelector,h=angular.element(this.loadingBarTemplate),f=h.find("div").eq(0),g=angular.element(this.spinnerTemplate),m=!1,v=0,y=this.autoIncrement,w=this.includeSpinner,b=this.includeBar,$=this.startSize;return{start:function(){if(u||(u=i.get("$animate")),a.cancel(d),!m){var e=o[0],t=e.querySelector?e.querySelector(p):o.find(p)[0];t||(t=e.getElementsByTagName("body")[0]);var n=angular.element(t),r=t.lastChild&&angular.element(t.lastChild);s.$broadcast("cfpLoadingBar:started"),m=!0,b&&u.enter(h,n,r),w&&u.enter(g,n,h),l($)}},set:l,status:r,inc:n,complete:function(){u||(u=i.get("$animate")),s.$broadcast("cfpLoadingBar:completed"),l(1),a.cancel(d),d=a(function(){var e=u.leave(h,t);e&&e.then&&e.then(t),u.leave(g)},500)},autoIncrement:this.autoIncrement,includeSpinner:this.includeSpinner,latencyThreshold:this.latencyThreshold,parentSelector:this.parentSelector,startSize:this.startSize}}]})}(),angular.module("ui.bootstrap",["ui.bootstrap.tpls","ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]),angular.module("ui.bootstrap.tpls",["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/datepicker/day.html","template/datepicker/month.html","template/datepicker/popup.html","template/datepicker/year.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/timepicker/timepicker.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]),angular.module("ui.bootstrap.transition",[]).factory("$transition",["$q","$timeout","$rootScope",function(a,s,l){function e(e){for(var t in e)if(void 0!==n.style[t])return e[t]}var u=function(e,t,n){n=n||{};var r=a.defer(),i=u[n.animation?"animationEndEventName":"transitionEndEventName"],o=function(){l.$apply(function(){e.unbind(i,o),r.resolve(e)})};return i&&e.bind(i,o),s(function(){angular.isString(t)?e.addClass(t):angular.isFunction(t)?t(e):angular.isObject(t)&&e.css(t),i||r.resolve(e)}),r.promise.cancel=function(){i&&e.unbind(i,o),r.reject("Transition cancelled")},r.promise},n=document.createElement("trans");return u.transitionEndEventName=e({WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",transition:"transitionend"}),u.animationEndEventName=e({WebkitTransition:"webkitAnimationEnd",MozTransition:"animationend",OTransition:"oAnimationEnd",transition:"animationend"}),u}]),angular.module("ui.bootstrap.collapse",["ui.bootstrap.transition"]).directive("collapse",["$transition",function(l){return{link:function(e,r,t){function n(e){function t(){a===n&&(a=void 0)}var n=l(r,e);return a&&a.cancel(),(a=n).then(t,t),n}function i(){r.removeClass("collapsing"),r.addClass("collapse in"),r.css({height:"auto"})}function o(){r.removeClass("collapsing"),r.addClass("collapse")}var a,s=!0;e.$watch(t.collapse,function(e){e?s?(s=!1,o(),r.css({height:0})):(r.css({height:r[0].scrollHeight+"px"}),r[0].offsetWidth,r.removeClass("collapse in").addClass("collapsing"),n({height:0}).then(o)):s?(s=!1,i()):(r.removeClass("collapse").addClass("collapsing"),n({height:r[0].scrollHeight+"px"}).then(i))})}}}]),angular.module("ui.bootstrap.accordion",["ui.bootstrap.collapse"]).constant("accordionConfig",{closeOthers:!0}).controller("AccordionController",["$scope","$attrs","accordionConfig",function(e,n,r){this.groups=[],this.closeOthers=function(t){(angular.isDefined(n.closeOthers)?e.$eval(n.closeOthers):r.closeOthers)&&angular.forEach(this.groups,function(e){e!==t&&(e.isOpen=!1)})},this.addGroup=function(e){var t=this;this.groups.push(e),e.$on("$destroy",function(){t.removeGroup(e)})},this.removeGroup=function(e){var t=this.groups.indexOf(e);-1!==t&&this.groups.splice(t,1)}}]).directive("accordion",function(){return{restrict:"EA",controller:"AccordionController",transclude:!0,replace:!1,templateUrl:"template/accordion/accordion.html"}}).directive("accordionGroup",function(){return{require:"^accordion",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/accordion/accordion-group.html",scope:{heading:"@",isOpen:"=?",isDisabled:"=?"},controller:function(){this.setHeading=function(e){this.heading=e}},link:function(t,e,n,r){r.addGroup(t),t.$watch("isOpen",function(e){e&&r.closeOthers(t)}),t.toggleOpen=function(){t.isDisabled||(t.isOpen=!t.isOpen)}}}}).directive("accordionHeading",function(){return{restrict:"EA",transclude:!0,template:"",replace:!0,require:"^accordionGroup",link:function(e,t,n,r,i){r.setHeading(i(e,function(){}))}}}).directive("accordionTransclude",function(){return{require:"^accordionGroup",link:function(e,t,n,r){e.$watch(function(){return r[n.accordionTransclude]},function(e){e&&(t.html(""),t.append(e))})}}}),angular.module("ui.bootstrap.alert",[]).controller("AlertController",["$scope","$attrs",function(e,t){e.closeable="close"in t,this.close=e.close}]).directive("alert",function(){return{restrict:"EA",controller:"AlertController",templateUrl:"template/alert/alert.html",transclude:!0,replace:!0,scope:{type:"@",close:"&"}}}).directive("dismissOnTimeout",["$timeout",function(i){return{require:"alert",link:function(e,t,n,r){i(function(){r.close()},parseInt(n.dismissOnTimeout,10))}}}]),angular.module("ui.bootstrap.bindHtml",[]).directive("bindHtmlUnsafe",function(){return function(e,t,n){t.addClass("ng-binding").data("$binding",n.bindHtmlUnsafe),e.$watch(n.bindHtmlUnsafe,function(e){t.html(e||"")})}}),angular.module("ui.bootstrap.buttons",[]).constant("buttonConfig",{activeClass:"active",toggleEvent:"click"}).controller("ButtonsController",["buttonConfig",function(e){this.activeClass=e.activeClass||"active",this.toggleEvent=e.toggleEvent||"click"}]).directive("btnRadio",function(){return{require:["btnRadio","ngModel"],controller:"ButtonsController",link:function(t,n,r,e){var i=e[0],o=e[1];o.$render=function(){n.toggleClass(i.activeClass,angular.equals(o.$modelValue,t.$eval(r.btnRadio)))},n.bind(i.toggleEvent,function(){var e=n.hasClass(i.activeClass);(!e||angular.isDefined(r.uncheckable))&&t.$apply(function(){o.$setViewValue(e?null:t.$eval(r.btnRadio)),o.$render()})})}}}).directive("btnCheckbox",function(){return{require:["btnCheckbox","ngModel"],controller:"ButtonsController",link:function(r,e,t,n){function i(){return o(t.btnCheckboxTrue,!0)}function o(e,t){var n=r.$eval(e);return angular.isDefined(n)?n:t}var a=n[0],s=n[1];s.$render=function(){e.toggleClass(a.activeClass,angular.equals(s.$modelValue,i()))},e.bind(a.toggleEvent,function(){r.$apply(function(){s.$setViewValue(e.hasClass(a.activeClass)?o(t.btnCheckboxFalse,!1):i()),s.$render()})})}}}),angular.module("ui.bootstrap.carousel",["ui.bootstrap.transition"]).controller("CarouselController",["$scope","$timeout","$interval","$transition",function(a,t,n,s){function l(){r();var e=+a.interval;!isNaN(e)&&0=d.length?d[t-1]:d[t]):t
    ");e.attr({"ng-model":"date","ng-change":"dateSelection()"});var c=angular.element(e.children()[0]);i.datepickerOptions&&angular.forEach(r.$parent.$eval(i.datepickerOptions),function(e,t){c.attr(o(t),e)}),r.watchData={},angular.forEach(["minDate","maxDate","datepickerMode"],function(t){if(i[t]){var e=g(i[t]);if(r.$parent.$watch(e,function(e){r.watchData[t]=e}),c.attr(o(t),"watchData."+t),"datepickerMode"===t){var n=e.assign;r.$watch("watchData."+t,function(e,t){e!==t&&n(r.$parent,e)})}}}),i.dateDisabled&&c.attr("date-disabled","dateDisabled({ date: date, mode: mode })"),n.$parsers.unshift(a),r.dateSelection=function(e){angular.isDefined(e)&&(r.date=e),n.$setViewValue(r.date),n.$render(),l&&(r.isOpen=!1,t[0].focus())},t.bind("input change keyup",function(){r.$apply(function(){r.date=n.$modelValue})}),n.$render=function(){var e=n.$viewValue?y(n.$viewValue,s):"";t.val(e),r.date=a(n.$modelValue)};var d=function(e){r.isOpen&&e.target!==t[0]&&r.$apply(function(){r.isOpen=!1})},p=function(e){r.keydown(e)};t.bind("keydown",p),r.keydown=function(e){27===e.which?(e.preventDefault(),e.stopPropagation(),r.close()):40!==e.which||r.isOpen||(r.isOpen=!0)},r.$watch("isOpen",function(e){e?(r.$broadcast("datepicker.focus"),r.position=u?v.offset(t):v.position(t),r.position.top=r.position.top+t.prop("offsetHeight"),m.bind("click",d)):m.unbind("click",d)}),r.select=function(e){if("today"===e){var t=new Date;angular.isDate(n.$modelValue)?(e=new Date(n.$modelValue)).setFullYear(t.getFullYear(),t.getMonth(),t.getDate()):e=new Date(t.setHours(0,0,0,0))}r.dateSelection(e)},r.close=function(){r.isOpen=!1,t[0].focus()};var h=f(e)(r);e.remove(),u?m.find("body").append(h):t.after(h),r.$on("$destroy",function(){h.remove(),t.unbind("keydown",p),m.unbind("click",d)})}}}]).directive("datepickerPopupWrap",function(){return{restrict:"EA",replace:!0,transclude:!0,templateUrl:"template/datepicker/popup.html",link:function(e,t){t.bind("click",function(e){e.preventDefault(),e.stopPropagation()})}}}),angular.module("ui.bootstrap.dropdown",[]).constant("dropdownConfig",{openClass:"open"}).service("dropdownService",["$document",function(t){var n=null;this.open=function(e){n||(t.bind("click",r),t.bind("keydown",i)),n&&n!==e&&(n.isOpen=!1),n=e},this.close=function(e){n===e&&(n=null,t.unbind("click",r),t.unbind("keydown",i))};var r=function(e){if(n){var t=n.getToggleElement();e&&t&&t[0].contains(e.target)||n.$apply(function(){n.isOpen=!1})}},i=function(e){27===e.which&&(n.focusToggleElement(),r())}}]).controller("DropdownController",["$scope","$attrs","$parse","dropdownConfig","dropdownService","$animate",function(n,t,r,e,i,o){var a,s=this,l=n.$new(),u=e.openClass,c=angular.noop,d=t.onToggle?r(t.onToggle):angular.noop;this.init=function(e){s.$element=e,t.isOpen&&(a=r(t.isOpen),c=a.assign,n.$watch(a,function(e){l.isOpen=!!e}))},this.toggle=function(e){return l.isOpen=arguments.length?!!e:!l.isOpen},this.isOpen=function(){return l.isOpen},l.getToggleElement=function(){return s.toggleElement},l.focusToggleElement=function(){s.toggleElement&&s.toggleElement[0].focus()},l.$watch("isOpen",function(e,t){o[e?"addClass":"removeClass"](s.$element,u),e?(l.focusToggleElement(),i.open(l)):i.close(l),c(n,e),angular.isDefined(e)&&e!==t&&d(n,{open:!!e})}),n.$on("$locationChangeSuccess",function(){l.isOpen=!1}),n.$on("$destroy",function(){l.$destroy()})}]).directive("dropdown",function(){return{controller:"DropdownController",link:function(e,t,n,r){r.init(t)}}}).directive("dropdownToggle",function(){return{require:"?^dropdown",link:function(t,n,r,i){if(i){i.toggleElement=n;var e=function(e){e.preventDefault(),n.hasClass("disabled")||r.disabled||t.$apply(function(){i.toggle()})};n.bind("click",e),n.attr({"aria-haspopup":!0,"aria-expanded":!1}),t.$watch(i.isOpen,function(e){n.attr("aria-expanded",!!e)}),t.$on("$destroy",function(){n.unbind("click",e)})}}}}),angular.module("ui.bootstrap.modal",["ui.bootstrap.transition"]).factory("$$stackedMap",function(){return{createNew:function(){var r=[];return{add:function(e,t){r.push({key:e,value:t})},get:function(e){for(var t=0;t");i.attr("backdrop-class",t.backdropClass),h=c(i)(f),n.append(h)}var o=angular.element("
    ");o.attr({"template-url":t.windowTemplateUrl,"window-class":t.windowClass,size:t.size,index:m.length()-1,animate:"animate"}).html(t.content);var a=c(o)(t.scope);m.top().value.modalDomEl=a,n.append(a),n.addClass(g)},n.close=function(e,t){var n=m.get(e);n&&(n.value.deferred.resolve(t),r(e))},n.dismiss=function(e,t){var n=m.get(e);n&&(n.value.deferred.reject(t),r(e))},n.dismissAll=function(e){for(var t=this.getTop();t;)this.dismiss(t.key,e),t=this.getTop()},n.getTop=function(){return m.top()},n}]).provider("$modal",function(){var g={options:{backdrop:!0,keyboard:!0},$get:["$injector","$rootScope","$q","$http","$templateCache","$controller","$modalStack",function(l,u,c,d,p,h,f){var e={};return e.open=function(o){var a=c.defer(),e=c.defer(),s={result:a.promise,opened:e.promise,close:function(e){f.close(s,e)},dismiss:function(e){f.dismiss(s,e)}};if((o=angular.extend({},g.options,o)).resolve=o.resolve||{},!o.template&&!o.templateUrl)throw new Error("One of template or templateUrl options is required.");var t,n,r,i=c.all([(r=o,r.template?c.when(r.template):d.get(angular.isFunction(r.templateUrl)?r.templateUrl():r.templateUrl,{cache:p}).then(function(e){return e.data}))].concat((t=o.resolve,n=[],angular.forEach(t,function(e){(angular.isFunction(e)||angular.isArray(e))&&n.push(c.when(l.invoke(e)))}),n)));return i.then(function(n){var e=(o.scope||u).$new();e.$close=s.close,e.$dismiss=s.dismiss;var t,r={},i=1;o.controller&&(r.$scope=e,r.$modalInstance=s,angular.forEach(o.resolve,function(e,t){r[t]=n[i++]}),t=h(o.controller,r),o.controllerAs&&(e[o.controllerAs]=t)),f.open(s,{scope:e,deferred:a,content:n[0],backdrop:o.backdrop,keyboard:o.keyboard,backdropClass:o.backdropClass,windowClass:o.windowClass,windowTemplateUrl:o.windowTemplateUrl,size:o.size})},function(e){a.reject(e)}),i.then(function(){e.resolve(!0)},function(){e.reject(!1)}),s},e}]};return g}),angular.module("ui.bootstrap.pagination",[]).controller("PaginationController",["$scope","$attrs","$parse",function(n,r,i){var o=this,a={$setViewValue:angular.noop},t=r.numPages?i(r.numPages).assign:angular.noop;this.init=function(e,t){a=e,this.config=t,a.$render=function(){o.render()},r.itemsPerPage?n.$parent.$watch(i(r.itemsPerPage),function(e){o.itemsPerPage=parseInt(e,10),n.totalPages=o.calculateTotalPages()}):this.itemsPerPage=t.itemsPerPage},this.calculateTotalPages=function(){var e=this.itemsPerPage<1?1:Math.ceil(n.totalItems/this.itemsPerPage);return Math.max(e||0,1)},this.render=function(){n.page=parseInt(a.$viewValue,10)||1},n.selectPage=function(e){n.page!==e&&0e?n.selectPage(e):a.$render()})}]).constant("paginationConfig",{itemsPerPage:10,boundaryLinks:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0}).directive("pagination",["$parse","paginationConfig",function(s,l){return{restrict:"EA",scope:{totalItems:"=",firstText:"@",previousText:"@",nextText:"@",lastText:"@"},require:["pagination","?ngModel"],controller:"PaginationController",templateUrl:"template/pagination/pagination.html",replace:!0,link:function(e,t,n,r){function c(e,t,n){return{number:e,text:t,active:n}}var i=r[0],o=r[1];if(o){var d=angular.isDefined(n.maxSize)?e.$parent.$eval(n.maxSize):l.maxSize,p=angular.isDefined(n.rotate)?e.$parent.$eval(n.rotate):l.rotate;e.boundaryLinks=angular.isDefined(n.boundaryLinks)?e.$parent.$eval(n.boundaryLinks):l.boundaryLinks,e.directionLinks=angular.isDefined(n.directionLinks)?e.$parent.$eval(n.directionLinks):l.directionLinks,i.init(o,l),n.maxSize&&e.$parent.$watch(s(n.maxSize),function(e){d=parseInt(e,10),i.render()});var a=i.render;i.render=function(){a(),0';return{restrict:"EA",compile:function(){var _=o(i);return function(r,t,i){function e(){m.isOpen?o():n()}function n(){var e,t,n;(!g||r.$eval(i[S+"Enable"]))&&(n=i[S+"Placement"],m.placement=angular.isDefined(n)?n:D.placement,e=i[S+"PopupDelay"],t=parseInt(e,10),m.popupDelay=isNaN(t)?D.popupDelay:t,m.popupDelay?p||(p=x(a,m.popupDelay,!1)).then(function(e){e()}):a()())}function o(){r.$apply(function(){s()})}function a(){return p=null,d&&(x.cancel(d),d=null),m.content?(u&&l(),c=m.$new(),(u=_(c,function(e){h?O.find("body").append(e):t.after(e)})).css({top:0,left:0,display:"block"}),m.$digest(),v(),m.isOpen=!0,m.$digest(),v):angular.noop}function s(){m.isOpen=!1,x.cancel(p),p=null,m.animation?d||(d=x(l,500)):l()}function l(){d=null,u&&(u.remove(),u=null),c&&(c.$destroy(),c=null)}var u,c,d,p,h=!!angular.isDefined(D.appendToBody)&&D.appendToBody,f=k(void 0),g=angular.isDefined(i[S+"Enable"]),m=r.$new(!0),v=function(){var e=T.positionElements(t,u,m.placement,h);e.top+="px",e.left+="px",u.css(e)};m.isOpen=!1,i.$observe(C,function(e){!(m.content=e)&&m.isOpen&&s()}),i.$observe(S+"Title",function(e){m.title=e});var y,w=function(){t.unbind(f.show,n),t.unbind(f.hide,o)};y=i[S+"Trigger"],w(),(f=k(y)).show===f.hide?t.bind(f.show,e):(t.bind(f.show,n),t.bind(f.hide,o));var b=r.$eval(i[S+"Animation"]);m.animation=angular.isDefined(b)?!!b:D.animation;var $=r.$eval(i[S+"AppendToBody"]);(h=angular.isDefined($)?$:h)&&r.$on("$locationChangeSuccess",function(){m.isOpen&&s()}),r.$on("$destroy",function(){x.cancel(d),x.cancel(p),w(),l(),m=null})}}}}}]}).directive("tooltipPopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-popup.html"}}).directive("tooltip",["$tooltip",function(e){return e("tooltip","tooltip","mouseenter")}]).directive("tooltipHtmlUnsafePopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-unsafe-popup.html"}}).directive("tooltipHtmlUnsafe",["$tooltip",function(e){return e("tooltipHtmlUnsafe","tooltip","mouseenter")}]),angular.module("ui.bootstrap.popover",["ui.bootstrap.tooltip"]).directive("popoverPopup",function(){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover.html"}}).directive("popover",["$tooltip",function(e){return e("popover","popover","click")}]),angular.module("ui.bootstrap.progressbar",[]).constant("progressConfig",{animate:!0,max:100}).controller("ProgressController",["$scope","$attrs","progressConfig",function(n,e,t){var r=this,i=angular.isDefined(e.animate)?n.$parent.$eval(e.animate):t.animate;this.bars=[],n.max=angular.isDefined(e.max)?n.$parent.$eval(e.max):t.max,this.addBar=function(t,e){i||e.css({transition:"none"}),this.bars.push(t),t.$watch("value",function(e){t.percent=+(100*e/n.max).toFixed(2)}),t.$on("$destroy",function(){e=null,r.removeBar(t)})},this.removeBar=function(e){this.bars.splice(this.bars.indexOf(e),1)}}]).directive("progress",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",require:"progress",scope:{},templateUrl:"template/progressbar/progress.html"}}).directive("bar",function(){return{restrict:"EA",replace:!0,transclude:!0,require:"^progress",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/bar.html",link:function(e,t,n,r){r.addBar(e,t)}}}).directive("progressbar",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/progressbar.html",link:function(e,t,n,r){r.addBar(e,angular.element(t.children()[0]))}}}),angular.module("ui.bootstrap.rating",[]).constant("ratingConfig",{max:5,stateOn:null,stateOff:null}).controller("RatingController",["$scope","$attrs","ratingConfig",function(n,r,i){var o={$setViewValue:angular.noop};this.init=function(e){(o=e).$render=this.render,this.stateOn=angular.isDefined(r.stateOn)?n.$parent.$eval(r.stateOn):i.stateOn,this.stateOff=angular.isDefined(r.stateOff)?n.$parent.$eval(r.stateOff):i.stateOff;var t=angular.isDefined(r.ratingStates)?n.$parent.$eval(r.ratingStates):new Array(angular.isDefined(r.max)?n.$parent.$eval(r.max):i.max);n.range=this.buildTemplateObjects(t)},this.buildTemplateObjects=function(e){for(var t=0,n=e.length;t");v.attr({id:t,matches:"matches",active:"activeIdx",select:"select(activeIdx)",query:"query",position:"position"}),angular.isDefined(e.typeaheadTemplateUrl)&&v.attr("template-url",e.typeaheadTemplateUrl);var y=function(){m.matches=[],m.activeIdx=-1,a.attr("aria-expanded",!1)},w=function(e){return t+"-option-"+e};m.$watch("activeIdx",function(e){e<0?a.removeAttr("aria-activedescendant"):a.attr("aria-activedescendant",w(e))});var b=function(r){var i={$viewValue:r};u(o,!0),x.when(g.source(o,i)).then(function(e){var t=r===s.$viewValue;if(t&&l)if(0=n?0$&"):e}}),angular.module("template/accordion/accordion-group.html",[]).run(["$templateCache",function(e){e.put("template/accordion/accordion-group.html",'
    \n
    \n

    \n {{heading}}\n

    \n
    \n
    \n\t
    \n
    \n
    \n')}]),angular.module("template/accordion/accordion.html",[]).run(["$templateCache",function(e){e.put("template/accordion/accordion.html",'
    ')}]),angular.module("template/alert/alert.html",[]).run(["$templateCache",function(e){e.put("template/alert/alert.html",'\n')}]),angular.module("template/carousel/carousel.html",[]).run(["$templateCache",function(e){e.put("template/carousel/carousel.html",'\n')}]),angular.module("template/carousel/slide.html",[]).run(["$templateCache",function(e){e.put("template/carousel/slide.html","
    \n")}]),angular.module("template/datepicker/datepicker.html",[]).run(["$templateCache",function(e){e.put("template/datepicker/datepicker.html",'
    \n \n \n \n
    ')}]),angular.module("template/datepicker/day.html",[]).run(["$templateCache",function(e){e.put("template/datepicker/day.html",'
    \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
    {{label.abbr}}
    {{ weekNumbers[$index] }}\n \n
    \n')}]),angular.module("template/datepicker/month.html",[]).run(["$templateCache",function(e){e.put("template/datepicker/month.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n')}]),angular.module("template/datepicker/popup.html",[]).run(["$templateCache",function(e){e.put("template/datepicker/popup.html",'\n')}]),angular.module("template/datepicker/year.html",[]).run(["$templateCache",function(e){e.put("template/datepicker/year.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n')}]),angular.module("template/modal/backdrop.html",[]).run(["$templateCache",function(e){e.put("template/modal/backdrop.html",'\n')}]),angular.module("template/modal/window.html",[]).run(["$templateCache",function(e){e.put("template/modal/window.html",'')}]),angular.module("template/pagination/pager.html",[]).run(["$templateCache",function(e){e.put("template/pagination/pager.html",'')}]),angular.module("template/pagination/pagination.html",[]).run(["$templateCache",function(e){e.put("template/pagination/pagination.html",'')}]),angular.module("template/tooltip/tooltip-html-unsafe-popup.html",[]).run(["$templateCache",function(e){e.put("template/tooltip/tooltip-html-unsafe-popup.html",'
    \n
    \n
    \n
    \n')}]),angular.module("template/tooltip/tooltip-popup.html",[]).run(["$templateCache",function(e){e.put("template/tooltip/tooltip-popup.html",'
    \n
    \n
    \n
    \n')}]),angular.module("template/popover/popover.html",[]).run(["$templateCache",function(e){e.put("template/popover/popover.html",'
    \n
    \n\n
    \n

    \n
    \n
    \n
    \n')}]),angular.module("template/progressbar/bar.html",[]).run(["$templateCache",function(e){e.put("template/progressbar/bar.html",'
    ')}]),angular.module("template/progressbar/progress.html",[]).run(["$templateCache",function(e){e.put("template/progressbar/progress.html",'
    ')}]),angular.module("template/progressbar/progressbar.html",[]).run(["$templateCache",function(e){e.put("template/progressbar/progressbar.html",'
    \n
    \n
    ')}]),angular.module("template/rating/rating.html",[]).run(["$templateCache",function(e){e.put("template/rating/rating.html",'\n \n ({{ $index < value ? \'*\' : \' \' }})\n \n')}]),angular.module("template/tabs/tab.html",[]).run(["$templateCache",function(e){e.put("template/tabs/tab.html",'
  • \n {{heading}}\n
  • \n')}]),angular.module("template/tabs/tabset.html",[]).run(["$templateCache",function(e){e.put("template/tabs/tabset.html",'
    \n \n
    \n
    \n
    \n
    \n
    \n')}]),angular.module("template/timepicker/timepicker.html",[]).run(["$templateCache",function(e){e.put("template/timepicker/timepicker.html",'\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n
     
    \n\t\t\t\t\n\t\t\t:\n\t\t\t\t\n\t\t\t
     
    \n')}]),angular.module("template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(e){e.put("template/typeahead/typeahead-match.html",'')}]),angular.module("template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(e){e.put("template/typeahead/typeahead-popup.html",'\n')}]),function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.moment=t()}(this,function(){"use strict";var e,i;function p(){return e.apply(null,arguments)}function s(e){return e instanceof Array||"[object Array]"===Object.prototype.toString.call(e)}function l(e){return null!=e&&"[object Object]"===Object.prototype.toString.call(e)}function u(e){return void 0===e}function c(e){return"number"==typeof e||"[object Number]"===Object.prototype.toString.call(e)}function d(e){return e instanceof Date||"[object Date]"===Object.prototype.toString.call(e)}function h(e,t){var n,r=[];for(n=0;n>>0,r=0;rCe(e)?(o=e+1,s-Ce(e)):(o=e,s),{year:o,dayOfYear:a}}function Ne(e,t,n){var r,i,o=He(e.year(),t,n),a=Math.floor((e.dayOfYear()-o-1)/7)+1;return a<1?r=a+qe(i=e.year()-1,t,n):a>qe(e.year(),t,n)?(r=a-qe(e.year(),t,n),i=e.year()+1):(i=e.year(),r=a),{week:r,year:i}}function qe(e,t,n){var r=He(e,t,n),i=He(e+1,t,n);return(Ce(e)-r+i)/7}N("w",["ww",2],"wo","week"),N("W",["WW",2],"Wo","isoWeek"),P("week","w"),P("isoWeek","W"),V("week",5),V("isoWeek",5),le("w",Q),le("ww",Q,B),le("W",Q),le("WW",Q,B),he(["w","ww","W","WW"],function(e,t,n,r){t[r.substr(0,1)]=S(e)});N("d",0,"do","day"),N("dd",0,0,function(e){return this.localeData().weekdaysMin(this,e)}),N("ddd",0,0,function(e){return this.localeData().weekdaysShort(this,e)}),N("dddd",0,0,function(e){return this.localeData().weekdays(this,e)}),N("e",0,0,"weekday"),N("E",0,0,"isoWeekday"),P("day","d"),P("weekday","e"),P("isoWeekday","E"),V("day",11),V("weekday",11),V("isoWeekday",11),le("d",Q),le("e",Q),le("E",Q),le("dd",function(e,t){return t.weekdaysMinRegex(e)}),le("ddd",function(e,t){return t.weekdaysShortRegex(e)}),le("dddd",function(e,t){return t.weekdaysRegex(e)}),he(["dd","ddd","dddd"],function(e,t,n,r){var i=n._locale.weekdaysParse(e,r,n._strict);null!=i?t.d=i:v(n).invalidWeekday=e}),he(["d","e","E"],function(e,t,n,r){t[r]=S(e)});var Ue="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_");var ze="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_");var Be="Su_Mo_Tu_We_Th_Fr_Sa".split("_");var We=ae;var Ge=ae;var Ke=ae;function Qe(){function e(e,t){return t.length-e.length}var t,n,r,i,o,a=[],s=[],l=[],u=[];for(t=0;t<7;t++)n=m([2e3,1]).day(t),r=this.weekdaysMin(n,""),i=this.weekdaysShort(n,""),o=this.weekdays(n,""),a.push(r),s.push(i),l.push(o),u.push(r),u.push(i),u.push(o);for(a.sort(e),s.sort(e),l.sort(e),u.sort(e),t=0;t<7;t++)s[t]=ce(s[t]),l[t]=ce(l[t]),u[t]=ce(u[t]);this._weekdaysRegex=new RegExp("^("+u.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+l.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+s.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+a.join("|")+")","i")}function Ze(){return this.hours()%12||12}function Xe(e,t){N(e,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),t)})}function Je(e,t){return t._meridiemParse}N("H",["HH",2],0,"hour"),N("h",["hh",2],0,Ze),N("k",["kk",2],0,function(){return this.hours()||24}),N("hmm",0,0,function(){return""+Ze.apply(this)+F(this.minutes(),2)}),N("hmmss",0,0,function(){return""+Ze.apply(this)+F(this.minutes(),2)+F(this.seconds(),2)}),N("Hmm",0,0,function(){return""+this.hours()+F(this.minutes(),2)}),N("Hmmss",0,0,function(){return""+this.hours()+F(this.minutes(),2)+F(this.seconds(),2)}),Xe("a",!0),Xe("A",!1),P("hour","h"),V("hour",13),le("a",Je),le("A",Je),le("H",Q),le("h",Q),le("k",Q),le("HH",Q,B),le("hh",Q,B),le("kk",Q,B),le("hmm",Z),le("hmmss",X),le("Hmm",Z),le("Hmmss",X),pe(["H","HH"],ve),pe(["k","kk"],function(e,t,n){var r=S(e);t[ve]=24===r?0:r}),pe(["a","A"],function(e,t,n){n._isPm=n._locale.isPM(e),n._meridiem=e}),pe(["h","hh"],function(e,t,n){t[ve]=S(e),v(n).bigHour=!0}),pe("hmm",function(e,t,n){var r=e.length-2;t[ve]=S(e.substr(0,r)),t[ye]=S(e.substr(r)),v(n).bigHour=!0}),pe("hmmss",function(e,t,n){var r=e.length-4,i=e.length-2;t[ve]=S(e.substr(0,r)),t[ye]=S(e.substr(r,2)),t[we]=S(e.substr(i)),v(n).bigHour=!0}),pe("Hmm",function(e,t,n){var r=e.length-2;t[ve]=S(e.substr(0,r)),t[ye]=S(e.substr(r))}),pe("Hmmss",function(e,t,n){var r=e.length-4,i=e.length-2;t[ve]=S(e.substr(0,r)),t[ye]=S(e.substr(r,2)),t[we]=S(e.substr(i))});var et,tt=xe("Hours",!0),nt={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Pe,monthsShort:Me,week:{dow:0,doy:6},weekdays:Ue,weekdaysMin:Be,weekdaysShort:ze,meridiemParse:/[ap]\.?m?\.?/i},rt={},it={};function ot(e){return e?e.toLowerCase().replace("_","-"):e}function at(e){var t=null;if(!rt[e]&&"undefined"!=typeof module&&module&&module.exports)try{t=et._abbr,require("./locale/"+e),st(t)}catch(e){}return rt[e]}function st(e,t){var n;return e&&((n=u(t)?ut(e):lt(e,t))?et=n:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+e+" not found. Did you forget to load it?")),et._abbr}function lt(e,t){if(null===t)return delete rt[e],null;var n,r=nt;if(t.abbr=e,null!=rt[e])x("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),r=rt[e]._config;else if(null!=t.parentLocale)if(null!=rt[t.parentLocale])r=rt[t.parentLocale]._config;else{if(null==(n=at(t.parentLocale)))return it[t.parentLocale]||(it[t.parentLocale]=[]),it[t.parentLocale].push({name:e,config:t}),null;r=n._config}return rt[e]=new E(T(r,t)),it[e]&&it[e].forEach(function(e){lt(e.name,e.config)}),st(e),rt[e]}function ut(e){var t;if(e&&e._locale&&e._locale._abbr&&(e=e._locale._abbr),!e)return et;if(!s(e)){if(t=at(e))return t;e=[e]}return function(e){for(var t,n,r,i,o=0;o=t&&a(i,n,!0)>=t-1)break;t--}o++}return et}(e)}function ct(e){var t,n=e._a;return n&&-2===v(e).overflow&&(t=n[ge]<0||11Ee(n[fe],n[ge])?me:n[ve]<0||24qe(n,o,a)?v(e)._overflowWeeks=!0:null!=l?v(e)._overflowWeekday=!0:(s=Ye(n,r,i,o,a),e._a[fe]=s.year,e._dayOfYear=s.dayOfYear)}(e),null!=e._dayOfYear&&(o=dt(e._a[fe],r[fe]),(e._dayOfYear>Ce(o)||0===e._dayOfYear)&&(v(e)._overflowDayOfYear=!0),n=je(o,0,e._dayOfYear),e._a[ge]=n.getUTCMonth(),e._a[me]=n.getUTCDate()),t=0;t<3&&null==e._a[t];++t)e._a[t]=a[t]=r[t];for(;t<7;t++)e._a[t]=a[t]=null==e._a[t]?2===t?1:0:e._a[t];24===e._a[ve]&&0===e._a[ye]&&0===e._a[we]&&0===e._a[be]&&(e._nextDay=!0,e._a[ve]=0),e._d=(e._useUTC?je:function(e,t,n,r,i,o,a){var s=new Date(e,t,n,r,i,o,a);return e<100&&0<=e&&isFinite(s.getFullYear())&&s.setFullYear(e),s}).apply(null,a),i=e._useUTC?e._d.getUTCDay():e._d.getDay(),null!=e._tzm&&e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),e._nextDay&&(e._a[ve]=24),e._w&&void 0!==e._w.d&&e._w.d!==i&&(v(e).weekdayMismatch=!0)}}var ht=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,ft=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,gt=/Z|[+-]\d\d(?::?\d\d)?/,mt=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],vt=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],yt=/^\/?Date\((\-?\d+)/i;function wt(e){var t,n,r,i,o,a,s=e._i,l=ht.exec(s)||ft.exec(s);if(l){for(v(e).iso=!0,t=0,n=mt.length;tn.valueOf():n.valueOf()this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},un.isLocal=function(){return!!this.isValid()&&!this._isUTC},un.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},un.isUtc=Ht,un.isUTC=Ht,un.zoneAbbr=function(){return this._isUTC?"UTC":""},un.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},un.dates=n("dates accessor is deprecated. Use date instead.",nn),un.months=n("months accessor is deprecated. Use month instead",Ie),un.years=n("years accessor is deprecated. Use year instead",De),un.zone=n("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function(e,t){return null!=e?("string"!=typeof e&&(e=-e),this.utcOffset(e,t),this):-this.utcOffset()}),un.isDSTShifted=n("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function(){if(!u(this._isDSTShifted))return this._isDSTShifted;var e={};if(b(e,this),(e=kt(e))._a){var t=e._isUTC?m(e._a):xt(e._a);this._isDSTShifted=this.isValid()&&0-1/0&&t.selectable&&c[e]){var r=c[e](t.utcDateValue),i=[];if(r.weeks)for(var o=0;on+9,past:u.year()r.indexOf(e.startView))throw new Error("startView must be greater than minView");if(!a.isNumber(e.minuteStep))throw new Error("minuteStep must be numeric");if(e.minuteStep<=0||60<=e.minuteStep)throw new Error("minuteStep must be greater than zero and less than 60");if(null!==e.configureOn&&!a.isString(e.configureOn))throw new Error("configureOn must be a string");if(null!==e.configureOn&&e.configureOn.length<1)throw new Error("configureOn must not be an empty string");if(null!==e.renderOn&&!a.isString(e.renderOn))throw new Error("renderOn must be a string");if(null!==e.renderOn&&e.renderOn.length<1)throw new Error("renderOn must not be an empty string");if(null!==e.modelType&&!a.isString(e.modelType))throw new Error("modelType must be a string");if(null!==e.modelType&&e.modelType.length<1)throw new Error("modelType must not be an empty string");"Date"!==e.modelType&&"moment"!==e.modelType&&"milliseconds"!==e.modelType&&(e.parseFormat=e.modelType);if(null!==e.dropdownSelector&&!a.isString(e.dropdownSelector))throw new Error("dropdownSelector must be a string");null===e.dropdownSelector||"undefined"!=typeof jQuery&&"function"==typeof jQuery().dropdown||(i.error("Please DO NOT specify the dropdownSelector option unless you are using jQuery AND Bootstrap.js. Please include jQuery AND Bootstrap.js, or write code to close the dropdown in the on-set-time callback. \n\nThe dropdownSelector configuration option is being removed because it will not function properly."),delete e.dropdownSelector)}}}a.module("ui.bootstrap.datetimepicker",[]).service("dateTimePickerConfig",function(){var e={bg:{previous:"предишна",next:"следваща"},ca:{previous:"anterior",next:"següent"},da:{previous:"forrige",next:"næste"},de:{previous:"vorige",next:"weiter"},"en-au":{previous:"previous",next:"next"},"en-gb":{previous:"previous",next:"next"},en:{previous:"previous",next:"next"},"es-us":{previous:"atrás",next:"siguiente"},es:{previous:"atrás",next:"siguiente"},fi:{previous:"edellinen",next:"seuraava"},fr:{previous:"précédent",next:"suivant"},hu:{previous:"előző",next:"következő"},it:{previous:"precedente",next:"successivo"},ja:{previous:"前へ",next:"次へ"},ml:{previous:"മുൻപുള്ളത്",next:"അടുത്തത്"},nl:{previous:"vorige",next:"volgende"},pl:{previous:"poprzednia",next:"następna"},"pt-br":{previous:"anteriores",next:"próximos"},pt:{previous:"anterior",next:"próximo"},ro:{previous:"anterior",next:"următor"},ru:{previous:"предыдущая",next:"следующая"},sk:{previous:"predošlá",next:"ďalšia"},sv:{previous:"föregående",next:"nästa"},tr:{previous:"önceki",next:"sonraki"},uk:{previous:"назад",next:"далі"},"zh-cn":{previous:"上一页",next:"下一页"},"zh-tw":{previous:"上一頁",next:"下一頁"}}[b.locale().toLowerCase()];return a.extend({},{configureOn:null,dropdownSelector:null,minuteStep:5,minView:"minute",modelType:"Date",parseFormat:"YYYY-MM-DDTHH:mm:ss.SSSZZ",renderOn:null,startView:"day"},{screenReader:e})}).service("dateTimePickerValidator",t).directive("datetimepicker",e),e.$inject=["dateTimePickerConfig","dateTimePickerValidator"],t.$inject=["$log"]}),angular.module("rzTable",[]),angular.module("rzTable").directive("rzTable",["resizeStorage","$injector","$parse",function(g,i,u){function e(e){}function c(n,r,i){return function(e,t){!0!==i.busy&&void 0!==t&&t!==e&&(d(n),p(n,r,i))}}function d(e){k=!0,l.map(function(e){e.remove()}),l=[]}function p(e,t,n){if(!n.busy){b=$(e).find("th"),v=n.mode,y=!angular.isDefined(n.saveTableSizes)||n.saveTableSizes,w=n.profile;var r=function(t,e){try{var n=e.rzMode?t.mode:"BasicResizer",r=i.get(n);return r}catch(e){return console.error("The resizer "+t.mode+" was not found"),null}}(n,t);r&&(S=new r(e,b,h),y&&(D=g.loadTableSizes(e,n.mode,n.profile)),s=S.handles(b),a=S.ctrlColumns,S.setup(),(o=D)&&($(C).width("auto"),a.each(function(e,t){var n=angular.element(t).scope(),r=n.rzCol||$(t).attr("id"),i=o[r];$(t).css({width:i})}),S.onTableReady()),s.each(function(e,t){!function(e,t,n){var r=$("
    ",{class:e.options.handleClass||"rz-handle"});$(n).prepend(r),l.push(r);var i=S.handleMiddleware(r,n);p=e,h=r,f=i,$(h).mousedown(function(e){k&&(S.onFirstDrag(f,h),S.onTableReady(),k=!1),p.options.onResizeStarted&&p.options.onResizeStarted(f);var t={};S.intervene&&(((t=S.intervene.selector(f)).column=t).orgWidth=$(t).width()),e.preventDefault(),$(h).addClass(p.options.handleClassActive||"rz-handle-active");var n,r,i,o,a,s,l,u,c=e.clientX,d=$(f).width();o=p,a=f,s=c,l=d,u=t,_=function(e){var t=e.clientX,n=t-s,r=S.calculate(l,n);if(!(rt)||(this.fixedColumn.width()<=this.getMinWidth(this.fixedColumn)?(this.bound=t,$(this.fixedColumn).width(this.minWidth),!0):void 0)},e.prototype.onEndDrag=function(){this.bound=!1},e.prototype.calculate=function(e,t){return e-t},e}]),angular.module("rzTable").factory("OverflowResizer",["ResizerModel",function(r){function e(e,t,n){r.call(this,e,t,n)}return(e.prototype=Object.create(r.prototype)).setup=function(){$(this.container).css({overflow:"auto"})},e.prototype.onTableReady=function(){$(this.table).width(1)},e}]),function(e,t){"function"==typeof define&&define.amd?define(["angular"],t):"object"==typeof module&&module.exports?module.exports=t(require("angular")):e.angularClipboard=t(e.angular)}(this,function(i){return i.module("angular-clipboard",[]).factory("clipboard",["$document","$window",function(s,l){return{copyText:function(e,t){var n,r,i=l.pageXOffset||s[0].documentElement.scrollLeft,o=l.pageYOffset||s[0].documentElement.scrollTop,a=(n=e,(r=s[0].createElement("textarea")).style.position="absolute",r.style.fontSize="12pt",r.style.border="0",r.style.padding="0",r.style.margin="0",r.style.left="-10000px",r.style.top=(l.pageYOffset||s[0].documentElement.scrollTop)+"px",r.textContent=n,r);s[0].body.appendChild(a),function(e){try{s[0].body.style.webkitUserSelect="initial";var t=s[0].getSelection();t.removeAllRanges();var n=document.createRange();n.selectNodeContents(e),t.addRange(n),e.select(),e.setSelectionRange(0,999999);try{if(!s[0].execCommand("copy"))throw"failure copy"}finally{t.removeAllRanges()}}finally{s[0].body.style.webkitUserSelect=""}}(a),l.scrollTo(i,o),s[0].body.removeChild(a)},supported:"queryCommandSupported"in s[0]&&s[0].queryCommandSupported("copy")}}]).directive("clipboard",["clipboard",function(r){return{restrict:"A",scope:{onCopied:"&",onError:"&",text:"=",supported:"=?"},link:function(t,n){t.supported=r.supported,n.on("click",function(e){try{r.copyText(t.text,n[0]),i.isFunction(t.onCopied)&&t.$evalAsync(t.onCopied())}catch(e){i.isFunction(t.onError)&&t.$evalAsync(t.onError({err:e}))}})}}}])}),function(e,t){"function"==typeof define&&define.amd?define("sifter",t):"object"==typeof exports?module.exports=t():e.Sifter=t()}(this,function(){var e=function(e,t){this.items=e,this.settings=t||{diacritics:!0}};e.prototype.tokenize=function(e){if(!(e=s(String(e||"").toLowerCase()))||!e.length)return[];var t,n,r,i,o=[],a=e.split(/ +/);for(t=0,n=a.length;t/g,">").replace(/"/g,""")},t={before:function(e,t,n){var r=e[t];e[t]=function(){return n.apply(e,arguments),r.apply(e,arguments)}},after:function(t,e,n){var r=t[e];t[e]=function(){var e=r.apply(t,arguments);return n.apply(t,arguments),e}}},n=function(t,n,e){var r,i=t.trigger,o={};for(r in t.trigger=function(){var e=arguments[0];if(-1===n.indexOf(e))return i.apply(t,arguments);o[e]=arguments},e.apply(t,[]),t.trigger=i,o)o.hasOwnProperty(r)&&i.apply(t,o[r])},f=function(e){var t={};if("selectionStart"in e)t.start=e.selectionStart,t.length=e.selectionEnd-t.start;else if(document.selection){e.focus();var n=document.selection.createRange(),r=document.selection.createRange().text.length;n.moveStart("character",-e.value.length),t.start=n.text.length-r,t.length=r}return t},O=function(p){var h=null,e=function(e,t){var n,r,i,o,a,s,l,u,c,d;(t=t||{},(e=e||window.event||{}).metaKey||e.altKey)||(t.force||!1!==p.data("grow"))&&(n=p.val(),e.type&&"keydown"===e.type.toLowerCase()&&(i=48<=(r=e.keyCode)&&r<=57||65<=r&&r<=90||96<=r&&r<=111||186<=r&&r<=222||32===r,46===r||8===r?(u=f(p[0])).length?n=n.substring(0,u.start)+n.substring(u.start+u.length):8===r&&u.start?n=n.substring(0,u.start-1)+n.substring(u.start+1):46===r&&void 0!==u.start&&(n=n.substring(0,u.start)+n.substring(u.start+1)):i&&(s=e.shiftKey,l=String.fromCharCode(e.keyCode),n+=l=s?l.toUpperCase():l.toLowerCase())),o=p.attr("placeholder"),!n&&o&&(n=o),d=p,(a=((c=n)?(w.$testInput||(w.$testInput=S("").css({position:"absolute",top:-99999,left:-99999,width:"auto",padding:0,whiteSpace:"pre"}).appendTo("body")),w.$testInput.text(c),function(e,t,n){var r,i,o={};if(n)for(r=0,i=n.length;r").addClass(g.wrapperClass).addClass(s).addClass(a),t=S("
    ").addClass(g.inputClass).addClass("items").appendTo(e),n=S('').appendTo(t).attr("tabindex",w.is(":disabled")?"-1":f.tabIndex),o=S(g.dropdownParent||e),r=S("
    ").addClass(g.dropdownClass).addClass(a).hide().appendTo(o),i=S("
    ").addClass(g.dropdownContentClass).appendTo(r),(u=w.attr("id"))&&(n.attr("id",u+"-selectized"),S("label[for='"+u+"']").attr("for",u+"-selectized")),f.settings.copyClassesToDropdown&&r.addClass(s),e.css({width:w[0].style.width}),f.plugins.names.length&&(l="plugin-"+f.plugins.names.join(" plugin-"),e.addClass(l),r.addClass(l)),(null===g.maxItems||1[data-selectable]",function(e){e.stopImmediatePropagation()}),r.on("mouseenter","[data-selectable]",function(){return f.onOptionHover.apply(f,arguments)}),r.on("mousedown click","[data-selectable]",function(){return f.onOptionSelect.apply(f,arguments)}),d="mousedown",p="*:not(input)",h=function(){return f.onItemSelect.apply(f,arguments)},(c=t).on(d,p,function(e){for(var t=e.target;t&&t.parentNode!==c[0];)t=t.parentNode;return e.currentTarget=t,h.apply(this,[e])}),O(n),t.on({mousedown:function(){return f.onMouseDown.apply(f,arguments)},click:function(){return f.onClick.apply(f,arguments)}}),n.on({mousedown:function(e){e.stopPropagation()},keydown:function(){return f.onKeyDown.apply(f,arguments)},keyup:function(){return f.onKeyUp.apply(f,arguments)},keypress:function(){return f.onKeyPress.apply(f,arguments)},resize:function(){f.positionDropdown.apply(f,[])},blur:function(){return f.onBlur.apply(f,arguments)},focus:function(){return f.ignoreBlur=!1,f.onFocus.apply(f,arguments)},paste:function(){return f.onPaste.apply(f,arguments)}}),y.on("keydown"+m,function(e){f.isCmdDown=e[$?"metaKey":"ctrlKey"],f.isCtrlDown=e[$?"altKey":"ctrlKey"],f.isShiftDown=e.shiftKey}),y.on("keyup"+m,function(e){e.keyCode===C&&(f.isCtrlDown=!1),16===e.keyCode&&(f.isShiftDown=!1),e.keyCode===_&&(f.isCmdDown=!1)}),y.on("mousedown"+m,function(e){if(f.isFocused){if(e.target===f.$dropdown[0]||e.target.parentNode===f.$dropdown[0])return!1;f.$control.has(e.target).length||e.target===f.$control[0]||f.blur(e.target)}}),v.on(["scroll"+m,"resize"+m].join(" "),function(){f.isOpen&&f.positionDropdown.apply(f,arguments)}),v.on("mousemove"+m,function(){f.ignoreHover=!1}),this.revertSettings={$children:w.children().detach(),tabindex:w.attr("tabindex")},w.attr("tabindex",-1).hide().after(f.$wrapper),S.isArray(g.items)&&(f.setValue(g.items),delete g.items),D&&w.on("invalid"+m,function(e){e.preventDefault(),f.isInvalid=!0,f.refreshState()}),f.updateOriginalInput(),f.refreshItems(),f.refreshState(),f.updatePlaceholder(),f.isSetup=!0,w.is(":disabled")&&f.disable(),f.on("change",this.onChange),w.data("selectize",f),w.addClass("selectized"),f.trigger("initialize"),!0===g.preload&&f.onSearchChange("")},setupTemplates:function(){var n=this.settings.labelField,r=this.settings.optgroupLabelField,e={optgroup:function(e){return'
    '+e.html+"
    "},optgroup_header:function(e,t){return'
    '+t(e[r])+"
    "},option:function(e,t){return'
    '+t(e[n])+"
    "},item:function(e,t){return'
    '+t(e[n])+"
    "},option_create:function(e,t){return'
    Add '+t(e.input)+"
    "}};this.settings.render=S.extend({},e,this.settings.render)},setupCallbacks:function(){var e,t,n={initialize:"onInitialize",change:"onChange",item_add:"onItemAdd",item_remove:"onItemRemove",clear:"onClear",option_add:"onOptionAdd",option_remove:"onOptionRemove",option_clear:"onOptionClear",optgroup_add:"onOptionGroupAdd",optgroup_remove:"onOptionGroupRemove",optgroup_clear:"onOptionGroupClear",dropdown_open:"onDropdownOpen",dropdown_close:"onDropdownClose",type:"onType",load:"onLoad",focus:"onFocus",blur:"onBlur"};for(e in n)n.hasOwnProperty(e)&&(t=this.settings[n[e]])&&this.on(e,t)},onClick:function(e){this.isFocused&&this.isOpen||(this.focus(),e.preventDefault())},onMouseDown:function(e){var t=this,n=e.isDefaultPrevented();S(e.target);if(t.isFocused){if(e.target!==t.$control_input[0])return"single"===t.settings.mode?t.isOpen?t.close():t.open():n||t.setActiveItem(null),!1}else n||window.setTimeout(function(){t.focus()},0)},onChange:function(){this.$input.trigger("change")},onPaste:function(e){var i=this;i.isFull()||i.isInputHidden||i.isLocked?e.preventDefault():i.settings.splitOn&&setTimeout(function(){var e=i.$control_input.val();if(e.match(i.settings.splitOn))for(var t=S.trim(e).split(i.settings.splitOn),n=0,r=t.length;n=this.settings.maxItems},updateOriginalInput:function(e){var t,n,r,i,o=this;if(e=e||{},1===o.tagType){for(r=[],t=0,n=o.items.length;t'+s(i)+"");r.length||this.$input.attr("multiple")||r.push(''),o.$input.html(r.join(""))}else o.$input.val(o.getValue()),o.$input.attr("value",o.$input.val());o.isSetup&&(e.silent||o.trigger("change",o.$input.val()))},updatePlaceholder:function(){if(this.settings.placeholder){var e=this.$control_input;this.items.length?e.removeAttr("placeholder"):e.attr("placeholder",this.settings.placeholder),e.triggerHandler("update",{force:!0})}},open:function(){var e=this;e.isLocked||e.isOpen||"multi"===e.settings.mode&&e.isFull()||(e.focus(),e.isOpen=!0,e.refreshState(),e.$dropdown.css({visibility:"hidden",display:"block"}),e.positionDropdown(),e.$dropdown.css({visibility:"visible"}),e.trigger("dropdown_open",e.$dropdown))},close:function(){var e=this,t=e.isOpen;"single"===e.settings.mode&&e.items.length&&(e.hideInput(),e.isBlurring||e.$control_input.blur()),e.isOpen=!1,e.$dropdown.hide(),e.setActiveOption(null),e.refreshState(),t&&e.trigger("dropdown_close",e.$dropdown)},positionDropdown:function(){var e=this.$control,t="body"===this.settings.dropdownParent?e.offset():e.position();t.top+=e.outerHeight(!0),this.$dropdown.css({width:e[0].getBoundingClientRect().width,top:t.top,left:t.left})},clear:function(e){var t=this;t.items.length&&(t.$control.children(":not(input)").remove(),t.items=[],t.lastQuery=null,t.setCaret(0),t.setActiveItem(null),t.updatePlaceholder(),t.updateOriginalInput({silent:e}),t.refreshState(),t.showInput(),t.trigger("clear"))},insertAtCaret:function(e){var t=Math.min(this.caretPos,this.items.length),n=e[0],r=this.buffer||this.$control[0];0===t?r.insertBefore(n,r.firstChild):r.insertBefore(n,r.childNodes[t]),this.setCaret(t+1)},deleteSelection:function(e){var t,n,r,i,o,a,s,l,u,c=this;if(r=e&&8===e.keyCode?-1:1,i=f(c.$control_input[0]),c.$activeOption&&!c.settings.hideSelected&&(s=c.getAdjacentOption(c.$activeOption,-1).attr("data-value")),o=[],c.$activeItems.length){for(u=c.$control.children(".active:"+(0
    '+e.title+'×
    '}},e),n.setup=(t=n.setup,function(){t.apply(n,arguments),n.$dropdown_header=S(e.html(e)),n.$dropdown.prepend(n.$dropdown_header)})}),w.define("optgroup_columns",function(s){var o,l=this;s=S.extend({equalizeWidth:!0,equalizeHeight:!0},s),this.getAdjacentOption=function(e,t){var n=e.closest("[data-group]").find("[data-selectable]"),r=n.index(e)+t;return 0<=r&&r
    ',e=e.firstChild,n.body.appendChild(e),t=u.width=e.offsetWidth-e.clientWidth,n.body.removeChild(e)),t},e=function(){var e,t,n,r,i,o,a;if((t=(a=S("[data-group]",l.$dropdown_content)).length)&&l.$dropdown_content.width()){if(s.equalizeHeight){for(e=n=0;e'+t.label+"",o.setup=(n=r.setup,function(){if(t.append){var i=r.settings.render.item;r.settings.render.item=function(e){return t=i.apply(o,arguments),n=a,r=t.search(/(<\/[^>]+>\s*)$/),t.substring(0,r)+n+t.substring(r);var t,n,r}}n.apply(o,arguments),o.$control.on("click","."+t.className,function(e){if(e.preventDefault(),!r.isLocked){var t=S(e.currentTarget).parent();r.setActiveItem(t),r.deleteSelection()&&r.setCaret(r.items.length)}})})):function(i,t){t.className="remove-single";var n,o=i,a=''+t.label+"";i.setup=(n=o.setup,function(){if(t.append){var e=S(o.$input.context).attr("id"),r=(S("#"+e),o.settings.render.item);o.settings.render.item=function(e){return t=r.apply(i,arguments),n=a,S("").append(t).append(n);var t,n}}n.apply(i,arguments),i.$control.on("click","."+t.className,function(e){e.preventDefault(),o.isLocked||o.clear()})})}(this,e)}),w.define("restore_on_backspace",function(r){var i,e=this;r.text=r.text||function(e){return e[this.settings.labelField]},this.onKeyDown=(i=e.onKeyDown,function(e){var t,n;return 8===e.keyCode&&""===this.$control_input.val()&&!this.$activeItems.length&&0<=(t=this.caretPos-1)&&t",{class:function(){var e=[];return e.push(i.options.state?"on":"off"),i.options.size&&e.push(i.options.size),i.options.disabled&&e.push("disabled"),i.options.readonly&&e.push("readonly"),i.options.indeterminate&&e.push("indeterminate"),i.options.inverse&&e.push("inverse"),i.$element.attr("id")&&e.push("id-"+i.$element.attr("id")),e.map(i._getClass.bind(i)).concat([i.options.baseClass],i._getClasses(i.options.wrapperClass)).join(" ")}}),this.$container=s("
    ",{class:this._getClass("container")}),this.$on=s("",{html:this.options.onText,class:this._getClass("handle-on")+" "+this._getClass(this.options.onColor)}),this.$off=s("",{html:this.options.offText,class:this._getClass("handle-off")+" "+this._getClass(this.options.offColor)}),this.$label=s("",{html:this.options.labelText,class:this._getClass("label")}),this.$element.on("init.bootstrapSwitch",this.options.onInit.bind(this,r)),this.$element.on("switchChange.bootstrapSwitch",function(){for(var e=arguments.length,t=Array(e),n=0;n-n._handleWidth/2;n._dragEnd=!1,n.state(n.options.inverse?!t:t)}else n.state(!n.options.state);n._dragStart=!1}},"mouseleave.bootstrapSwitch":function(){n.$label.trigger("mouseup.bootstrapSwitch")}})}},{key:"_externalLabelHandler",value:function(){var t=this,n=this.$element.closest("label");n.on("click",function(e){e.preventDefault(),e.stopImmediatePropagation(),e.target===n[0]&&t.toggleState()})}},{key:"_formHandler",value:function(){var e=this.$element.closest("form");e.data("bootstrap-switch")||e.on("reset.bootstrapSwitch",function(){window.setTimeout(function(){e.find("input").filter(function(){return s(this).data("bootstrap-switch")}).each(function(){return s(this).bootstrapSwitch("state",this.checked)})},1)}).data("bootstrap-switch",!0)}},{key:"_getClass",value:function(e){return this.options.baseClass+"-"+e}},{key:"_getClasses",value:function(e){return s.isArray(e)?e.map(this._getClass.bind(this)):[this._getClass(e)]}}]),t}();s.fn.bootstrapSwitch=function(o){for(var e=arguments.length,a=Array(1
    ');var r,i=f.overlay?"":" ngdialog-no-overlay";if((d=T('
    ')).html(f.overlay?'
    '+t+"
    ":'
    '+t+"
    "),d.data("$ngDialogOptions",f),c.ngDialogId=u,f.data&&O.isString(f.data)){var o=f.data.replace(/^\s*/,"")[0];c.ngDialogData="{"===o||"["===o?O.fromJson(f.data):new String(f.data),c.ngDialogData.ngDialogId=u}else f.data&&O.isObject(f.data)&&(c.ngDialogData=f.data,c.ngDialogData.ngDialogId=u);(f.className&&d.addClass(f.className),f.appendClassName&&d.addClass(f.appendClassName),f.width&&(h=d[0].querySelector(".ngdialog-content"),O.isString(f.width)?h.style.width=f.width:h.style.width=f.width+"px"),f.height&&(h=d[0].querySelector(".ngdialog-content"),O.isString(f.height)?h.style.height=f.height:h.style.height=f.height+"px"),f.disableAnimation&&d.addClass("ngdialog-disabled-animation"),p=f.appendTo&&O.isString(f.appendTo)?O.element(document.querySelector(f.appendTo)):b.body,$.applyAriaAttributes(d,f),f.preCloseCallback)&&(O.isFunction(f.preCloseCallback)?r=f.preCloseCallback:O.isString(f.preCloseCallback)&&c&&(O.isFunction(c[f.preCloseCallback])?r=c[f.preCloseCallback]:c.$parent&&O.isFunction(c.$parent[f.preCloseCallback])?r=c.$parent[f.preCloseCallback]:m&&O.isFunction(m[f.preCloseCallback])&&(r=m[f.preCloseCallback])),r&&d.data("$ngDialogPreCloseCallback",r));if(c.closeThisDialog=function(e){$.closeDialog(d,e)},f.controller&&(O.isString(f.controller)||O.isArray(f.controller)||O.isFunction(f.controller))){var a;f.controllerAs&&O.isString(f.controllerAs)&&(a=f.controllerAs);var s=w(f.controller,O.extend(n,{$scope:c,$element:d}),!0,a);f.bindToController&&O.extend(s.instance,{ngDialogId:c.ngDialogId,ngDialogData:c.ngDialogData,closeThisDialog:c.closeThisDialog,confirm:c.confirm}),"function"==typeof s?d.data("$ngDialogControllerController",s()):d.data("$ngDialogControllerController",s)}if(v(function(){var e=document.querySelectorAll(".ngdialog");$.deactivateAll(e),g(d)(c);var t=y.innerWidth-b.body.prop("clientWidth");b.html.addClass(f.bodyClassName),b.body.addClass(f.bodyClassName);var n=t-(y.innerWidth-b.body.prop("clientWidth"));0window.innerHeight&&(l=v,t++,e=0);var u=l?0===e?l:l+w:v,c=n+t*(b+s);o.css(o._positionY,u+"px"),"center"==o._positionX?o.css("left",parseInt(window.innerWidth/2-s/2)+"px"):o.css(o._positionX,c+"px"),r[o._positionY+o._positionX]=u+a,0m.maxCount&&0===i&&o.scope().kill(!0),e++}}},i=c(e)(n);i._positionY=h.positionY,i._positionX=h.positionX,i._priority=h.priority,i.addClass(h.type);var o=function(e){("click"===(e=e.originalEvent||e).type||"opacity"===e.propertyName&&1<=e.elapsedTime)&&(n.onClose&&n.$apply(n.onClose(i)),i.remove(),$.splice($.indexOf(i),1),n.$destroy(),r())};h.closeOnClick&&(i.addClass("clickable"),i.bind("click",o)),i.bind("webkitTransitionEnd oTransitionEnd otransitionend transitionend msTransitionEnd",o),angular.isNumber(h.delay)&&u(function(){i.addClass("killed")},h.delay),t("none"),angular.element(document.querySelector(h.container)).append(i);var a=-(parseInt(i[0].offsetHeight)+50);if(i.css(i._positionY,a+"px"),$.push(i),"center"==h.positionX){var s=parseInt(i[0].offsetWidth);i.css("left",parseInt(window.innerWidth/2-s/2)+"px")}u(function(){t("")}),n._templateElement=i,n.kill=function(e){e?(n.onClose&&n.$apply(n.onClose(n._templateElement)),$.splice($.indexOf(n._templateElement),1),n._templateElement.remove(),n.$destroy(),u(r)):n._templateElement.addClass("killed")},u(r),_||(angular.element(g).bind("resize",function(e){u(r)}),_=!0),l.resolve(n)}var l=a.defer();"object"==typeof h&&null!==h||(h={message:h}),h.scope=h.scope?h.scope:o,h.template=h.templateUrl?h.templateUrl:m.templateUrl,h.delay=angular.isUndefined(h.delay)?s:h.delay,h.type=e||h.type||m.type||"",h.positionY=h.positionY?h.positionY:m.positionY,h.positionX=h.positionX?h.positionX:m.positionX,h.replaceMessage=h.replaceMessage?h.replaceMessage:m.replaceMessage,h.onClose=h.onClose?h.onClose:m.onClose,h.closeOnClick=null!==h.closeOnClick&&void 0!==h.closeOnClick?h.closeOnClick:m.closeOnClick,h.container=h.container?h.container:m.container,h.priority=h.priority?h.priority:m.priority;var n=i.get(h.template);return n?t(n):r.get(h.template,{cache:!0}).then(function(e){t(e.data)}).catch(function(e){throw new Error("Template ("+h.template+") could not be loaded. "+e)}),l.promise};return t.primary=function(e){return this(e,"primary")},t.error=function(e){return this(e,"error")},t.success=function(e){return this(e,"success")},t.info=function(e){return this(e,"info")},t.warning=function(e){return this(e,"warning")},t.clearAll=function(){angular.forEach($,function(e){e.addClass("killed")})},t}]}),angular.module("ui-notification").run(["$templateCache",function(e){e.put("angular-ui-notification.html",'

    ')}]),function(){var w="__default";angular.module("angularUtils.directives.dirPagination",[]).directive("dirPaginate",["$compile","$parse","paginationService",function(m,v,y){return{terminal:!0,multiElement:!0,priority:100,compile:function(e,t){var f=t.dirPaginate,n=f.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/),r=/\|\s*itemsPerPage\s*:\s*(.*\(\s*\w*\)|([^\)]*?(?=\s+as\s+))|[^\)]*)/;if(null===n[2].match(r))throw"pagination directive: the 'itemsPerPage' filter must be set.";var i=n[2].replace(r,""),g=v(i);o=e,angular.forEach(o,function(e){1===e.nodeType&&angular.element(e).attr("dir-paginate-no-compile",!0)});var o;var a=t.paginationId||w;return y.registerInstance(a),function(e,t,n){var r=v(n.paginationId)(e)||n.paginationId||w;y.registerInstance(r);var i,o,a,s,l,u,c,d=(u=r,c=!!(l=f).match(/(\|\s*itemsPerPage\s*:[^|]*:[^|]*)/),u===w||c?l:l.replace(/(\|\s*itemsPerPage\s*:\s*[^|\s]*)/,"$1 : '"+u+"'"));o=n,a=d,(i=t)[0].hasAttribute("dir-paginate-start")||i[0].hasAttribute("data-dir-paginate-start")?(o.$set("ngRepeatStart",a),i.eq(i.length-1).attr("ng-repeat-end",!0)):o.$set("ngRepeat",a),s=t,angular.forEach(s,function(e){1===e.nodeType&&angular.element(e).removeAttr("dir-paginate-no-compile")}),s.eq(0).removeAttr("dir-paginate-start").removeAttr("dir-paginate").removeAttr("data-dir-paginate-start").removeAttr("data-dir-paginate"),s.eq(s.length-1).removeAttr("dir-paginate-end").removeAttr("data-dir-paginate-end");var p=m(t),h=function(e,t,n){var r;if(t.currentPage)r=v(t.currentPage);else{var i=(n+"__currentPage").replace(/\W/g,"_");e[i]=1,r=v(i)}return r}(e,n,r);y.setCurrentPageParser(r,h,e),void 0!==n.totalItems?(y.setAsyncModeTrue(r),e.$watch(function(){return v(n.totalItems)(e)},function(e){0<=e&&y.setCollectionLength(r,e)})):(y.setAsyncModeFalse(r),e.$watchCollection(function(){return g(e)},function(e){if(e){var t=e instanceof Array?e.length:Object.keys(e).length;y.setCollectionLength(r,t)}})),p(e)}}}}]).directive("dirPaginateNoCompile",function(){return{priority:5e3,terminal:!0}}).directive("dirPaginationControls",["paginationService","paginationTemplate",function(d,n){var p=/^\d+$/,e={restrict:"AE",scope:{maxSize:"=?",onPageChange:"&?",paginationId:"=?",autoHide:"=?"},link:function(r,e,t){var n=t.paginationId||w,i=r.paginationId||t.paginationId||w;if(!d.isRegistered(i)&&!d.isRegistered(n)){var o=i!==w?" (id: "+i+") ":" ";window.console&&console.warn("Pagination directive: the pagination controls"+o+"cannot be used without the corresponding pagination directive, which was not found at link time.")}r.maxSize||(r.maxSize=9);r.autoHide=void 0===r.autoHide||r.autoHide,r.directionLinks=!angular.isDefined(t.directionLinks)||r.$parent.$eval(t.directionLinks),r.boundaryLinks=!!angular.isDefined(t.boundaryLinks)&&r.$parent.$eval(t.boundaryLinks);var a=Math.max(r.maxSize,5);function s(e){if(d.isRegistered(i)&&c(e)){var t=r.pagination.current;r.pages=h(e,d.getCollectionLength(i),d.getItemsPerPage(i),a),r.pagination.current=e,u(),r.onPageChange&&r.onPageChange({newPageNumber:e,oldPageNumber:t})}}function l(){if(d.isRegistered(i)){var e=parseInt(d.getCurrentPage(i))||1;r.pages=h(e,d.getCollectionLength(i),d.getItemsPerPage(i),a),r.pagination.current=e,r.pagination.last=r.pages[r.pages.length-1],r.pagination.last
  • «
  • {{ pageNumber }}
  • »
  • ')}])}();var com_github_culmat_jsTreeTable=function(){function l(e,r,i){return i=i||"children",$.each(e,function(e,t){!function n(e){e[i]&&$.each(e[i],function(e,t){n(t)}),r(e)}(t)}),e}function t(e,n,o,a){var t=e;n=n||"id",o=o||"parent",a=a||"children";var s=[];$.each(t,function(e,t){s[t[n]]=t});var l=[];return $.each(t,function(e,r){var t=r[o];if($.isArray(t)||(t=[t]),0==t.length)l.push(r);else{var i=!1;$.each(t,function(e,t){var n=s[t];n&&(n[a]||(n[a]=[]),$.inArray(r,n[a])<0&&n[a].push(r),i=!0)}),i||l.push(r)}}),l}function u(e,u,c,d,p,t){u=u||"children",c=c||"id",t=t||{};var n=0,r=$("");$.each(t,function(e,t){"class"==e&&"jsTT"!=t?r.addClass(t):r.attr(e,t)});var i=$(""),o=$(""),h=$("");return r.append(i),i.append(o),r.append(h),d?$.each(d,function(e,t){$(o).append($(""))}):($(o).append($("")),$.each(e[0],function(e,t){e!=u&&e!=c&&$(o).append($(""))})),function o(e,a,s,l){n=Math.max(n,s),$.each(e,function(e,n){var r,t,i;n["data-tt-level"]=s,r=n,t=l,i=$(""),$(i).attr("data-tt-id",r[c]),$(i).attr("data-tt-level",r["data-tt-level"]),r[u]&&0!=r[u].length?$(i).attr("data-tt-isnode",!0):$(i).attr("data-tt-isleaf",!0),t&&$(i).attr("data-tt-parent-id",t[c]),p?p($(i),r):d?$.each(d,function(e,t){$(i).append($(""))}):($(i).append($("")),$.each(r,function(e,t){e!=u&&e!=c&&"data-tt-level"!=e&&$(i).append($(""))})),h.append(i),n[a]&&$.each(n[a],function(e,t){o([t],a,s+1,n)})})}(e,u,1),e[0]&&(e[0].maxLevel=n),r}function n(e,t){return $.each(e,function(e,n){$.each(t,function(e,t){n[t]=$(n).attr(t)})}),e}function c(i){i.addClass("jsTT"),i.expandLevel=function(n){$("tr[data-tt-level]",i).each(function(e){var t=parseInt($(this).attr("data-tt-level"));n-1')):r.prepend($('')),r.prepend($('')),t.trExpand=function(e){if(!(this.trChildren.length<1)){e&&(this.trChildrenVisible=!0,$("#state",this).get(0).src=o);var n=e||this.trChildrenVisible;$.each(this.trChildren,function(e,t){n&&$(t).css("display","table-row"),t.trExpand()})}},t.trCollapse=function(e){this.trChildren.length<1||(e&&(this.trChildrenVisible=!1,$("#state",this).get(0).src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAAHlJREFUeNrcU1sNgDAQ6wgmcAM2MICGGlg1gJnNzWQcvwQGy1j4oUl/7tH0mpwzM7SgQyO+EZAUWh2MkkzSWhJwuRAlHYsJwEwyvs1gABDuzqoJcTw5qxaIJN0bgQRgIjnlmn1heSO5PE6Y2YXe+5Cr5+h++gs12AcAS6FS+7YOsj4AAAAASUVORK5CYII="),$.each(this.trChildren,function(e,t){$(t).css("display","none"),t.trCollapse()}))},$(t).click(function(){this.trChildrenVisible?this.trCollapse(!0):this.trExpand(!0)})}),i}return{depthFirst:l,makeTree:t,renderTree:u,attr2attr:n,treeTable:c,appendTreetable:function(e,t){(t=t||{}).idAttr=t.idAttr||"id",t.childrenAttr=t.childrenAttr||"children";var n=t.controls||[];t.mountPoint||(t.mountPoint=$("body")),t.depthFirst&&l(e,t.depthFirst,t.childrenAttr);var r=u(e,t.childrenAttr,t.idAttr,t.renderedAttr,t.renderer,t.tableAttributes);c(r),t.replaceContent&&t.mountPoint.html("");var i,o,a=t.initialExpandLevel?parseInt(t.initialExpandLevel):-1;if(a=Math.min(a,e[0].maxLevel),r.expandLevel(a),t.slider){var s=$('
    ');s.width("200px"),s.slider({min:1,max:e[0].maxLevel,range:"min",value:a,slide:function(e,t){r.expandLevel(t.value)}}),n=[s].concat(t.controls)}return 0"),$.each(i,function(e,t){o.append($('
    "+t+""+c+""+e+"
    "+r[e]+""+r[c]+""+t+"').append(t))}),$('').append(o))),t.mountPoint.append(r),r},jsTreeTable:"1.0",register:function(n){$.each(this,function(e,t){"register"!=e&&(n[e]=t)})}}}(); \ No newline at end of file diff --git a/sentinel-dashboard/src/main/webapp/resources/gulpfile.js b/sentinel-dashboard/src/main/webapp/resources/gulpfile.js index 6389d2d372..6bf496fd30 100755 --- a/sentinel-dashboard/src/main/webapp/resources/gulpfile.js +++ b/sentinel-dashboard/src/main/webapp/resources/gulpfile.js @@ -43,6 +43,7 @@ const CSS_APP = [ const JS_APP = [ 'app/scripts/app.js', 'app/scripts/filters/filters.js', + 'app/scripts/services/version_service.js', 'app/scripts/services/auth_service.js', 'app/scripts/services/appservice.js', 'app/scripts/services/flow_service_v1.js', @@ -55,6 +56,8 @@ const JS_APP = [ 'app/scripts/services/param_flow_service.js', 'app/scripts/services/authority_service.js', 'app/scripts/services/cluster_state_service.js', + 'app/scripts/services/gateway/api_service.js', + 'app/scripts/services/gateway/flow_service.js', ]; gulp.task('lib', function () { diff --git a/sentinel-dashboard/src/main/webapp/resources/package-lock.json b/sentinel-dashboard/src/main/webapp/resources/package-lock.json index bfd4cc210b..b18d46695c 100644 --- a/sentinel-dashboard/src/main/webapp/resources/package-lock.json +++ b/sentinel-dashboard/src/main/webapp/resources/package-lock.json @@ -11,7 +11,7 @@ }, "accepts": { "version": "1.3.5", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "resolved": "http://registry.npm.alibaba-inc.com/accepts/download/accepts-1.3.5.tgz", "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", "dev": true, "requires": { @@ -225,7 +225,7 @@ }, "array-slice": { "version": "1.1.0", - "resolved": "http://registry.npm.alibaba-inc.com/array-slice/download/array-slice-1.1.0.tgz", + "resolved": "http://registry.npm.taobao.org/array-slice/download/array-slice-1.1.0.tgz", "integrity": "sha1-42jqFfibxwaff/uJrsOmx9SsItQ=", "dev": true }, @@ -324,7 +324,7 @@ }, "batch": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "resolved": "http://registry.npm.alibaba-inc.com/batch/download/batch-0.6.1.tgz", "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", "dev": true }, @@ -485,39 +485,9 @@ } } }, - "clean-css": { - "version": "3.4.28", - "resolved": "http://registry.npm.alibaba-inc.com/clean-css/download/clean-css-3.4.28.tgz", - "integrity": "sha1-vxlF6C/ICPVWlebd6uwBQA79A/8=", - "dev": true, - "requires": { - "commander": "2.8.x", - "source-map": "0.4.x" - }, - "dependencies": { - "commander": { - "version": "2.8.1", - "resolved": "http://registry.npm.alibaba-inc.com/commander/download/commander-2.8.1.tgz", - "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", - "dev": true, - "requires": { - "graceful-readlink": ">= 1.0.0" - } - }, - "source-map": { - "version": "0.4.4", - "resolved": "http://registry.npm.alibaba-inc.com/source-map/download/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, "cli": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz", + "resolved": "http://registry.npm.alibaba-inc.com/cli/download/cli-1.0.1.tgz", "integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", "dev": true, "requires": { @@ -682,7 +652,7 @@ }, "console-browserify": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "resolved": "http://registry.npm.alibaba-inc.com/console-browserify/download/console-browserify-1.1.0.tgz", "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", "dev": true, "requires": { @@ -759,7 +729,7 @@ }, "date-now": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "resolved": "http://registry.npm.alibaba-inc.com/date-now/download/date-now-0.1.4.tgz", "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", "dev": true }, @@ -792,7 +762,7 @@ }, "defaults": { "version": "1.0.3", - "resolved": "http://registry.npm.alibaba-inc.com/defaults/download/defaults-1.0.3.tgz", + "resolved": "https://registry.npm.taobao.org/defaults/download/defaults-1.0.3.tgz", "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", "dev": true, "requires": { @@ -848,7 +818,7 @@ }, "deprecated": { "version": "0.0.1", - "resolved": "http://registry.npm.alibaba-inc.com/deprecated/download/deprecated-0.0.1.tgz", + "resolved": "https://registry.npm.taobao.org/deprecated/download/deprecated-0.0.1.tgz", "integrity": "sha1-+cmvVGSvoeepcUWKi97yqpTVuxk=", "dev": true }, @@ -890,7 +860,7 @@ }, "domhandler": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", + "resolved": "http://registry.npm.alibaba-inc.com/domhandler/download/domhandler-2.3.0.tgz", "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", "dev": true, "requires": { @@ -899,7 +869,7 @@ }, "domutils": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "resolved": "http://registry.npm.alibaba-inc.com/domutils/download/domutils-1.5.1.tgz", "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", "dev": true, "requires": { @@ -930,7 +900,7 @@ }, "end-of-stream": { "version": "0.1.5", - "resolved": "http://registry.npm.alibaba-inc.com/end-of-stream/download/end-of-stream-0.1.5.tgz", + "resolved": "https://registry.npm.taobao.org/end-of-stream/download/end-of-stream-0.1.5.tgz", "integrity": "sha1-jhdyBsPICDfYVjLouTWd/osvbq8=", "dev": true, "requires": { @@ -939,7 +909,7 @@ }, "entities": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", + "resolved": "http://registry.npm.alibaba-inc.com/entities/download/entities-1.0.0.tgz", "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=", "dev": true }, @@ -987,7 +957,7 @@ }, "exit": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "resolved": "http://registry.npm.alibaba-inc.com/exit/download/exit-0.1.2.tgz", "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", "dev": true }, @@ -1026,63 +996,6 @@ } } }, - "expand-range": { - "version": "1.8.2", - "resolved": "http://registry.npm.alibaba-inc.com/expand-range/download/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", - "dev": true, - "requires": { - "fill-range": "^2.1.0" - }, - "dependencies": { - "fill-range": { - "version": "2.2.4", - "resolved": "http://registry.npm.alibaba-inc.com/fill-range/download/fill-range-2.2.4.tgz", - "integrity": "sha1-6x53OrsFbc2N8r/favWbizqTZWU=", - "dev": true, - "requires": { - "is-number": "^2.1.0", - "isobject": "^2.0.0", - "randomatic": "^3.0.0", - "repeat-element": "^1.1.2", - "repeat-string": "^1.5.2" - } - }, - "is-number": { - "version": "2.1.0", - "resolved": "http://registry.npm.alibaba-inc.com/is-number/download/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "http://registry.npm.alibaba-inc.com/isarray/download/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isobject": { - "version": "2.1.0", - "resolved": "http://registry.npm.alibaba-inc.com/isobject/download/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "http://registry.npm.alibaba-inc.com/kind-of/download/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "expand-tilde": { "version": "2.0.2", "resolved": "http://registry.npm.alibaba-inc.com/expand-tilde/download/expand-tilde-2.0.2.tgz", @@ -1094,7 +1007,7 @@ }, "extend": { "version": "3.0.2", - "resolved": "http://registry.npm.alibaba-inc.com/extend/download/extend-3.0.2.tgz", + "resolved": "https://registry.npm.taobao.org/extend/download/extend-3.0.2.tgz", "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=", "dev": true }, @@ -1197,19 +1110,13 @@ }, "faye-websocket": { "version": "0.10.0", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "resolved": "http://registry.npm.alibaba-inc.com/faye-websocket/download/faye-websocket-0.10.0.tgz", "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", "dev": true, "requires": { "websocket-driver": ">=0.5.1" } }, - "filename-regex": { - "version": "2.0.1", - "resolved": "http://registry.npm.alibaba-inc.com/filename-regex/download/filename-regex-2.0.1.tgz", - "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", - "dev": true - }, "filesize": { "version": "2.0.4", "resolved": "http://registry.npm.alibaba-inc.com/filesize/download/filesize-2.0.4.tgz", @@ -1256,7 +1163,7 @@ }, "find-index": { "version": "0.1.1", - "resolved": "http://registry.npm.alibaba-inc.com/find-index/download/find-index-0.1.1.tgz", + "resolved": "https://registry.npm.taobao.org/find-index/download/find-index-0.1.1.tgz", "integrity": "sha1-Z101iyyjiS15Whq0cjL4tuLg3eQ=", "dev": true }, @@ -1272,7 +1179,7 @@ }, "findup-sync": { "version": "2.0.0", - "resolved": "http://registry.npm.alibaba-inc.com/findup-sync/download/findup-sync-2.0.0.tgz", + "resolved": "https://registry.npm.taobao.org/findup-sync/download/findup-sync-2.0.0.tgz", "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", "dev": true, "requires": { @@ -1283,9 +1190,9 @@ } }, "fined": { - "version": "1.1.0", - "resolved": "http://registry.npm.alibaba-inc.com/fined/download/fined-1.1.0.tgz", - "integrity": "sha1-s33IRLdqL15wgeiE98CuNE8VNHY=", + "version": "1.2.0", + "resolved": "https://registry.npm.taobao.org/fined/download/fined-1.2.0.tgz", + "integrity": "sha1-0AvszxqitHXRbUI7Aji3E6LEo3s=", "dev": true, "requires": { "expand-tilde": "^2.0.2", @@ -1297,14 +1204,14 @@ }, "first-chunk-stream": { "version": "1.0.0", - "resolved": "http://registry.npm.alibaba-inc.com/first-chunk-stream/download/first-chunk-stream-1.0.0.tgz", + "resolved": "https://registry.npm.taobao.org/first-chunk-stream/download/first-chunk-stream-1.0.0.tgz", "integrity": "sha1-Wb+1DNkF9g18OUzT2ayqtOatk04=", "dev": true }, "flagged-respawn": { - "version": "1.0.0", - "resolved": "http://registry.npm.alibaba-inc.com/flagged-respawn/download/flagged-respawn-1.0.0.tgz", - "integrity": "sha1-Tnmumy6zi/hrO7Vr8+ClaqX8q9c=", + "version": "1.0.1", + "resolved": "https://registry.npm.taobao.org/flagged-respawn/download/flagged-respawn-1.0.1.tgz", + "integrity": "sha1-595vEnnd2cqarIpZcdYYYGs6q0E=", "dev": true }, "for-in": { @@ -1337,12 +1244,6 @@ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", "dev": true }, - "fs-exists-sync": { - "version": "0.1.0", - "resolved": "http://registry.npm.alibaba-inc.com/fs-exists-sync/download/fs-exists-sync-0.1.0.tgz", - "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=", - "dev": true - }, "fs.realpath": { "version": "1.0.0", "resolved": "http://registry.npm.alibaba-inc.com/fs.realpath/download/fs.realpath-1.0.0.tgz", @@ -1351,7 +1252,7 @@ }, "gaze": { "version": "0.5.2", - "resolved": "http://registry.npm.alibaba-inc.com/gaze/download/gaze-0.5.2.tgz", + "resolved": "https://registry.npm.taobao.org/gaze/download/gaze-0.5.2.tgz", "integrity": "sha1-QLcJU30k0dRXZ9takIaJ3+aaxE8=", "dev": true, "requires": { @@ -1372,7 +1273,7 @@ }, "glob": { "version": "4.5.3", - "resolved": "http://registry.npm.alibaba-inc.com/glob/download/glob-4.5.3.tgz", + "resolved": "https://registry.npm.taobao.org/glob/download/glob-4.5.3.tgz", "integrity": "sha1-xstz0yJsHv7wTePFbQEvAzd+4V8=", "dev": true, "requires": { @@ -1382,62 +1283,9 @@ "once": "^1.3.0" } }, - "glob-base": { - "version": "0.3.0", - "resolved": "http://registry.npm.alibaba-inc.com/glob-base/download/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", - "dev": true, - "requires": { - "glob-parent": "^2.0.0", - "is-glob": "^2.0.0" - }, - "dependencies": { - "is-extglob": { - "version": "1.0.0", - "resolved": "http://registry.npm.alibaba-inc.com/is-extglob/download/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true - }, - "is-glob": { - "version": "2.0.1", - "resolved": "http://registry.npm.alibaba-inc.com/is-glob/download/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "dev": true, - "requires": { - "is-extglob": "^1.0.0" - } - } - } - }, - "glob-parent": { - "version": "2.0.0", - "resolved": "http://registry.npm.alibaba-inc.com/glob-parent/download/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", - "dev": true, - "requires": { - "is-glob": "^2.0.0" - }, - "dependencies": { - "is-extglob": { - "version": "1.0.0", - "resolved": "http://registry.npm.alibaba-inc.com/is-extglob/download/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true - }, - "is-glob": { - "version": "2.0.1", - "resolved": "http://registry.npm.alibaba-inc.com/is-glob/download/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "dev": true, - "requires": { - "is-extglob": "^1.0.0" - } - } - } - }, "glob-stream": { "version": "3.1.18", - "resolved": "http://registry.npm.alibaba-inc.com/glob-stream/download/glob-stream-3.1.18.tgz", + "resolved": "https://registry.npm.taobao.org/glob-stream/download/glob-stream-3.1.18.tgz", "integrity": "sha1-kXCl8St5Awb9/lmPMT+PeVT9FDs=", "dev": true, "requires": { @@ -1451,7 +1299,7 @@ "dependencies": { "readable-stream": { "version": "1.0.34", - "resolved": "http://registry.npm.alibaba-inc.com/readable-stream/download/readable-stream-1.0.34.tgz", + "resolved": "https://registry.npm.taobao.org/readable-stream/download/readable-stream-1.0.34.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Freadable-stream%2Fdownload%2Freadable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, "requires": { @@ -1463,7 +1311,7 @@ }, "through2": { "version": "0.6.5", - "resolved": "http://registry.npm.alibaba-inc.com/through2/download/through2-0.6.5.tgz", + "resolved": "https://registry.npm.taobao.org/through2/download/through2-0.6.5.tgz", "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", "dev": true, "requires": { @@ -1475,7 +1323,7 @@ }, "glob-watcher": { "version": "0.0.6", - "resolved": "http://registry.npm.alibaba-inc.com/glob-watcher/download/glob-watcher-0.0.6.tgz", + "resolved": "https://registry.npm.taobao.org/glob-watcher/download/glob-watcher-0.0.6.tgz", "integrity": "sha1-uVtKjfdLOcgymLDAXJeLTZo7cQs=", "dev": true, "requires": { @@ -1484,7 +1332,7 @@ }, "glob2base": { "version": "0.0.12", - "resolved": "http://registry.npm.alibaba-inc.com/glob2base/download/glob2base-0.0.12.tgz", + "resolved": "https://registry.npm.taobao.org/glob2base/download/glob2base-0.0.12.tgz", "integrity": "sha1-nUGbPijxLoOjYhZKJ3BVkiycDVY=", "dev": true, "requires": { @@ -1517,7 +1365,7 @@ }, "globule": { "version": "0.1.0", - "resolved": "http://registry.npm.alibaba-inc.com/globule/download/globule-0.1.0.tgz", + "resolved": "https://registry.npm.taobao.org/globule/download/globule-0.1.0.tgz", "integrity": "sha1-2cjt3h2nnRJaFRt5UzuXhnY0auU=", "dev": true, "requires": { @@ -1528,7 +1376,7 @@ "dependencies": { "glob": { "version": "3.1.21", - "resolved": "http://registry.npm.alibaba-inc.com/glob/download/glob-3.1.21.tgz", + "resolved": "https://registry.npm.taobao.org/glob/download/glob-3.1.21.tgz", "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", "dev": true, "requires": { @@ -1539,25 +1387,25 @@ }, "graceful-fs": { "version": "1.2.3", - "resolved": "http://registry.npm.alibaba-inc.com/graceful-fs/download/graceful-fs-1.2.3.tgz", + "resolved": "https://registry.npm.taobao.org/graceful-fs/download/graceful-fs-1.2.3.tgz", "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", "dev": true }, "inherits": { "version": "1.0.2", - "resolved": "http://registry.npm.alibaba-inc.com/inherits/download/inherits-1.0.2.tgz", + "resolved": "https://registry.npm.taobao.org/inherits/download/inherits-1.0.2.tgz", "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", "dev": true }, "lodash": { "version": "1.0.2", - "resolved": "http://registry.npm.alibaba-inc.com/lodash/download/lodash-1.0.2.tgz", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-1.0.2.tgz", "integrity": "sha1-j1dWDIO1n8JwvT1WG2kAQ0MOJVE=", "dev": true }, "minimatch": { "version": "0.2.14", - "resolved": "http://registry.npm.alibaba-inc.com/minimatch/download/minimatch-0.2.14.tgz", + "resolved": "https://registry.npm.taobao.org/minimatch/download/minimatch-0.2.14.tgz", "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", "dev": true, "requires": { @@ -1584,7 +1432,7 @@ }, "graceful-fs": { "version": "3.0.11", - "resolved": "http://registry.npm.alibaba-inc.com/graceful-fs/download/graceful-fs-3.0.11.tgz", + "resolved": "https://registry.npm.taobao.org/graceful-fs/download/graceful-fs-3.0.11.tgz", "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=", "dev": true, "requires": { @@ -1599,7 +1447,7 @@ }, "gulp": { "version": "3.9.1", - "resolved": "http://registry.npm.alibaba-inc.com/gulp/download/gulp-3.9.1.tgz", + "resolved": "https://registry.npm.taobao.org/gulp/download/gulp-3.9.1.tgz", "integrity": "sha1-VxzkWSjdQK9lFPxAEYZgFsE4RbQ=", "dev": true, "requires": { @@ -1620,7 +1468,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "http://registry.npm.alibaba-inc.com/minimist/download/minimist-1.2.0.tgz", + "resolved": "https://registry.npm.taobao.org/minimist/download/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true } @@ -1794,6 +1642,25 @@ "supports-color": "^0.2.0" } }, + "clean-css": { + "version": "3.4.28", + "resolved": "https://registry.npm.taobao.org/clean-css/download/clean-css-3.4.28.tgz", + "integrity": "sha1-vxlF6C/ICPVWlebd6uwBQA79A/8=", + "dev": true, + "requires": { + "commander": "2.8.x", + "source-map": "0.4.x" + } + }, + "commander": { + "version": "2.8.1", + "resolved": "https://registry.npm.taobao.org/commander/download/commander-2.8.1.tgz", + "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", + "dev": true, + "requires": { + "graceful-readlink": ">= 1.0.0" + } + }, "dateformat": { "version": "1.0.12", "resolved": "http://registry.npm.alibaba-inc.com/dateformat/download/dateformat-1.0.12.tgz", @@ -1912,6 +1779,15 @@ "string_decoder": "~0.10.x" } }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + }, "strip-ansi": { "version": "0.3.0", "resolved": "http://registry.npm.alibaba-inc.com/strip-ansi/download/strip-ansi-0.3.0.tgz", @@ -1979,175 +1855,45 @@ } }, "gulp-load-plugins": { - "version": "1.5.0", - "resolved": "http://registry.npm.alibaba-inc.com/gulp-load-plugins/download/gulp-load-plugins-1.5.0.tgz", - "integrity": "sha1-TEGffldk2aDjMGG6uWGPgbc9QXE=", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/gulp-load-plugins/-/gulp-load-plugins-1.6.0.tgz", + "integrity": "sha512-HlCODki0WHJvQIgAsJYOTkyo0c7TsDCetvfhrdGz9JYPL6A4mFRMGmKfoi6JmXjA/vvzg+fkT91c9FBh7rnkyg==", "dev": true, "requires": { "array-unique": "^0.2.1", "fancy-log": "^1.2.0", - "findup-sync": "^0.4.0", + "findup-sync": "^3.0.0", "gulplog": "^1.0.0", "has-gulplog": "^0.1.0", - "micromatch": "^2.3.8", + "micromatch": "^3.1.10", "resolve": "^1.1.7" }, "dependencies": { - "arr-diff": { - "version": "2.0.0", - "resolved": "http://registry.npm.alibaba-inc.com/arr-diff/download/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", - "dev": true, - "requires": { - "arr-flatten": "^1.0.1" - } - }, "array-unique": { "version": "0.2.1", "resolved": "http://registry.npm.alibaba-inc.com/array-unique/download/array-unique-0.2.1.tgz", "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", "dev": true }, - "braces": { - "version": "1.8.5", - "resolved": "http://registry.npm.alibaba-inc.com/braces/download/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", - "dev": true, - "requires": { - "expand-range": "^1.8.1", - "preserve": "^0.2.0", - "repeat-element": "^1.1.2" - } - }, - "detect-file": { - "version": "0.1.0", - "resolved": "http://registry.npm.alibaba-inc.com/detect-file/download/detect-file-0.1.0.tgz", - "integrity": "sha1-STXe39lIhkjgBrASlWbpOGcR6mM=", - "dev": true, - "requires": { - "fs-exists-sync": "^0.1.0" - } - }, - "expand-brackets": { - "version": "0.1.5", - "resolved": "http://registry.npm.alibaba-inc.com/expand-brackets/download/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", - "dev": true, - "requires": { - "is-posix-bracket": "^0.1.0" - } - }, - "expand-tilde": { - "version": "1.2.2", - "resolved": "http://registry.npm.alibaba-inc.com/expand-tilde/download/expand-tilde-1.2.2.tgz", - "integrity": "sha1-C4HrqJflo9MdHD0QL48BRB5VlEk=", - "dev": true, - "requires": { - "os-homedir": "^1.0.1" - } - }, - "extglob": { - "version": "0.3.2", - "resolved": "http://registry.npm.alibaba-inc.com/extglob/download/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", - "dev": true, - "requires": { - "is-extglob": "^1.0.0" - } - }, "findup-sync": { - "version": "0.4.3", - "resolved": "http://registry.npm.alibaba-inc.com/findup-sync/download/findup-sync-0.4.3.tgz", - "integrity": "sha1-QAQ5Kee8YK3wt/SCfExudaDeyhI=", - "dev": true, - "requires": { - "detect-file": "^0.1.0", - "is-glob": "^2.0.1", - "micromatch": "^2.3.7", - "resolve-dir": "^0.1.0" - } - }, - "global-modules": { - "version": "0.2.3", - "resolved": "http://registry.npm.alibaba-inc.com/global-modules/download/global-modules-0.2.3.tgz", - "integrity": "sha1-6lo77ULG1s6ZWk+KEmm12uIjgo0=", - "dev": true, - "requires": { - "global-prefix": "^0.1.4", - "is-windows": "^0.2.0" - } - }, - "global-prefix": { - "version": "0.1.5", - "resolved": "http://registry.npm.alibaba-inc.com/global-prefix/download/global-prefix-0.1.5.tgz", - "integrity": "sha1-jTvGuNo8qBEqFg2NSW/wRiv+948=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", "dev": true, "requires": { - "homedir-polyfill": "^1.0.0", - "ini": "^1.3.4", - "is-windows": "^0.2.0", - "which": "^1.2.12" + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" } }, - "is-extglob": { - "version": "1.0.0", - "resolved": "http://registry.npm.alibaba-inc.com/is-extglob/download/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true - }, "is-glob": { - "version": "2.0.1", - "resolved": "http://registry.npm.alibaba-inc.com/is-glob/download/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "dev": true, - "requires": { - "is-extglob": "^1.0.0" - } - }, - "is-windows": { - "version": "0.2.0", - "resolved": "http://registry.npm.alibaba-inc.com/is-windows/download/is-windows-0.2.0.tgz", - "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=", - "dev": true - }, - "kind-of": { - "version": "3.2.2", - "resolved": "http://registry.npm.alibaba-inc.com/kind-of/download/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - }, - "micromatch": { - "version": "2.3.11", - "resolved": "http://registry.npm.alibaba-inc.com/micromatch/download/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", - "dev": true, - "requires": { - "arr-diff": "^2.0.0", - "array-unique": "^0.2.1", - "braces": "^1.8.2", - "expand-brackets": "^0.1.4", - "extglob": "^0.3.1", - "filename-regex": "^2.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.1", - "kind-of": "^3.0.2", - "normalize-path": "^2.0.1", - "object.omit": "^2.0.0", - "parse-glob": "^3.0.4", - "regex-cache": "^0.4.2" - } - }, - "resolve-dir": { - "version": "0.1.1", - "resolved": "http://registry.npm.alibaba-inc.com/resolve-dir/download/resolve-dir-0.1.1.tgz", - "integrity": "sha1-shklmlYC+sXFxJatiUpujMQwJh4=", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", "dev": true, "requires": { - "expand-tilde": "^1.2.2", - "global-modules": "^0.2.3" + "is-extglob": "^2.1.1" } } } @@ -2297,7 +2043,7 @@ }, "htmlparser2": { "version": "3.8.3", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", + "resolved": "http://registry.npm.alibaba-inc.com/htmlparser2/download/htmlparser2-3.8.3.tgz", "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", "dev": true, "requires": { @@ -2371,14 +2117,14 @@ "dev": true }, "interpret": { - "version": "1.1.0", - "resolved": "http://registry.npm.alibaba-inc.com/interpret/download/interpret-1.1.0.tgz", - "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", + "version": "1.2.0", + "resolved": "https://registry.npm.taobao.org/interpret/download/interpret-1.2.0.tgz", + "integrity": "sha1-1QYaYiS+WOgIOYX1AU2EQ1lXYpY=", "dev": true }, "is-absolute": { "version": "1.0.0", - "resolved": "http://registry.npm.alibaba-inc.com/is-absolute/download/is-absolute-1.0.0.tgz", + "resolved": "http://registry.npm.taobao.org/is-absolute/download/is-absolute-1.0.0.tgz", "integrity": "sha1-OV4a6EsR8mrReV5zwXN45IowFXY=", "dev": true, "requires": { @@ -2466,21 +2212,6 @@ } } }, - "is-dotfile": { - "version": "1.0.3", - "resolved": "http://registry.npm.alibaba-inc.com/is-dotfile/download/is-dotfile-1.0.3.tgz", - "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", - "dev": true - }, - "is-equal-shallow": { - "version": "0.1.3", - "resolved": "http://registry.npm.alibaba-inc.com/is-equal-shallow/download/is-equal-shallow-0.1.3.tgz", - "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", - "dev": true, - "requires": { - "is-primitive": "^2.0.0" - } - }, "is-extendable": { "version": "0.1.1", "resolved": "http://registry.npm.alibaba-inc.com/is-extendable/download/is-extendable-0.1.1.tgz", @@ -2504,7 +2235,7 @@ }, "is-glob": { "version": "3.1.0", - "resolved": "http://registry.npm.alibaba-inc.com/is-glob/download/is-glob-3.1.0.tgz", + "resolved": "https://registry.npm.taobao.org/is-glob/download/is-glob-3.1.0.tgz", "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", "dev": true, "requires": { @@ -2540,21 +2271,9 @@ "isobject": "^3.0.1" } }, - "is-posix-bracket": { - "version": "0.1.1", - "resolved": "http://registry.npm.alibaba-inc.com/is-posix-bracket/download/is-posix-bracket-0.1.1.tgz", - "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", - "dev": true - }, - "is-primitive": { - "version": "2.0.0", - "resolved": "http://registry.npm.alibaba-inc.com/is-primitive/download/is-primitive-2.0.0.tgz", - "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", - "dev": true - }, "is-relative": { "version": "1.0.0", - "resolved": "http://registry.npm.alibaba-inc.com/is-relative/download/is-relative-1.0.0.tgz", + "resolved": "http://registry.npm.taobao.org/is-relative/download/is-relative-1.0.0.tgz", "integrity": "sha1-obtpNc6MXboei5dUubLcwCDiJg0=", "dev": true, "requires": { @@ -2563,7 +2282,7 @@ }, "is-unc-path": { "version": "1.0.0", - "resolved": "http://registry.npm.alibaba-inc.com/is-unc-path/download/is-unc-path-1.0.0.tgz", + "resolved": "http://registry.npm.taobao.org/is-unc-path/download/is-unc-path-1.0.0.tgz", "integrity": "sha1-1zHoiY7QkKEsNSrS6u1Qla0yLJ0=", "dev": true, "requires": { @@ -2582,6 +2301,12 @@ "integrity": "sha1-0YUOuXkezRjmGCzhKjDzlmNLsZ0=", "dev": true }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + }, "isarray": { "version": "0.0.1", "resolved": "http://registry.npm.alibaba-inc.com/isarray/download/isarray-0.0.1.tgz", @@ -2601,9 +2326,9 @@ "dev": true }, "jquery": { - "version": "3.3.1", - "resolved": "http://registry.npm.alibaba-inc.com/jquery/download/jquery-3.3.1.tgz", - "integrity": "sha1-lYzinoHJeQ8xvneS311NlfxX+8o=" + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz", + "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==" }, "jshint": { "version": "2.10.2", @@ -2640,7 +2365,7 @@ }, "liftoff": { "version": "2.5.0", - "resolved": "http://registry.npm.alibaba-inc.com/liftoff/download/liftoff-2.5.0.tgz", + "resolved": "https://registry.npm.taobao.org/liftoff/download/liftoff-2.5.0.tgz", "integrity": "sha1-IAkpG7Mc6oYbvxCnwVooyvdcMew=", "dev": true, "requires": { @@ -2691,9 +2416,9 @@ } }, "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, "lodash._basecopy": { "version": "3.0.1", @@ -2891,9 +2616,9 @@ } }, "lodash.merge": { - "version": "4.6.1", - "resolved": "http://registry.npm.alibaba-inc.com/lodash.merge/download/lodash.merge-4.6.1.tgz", - "integrity": "sha1-rcJdnLmbk5HFliTzefu6YNcRHVQ=", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, "lodash.restparam": { @@ -2984,7 +2709,7 @@ }, "make-iterator": { "version": "1.0.1", - "resolved": "http://registry.npm.alibaba-inc.com/make-iterator/download/make-iterator-1.0.1.tgz", + "resolved": "http://registry.npm.taobao.org/make-iterator/download/make-iterator-1.0.1.tgz", "integrity": "sha1-KbM/MSqo9UfEpeSQ9Wr87JkTOtY=", "dev": true, "requires": { @@ -3018,12 +2743,6 @@ "object-visit": "^1.0.0" } }, - "math-random": { - "version": "1.0.1", - "resolved": "http://registry.npm.alibaba-inc.com/math-random/download/math-random-1.0.1.tgz", - "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", - "dev": true - }, "meow": { "version": "3.7.0", "resolved": "http://registry.npm.alibaba-inc.com/meow/download/meow-3.7.0.tgz", @@ -3105,7 +2824,7 @@ }, "minimatch": { "version": "2.0.10", - "resolved": "http://registry.npm.alibaba-inc.com/minimatch/download/minimatch-2.0.10.tgz", + "resolved": "https://registry.npm.taobao.org/minimatch/download/minimatch-2.0.10.tgz", "integrity": "sha1-jQh8OcazjAAbl/ynzm0OHoCvusc=", "dev": true, "requires": { @@ -3118,9 +2837,9 @@ "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" }, "mixin-deep": { - "version": "1.3.1", - "resolved": "http://registry.npm.alibaba-inc.com/mixin-deep/download/mixin-deep-1.3.1.tgz", - "integrity": "sha1-pJ5yaNzhoNlpjkUybFYm3zVD0P4=", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", "dev": true, "requires": { "for-in": "^1.0.2", @@ -3129,8 +2848,8 @@ "dependencies": { "is-extendable": { "version": "1.0.1", - "resolved": "http://registry.npm.alibaba-inc.com/is-extendable/download/is-extendable-1.0.1.tgz", - "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "dev": true, "requires": { "is-plain-object": "^2.0.4" @@ -3140,7 +2859,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "http://registry.npm.alibaba-inc.com/mkdirp/download/mkdirp-0.5.1.tgz", + "resolved": "https://registry.npm.taobao.org/mkdirp/download/mkdirp-0.5.1.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmkdirp%2Fdownload%2Fmkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "requires": { @@ -3149,7 +2868,7 @@ "dependencies": { "minimist": { "version": "0.0.8", - "resolved": "http://registry.npm.alibaba-inc.com/minimist/download/minimist-0.0.8.tgz", + "resolved": "https://registry.npm.taobao.org/minimist/download/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true } @@ -3195,14 +2914,14 @@ } }, "natives": { - "version": "1.1.4", - "resolved": "http://registry.npm.alibaba-inc.com/natives/download/natives-1.1.4.tgz", - "integrity": "sha1-Lw8iT8mn3VNAfHZnyEz42+dz3lg=", + "version": "1.1.6", + "resolved": "https://registry.npm.taobao.org/natives/download/natives-1.1.6.tgz", + "integrity": "sha1-pgO0pJirdxc2ErnqGs3sTZgPALs=", "dev": true }, "negotiator": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "resolved": "http://registry.npm.alibaba-inc.com/negotiator/download/negotiator-0.6.1.tgz", "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", "dev": true }, @@ -3234,15 +2953,6 @@ "validate-npm-package-license": "^3.0.1" } }, - "normalize-path": { - "version": "2.1.1", - "resolved": "http://registry.npm.alibaba-inc.com/normalize-path/download/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, "number-is-nan": { "version": "1.0.1", "resolved": "http://registry.npm.alibaba-inc.com/number-is-nan/download/number-is-nan-1.0.1.tgz", @@ -3317,27 +3027,6 @@ "make-iterator": "^1.0.0" } }, - "object.omit": { - "version": "2.0.1", - "resolved": "http://registry.npm.alibaba-inc.com/object.omit/download/object.omit-2.0.1.tgz", - "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", - "dev": true, - "requires": { - "for-own": "^0.1.4", - "is-extendable": "^0.1.1" - }, - "dependencies": { - "for-own": { - "version": "0.1.5", - "resolved": "http://registry.npm.alibaba-inc.com/for-own/download/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", - "dev": true, - "requires": { - "for-in": "^1.0.1" - } - } - } - }, "object.pick": { "version": "1.3.0", "resolved": "http://registry.npm.alibaba-inc.com/object.pick/download/object.pick-1.3.0.tgz", @@ -3371,10 +3060,13 @@ } }, "open": { - "version": "0.0.5", - "resolved": "http://registry.npm.alibaba-inc.com/open/download/open-0.0.5.tgz", - "integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw=", - "dev": true + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/open/-/open-6.3.0.tgz", + "integrity": "sha512-6AHdrJxPvAXIowO/aIaeHZ8CeMdDf7qCyRNq8NwJpinmCdXhz+NZR7ie1Too94lpciCDsG+qHGO9Mt0svA4OqA==", + "dev": true, + "requires": { + "is-wsl": "^1.1.0" + } }, "optimist": { "version": "0.6.1", @@ -3387,7 +3079,7 @@ }, "orchestrator": { "version": "0.3.8", - "resolved": "http://registry.npm.alibaba-inc.com/orchestrator/download/orchestrator-0.3.8.tgz", + "resolved": "https://registry.npm.taobao.org/orchestrator/download/orchestrator-0.3.8.tgz", "integrity": "sha1-FOfp4nZPcxX7rBhOUGx6pt+UrX4=", "dev": true, "requires": { @@ -3398,13 +3090,13 @@ }, "ordered-read-streams": { "version": "0.1.0", - "resolved": "http://registry.npm.alibaba-inc.com/ordered-read-streams/download/ordered-read-streams-0.1.0.tgz", + "resolved": "https://registry.npm.taobao.org/ordered-read-streams/download/ordered-read-streams-0.1.0.tgz", "integrity": "sha1-/VZamvjrRHO6abbtijQ1LLVS8SY=", "dev": true }, "os-homedir": { "version": "1.0.2", - "resolved": "http://registry.npm.alibaba-inc.com/os-homedir/download/os-homedir-1.0.2.tgz", + "resolved": "https://registry.npm.taobao.org/os-homedir/download/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true }, @@ -3419,35 +3111,6 @@ "path-root": "^0.1.1" } }, - "parse-glob": { - "version": "3.0.4", - "resolved": "http://registry.npm.alibaba-inc.com/parse-glob/download/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", - "dev": true, - "requires": { - "glob-base": "^0.3.0", - "is-dotfile": "^1.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.0" - }, - "dependencies": { - "is-extglob": { - "version": "1.0.0", - "resolved": "http://registry.npm.alibaba-inc.com/is-extglob/download/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true - }, - "is-glob": { - "version": "2.0.1", - "resolved": "http://registry.npm.alibaba-inc.com/is-glob/download/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "dev": true, - "requires": { - "is-extglob": "^1.0.0" - } - } - } - }, "parse-json": { "version": "2.2.0", "resolved": "http://registry.npm.alibaba-inc.com/parse-json/download/parse-json-2.2.0.tgz", @@ -3609,12 +3272,6 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true }, - "preserve": { - "version": "0.2.0", - "resolved": "http://registry.npm.alibaba-inc.com/preserve/download/preserve-0.2.0.tgz", - "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", - "dev": true - }, "pretty-hrtime": { "version": "1.0.3", "resolved": "http://registry.npm.alibaba-inc.com/pretty-hrtime/download/pretty-hrtime-1.0.3.tgz", @@ -3633,25 +3290,6 @@ "integrity": "sha512-KIJqT9jQJDQx5h5uAVPimw6yVg2SekOKu959OCtktD3FjzbpvaPr8i4zzg07DOMz+igA4W/aNM7OV8H37pFYfA==", "dev": true }, - "randomatic": { - "version": "3.0.0", - "resolved": "http://registry.npm.alibaba-inc.com/randomatic/download/randomatic-3.0.0.tgz", - "integrity": "sha1-01SQAw6091eN4pLObfsEqRoSiSM=", - "dev": true, - "requires": { - "is-number": "^4.0.0", - "kind-of": "^6.0.0", - "math-random": "^1.0.1" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "http://registry.npm.alibaba-inc.com/is-number/download/is-number-4.0.0.tgz", - "integrity": "sha1-ACbjf1RU1z41bf5lZGmYZ8an8P8=", - "dev": true - } - } - }, "range-parser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", @@ -3757,15 +3395,6 @@ "esprima": "~3.0.0" } }, - "regex-cache": { - "version": "0.4.4", - "resolved": "http://registry.npm.alibaba-inc.com/regex-cache/download/regex-cache-0.4.4.tgz", - "integrity": "sha1-db3FiioUls7EihKDW8VMjVYjNt0=", - "dev": true, - "requires": { - "is-equal-shallow": "^0.1.3" - } - }, "regex-not": { "version": "1.0.2", "resolved": "http://registry.npm.alibaba-inc.com/regex-not/download/regex-not-1.0.2.tgz", @@ -3941,13 +3570,13 @@ }, "sequencify": { "version": "0.0.7", - "resolved": "http://registry.npm.alibaba-inc.com/sequencify/download/sequencify-0.0.7.tgz", + "resolved": "https://registry.npm.taobao.org/sequencify/download/sequencify-0.0.7.tgz", "integrity": "sha1-kM/xnQLgcCf9dn9erT57ldHnOAw=", "dev": true }, "serve-index": { "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "resolved": "http://registry.npm.alibaba-inc.com/serve-index/download/serve-index-1.9.1.tgz", "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", "dev": true, "requires": { @@ -4038,9 +3667,9 @@ } }, "set-value": { - "version": "2.0.0", - "resolved": "http://registry.npm.alibaba-inc.com/set-value/download/set-value-2.0.0.tgz", - "integrity": "sha1-ca5KiPD+77v1LR6mBPP7MV67YnQ=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", "dev": true, "requires": { "extend-shallow": "^2.0.1", @@ -4068,7 +3697,7 @@ }, "shelljs": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", + "resolved": "http://registry.npm.alibaba-inc.com/shelljs/download/shelljs-0.3.0.tgz", "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=", "dev": true }, @@ -4310,7 +3939,7 @@ }, "stream-consume": { "version": "0.1.1", - "resolved": "http://registry.npm.alibaba-inc.com/stream-consume/download/stream-consume-0.1.1.tgz", + "resolved": "http://registry.npm.taobao.org/stream-consume/download/stream-consume-0.1.1.tgz", "integrity": "sha1-0721mMK9CugrjKx6xQsRB6eZbEg=", "dev": true }, @@ -4337,7 +3966,7 @@ }, "strip-bom": { "version": "1.0.0", - "resolved": "http://registry.npm.alibaba-inc.com/strip-bom/download/strip-bom-1.0.0.tgz", + "resolved": "https://registry.npm.taobao.org/strip-bom/download/strip-bom-1.0.0.tgz", "integrity": "sha1-hbiGLzhEtabV7IRnqTWYFzo295Q=", "dev": true, "requires": { @@ -4356,7 +3985,7 @@ }, "strip-json-comments": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", + "resolved": "http://registry.npm.alibaba-inc.com/strip-json-comments/download/strip-json-comments-1.0.4.tgz", "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", "dev": true }, @@ -4437,7 +4066,7 @@ }, "tildify": { "version": "1.2.0", - "resolved": "http://registry.npm.alibaba-inc.com/tildify/download/tildify-1.2.0.tgz", + "resolved": "https://registry.npm.taobao.org/tildify/download/tildify-1.2.0.tgz", "integrity": "sha1-3OwD9V3Km3qj5bBPIYF+tW5jWIo=", "dev": true, "requires": { @@ -4566,43 +4195,20 @@ "dev": true }, "union-value": { - "version": "1.0.0", - "resolved": "http://registry.npm.alibaba-inc.com/union-value/download/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", "dev": true, "requires": { "arr-union": "^3.1.0", "get-value": "^2.0.6", "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "http://registry.npm.alibaba-inc.com/extend-shallow/download/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "http://registry.npm.alibaba-inc.com/set-value/download/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } + "set-value": "^2.0.1" } }, "unique-stream": { "version": "1.0.0", - "resolved": "http://registry.npm.alibaba-inc.com/unique-stream/download/unique-stream-1.0.0.tgz", + "resolved": "https://registry.npm.taobao.org/unique-stream/download/unique-stream-1.0.0.tgz", "integrity": "sha1-1ZpKdUJ0R9mqbJHnAmP40mpLEEs=", "dev": true }, @@ -4672,7 +4278,7 @@ }, "user-home": { "version": "1.1.1", - "resolved": "http://registry.npm.alibaba-inc.com/user-home/download/user-home-1.1.1.tgz", + "resolved": "https://registry.npm.taobao.org/user-home/download/user-home-1.1.1.tgz", "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=", "dev": true }, @@ -4696,7 +4302,7 @@ }, "v8flags": { "version": "2.1.1", - "resolved": "http://registry.npm.alibaba-inc.com/v8flags/download/v8flags-2.1.1.tgz", + "resolved": "https://registry.npm.taobao.org/v8flags/download/v8flags-2.1.1.tgz", "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=", "dev": true, "requires": { @@ -4726,7 +4332,7 @@ }, "vinyl-fs": { "version": "0.3.14", - "resolved": "http://registry.npm.alibaba-inc.com/vinyl-fs/download/vinyl-fs-0.3.14.tgz", + "resolved": "https://registry.npm.taobao.org/vinyl-fs/download/vinyl-fs-0.3.14.tgz", "integrity": "sha1-mmhRzhysHBzqX+hsCTHWIMLPqeY=", "dev": true, "requires": { @@ -4742,13 +4348,13 @@ "dependencies": { "clone": { "version": "0.2.0", - "resolved": "http://registry.npm.alibaba-inc.com/clone/download/clone-0.2.0.tgz", + "resolved": "https://registry.npm.taobao.org/clone/download/clone-0.2.0.tgz", "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", "dev": true }, "readable-stream": { "version": "1.0.34", - "resolved": "http://registry.npm.alibaba-inc.com/readable-stream/download/readable-stream-1.0.34.tgz", + "resolved": "https://registry.npm.taobao.org/readable-stream/download/readable-stream-1.0.34.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Freadable-stream%2Fdownload%2Freadable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, "requires": { @@ -4760,7 +4366,7 @@ }, "through2": { "version": "0.6.5", - "resolved": "http://registry.npm.alibaba-inc.com/through2/download/through2-0.6.5.tgz", + "resolved": "https://registry.npm.taobao.org/through2/download/through2-0.6.5.tgz", "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", "dev": true, "requires": { @@ -4770,7 +4376,7 @@ }, "vinyl": { "version": "0.4.6", - "resolved": "http://registry.npm.alibaba-inc.com/vinyl/download/vinyl-0.4.6.tgz", + "resolved": "https://registry.npm.taobao.org/vinyl/download/vinyl-0.4.6.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvinyl%2Fdownload%2Fvinyl-0.4.6.tgz", "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", "dev": true, "requires": { @@ -4848,7 +4454,7 @@ }, "websocket-driver": { "version": "0.7.0", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", + "resolved": "http://registry.npm.alibaba-inc.com/websocket-driver/download/websocket-driver-0.7.0.tgz", "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=", "dev": true, "requires": { diff --git a/sentinel-dashboard/src/main/webapp/resources/package.json b/sentinel-dashboard/src/main/webapp/resources/package.json index 8b511a14dd..b62fb328fb 100755 --- a/sentinel-dashboard/src/main/webapp/resources/package.json +++ b/sentinel-dashboard/src/main/webapp/resources/package.json @@ -30,12 +30,12 @@ "angularjs-bootstrap-datetimepicker": "^1.1.4", "bootstrap-switch": "^3.3.4", "bootstrap-tagsinput": "~0.7.1", + "lodash": "^4.17.15", "moment": "^2.12.0", "ng-dialog": "^0.6.6", "ng-tags-input": "~3.0.0", "oclazyload": "^1.1.0", - "selectize": "^0.12.1", - "lodash": ">=4.17.11" + "selectize": "^0.12.1" }, "devDependencies": { "gulp": "^3.9.1", @@ -45,11 +45,11 @@ "gulp-csscomb": "^3.0.8", "gulp-cssmin": "^0.2.0", "gulp-jshint": "^2.1.0", - "gulp-load-plugins": "^1.5.0", + "gulp-load-plugins": "^1.6.0", "gulp-serv": "0.0.1", "gulp-uglify": "^3.0.0", "jshint": "^2.10.2", - "open": "0.0.5", + "open": "^6.3.0", "source-map": "^0.7.3" } } diff --git a/sentinel-dashboard/src/test/java/com/alibaba/csp/sentinel/dashboard/client/SentinelApiClientTest.java b/sentinel-dashboard/src/test/java/com/alibaba/csp/sentinel/dashboard/client/SentinelApiClientTest.java new file mode 100644 index 0000000000..08e35e6323 --- /dev/null +++ b/sentinel-dashboard/src/test/java/com/alibaba/csp/sentinel/dashboard/client/SentinelApiClientTest.java @@ -0,0 +1,55 @@ +/* + * 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.dashboard.client; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.apache.http.HttpException; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.protocol.RequestContent; +import org.junit.Test; + +public class SentinelApiClientTest { + @Test + public void postRequest() throws HttpException, IOException { + // Processor is required because it will determine the final request body including + // headers before outgoing. + RequestContent processor = new RequestContent(); + Map params = new HashMap(); + params.put("a", "1"); + params.put("b", "2+"); + params.put("c", "3 "); + + HttpUriRequest request; + + request = SentinelApiClient.postRequest("/test", params, false); + assertNotNull(request); + processor.process(request, null); + assertNotNull(request.getFirstHeader("Content-Type")); + assertEquals("application/x-www-form-urlencoded", request.getFirstHeader("Content-Type").getValue()); + + request = SentinelApiClient.postRequest("/test", params, true); + assertNotNull(request); + processor.process(request, null); + assertNotNull(request.getFirstHeader("Content-Type")); + assertEquals("application/x-www-form-urlencoded; charset=UTF-8", request.getFirstHeader("Content-Type").getValue()); + } +} diff --git a/sentinel-dashboard/src/test/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayApiControllerTest.java b/sentinel-dashboard/src/test/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayApiControllerTest.java new file mode 100644 index 0000000000..9751c8311e --- /dev/null +++ b/sentinel-dashboard/src/test/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayApiControllerTest.java @@ -0,0 +1,328 @@ +/* + * 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.dashboard.controller.gateway; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthorizationInterceptor; +import com.alibaba.csp.sentinel.dashboard.auth.FakeAuthServiceImpl; +import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiPredicateItemEntity; +import com.alibaba.csp.sentinel.dashboard.discovery.AppManagement; +import com.alibaba.csp.sentinel.dashboard.discovery.SimpleMachineDiscovery; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api.AddApiReqVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api.ApiPredicateItemVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api.UpdateApiReqVo; +import com.alibaba.csp.sentinel.dashboard.repository.gateway.InMemApiDefinitionStore; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.TypeReference; +import org.apache.commons.lang3.time.DateUtils; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +import static com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants.*; +import static org.junit.Assert.*; +import static org.mockito.BDDMockito.*; + +/** + * Test cases for {@link GatewayApiController}. + * + * @author cdfive + */ +@RunWith(SpringRunner.class) +@WebMvcTest(GatewayApiController.class) +@Import({FakeAuthServiceImpl.class, InMemApiDefinitionStore.class, AppManagement.class, SimpleMachineDiscovery.class, AuthorizationInterceptor.class}) +public class GatewayApiControllerTest { + + private static final String TEST_APP = "test_app"; + + private static final String TEST_IP = "localhost"; + + private static final Integer TEST_PORT = 8719; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private InMemApiDefinitionStore repository; + + @MockBean + private SentinelApiClient sentinelApiClient; + + @Before + public void before() { + repository.clearAll(); + } + + @Test + public void testQueryApis() throws Exception { + String path = "/gateway/api/list.json"; + + List entities = new ArrayList<>(); + + // Mock two entities + ApiDefinitionEntity entity = new ApiDefinitionEntity(); + entity.setId(1L); + entity.setApp(TEST_APP); + entity.setIp(TEST_IP); + entity.setPort(TEST_PORT); + entity.setApiName("foo"); + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + + Set itemEntities = new LinkedHashSet<>(); + entity.setPredicateItems(itemEntities); + ApiPredicateItemEntity itemEntity = new ApiPredicateItemEntity(); + itemEntity.setPattern("/aaa"); + itemEntity.setMatchStrategy(URL_MATCH_STRATEGY_EXACT); + + itemEntities.add(itemEntity); + entities.add(entity); + + ApiDefinitionEntity entity2 = new ApiDefinitionEntity(); + entity2.setId(2L); + entity2.setApp(TEST_APP); + entity2.setIp(TEST_IP); + entity2.setPort(TEST_PORT); + entity2.setApiName("biz"); + entity.setGmtCreate(date); + entity.setGmtModified(date); + + Set itemEntities2 = new LinkedHashSet<>(); + entity2.setPredicateItems(itemEntities2); + ApiPredicateItemEntity itemEntity2 = new ApiPredicateItemEntity(); + itemEntity2.setPattern("/bbb"); + itemEntity2.setMatchStrategy(URL_MATCH_STRATEGY_PREFIX); + + itemEntities2.add(itemEntity2); + entities.add(entity2); + + CompletableFuture> completableFuture = mock(CompletableFuture.class); + given(completableFuture.get()).willReturn(entities); + given(sentinelApiClient.fetchApis(TEST_APP, TEST_IP, TEST_PORT)).willReturn(completableFuture); + + MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get(path); + requestBuilder.param("app", TEST_APP); + requestBuilder.param("ip", TEST_IP); + requestBuilder.param("port", String.valueOf(TEST_PORT)); + + // Do controller logic + MvcResult mvcResult = mockMvc.perform(requestBuilder) + .andExpect(MockMvcResultMatchers.status().isOk()).andDo(MockMvcResultHandlers.print()).andReturn(); + + // Verify the fetchApis method has been called + verify(sentinelApiClient).fetchApis(TEST_APP, TEST_IP, TEST_PORT); + + // Verify if two same entities are got + Result> result = JSONObject.parseObject(mvcResult.getResponse().getContentAsString(), new TypeReference>>(){}); + assertTrue(result.isSuccess()); + + List data = result.getData(); + assertEquals(2, data.size()); + assertEquals(entities, data); + + // Verify the entities are add into memory repository + List entitiesInMem = repository.findAllByApp(TEST_APP); + assertEquals(2, entitiesInMem.size()); + assertEquals(entities, entitiesInMem); + } + + @Test + public void testAddApi() throws Exception { + String path = "/gateway/api/new.json"; + + AddApiReqVo reqVo = new AddApiReqVo(); + reqVo.setApp(TEST_APP); + reqVo.setIp(TEST_IP); + reqVo.setPort(TEST_PORT); + + reqVo.setApiName("customized_api"); + + List itemVos = new ArrayList<>(); + ApiPredicateItemVo itemVo = new ApiPredicateItemVo(); + itemVo.setMatchStrategy(URL_MATCH_STRATEGY_EXACT); + itemVo.setPattern("/product"); + itemVos.add(itemVo); + reqVo.setPredicateItems(itemVos); + + given(sentinelApiClient.modifyApis(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any())).willReturn(true); + + MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.post(path); + requestBuilder.content(JSON.toJSONString(reqVo)).contentType(MediaType.APPLICATION_JSON); + + // Do controller logic + MvcResult mvcResult = mockMvc.perform(requestBuilder) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcResultHandlers.print()).andReturn(); + + // Verify the modifyApis method has been called + verify(sentinelApiClient).modifyApis(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any()); + + Result result = JSONObject.parseObject(mvcResult.getResponse().getContentAsString(), new TypeReference>() {}); + assertTrue(result.isSuccess()); + + // Verify the result + ApiDefinitionEntity entity = result.getData(); + assertNotNull(entity); + assertEquals(TEST_APP, entity.getApp()); + assertEquals(TEST_IP, entity.getIp()); + assertEquals(TEST_PORT, entity.getPort()); + assertEquals("customized_api", entity.getApiName()); + assertNotNull(entity.getId()); + assertNotNull(entity.getGmtCreate()); + assertNotNull(entity.getGmtModified()); + + Set predicateItemEntities = entity.getPredicateItems(); + assertEquals(1, predicateItemEntities.size()); + ApiPredicateItemEntity predicateItemEntity = predicateItemEntities.iterator().next(); + assertEquals(URL_MATCH_STRATEGY_EXACT, predicateItemEntity.getMatchStrategy().intValue()); + assertEquals("/product", predicateItemEntity.getPattern()); + + // Verify the entity which is add in memory repository + List entitiesInMem = repository.findAllByApp(TEST_APP); + assertEquals(1, entitiesInMem.size()); + assertEquals(entity, entitiesInMem.get(0)); + } + + @Test + public void testUpdateApi() throws Exception { + String path = "/gateway/api/save.json"; + + // Add one entity to memory repository for update + ApiDefinitionEntity addEntity = new ApiDefinitionEntity(); + addEntity.setApp(TEST_APP); + addEntity.setIp(TEST_IP); + addEntity.setPort(TEST_PORT); + addEntity.setApiName("bbb"); + Date date = new Date(); + // To make the gmtModified different when do update + date = DateUtils.addSeconds(date, -1); + addEntity.setGmtCreate(date); + addEntity.setGmtModified(date); + Set addRedicateItemEntities = new HashSet<>(); + addEntity.setPredicateItems(addRedicateItemEntities); + ApiPredicateItemEntity addPredicateItemEntity = new ApiPredicateItemEntity(); + addPredicateItemEntity.setMatchStrategy(URL_MATCH_STRATEGY_EXACT); + addPredicateItemEntity.setPattern("/order"); + addEntity = repository.save(addEntity); + + UpdateApiReqVo reqVo = new UpdateApiReqVo(); + reqVo.setId(addEntity.getId()); + reqVo.setApp(TEST_APP); + List itemVos = new ArrayList<>(); + ApiPredicateItemVo itemVo = new ApiPredicateItemVo(); + itemVo.setMatchStrategy(URL_MATCH_STRATEGY_PREFIX); + itemVo.setPattern("/my_order"); + itemVos.add(itemVo); + reqVo.setPredicateItems(itemVos); + + given(sentinelApiClient.modifyApis(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any())).willReturn(true); + + MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.post(path); + requestBuilder.content(JSON.toJSONString(reqVo)).contentType(MediaType.APPLICATION_JSON); + + // Do controller logic + MvcResult mvcResult = mockMvc.perform(requestBuilder) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcResultHandlers.print()).andReturn(); + + // Verify the modifyApis method has been called + verify(sentinelApiClient).modifyApis(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any()); + + Result result = JSONObject.parseObject(mvcResult.getResponse().getContentAsString(), new TypeReference>() {}); + assertTrue(result.isSuccess()); + + ApiDefinitionEntity entity = result.getData(); + assertNotNull(entity); + assertEquals("bbb", entity.getApiName()); + assertEquals(date, entity.getGmtCreate()); + // To make sure gmtModified has been set and it's different from gmtCreate + assertNotNull(entity.getGmtModified()); + assertNotEquals(entity.getGmtCreate(), entity.getGmtModified()); + + Set predicateItemEntities = entity.getPredicateItems(); + assertEquals(1, predicateItemEntities.size()); + ApiPredicateItemEntity predicateItemEntity = predicateItemEntities.iterator().next(); + assertEquals(URL_MATCH_STRATEGY_PREFIX, predicateItemEntity.getMatchStrategy().intValue()); + assertEquals("/my_order", predicateItemEntity.getPattern()); + + // Verify the entity which is update in memory repository + List entitiesInMem = repository.findAllByApp(TEST_APP); + assertEquals(1, entitiesInMem.size()); + assertEquals(entity, entitiesInMem.get(0)); + } + + @Test + public void testDeleteApi() throws Exception { + String path = "/gateway/api/delete.json"; + + // Add one entity into memory repository for delete + ApiDefinitionEntity addEntity = new ApiDefinitionEntity(); + addEntity.setApp(TEST_APP); + addEntity.setIp(TEST_IP); + addEntity.setPort(TEST_PORT); + addEntity.setApiName("ccc"); + Date date = new Date(); + addEntity.setGmtCreate(date); + addEntity.setGmtModified(date); + Set addRedicateItemEntities = new HashSet<>(); + addEntity.setPredicateItems(addRedicateItemEntities); + ApiPredicateItemEntity addPredicateItemEntity = new ApiPredicateItemEntity(); + addPredicateItemEntity.setMatchStrategy(URL_MATCH_STRATEGY_EXACT); + addPredicateItemEntity.setPattern("/user/add"); + addEntity = repository.save(addEntity); + + given(sentinelApiClient.modifyApis(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any())).willReturn(true); + + MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.post(path); + requestBuilder.param("id", String.valueOf(addEntity.getId())); + + // Do controller logic + MvcResult mvcResult = mockMvc.perform(requestBuilder) + .andExpect(MockMvcResultMatchers.status().isOk()).andDo(MockMvcResultHandlers.print()).andReturn(); + + // Verify the modifyApis method has been called + verify(sentinelApiClient).modifyApis(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any()); + + // Verify the result + Result result = JSONObject.parseObject(mvcResult.getResponse().getContentAsString(), new TypeReference>() {}); + assertTrue(result.isSuccess()); + + assertEquals(addEntity.getId(), result.getData()); + + // Now no entities in memory + List entitiesInMem = repository.findAllByApp(TEST_APP); + assertEquals(0, entitiesInMem.size()); + } +} diff --git a/sentinel-dashboard/src/test/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayFlowRuleControllerTest.java b/sentinel-dashboard/src/test/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayFlowRuleControllerTest.java new file mode 100644 index 0000000000..b9c2353e47 --- /dev/null +++ b/sentinel-dashboard/src/test/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayFlowRuleControllerTest.java @@ -0,0 +1,357 @@ +/* + * 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.dashboard.controller.gateway; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthorizationInterceptor; +import com.alibaba.csp.sentinel.dashboard.auth.FakeAuthServiceImpl; +import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayParamFlowItemEntity; +import com.alibaba.csp.sentinel.dashboard.discovery.AppManagement; +import com.alibaba.csp.sentinel.dashboard.discovery.SimpleMachineDiscovery; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule.AddFlowRuleReqVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule.GatewayParamFlowItemVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule.UpdateFlowRuleReqVo; +import com.alibaba.csp.sentinel.dashboard.repository.gateway.InMemGatewayFlowRuleStore; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.TypeReference; +import org.apache.commons.lang3.time.DateUtils; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import static com.alibaba.csp.sentinel.slots.block.RuleConstant.*; +import static com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants.*; +import static org.junit.Assert.*; +import static org.mockito.BDDMockito.*; + +/** + * Test cases for {@link GatewayFlowRuleController}. + * + * @author cdfive + */ +@RunWith(SpringRunner.class) +@WebMvcTest(GatewayFlowRuleController.class) +@Import({FakeAuthServiceImpl.class, InMemGatewayFlowRuleStore.class, AppManagement.class, SimpleMachineDiscovery.class, + AuthorizationInterceptor.class }) +public class GatewayFlowRuleControllerTest { + + private static final String TEST_APP = "test_app"; + + private static final String TEST_IP = "localhost"; + + private static final Integer TEST_PORT = 8719; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private InMemGatewayFlowRuleStore repository; + + @MockBean + private SentinelApiClient sentinelApiClient; + + @Before + public void before() { + repository.clearAll(); + } + + @Test + public void testQueryFlowRules() throws Exception { + String path = "/gateway/flow/list.json"; + + List entities = new ArrayList<>(); + + // Mock two entities + GatewayFlowRuleEntity entity = new GatewayFlowRuleEntity(); + entity.setId(1L); + entity.setApp(TEST_APP); + entity.setIp(TEST_IP); + entity.setPort(TEST_PORT); + entity.setResource("httpbin_route"); + entity.setResourceMode(RESOURCE_MODE_ROUTE_ID); + entity.setGrade(FLOW_GRADE_QPS); + entity.setCount(5D); + entity.setInterval(30L); + entity.setIntervalUnit(GatewayFlowRuleEntity.INTERVAL_UNIT_SECOND); + entity.setControlBehavior(CONTROL_BEHAVIOR_DEFAULT); + entity.setBurst(0); + entity.setMaxQueueingTimeoutMs(0); + + GatewayParamFlowItemEntity itemEntity = new GatewayParamFlowItemEntity(); + entity.setParamItem(itemEntity); + itemEntity.setParseStrategy(PARAM_PARSE_STRATEGY_CLIENT_IP); + entities.add(entity); + + GatewayFlowRuleEntity entity2 = new GatewayFlowRuleEntity(); + entity2.setId(2L); + entity2.setApp(TEST_APP); + entity2.setIp(TEST_IP); + entity2.setPort(TEST_PORT); + entity2.setResource("some_customized_api"); + entity2.setResourceMode(RESOURCE_MODE_CUSTOM_API_NAME); + entity2.setCount(30D); + entity2.setInterval(2L); + entity2.setIntervalUnit(GatewayFlowRuleEntity.INTERVAL_UNIT_MINUTE); + entity2.setControlBehavior(CONTROL_BEHAVIOR_DEFAULT); + entity2.setBurst(0); + entity2.setMaxQueueingTimeoutMs(0); + + GatewayParamFlowItemEntity itemEntity2 = new GatewayParamFlowItemEntity(); + entity2.setParamItem(itemEntity2); + itemEntity2.setParseStrategy(PARAM_PARSE_STRATEGY_CLIENT_IP); + entities.add(entity2); + + CompletableFuture> completableFuture = mock(CompletableFuture.class); + given(completableFuture.get()).willReturn(entities); + given(sentinelApiClient.fetchGatewayFlowRules(TEST_APP, TEST_IP, TEST_PORT)).willReturn(completableFuture); + + MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get(path); + requestBuilder.param("app", TEST_APP); + requestBuilder.param("ip", TEST_IP); + requestBuilder.param("port", String.valueOf(TEST_PORT)); + + // Do controller logic + MvcResult mvcResult = mockMvc.perform(requestBuilder) + .andExpect(MockMvcResultMatchers.status().isOk()).andDo(MockMvcResultHandlers.print()).andReturn(); + + // Verify the fetchGatewayFlowRules method has been called + verify(sentinelApiClient).fetchGatewayFlowRules(TEST_APP, TEST_IP, TEST_PORT); + + // Verify if two same entities are got + Result> result = JSONObject.parseObject(mvcResult.getResponse().getContentAsString(), new TypeReference>>(){}); + assertTrue(result.isSuccess()); + + List data = result.getData(); + assertEquals(2, data.size()); + assertEquals(entities, data); + + // Verify the entities are add into memory repository + List entitiesInMem = repository.findAllByApp(TEST_APP); + assertEquals(2, entitiesInMem.size()); + assertEquals(entities, entitiesInMem); + } + + @Test + public void testAddFlowRule() throws Exception { + String path = "/gateway/flow/new.json"; + + AddFlowRuleReqVo reqVo = new AddFlowRuleReqVo(); + reqVo.setApp(TEST_APP); + reqVo.setIp(TEST_IP); + reqVo.setPort(TEST_PORT); + + reqVo.setResourceMode(RESOURCE_MODE_ROUTE_ID); + reqVo.setResource("httpbin_route"); + + reqVo.setGrade(FLOW_GRADE_QPS); + reqVo.setCount(5D); + reqVo.setInterval(30L); + reqVo.setIntervalUnit(GatewayFlowRuleEntity.INTERVAL_UNIT_SECOND); + reqVo.setControlBehavior(CONTROL_BEHAVIOR_DEFAULT); + reqVo.setBurst(0); + reqVo.setMaxQueueingTimeoutMs(0); + + given(sentinelApiClient.modifyGatewayFlowRules(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any())).willReturn(true); + + MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.post(path); + requestBuilder.content(JSON.toJSONString(reqVo)).contentType(MediaType.APPLICATION_JSON); + + // Do controller logic + MvcResult mvcResult = mockMvc.perform(requestBuilder) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcResultHandlers.print()).andReturn(); + + // Verify the modifyGatewayFlowRules method has been called + verify(sentinelApiClient).modifyGatewayFlowRules(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any()); + + Result result = JSONObject.parseObject(mvcResult.getResponse().getContentAsString(), new TypeReference>() {}); + assertTrue(result.isSuccess()); + + // Verify the result + GatewayFlowRuleEntity entity = result.getData(); + assertNotNull(entity); + assertEquals(TEST_APP, entity.getApp()); + assertEquals(TEST_IP, entity.getIp()); + assertEquals(TEST_PORT, entity.getPort()); + assertEquals(RESOURCE_MODE_ROUTE_ID, entity.getResourceMode().intValue()); + assertEquals("httpbin_route", entity.getResource()); + assertNotNull(entity.getId()); + assertNotNull(entity.getGmtCreate()); + assertNotNull(entity.getGmtModified()); + + // Verify the entity which is add in memory repository + List entitiesInMem = repository.findAllByApp(TEST_APP); + assertEquals(1, entitiesInMem.size()); + assertEquals(entity, entitiesInMem.get(0)); + } + + @Test + public void testUpdateFlowRule() throws Exception { + String path = "/gateway/flow/save.json"; + + // Add one entity into memory repository for update + GatewayFlowRuleEntity addEntity = new GatewayFlowRuleEntity(); + addEntity.setId(1L); + addEntity.setApp(TEST_APP); + addEntity.setIp(TEST_IP); + addEntity.setPort(TEST_PORT); + addEntity.setResource("httpbin_route"); + addEntity.setResourceMode(RESOURCE_MODE_ROUTE_ID); + addEntity.setGrade(FLOW_GRADE_QPS); + addEntity.setCount(5D); + addEntity.setInterval(30L); + addEntity.setIntervalUnit(GatewayFlowRuleEntity.INTERVAL_UNIT_SECOND); + addEntity.setControlBehavior(CONTROL_BEHAVIOR_DEFAULT); + addEntity.setBurst(0); + addEntity.setMaxQueueingTimeoutMs(0); + Date date = new Date(); + // To make the gmtModified different when do update + date = DateUtils.addSeconds(date, -1); + addEntity.setGmtCreate(date); + addEntity.setGmtModified(date); + + GatewayParamFlowItemEntity addItemEntity = new GatewayParamFlowItemEntity(); + addEntity.setParamItem(addItemEntity); + addItemEntity.setParseStrategy(PARAM_PARSE_STRATEGY_CLIENT_IP); + + repository.save(addEntity); + + UpdateFlowRuleReqVo reqVo = new UpdateFlowRuleReqVo(); + reqVo.setId(addEntity.getId()); + reqVo.setApp(TEST_APP); + reqVo.setGrade(FLOW_GRADE_QPS); + reqVo.setCount(6D); + reqVo.setInterval(2L); + reqVo.setIntervalUnit(GatewayFlowRuleEntity.INTERVAL_UNIT_MINUTE); + reqVo.setControlBehavior(CONTROL_BEHAVIOR_RATE_LIMITER); + reqVo.setMaxQueueingTimeoutMs(500); + + GatewayParamFlowItemVo itemVo = new GatewayParamFlowItemVo(); + reqVo.setParamItem(itemVo); + itemVo.setParseStrategy(PARAM_PARSE_STRATEGY_URL_PARAM); + itemVo.setFieldName("pa"); + + given(sentinelApiClient.modifyGatewayFlowRules(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any())).willReturn(true); + + MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.post(path); + requestBuilder.content(JSON.toJSONString(reqVo)).contentType(MediaType.APPLICATION_JSON); + + // Do controller logic + MvcResult mvcResult = mockMvc.perform(requestBuilder) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcResultHandlers.print()).andReturn(); + + // Verify the modifyGatewayFlowRules method has been called + verify(sentinelApiClient).modifyGatewayFlowRules(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any()); + + Result result = JSONObject.parseObject(mvcResult.getResponse().getContentAsString(), new TypeReference>() { + }); + assertTrue(result.isSuccess()); + + GatewayFlowRuleEntity entity = result.getData(); + assertNotNull(entity); + assertEquals(RESOURCE_MODE_ROUTE_ID, entity.getResourceMode().intValue()); + assertEquals("httpbin_route", entity.getResource()); + assertEquals(6D, entity.getCount().doubleValue(), 0); + assertEquals(2L, entity.getInterval().longValue()); + assertEquals(GatewayFlowRuleEntity.INTERVAL_UNIT_MINUTE, entity.getIntervalUnit().intValue()); + assertEquals(CONTROL_BEHAVIOR_RATE_LIMITER, entity.getControlBehavior().intValue()); + assertEquals(0, entity.getBurst().intValue()); + assertEquals(500, entity.getMaxQueueingTimeoutMs().intValue()); + assertEquals(date, entity.getGmtCreate()); + // To make sure gmtModified has been set and it's different from gmtCreate + assertNotNull(entity.getGmtModified()); + assertNotEquals(entity.getGmtCreate(), entity.getGmtModified()); + + // Verify the entity which is update in memory repository + GatewayParamFlowItemEntity itemEntity = entity.getParamItem(); + assertEquals(PARAM_PARSE_STRATEGY_URL_PARAM, itemEntity.getParseStrategy().intValue()); + assertEquals("pa", itemEntity.getFieldName()); + } + + @Test + public void testDeleteFlowRule() throws Exception { + String path = "/gateway/flow/delete.json"; + + // Add one entity into memory repository for delete + GatewayFlowRuleEntity addEntity = new GatewayFlowRuleEntity(); + addEntity.setId(1L); + addEntity.setApp(TEST_APP); + addEntity.setIp(TEST_IP); + addEntity.setPort(TEST_PORT); + addEntity.setResource("httpbin_route"); + addEntity.setResourceMode(RESOURCE_MODE_ROUTE_ID); + addEntity.setGrade(FLOW_GRADE_QPS); + addEntity.setCount(5D); + addEntity.setInterval(30L); + addEntity.setIntervalUnit(GatewayFlowRuleEntity.INTERVAL_UNIT_SECOND); + addEntity.setControlBehavior(CONTROL_BEHAVIOR_DEFAULT); + addEntity.setBurst(0); + addEntity.setMaxQueueingTimeoutMs(0); + Date date = new Date(); + date = DateUtils.addSeconds(date, -1); + addEntity.setGmtCreate(date); + addEntity.setGmtModified(date); + + GatewayParamFlowItemEntity addItemEntity = new GatewayParamFlowItemEntity(); + addEntity.setParamItem(addItemEntity); + addItemEntity.setParseStrategy(PARAM_PARSE_STRATEGY_CLIENT_IP); + + repository.save(addEntity); + + given(sentinelApiClient.modifyGatewayFlowRules(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any())).willReturn(true); + + MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.post(path); + requestBuilder.param("id", String.valueOf(addEntity.getId())); + + // Do controller logic + MvcResult mvcResult = mockMvc.perform(requestBuilder) + .andExpect(MockMvcResultMatchers.status().isOk()).andDo(MockMvcResultHandlers.print()).andReturn(); + + // Verify the modifyGatewayFlowRules method has been called + verify(sentinelApiClient).modifyGatewayFlowRules(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any()); + + // Verify the result + Result result = JSONObject.parseObject(mvcResult.getResponse().getContentAsString(), new TypeReference>() {}); + assertTrue(result.isSuccess()); + + assertEquals(addEntity.getId(), result.getData()); + + // Now no entities in memory + List entitiesInMem = repository.findAllByApp(TEST_APP); + assertEquals(0, entitiesInMem.size()); + } +} diff --git a/sentinel-dashboard/src/test/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/JsonSerializeTest.java b/sentinel-dashboard/src/test/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/JsonSerializeTest.java new file mode 100644 index 0000000000..2b0718c45e --- /dev/null +++ b/sentinel-dashboard/src/test/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/JsonSerializeTest.java @@ -0,0 +1,60 @@ +/* + * 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.dashboard.datasource.entity; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.AuthorityRuleEntity; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.ParamFlowRuleEntity; +import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowClusterConfig; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule; +import com.alibaba.fastjson.JSON; +import org.junit.Assert; +import org.junit.Test; + +/** + * @author lianglin + * @since 1.7.0 + */ +public class JsonSerializeTest { + + @Test + public void authorityRuleJsonSerializeTest() { + AuthorityRuleEntity emptyRule = new AuthorityRuleEntity(); + Assert.assertTrue("{}".equals(JSON.toJSONString(emptyRule))); + + AuthorityRuleEntity authorityRule = new AuthorityRuleEntity(); + AuthorityRule rule = new AuthorityRule(); + rule.setStrategy(0).setLimitApp("default").setResource("rs"); + authorityRule.setRule(rule); + Assert.assertTrue("{\"rule\":{\"limitApp\":\"default\",\"resource\":\"rs\",\"strategy\":0}}".equals(JSON.toJSONString(authorityRule))); + } + + @Test + public void paramFlowRuleSerializeTest() { + ParamFlowRuleEntity emptyRule = new ParamFlowRuleEntity(); + Assert.assertTrue("{}".equals(JSON.toJSONString(emptyRule))); + + ParamFlowRuleEntity paramFlowRule = new ParamFlowRuleEntity(); + ParamFlowRule rule = new ParamFlowRule(); + rule.setClusterConfig(new ParamFlowClusterConfig()); + rule.setResource("rs").setLimitApp("default"); + paramFlowRule.setRule(rule); + Assert.assertTrue("{\"rule\":{\"burstCount\":0,\"clusterConfig\":{\"fallbackToLocalWhenFail\":false,\"sampleCount\":10,\"thresholdType\":0,\"windowIntervalMs\":1000},\"clusterMode\":false,\"controlBehavior\":0,\"count\":0.0,\"durationInSec\":1,\"grade\":1,\"limitApp\":\"default\",\"maxQueueingTimeMs\":0,\"paramFlowItemList\":[],\"resource\":\"rs\"}}" + .equals(JSON.toJSONString(paramFlowRule))); + + } + +} diff --git a/sentinel-demo/pom.xml b/sentinel-demo/pom.xml index 38735638dc..4a9ac4de39 100755 --- a/sentinel-demo/pom.xml +++ b/sentinel-demo/pom.xml @@ -6,7 +6,7 @@ com.alibaba.csp sentinel-parent - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT sentinel-demo pom @@ -34,6 +34,8 @@ sentinel-demo-apache-dubbo sentinel-demo-spring-cloud-gateway sentinel-demo-zuul-gateway + sentinel-demo-etcd-datasource + sentinel-demo-spring-webmvc @@ -53,6 +55,13 @@ true + + org.apache.maven.plugins + maven-gpg-plugin + + true + + diff --git a/sentinel-demo/sentinel-demo-annotation-spring-aop/pom.xml b/sentinel-demo/sentinel-demo-annotation-spring-aop/pom.xml index 79c2945a03..13191d51a3 100644 --- a/sentinel-demo/sentinel-demo-annotation-spring-aop/pom.xml +++ b/sentinel-demo/sentinel-demo-annotation-spring-aop/pom.xml @@ -5,7 +5,7 @@ sentinel-demo com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 diff --git a/sentinel-demo/sentinel-demo-apache-dubbo/pom.xml b/sentinel-demo/sentinel-demo-apache-dubbo/pom.xml index 9ecd614326..6f1ddb406f 100644 --- a/sentinel-demo/sentinel-demo-apache-dubbo/pom.xml +++ b/sentinel-demo/sentinel-demo-apache-dubbo/pom.xml @@ -5,7 +5,7 @@ sentinel-demo com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 @@ -15,7 +15,7 @@ org.apache.dubbo dubbo - 2.7.1 + 2.7.3 diff --git a/sentinel-demo/sentinel-demo-apache-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/apache/dubbo/FooConsumerBootstrap.java b/sentinel-demo/sentinel-demo-apache-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/apache/dubbo/FooConsumerBootstrap.java index dfe1dbcbce..38de48920b 100644 --- a/sentinel-demo/sentinel-demo-apache-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/apache/dubbo/FooConsumerBootstrap.java +++ b/sentinel-demo/sentinel-demo-apache-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/apache/dubbo/FooConsumerBootstrap.java @@ -15,12 +15,22 @@ */ package com.alibaba.csp.sentinel.demo.apache.dubbo; +import com.alibaba.csp.sentinel.adapter.dubbo.fallback.DubboFallbackRegistry; import com.alibaba.csp.sentinel.demo.apache.dubbo.consumer.ConsumerConfiguration; import com.alibaba.csp.sentinel.demo.apache.dubbo.consumer.FooServiceConsumer; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; import com.alibaba.csp.sentinel.slots.block.SentinelRpcException; - +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import org.apache.dubbo.rpc.AsyncRpcResult; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + /** * Please add the following VM arguments: *
    @@ -33,10 +43,14 @@
      */
     public class FooConsumerBootstrap {
     
    -    public static void main(String[] args) {
    +    private static final String INTERFACE_RES_KEY = FooService.class.getName();
    +    private static final String RES_KEY = INTERFACE_RES_KEY + ":sayHello(java.lang.String)";
    +
    +    public static void main(String[] args) throws InterruptedException {
             AnnotationConfigApplicationContext consumerContext = new AnnotationConfigApplicationContext();
             consumerContext.register(ConsumerConfiguration.class);
             consumerContext.refresh();
    +        initFlowRule(10, false);
     
             FooServiceConsumer service = consumerContext.getBean(FooServiceConsumer.class);
     
    @@ -50,5 +64,97 @@ public static void main(String[] args) {
                     ex.printStackTrace();
                 }
             }
    +
    +        // method flowcontrol
    +        Thread.sleep(1000);
    +        initFlowRule(20, true);
    +        for (int i = 0; i < 10; i++) {
    +            try {
    +                String message = service.sayHello("Eric");
    +                System.out.println("Success: " + message);
    +            } catch (SentinelRpcException ex) {
    +                System.out.println("Blocked");
    +                System.out.println("fallback:" + service.doAnother());
    +
    +            } catch (Exception ex) {
    +                ex.printStackTrace();
    +            }
    +        }
    +
    +        // fallback to result
    +        Thread.sleep(1000);
    +        registryCustomFallback();
    +
    +        for (int i = 0; i < 10; i++) {
    +            try {
    +                String message = service.sayHello("Eric");
    +                System.out.println("Result: " + message);
    +            } catch (SentinelRpcException ex) {
    +                System.out.println("Blocked");
    +            } catch (Exception ex) {
    +                ex.printStackTrace();
    +            }
    +        }
    +        // fallback to exception
    +        Thread.sleep(1000);
    +        registryCustomFallbackForCustomException();
    +
    +        for (int i = 0; i < 10; i++) {
    +            try {
    +                String message = service.sayHello("Eric");
    +                System.out.println("Result: " + message);
    +            } catch (SentinelRpcException ex) {
    +                System.out.println("Blocked");
    +            } catch (Exception ex) {
    +                ex.printStackTrace();
    +            }
    +        }
    +
    +        Thread.sleep(1000);
    +        registryCustomFallbackWhenFallbackError();
    +        for (int i = 0; i < 10; i++) {
    +            try {
    +                String message = service.sayHello("Eric");
    +                System.out.println("Result: " + message);
    +            } catch (SentinelRpcException ex) {
    +                System.out.println("Blocked");
    +            } catch (Exception ex) {
    +                ex.printStackTrace();
    +            }
    +        }
    +    }
    +
    +    public static void registryCustomFallback() {
    +        DubboFallbackRegistry.setConsumerFallback(
    +                (invoker, invocation, ex) -> AsyncRpcResult.newDefaultAsyncResult("fallback", invocation));
    +
    +    }
    +
    +    public static void registryCustomFallbackForCustomException() {
    +        DubboFallbackRegistry.setConsumerFallback(
    +                (invoker, invocation, ex) -> AsyncRpcResult.newDefaultAsyncResult(new RuntimeException("fallback"), invocation));
    +    }
    +
    +    public static void registryCustomFallbackWhenFallbackError() {
    +        DubboFallbackRegistry.setConsumerFallback(
    +                (invoker, invocation, ex) -> {
    +                    throw new RuntimeException("fallback");
    +                });
    +    }
    +
    +
    +    private static void initFlowRule(int interfaceFlowLimit, boolean method) {
    +        FlowRule flowRule = new FlowRule(INTERFACE_RES_KEY)
    +                .setCount(interfaceFlowLimit)
    +                .setGrade(RuleConstant.FLOW_GRADE_QPS);
    +        List list = new ArrayList<>();
    +        if (method) {
    +            FlowRule flowRule1 = new FlowRule(RES_KEY)
    +                    .setCount(5)
    +                    .setGrade(RuleConstant.FLOW_GRADE_QPS);
    +            list.add(flowRule1);
    +        }
    +        list.add(flowRule);
    +        FlowRuleManager.loadRules(list);
         }
     }
    diff --git a/sentinel-demo/sentinel-demo-apache-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/apache/dubbo/FooConsumerExceptionDegradeBootstrap.java b/sentinel-demo/sentinel-demo-apache-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/apache/dubbo/FooConsumerExceptionDegradeBootstrap.java
    new file mode 100644
    index 0000000000..2df72d8408
    --- /dev/null
    +++ b/sentinel-demo/sentinel-demo-apache-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/apache/dubbo/FooConsumerExceptionDegradeBootstrap.java
    @@ -0,0 +1,119 @@
    +/*
    + * 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.demo.apache.dubbo;
    +
    +import com.alibaba.csp.sentinel.adapter.dubbo.fallback.DubboFallbackRegistry;
    +import com.alibaba.csp.sentinel.demo.apache.dubbo.consumer.ConsumerConfiguration;
    +import com.alibaba.csp.sentinel.demo.apache.dubbo.consumer.FooServiceConsumer;
    +import com.alibaba.csp.sentinel.slots.block.RuleConstant;
    +import com.alibaba.csp.sentinel.slots.block.SentinelRpcException;
    +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
    +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
    +import org.apache.dubbo.rpc.AsyncRpcResult;
    +import org.apache.dubbo.rpc.RpcContext;
    +import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    +
    +import java.util.Collections;
    +import java.util.concurrent.CompletableFuture;
    +import java.util.concurrent.ExecutionException;
    +
    +/**
    + * Please add the following VM arguments:
    + * 
    + * -Djava.net.preferIPv4Stack=true
    + * -Dcsp.sentinel.api.port=8721
    + * -Dproject.name=dubbo-consumer-demo
    + * 
    + * + * @author Zechao zheng + */ +public class FooConsumerExceptionDegradeBootstrap { + + private static final String INTERFACE_RES_KEY = FooService.class.getName(); + private static final String RES_KEY = INTERFACE_RES_KEY + ":sayHello(java.lang.String)"; + + public static void main(String[] args) throws InterruptedException, ExecutionException { + AnnotationConfigApplicationContext consumerContext = new AnnotationConfigApplicationContext(); + consumerContext.register(ConsumerConfiguration.class); + consumerContext.refresh(); + + FooServiceConsumer service = consumerContext.getBean(FooServiceConsumer.class); + initExceptionFallback(3); + registryCustomFallback(); + for (int i = 0; i < 10; i++) { + try { + String message = service.exceptionTest(true, false); + System.out.println("Result: " + message); + } catch (SentinelRpcException ex) { + System.out.println("Blocked"); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + // sleep 3s to skip the time window + initExceptionFallback(3); + Thread.sleep(3000); + for (int i = 0; i < 10; i++) { + try { + String message = service.exceptionTest(false, true); + System.out.println("Result: " + message); + } catch (SentinelRpcException ex) { + System.out.println("Blocked"); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + initExceptionFallback(3); + Thread.sleep(3000); + + try { + // timeout to trigger the fallback + CompletableFuture completableFuture = RpcContext.getContext().asyncCall(() -> service.exceptionTest(false, true)); + System.out.println("Result: " + completableFuture.get()); + } catch (Exception e) { + e.printStackTrace(); + } + + for (int i = 0; i < 10; i++) { + try { + CompletableFuture result = RpcContext.getContext().asyncCall(() -> service.exceptionTest(false, true)); + System.out.println("Result: " + result.get()); + } catch (SentinelRpcException ex) { + System.out.println("Blocked"); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + } + + public static void registryCustomFallback() { + DubboFallbackRegistry.setConsumerFallback( + (invoker, invocation, ex) -> AsyncRpcResult.newDefaultAsyncResult("fallback", invocation)); + + } + + public static void initExceptionFallback(int timewindow) { + DegradeRule degradeRule = new DegradeRule(INTERFACE_RES_KEY) + .setCount(0.5) + .setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO) + .setTimeWindow(timewindow) + .setMinRequestAmount(1); + DegradeRuleManager.loadRules(Collections.singletonList(degradeRule)); + + } +} diff --git a/sentinel-demo/sentinel-demo-apache-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/apache/dubbo/FooProviderBootstrap.java b/sentinel-demo/sentinel-demo-apache-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/apache/dubbo/FooProviderBootstrap.java index a23c0ed05e..1e4619a982 100644 --- a/sentinel-demo/sentinel-demo-apache-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/apache/dubbo/FooProviderBootstrap.java +++ b/sentinel-demo/sentinel-demo-apache-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/apache/dubbo/FooProviderBootstrap.java @@ -37,15 +37,11 @@ */ public class FooProviderBootstrap { - private static final String INTERFACE_RES_KEY = FooService.class.getName(); - private static final String RES_KEY = INTERFACE_RES_KEY + ":sayHello(java.lang.String)"; - public static void main(String[] args) { // Users don't need to manually call this method. // Only for eager initialization. InitExecutor.doInit(); - initFlowRule(); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(ProviderConfiguration.class); @@ -54,10 +50,4 @@ public static void main(String[] args) { System.out.println("Service provider is ready"); } - private static void initFlowRule() { - FlowRule flowRule = new FlowRule(INTERFACE_RES_KEY) - .setCount(10) - .setGrade(RuleConstant.FLOW_GRADE_QPS); - FlowRuleManager.loadRules(Collections.singletonList(flowRule)); - } } diff --git a/sentinel-demo/sentinel-demo-apache-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/apache/dubbo/FooService.java b/sentinel-demo/sentinel-demo-apache-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/apache/dubbo/FooService.java index dcdabc8c7c..1ccff0840f 100644 --- a/sentinel-demo/sentinel-demo-apache-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/apache/dubbo/FooService.java +++ b/sentinel-demo/sentinel-demo-apache-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/apache/dubbo/FooService.java @@ -23,4 +23,6 @@ public interface FooService { String sayHello(String name); String doAnother(); + + String exceptionTest(boolean biz, boolean timeout); } diff --git a/sentinel-demo/sentinel-demo-apache-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/apache/dubbo/consumer/FooServiceConsumer.java b/sentinel-demo/sentinel-demo-apache-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/apache/dubbo/consumer/FooServiceConsumer.java index f83d98a008..5a224d5bdb 100644 --- a/sentinel-demo/sentinel-demo-apache-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/apache/dubbo/consumer/FooServiceConsumer.java +++ b/sentinel-demo/sentinel-demo-apache-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/apache/dubbo/consumer/FooServiceConsumer.java @@ -16,7 +16,6 @@ package com.alibaba.csp.sentinel.demo.apache.dubbo.consumer; import com.alibaba.csp.sentinel.demo.apache.dubbo.FooService; - import org.apache.dubbo.config.annotation.Reference; /** @@ -24,7 +23,7 @@ */ public class FooServiceConsumer { - @Reference(url = "dubbo://127.0.0.1:25758", timeout = 3000) + @Reference(url = "dubbo://127.0.0.1:25758", timeout = 500) private FooService fooService; public String sayHello(String name) { @@ -34,4 +33,8 @@ public String sayHello(String name) { public String doAnother() { return fooService.doAnother(); } + + public String exceptionTest(boolean biz, boolean timeout) { + return fooService.exceptionTest(biz, timeout); + } } diff --git a/sentinel-demo/sentinel-demo-apache-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/apache/dubbo/provider/FooServiceImpl.java b/sentinel-demo/sentinel-demo-apache-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/apache/dubbo/provider/FooServiceImpl.java index bd92567aa9..f73184b055 100644 --- a/sentinel-demo/sentinel-demo-apache-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/apache/dubbo/provider/FooServiceImpl.java +++ b/sentinel-demo/sentinel-demo-apache-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/apache/dubbo/provider/FooServiceImpl.java @@ -15,12 +15,11 @@ */ package com.alibaba.csp.sentinel.demo.apache.dubbo.provider; -import java.time.LocalDateTime; - import com.alibaba.csp.sentinel.demo.apache.dubbo.FooService; - import org.apache.dubbo.config.annotation.Service; +import java.time.LocalDateTime; + /** * @author Eric Zhao */ @@ -36,4 +35,20 @@ public String sayHello(String name) { public String doAnother() { return LocalDateTime.now().toString(); } + + @Override + public String exceptionTest(boolean biz, boolean timeout) { + if (biz) { + throw new RuntimeException("biz exception"); + } + if (timeout) { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + return "Success"; + } + } diff --git a/sentinel-demo/sentinel-demo-apollo-datasource/pom.xml b/sentinel-demo/sentinel-demo-apollo-datasource/pom.xml index 977a803d74..070032d1d2 100644 --- a/sentinel-demo/sentinel-demo-apollo-datasource/pom.xml +++ b/sentinel-demo/sentinel-demo-apollo-datasource/pom.xml @@ -5,7 +5,7 @@ sentinel-demo com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 diff --git a/sentinel-demo/sentinel-demo-basic/pom.xml b/sentinel-demo/sentinel-demo-basic/pom.xml index 1538bfaa15..e361a8b845 100755 --- a/sentinel-demo/sentinel-demo-basic/pom.xml +++ b/sentinel-demo/sentinel-demo-basic/pom.xml @@ -6,7 +6,7 @@ com.alibaba.csp sentinel-demo - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT sentinel-demo-basic diff --git a/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/degrade/ExceptionRatioDegradeDemo.java b/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/degrade/ExceptionRatioDegradeDemo.java index 8007908ca0..c84756aaac 100755 --- a/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/degrade/ExceptionRatioDegradeDemo.java +++ b/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/degrade/ExceptionRatioDegradeDemo.java @@ -15,11 +15,6 @@ */ package com.alibaba.csp.sentinel.demo.degrade; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - import com.alibaba.csp.sentinel.Entry; import com.alibaba.csp.sentinel.SphU; import com.alibaba.csp.sentinel.Tracer; @@ -29,6 +24,11 @@ import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; import com.alibaba.csp.sentinel.util.TimeUtil; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + /** *

    * Degrade is used when the resources are in an unstable state, these resources @@ -115,6 +115,7 @@ private static void initDegradeRule() { rule.setCount(0.1); rule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO); rule.setTimeWindow(10); + rule.setMinRequestAmount(20); rules.add(rule); DegradeRuleManager.loadRules(rules); } diff --git a/sentinel-demo/sentinel-demo-cluster/pom.xml b/sentinel-demo/sentinel-demo-cluster/pom.xml index 7f40848bcc..d4ff48cea0 100644 --- a/sentinel-demo/sentinel-demo-cluster/pom.xml +++ b/sentinel-demo/sentinel-demo-cluster/pom.xml @@ -5,7 +5,7 @@ sentinel-demo com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 diff --git a/sentinel-demo/sentinel-demo-cluster/sentinel-demo-cluster-embedded/pom.xml b/sentinel-demo/sentinel-demo-cluster/sentinel-demo-cluster-embedded/pom.xml index 521ef63540..069bbb012d 100644 --- a/sentinel-demo/sentinel-demo-cluster/sentinel-demo-cluster-embedded/pom.xml +++ b/sentinel-demo/sentinel-demo-cluster/sentinel-demo-cluster-embedded/pom.xml @@ -5,7 +5,7 @@ sentinel-demo-cluster com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 diff --git a/sentinel-demo/sentinel-demo-cluster/sentinel-demo-cluster-server-alone/pom.xml b/sentinel-demo/sentinel-demo-cluster/sentinel-demo-cluster-server-alone/pom.xml index 74b29d0f9a..df8d26dc7b 100644 --- a/sentinel-demo/sentinel-demo-cluster/sentinel-demo-cluster-server-alone/pom.xml +++ b/sentinel-demo/sentinel-demo-cluster/sentinel-demo-cluster-server-alone/pom.xml @@ -5,7 +5,7 @@ sentinel-demo-cluster com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 diff --git a/sentinel-demo/sentinel-demo-command-handler/pom.xml b/sentinel-demo/sentinel-demo-command-handler/pom.xml index a0ce8dff5c..cdfd930c78 100644 --- a/sentinel-demo/sentinel-demo-command-handler/pom.xml +++ b/sentinel-demo/sentinel-demo-command-handler/pom.xml @@ -5,7 +5,7 @@ sentinel-demo com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 diff --git a/sentinel-demo/sentinel-demo-dubbo/pom.xml b/sentinel-demo/sentinel-demo-dubbo/pom.xml index 19ca045b40..81445cedc0 100644 --- a/sentinel-demo/sentinel-demo-dubbo/pom.xml +++ b/sentinel-demo/sentinel-demo-dubbo/pom.xml @@ -5,7 +5,7 @@ sentinel-demo com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 @@ -25,7 +25,7 @@ io.netty netty-all - 4.1.31.Final + 4.1.42.Final diff --git a/sentinel-demo/sentinel-demo-dynamic-file-rule/pom.xml b/sentinel-demo/sentinel-demo-dynamic-file-rule/pom.xml index e488efe593..172e867107 100755 --- a/sentinel-demo/sentinel-demo-dynamic-file-rule/pom.xml +++ b/sentinel-demo/sentinel-demo-dynamic-file-rule/pom.xml @@ -6,7 +6,7 @@ com.alibaba.csp sentinel-demo - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT sentinel-demo-dynamic-file-rule diff --git a/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/FileDataSourceInit.java b/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/FileDataSourceInit.java index 76b20a80cf..888c2384a6 100644 --- a/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/FileDataSourceInit.java +++ b/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/FileDataSourceInit.java @@ -15,6 +15,7 @@ */ package com.alibaba.csp.sentinel.demo.file.rule; +import java.io.File; import java.util.List; import com.alibaba.csp.sentinel.datasource.FileRefreshableDataSource; @@ -45,9 +46,9 @@ public class FileDataSourceInit implements InitFunc { @Override public void init() throws Exception { // A fake path. - String flowRuleDir = System.getProperty("user.home") + "/sentinel/rules"; + String flowRuleDir = System.getProperty("user.home") + File.separator + "sentinel" + File.separator + "rules"; String flowRuleFile = "flowRule.json"; - String flowRulePath = flowRuleDir + "/" + flowRuleFile; + String flowRulePath = flowRuleDir + File.separator + flowRuleFile; ReadableDataSource> ds = new FileRefreshableDataSource<>( flowRulePath, source -> JSON.parseObject(source, new TypeReference>() {}) diff --git a/sentinel-demo/sentinel-demo-etcd-datasource/pom.xml b/sentinel-demo/sentinel-demo-etcd-datasource/pom.xml new file mode 100644 index 0000000000..8702d9cfd2 --- /dev/null +++ b/sentinel-demo/sentinel-demo-etcd-datasource/pom.xml @@ -0,0 +1,48 @@ + + + + sentinel-demo + com.alibaba.csp + 1.7.2-SNAPSHOT + + 4.0.0 + + sentinel-demo-etcd-datasource + + + + + com.alibaba.csp + sentinel-core + + + + com.alibaba.csp + sentinel-datasource-etcd + + + + + com.alibaba + fastjson + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven.compiler.version} + + 1.8 + 1.8 + ${java.encoding} + + + + + \ No newline at end of file diff --git a/sentinel-demo/sentinel-demo-etcd-datasource/src/main/java/com/alibaba/csp/sentinel/demo/datasource/etcd/EtcdConfigSender.java b/sentinel-demo/sentinel-demo-etcd-datasource/src/main/java/com/alibaba/csp/sentinel/demo/datasource/etcd/EtcdConfigSender.java new file mode 100644 index 0000000000..f57933247f --- /dev/null +++ b/sentinel-demo/sentinel-demo-etcd-datasource/src/main/java/com/alibaba/csp/sentinel/demo/datasource/etcd/EtcdConfigSender.java @@ -0,0 +1,58 @@ +/* + * 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.demo.datasource.etcd; + + +import io.etcd.jetcd.ByteSequence; +import io.etcd.jetcd.Client; + +/** + * Etcd config sender for demo. + * + * @author lianglin + * @since 1.7.0 + */ +public class EtcdConfigSender { + + public static void main(String[] args) throws InterruptedException { + + + String rule_key = "sentinel_demo_rule_key"; + + Client client = Client.builder() + .endpoints("http://127.0.0.1:2379") + .user(ByteSequence.from("root".getBytes())) + .password(ByteSequence.from("12345".getBytes())) + .build(); + final String rule = "[\n" + + " {\n" + + " \"resource\": \"TestResource\",\n" + + " \"controlBehavior\": 0,\n" + + " \"count\": 5.0,\n" + + " \"grade\": 1,\n" + + " \"limitApp\": \"default\",\n" + + " \"strategy\": 0\n" + + " }\n" + + "]"; + client.getKVClient() + .put(ByteSequence.from(rule_key.getBytes()), ByteSequence.from(rule.getBytes())); + + System.out.println("setting rule success"); + Thread.sleep(10000); + + } + +} diff --git a/sentinel-demo/sentinel-demo-etcd-datasource/src/main/java/com/alibaba/csp/sentinel/demo/datasource/etcd/EtcdDataSourceDemo.java b/sentinel-demo/sentinel-demo-etcd-datasource/src/main/java/com/alibaba/csp/sentinel/demo/datasource/etcd/EtcdDataSourceDemo.java new file mode 100644 index 0000000000..00f3e24c7b --- /dev/null +++ b/sentinel-demo/sentinel-demo-etcd-datasource/src/main/java/com/alibaba/csp/sentinel/demo/datasource/etcd/EtcdDataSourceDemo.java @@ -0,0 +1,52 @@ +/* + * 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.demo.datasource.etcd; + +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.datasource.ReadableDataSource; +import com.alibaba.csp.sentinel.datasource.etcd.EtcdConfig; +import com.alibaba.csp.sentinel.datasource.etcd.EtcdDataSource; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.fastjson.JSON; + +import java.util.List; + +/** + * @author lianglin + * @since 1.7.0 + */ +public class EtcdDataSourceDemo { + + public static void main(String[] args) { + + String rule_key = "sentinel_demo_rule_key"; + String yourUserName = "root"; + String yourPassWord = "12345"; + String endPoints = "http://127.0.0.1:2379"; + SentinelConfig.setConfig(EtcdConfig.END_POINTS, endPoints); + SentinelConfig.setConfig(EtcdConfig.USER, yourUserName); + SentinelConfig.setConfig(EtcdConfig.PASSWORD, yourPassWord); + SentinelConfig.setConfig(EtcdConfig.CHARSET, "utf-8"); + SentinelConfig.setConfig(EtcdConfig.AUTH_ENABLE, "true"); + + ReadableDataSource> flowRuleEtcdDataSource = new EtcdDataSource<>(rule_key, (rule) -> JSON.parseArray(rule, FlowRule.class)); + FlowRuleManager.register2Property(flowRuleEtcdDataSource.getProperty()); + List rules = FlowRuleManager.getRules(); + System.out.println(rules); + } + +} diff --git a/sentinel-demo/sentinel-demo-nacos-datasource/pom.xml b/sentinel-demo/sentinel-demo-nacos-datasource/pom.xml index 9fe557c340..46e31dc0bc 100644 --- a/sentinel-demo/sentinel-demo-nacos-datasource/pom.xml +++ b/sentinel-demo/sentinel-demo-nacos-datasource/pom.xml @@ -5,7 +5,7 @@ sentinel-demo com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 diff --git a/sentinel-demo/sentinel-demo-parameter-flow-control/pom.xml b/sentinel-demo/sentinel-demo-parameter-flow-control/pom.xml index 502c96e7bd..d7a902353d 100644 --- a/sentinel-demo/sentinel-demo-parameter-flow-control/pom.xml +++ b/sentinel-demo/sentinel-demo-parameter-flow-control/pom.xml @@ -5,7 +5,7 @@ sentinel-demo com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 diff --git a/sentinel-demo/sentinel-demo-rocketmq/pom.xml b/sentinel-demo/sentinel-demo-rocketmq/pom.xml index 9dda443a82..1dfda85f1b 100755 --- a/sentinel-demo/sentinel-demo-rocketmq/pom.xml +++ b/sentinel-demo/sentinel-demo-rocketmq/pom.xml @@ -5,7 +5,7 @@ com.alibaba.csp sentinel-demo - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 diff --git a/sentinel-demo/sentinel-demo-slot-chain-spi/pom.xml b/sentinel-demo/sentinel-demo-slot-chain-spi/pom.xml index 9dfa4c6686..40a9325aff 100644 --- a/sentinel-demo/sentinel-demo-slot-chain-spi/pom.xml +++ b/sentinel-demo/sentinel-demo-slot-chain-spi/pom.xml @@ -5,7 +5,7 @@ sentinel-demo com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 diff --git a/sentinel-demo/sentinel-demo-spring-cloud-gateway/pom.xml b/sentinel-demo/sentinel-demo-spring-cloud-gateway/pom.xml index 133f98ade4..ec8911b8ab 100644 --- a/sentinel-demo/sentinel-demo-spring-cloud-gateway/pom.xml +++ b/sentinel-demo/sentinel-demo-spring-cloud-gateway/pom.xml @@ -5,7 +5,7 @@ sentinel-demo com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 diff --git a/sentinel-demo/sentinel-demo-spring-cloud-gateway/src/main/java/com/alibaba/csp/sentinel/demo/spring/sc/gateway/GatewayConfiguration.java b/sentinel-demo/sentinel-demo-spring-cloud-gateway/src/main/java/com/alibaba/csp/sentinel/demo/spring/sc/gateway/GatewayConfiguration.java index 5f1cc56a72..293f7e18e1 100644 --- a/sentinel-demo/sentinel-demo-spring-cloud-gateway/src/main/java/com/alibaba/csp/sentinel/demo/spring/sc/gateway/GatewayConfiguration.java +++ b/sentinel-demo/sentinel-demo-spring-cloud-gateway/src/main/java/com/alibaba/csp/sentinel/demo/spring/sc/gateway/GatewayConfiguration.java @@ -83,12 +83,12 @@ private void initCustomizedApis() { .setPredicateItems(new HashSet() {{ add(new ApiPathPredicateItem().setPattern("/ahas")); add(new ApiPathPredicateItem().setPattern("/product/**") - .setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_PREFIX)); + .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX)); }}); ApiDefinition api2 = new ApiDefinition("another_customized_api") .setPredicateItems(new HashSet() {{ add(new ApiPathPredicateItem().setPattern("/**") - .setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_PREFIX)); + .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX)); }}); definitions.add(api1); definitions.add(api2); @@ -127,6 +127,16 @@ private void initGatewayRules() { .setFieldName("pa") ) ); + rules.add(new GatewayFlowRule("httpbin_route") + .setCount(2) + .setIntervalSec(30) + .setParamItem(new GatewayParamFlowItem() + .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM) + .setFieldName("type") + .setPattern("warn") + .setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_CONTAINS) + ) + ); rules.add(new GatewayFlowRule("some_customized_api") .setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME) diff --git a/sentinel-demo/sentinel-demo-spring-webflux/pom.xml b/sentinel-demo/sentinel-demo-spring-webflux/pom.xml index 53beea6783..a3d2e2e77a 100644 --- a/sentinel-demo/sentinel-demo-spring-webflux/pom.xml +++ b/sentinel-demo/sentinel-demo-spring-webflux/pom.xml @@ -5,7 +5,7 @@ sentinel-demo com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 diff --git a/sentinel-demo/sentinel-demo-spring-webmvc/pom.xml b/sentinel-demo/sentinel-demo-spring-webmvc/pom.xml new file mode 100644 index 0000000000..9c4292e79d --- /dev/null +++ b/sentinel-demo/sentinel-demo-spring-webmvc/pom.xml @@ -0,0 +1,44 @@ + + + + sentinel-demo + com.alibaba.csp + 1.7.2-SNAPSHOT + + 4.0.0 + + sentinel-demo-spring-webmvc + + + 2.1.3.RELEASE + + + + + com.alibaba.csp + sentinel-core + + + com.alibaba.csp + sentinel-transport-simple-http + + + com.alibaba.csp + sentinel-spring-webmvc-adapter + ${project.version} + + + org.springframework.boot + spring-boot-starter-web + ${spring.boot.version} + + + org.springframework.boot + spring-boot-starter-test + ${spring.boot.version} + + + + \ No newline at end of file diff --git a/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/WebMvcDemoApplication.java b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/WebMvcDemoApplication.java new file mode 100644 index 0000000000..2d198f9fc5 --- /dev/null +++ b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/WebMvcDemoApplication.java @@ -0,0 +1,33 @@ +/* + * Copyright 1999-2019 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.demo.spring.webmvc; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + *

    Add the JVM parameter to connect to the dashboard:

    + * {@code -Dcsp.sentinel.dashboard.server=127.0.0.1:8080 -Dproject.name=sentinel-demo-spring-webmvc} + * + * @author kaizi2009 + */ +@SpringBootApplication +public class WebMvcDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(WebMvcDemoApplication.class); + } +} diff --git a/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/config/InterceptorConfig.java b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/config/InterceptorConfig.java new file mode 100644 index 0000000000..58509df273 --- /dev/null +++ b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/config/InterceptorConfig.java @@ -0,0 +1,73 @@ +/* + * Copyright 1999-2019 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 + * + * https://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.demo.spring.webmvc.config; + +import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelWebInterceptor; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelWebTotalInterceptor; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.DefaultBlockExceptionHandler; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.config.SentinelWebMvcConfig; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.config.SentinelWebMvcTotalConfig; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * Config sentinel interceptor + * + * @author kaizi2009 + */ +@Configuration +public class InterceptorConfig implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + // Add Sentinel interceptor + addSpringMvcInterceptor(registry); + } + + private void addSpringMvcInterceptor(InterceptorRegistry registry) { + SentinelWebMvcConfig config = new SentinelWebMvcConfig(); + + // Depending on your situation, you can choose to process the BlockException via + // the BlockExceptionHandler or throw it directly, then handle it + // in Spring web global exception handler. + + // config.setBlockExceptionHandler((request, response, e) -> { throw e; }); + + // Use the default handler. + config.setBlockExceptionHandler(new DefaultBlockExceptionHandler()); + + // Custom configuration if necessary + config.setHttpMethodSpecify(true); + config.setOriginParser(request -> request.getHeader("S-user")); + + // Add sentinel interceptor + registry.addInterceptor(new SentinelWebInterceptor(config)).addPathPatterns("/**"); + } + + private void addSpringMvcTotalInterceptor(InterceptorRegistry registry) { + //Config + SentinelWebMvcTotalConfig config = new SentinelWebMvcTotalConfig(); + + //Custom configuration if necessary + config.setRequestAttributeName("my_sentinel_spring_mvc_total_entity_container"); + config.setTotalResourceName("my-spring-mvc-total-url-request"); + + //Add sentinel interceptor + registry.addInterceptor(new SentinelWebTotalInterceptor(config)).addPathPatterns("/**"); + } +} diff --git a/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/config/SentinelSpringMvcBlockHandlerConfig.java b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/config/SentinelSpringMvcBlockHandlerConfig.java new file mode 100644 index 0000000000..decaba9b93 --- /dev/null +++ b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/config/SentinelSpringMvcBlockHandlerConfig.java @@ -0,0 +1,47 @@ +/* + * Copyright 1999-2019 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 + * + * https://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.demo.spring.webmvc.config; + +import com.alibaba.csp.sentinel.demo.spring.webmvc.vo.ResultWrapper; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.Order; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * Spring configuration for global exception handler. + * This will be activated when the {@code BlockExceptionHandler} + * throws {@link BlockException directly}. + * + * @author kaizi2009 + */ +@ControllerAdvice +@Order(0) +public class SentinelSpringMvcBlockHandlerConfig { + + private Logger logger = LoggerFactory.getLogger(this.getClass()); + + @ExceptionHandler(BlockException.class) + @ResponseBody + public ResultWrapper sentinelBlockHandler(BlockException e) { + logger.warn("Blocked by Sentinel: {}", e.getRule()); + // Return the customized result. + return ResultWrapper.blocked(); + } +} diff --git a/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/controller/WebMvcTestController.java b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/controller/WebMvcTestController.java new file mode 100644 index 0000000000..c70342c860 --- /dev/null +++ b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/controller/WebMvcTestController.java @@ -0,0 +1,64 @@ +/* + * Copyright 1999-2019 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.demo.spring.webmvc.controller; + +import java.util.Random; +import java.util.concurrent.TimeUnit; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +/** + * Test controller + * @author kaizi2009 + */ +@RestController +public class WebMvcTestController { + + @GetMapping("/hello") + public String apiHello() { + doBusiness(); + return "Hello!"; + } + + @GetMapping("/err") + public String apiError() { + doBusiness(); + return "Oops..."; + } + + @GetMapping("/foo/{id}") + public String apiFoo(@PathVariable("id") Long id) { + doBusiness(); + return "Hello " + id; + } + + @GetMapping("/exclude/{id}") + public String apiExclude(@PathVariable("id") Long id) { + doBusiness(); + return "Exclude " + id; + } + + private void doBusiness() { + Random random = new Random(1); + try { + TimeUnit.MILLISECONDS.sleep(random.nextInt(100)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + +} diff --git a/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/vo/ResultWrapper.java b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/vo/ResultWrapper.java new file mode 100644 index 0000000000..c4c45abcba --- /dev/null +++ b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/java/com/alibaba/csp/sentinel/demo/spring/webmvc/vo/ResultWrapper.java @@ -0,0 +1,56 @@ +/* + * Copyright 1999-2019 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.demo.spring.webmvc.vo; + +import com.alibaba.fastjson.JSONObject; + +/** + * @author kaizi2009 + */ +public class ResultWrapper { + + private Integer code; + private String message; + + public ResultWrapper(Integer code, String message) { + this.code = code; + this.message = message; + } + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public static ResultWrapper blocked() { + return new ResultWrapper(-1, "Blocked by Sentinel"); + } + + public String toJsonString() { + return JSONObject.toJSONString(this); + } +} diff --git a/sentinel-demo/sentinel-demo-spring-webmvc/src/main/resources/application.properties b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/resources/application.properties new file mode 100644 index 0000000000..397dea55e8 --- /dev/null +++ b/sentinel-demo/sentinel-demo-spring-webmvc/src/main/resources/application.properties @@ -0,0 +1 @@ +server.port=10000 \ No newline at end of file diff --git a/sentinel-demo/sentinel-demo-zookeeper-datasource/pom.xml b/sentinel-demo/sentinel-demo-zookeeper-datasource/pom.xml index 6ad37d8ef4..4474f1a22c 100644 --- a/sentinel-demo/sentinel-demo-zookeeper-datasource/pom.xml +++ b/sentinel-demo/sentinel-demo-zookeeper-datasource/pom.xml @@ -5,14 +5,14 @@ sentinel-demo com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 sentinel-demo-zookeeper-datasource - 3.4.13 + 3.4.14 4.0.1 diff --git a/sentinel-demo/sentinel-demo-zuul-gateway/pom.xml b/sentinel-demo/sentinel-demo-zuul-gateway/pom.xml index 21784ca720..f6572aed2b 100644 --- a/sentinel-demo/sentinel-demo-zuul-gateway/pom.xml +++ b/sentinel-demo/sentinel-demo-zuul-gateway/pom.xml @@ -5,7 +5,7 @@ sentinel-demo com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 diff --git a/sentinel-demo/sentinel-demo-zuul-gateway/src/main/java/com/alibaba/csp/sentinel/demo/zuul/gateway/GatewayRuleConfig.java b/sentinel-demo/sentinel-demo-zuul-gateway/src/main/java/com/alibaba/csp/sentinel/demo/zuul/gateway/GatewayRuleConfig.java index ef2c386964..c4f8684bd7 100644 --- a/sentinel-demo/sentinel-demo-zuul-gateway/src/main/java/com/alibaba/csp/sentinel/demo/zuul/gateway/GatewayRuleConfig.java +++ b/sentinel-demo/sentinel-demo-zuul-gateway/src/main/java/com/alibaba/csp/sentinel/demo/zuul/gateway/GatewayRuleConfig.java @@ -52,12 +52,12 @@ private void initCustomizedApis() { .setPredicateItems(new HashSet() {{ add(new ApiPathPredicateItem().setPattern("/ahas")); add(new ApiPathPredicateItem().setPattern("/aliyun_product/**") - .setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_PREFIX)); + .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX)); }}); ApiDefinition api2 = new ApiDefinition("another_customized_api") .setPredicateItems(new HashSet() {{ add(new ApiPathPredicateItem().setPattern("/**") - .setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_PREFIX)); + .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX)); }}); definitions.add(api1); definitions.add(api2); diff --git a/sentinel-extension/pom.xml b/sentinel-extension/pom.xml index 075323a6d4..72bd6d92f9 100755 --- a/sentinel-extension/pom.xml +++ b/sentinel-extension/pom.xml @@ -6,7 +6,7 @@ com.alibaba.csp sentinel-parent - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT sentinel-extension pom @@ -19,6 +19,9 @@ sentinel-datasource-redis sentinel-annotation-aspectj sentinel-parameter-flow-control + sentinel-datasource-spring-cloud-config + sentinel-datasource-consul + sentinel-datasource-etcd diff --git a/sentinel-extension/sentinel-annotation-aspectj/README.md b/sentinel-extension/sentinel-annotation-aspectj/README.md index 00967d25e6..917291a6e7 100644 --- a/sentinel-extension/sentinel-annotation-aspectj/README.md +++ b/sentinel-extension/sentinel-annotation-aspectj/README.md @@ -1,7 +1,7 @@ # Sentinel Annotation AspectJ This extension is an AOP implementation using AspectJ for Sentinel annotations. -Currently only runtime waving is supported. +Currently only runtime weaving is supported. ## Annotation @@ -66,4 +66,4 @@ public class SentinelAspectConfiguration { } ``` -An example for using Sentinel Annotation AspectJ with Spring Boot can be found [here](https://github.com/alibaba/Sentinel/tree/master/sentinel-demo/sentinel-demo-annotation-spring-aop). \ No newline at end of file +An example for using Sentinel Annotation AspectJ with Spring Boot can be found [here](https://github.com/alibaba/Sentinel/tree/master/sentinel-demo/sentinel-demo-annotation-spring-aop). diff --git a/sentinel-extension/sentinel-annotation-aspectj/pom.xml b/sentinel-extension/sentinel-annotation-aspectj/pom.xml index 287b7a4834..b4ffaa30d1 100644 --- a/sentinel-extension/sentinel-annotation-aspectj/pom.xml +++ b/sentinel-extension/sentinel-annotation-aspectj/pom.xml @@ -5,7 +5,7 @@ sentinel-extension com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 diff --git a/sentinel-extension/sentinel-annotation-aspectj/src/main/java/com/alibaba/csp/sentinel/annotation/aspectj/AbstractSentinelAspectSupport.java b/sentinel-extension/sentinel-annotation-aspectj/src/main/java/com/alibaba/csp/sentinel/annotation/aspectj/AbstractSentinelAspectSupport.java index 13ef92d6c2..1eb9f43da9 100644 --- a/sentinel-extension/sentinel-annotation-aspectj/src/main/java/com/alibaba/csp/sentinel/annotation/aspectj/AbstractSentinelAspectSupport.java +++ b/sentinel-extension/sentinel-annotation-aspectj/src/main/java/com/alibaba/csp/sentinel/annotation/aspectj/AbstractSentinelAspectSupport.java @@ -21,10 +21,10 @@ import com.alibaba.csp.sentinel.slots.block.BlockException; import com.alibaba.csp.sentinel.util.MethodUtil; import com.alibaba.csp.sentinel.util.StringUtil; - import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.reflect.MethodSignature; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; @@ -100,10 +100,16 @@ protected Object handleFallback(ProceedingJoinPoint pjp, String fallback, String args = Arrays.copyOf(originArgs, originArgs.length + 1); args[args.length - 1] = ex; } - if (isStatic(fallbackMethod)) { - return fallbackMethod.invoke(null, args); + + try { + if (isStatic(fallbackMethod)) { + return fallbackMethod.invoke(null, args); + } + return fallbackMethod.invoke(pjp.getTarget(), args); + } catch (InvocationTargetException e) { + // throw the actual exception + throw e.getTargetException(); } - return fallbackMethod.invoke(pjp.getTarget(), args); } // If fallback is absent, we'll try the defaultFallback if provided. return handleDefaultFallback(pjp, defaultFallback, fallbackClass, ex); @@ -116,10 +122,15 @@ protected Object handleDefaultFallback(ProceedingJoinPoint pjp, String defaultFa if (fallbackMethod != null) { // Construct args. Object[] args = fallbackMethod.getParameterTypes().length == 0 ? new Object[0] : new Object[] {ex}; - if (isStatic(fallbackMethod)) { - return fallbackMethod.invoke(null, args); + try { + if (isStatic(fallbackMethod)) { + return fallbackMethod.invoke(null, args); + } + return fallbackMethod.invoke(pjp.getTarget(), args); + } catch (InvocationTargetException e) { + // throw the actual exception + throw e.getTargetException(); } - return fallbackMethod.invoke(pjp.getTarget(), args); } // If no any fallback is present, then directly throw the exception. @@ -137,10 +148,15 @@ protected Object handleBlockException(ProceedingJoinPoint pjp, SentinelResource // Construct args. Object[] args = Arrays.copyOf(originArgs, originArgs.length + 1); args[args.length - 1] = ex; - if (isStatic(blockHandlerMethod)) { - return blockHandlerMethod.invoke(null, args); + try { + if (isStatic(blockHandlerMethod)) { + return blockHandlerMethod.invoke(null, args); + } + return blockHandlerMethod.invoke(pjp.getTarget(), args); + } catch (InvocationTargetException e) { + // throw the actual exception + throw e.getTargetException(); } - return blockHandlerMethod.invoke(pjp.getTarget(), args); } // If no block handler is present, then go to fallback. diff --git a/sentinel-extension/sentinel-annotation-aspectj/src/main/java/com/alibaba/csp/sentinel/annotation/aspectj/SentinelResourceAspect.java b/sentinel-extension/sentinel-annotation-aspectj/src/main/java/com/alibaba/csp/sentinel/annotation/aspectj/SentinelResourceAspect.java index 0f5e9a9703..77c6772fe4 100644 --- a/sentinel-extension/sentinel-annotation-aspectj/src/main/java/com/alibaba/csp/sentinel/annotation/aspectj/SentinelResourceAspect.java +++ b/sentinel-extension/sentinel-annotation-aspectj/src/main/java/com/alibaba/csp/sentinel/annotation/aspectj/SentinelResourceAspect.java @@ -50,9 +50,10 @@ public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwab } String resourceName = getResourceName(annotation.value(), originMethod); EntryType entryType = annotation.entryType(); + int resourceType = annotation.resourceType(); Entry entry = null; try { - entry = SphU.entry(resourceName, entryType, 1, pjp.getArgs()); + entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs()); Object result = pjp.proceed(); return result; } catch (BlockException ex) { @@ -64,7 +65,7 @@ public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwab throw ex; } if (exceptionBelongsTo(ex, annotation.exceptionsToTrace())) { - traceException(ex, annotation); + traceException(ex); return handleFallback(pjp, annotation, ex); } diff --git a/sentinel-extension/sentinel-datasource-apollo/pom.xml b/sentinel-extension/sentinel-datasource-apollo/pom.xml index e802417fa8..0d193b1b94 100644 --- a/sentinel-extension/sentinel-datasource-apollo/pom.xml +++ b/sentinel-extension/sentinel-datasource-apollo/pom.xml @@ -5,14 +5,14 @@ sentinel-extension com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 sentinel-datasource-apollo - 1.3.0 + 1.5.0 diff --git a/sentinel-extension/sentinel-datasource-consul/README.md b/sentinel-extension/sentinel-datasource-consul/README.md new file mode 100644 index 0000000000..8a407cc873 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-consul/README.md @@ -0,0 +1,30 @@ +# Sentinel DataSource Consul + +Sentinel DataSource Consul provides integration with Consul. The data source leverages blocking query (backed by +long polling) of Consul. + +> **NOTE**: This module requires JDK 1.8 or later. + +## Usage + +To use Sentinel DataSource Consul, you could add the following dependency: + +```xml + + com.alibaba.csp + sentinel-datasource-consul + x.y.z + + +``` + +Then you can create a `ConsulDataSource` and register to rule managers. +For instance: + +```java +ReadableDataSource> dataSource = new ConsulDataSource<>(host, port, ruleKey, waitTimeoutInSecond, flowConfigParser); +FlowRuleManager.register2Property(dataSource.getProperty()); +``` + +- `ruleKey`: the rule persistence key +- `waitTimeoutInSecond`: long polling timeout (in second) of the Consul API client diff --git a/sentinel-extension/sentinel-datasource-consul/pom.xml b/sentinel-extension/sentinel-datasource-consul/pom.xml new file mode 100644 index 0000000000..11001712e3 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-consul/pom.xml @@ -0,0 +1,49 @@ + + + + sentinel-extension + com.alibaba.csp + 1.7.2-SNAPSHOT + + 4.0.0 + + sentinel-datasource-consul + jar + + + 1.8 + 1.8 + 1.4.2 + 2.0.0 + + + + + com.alibaba.csp + sentinel-datasource-extension + + + com.ecwid.consul + consul-api + ${consul.version} + + + com.pszymczyk.consul + embedded-consul + ${consul.process.version} + test + + + junit + junit + test + + + com.alibaba + fastjson + test + + + \ No newline at end of file diff --git a/sentinel-extension/sentinel-datasource-consul/src/main/java/com/alibaba/csp/sentinel/datasource/consul/ConsulDataSource.java b/sentinel-extension/sentinel-datasource-consul/src/main/java/com/alibaba/csp/sentinel/datasource/consul/ConsulDataSource.java new file mode 100644 index 0000000000..5b193a85c9 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-consul/src/main/java/com/alibaba/csp/sentinel/datasource/consul/ConsulDataSource.java @@ -0,0 +1,203 @@ +/* + * 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.consul; + +import com.alibaba.csp.sentinel.concurrent.NamedThreadFactory; +import com.alibaba.csp.sentinel.datasource.AbstractDataSource; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.util.AssertUtil; + +import com.ecwid.consul.v1.ConsulClient; +import com.ecwid.consul.v1.QueryParams; +import com.ecwid.consul.v1.Response; +import com.ecwid.consul.v1.kv.model.GetValue; + +import java.util.concurrent.*; + +/** + *

    + * A read-only {@code DataSource} with Consul backend. + *

    + *

    + * The data source first initial rules from a Consul during initialization. + * Then it start a watcher to observe the updates of rule date and update to memory. + * + * Consul do not provide http api to watch the update of KV,so it use a long polling and + * blocking queries of the Consul's feature + * to watch and update value easily.When Querying data by index will blocking until change or timeout. If + * the index of the current query is larger than before, it means that the data has changed. + *

    + * + * @author wavesZh + */ +public class ConsulDataSource extends AbstractDataSource { + + private static final int DEFAULT_PORT = 8500; + + private final String address; + private final String ruleKey; + /** + * Request of query will hang until timeout (in second) or get updated value. + */ + private final int watchTimeout; + + /** + * Record the data's index in Consul to watch the change. + * If lastIndex is smaller than the index of next query, it means that rule data has updated. + */ + private volatile long lastIndex; + + private final ConsulClient client; + + private final ConsulKVWatcher watcher = new ConsulKVWatcher(); + + @SuppressWarnings("PMD.ThreadPoolCreationRule") + private final ExecutorService watcherService = Executors.newSingleThreadExecutor( + new NamedThreadFactory("sentinel-consul-ds-watcher", true)); + + public ConsulDataSource(String host, String ruleKey, int watchTimeoutInSecond, Converter parser) { + this(host, DEFAULT_PORT, ruleKey, watchTimeoutInSecond, parser); + } + + /** + * Constructor of {@code ConsulDataSource}. + * + * @param parser customized data parser, cannot be empty + * @param host consul agent host + * @param port consul agent port + * @param ruleKey data key in Consul + * @param watchTimeout request for querying data will be blocked until new data or timeout. The unit is second (s) + */ + public ConsulDataSource(String host, int port, String ruleKey, int watchTimeout, Converter parser) { + super(parser); + AssertUtil.notNull(host, "Consul host can not be null"); + AssertUtil.notEmpty(ruleKey, "Consul ruleKey can not be empty"); + AssertUtil.isTrue(watchTimeout >= 0, "watchTimeout should not be negative"); + this.client = new ConsulClient(host, port); + this.address = host + ":" + port; + this.ruleKey = ruleKey; + this.watchTimeout = watchTimeout; + loadInitialConfig(); + startKVWatcher(); + } + + private void startKVWatcher() { + watcherService.submit(watcher); + } + + private void loadInitialConfig() { + try { + T newValue = loadConfig(); + if (newValue == null) { + RecordLog.warn( + "[ConsulDataSource] WARN: initial config is null, you may have to check your data source"); + } + getProperty().updateValue(newValue); + } catch (Exception ex) { + RecordLog.warn("[ConsulDataSource] Error when loading initial config", ex); + } + } + + @Override + public String readSource() throws Exception { + if (this.client == null) { + throw new IllegalStateException("Consul has not been initialized or error occurred"); + } + Response response = getValueImmediately(ruleKey); + if (response != null) { + GetValue value = response.getValue(); + lastIndex = response.getConsulIndex(); + return value != null ? value.getDecodedValue() : null; + } + return null; + } + + @Override + public void close() throws Exception { + watcher.stop(); + watcherService.shutdown(); + } + + private class ConsulKVWatcher implements Runnable { + private volatile boolean running = true; + + @Override + public void run() { + while (running) { + // It will be blocked until watchTimeout(s) if rule data has no update. + Response response = getValue(ruleKey, lastIndex, watchTimeout); + if (response == null) { + try { + TimeUnit.MILLISECONDS.sleep(watchTimeout * 1000); + } catch (InterruptedException e) { + } + continue; + } + GetValue getValue = response.getValue(); + Long currentIndex = response.getConsulIndex(); + if (currentIndex == null || currentIndex <= lastIndex) { + continue; + } + lastIndex = currentIndex; + if (getValue != null) { + String newValue = getValue.getDecodedValue(); + try { + getProperty().updateValue(parser.convert(newValue)); + RecordLog.info("[ConsulDataSource] New property value received for ({0}, {1}): {2}", + address, ruleKey, newValue); + } catch (Exception ex) { + // In case of parsing error. + RecordLog.warn("[ConsulDataSource] Failed to update value for ({0}, {1}), raw value: {2}", + address, ruleKey, newValue); + } + } + } + } + + private void stop() { + running = false; + } + } + + /** + * Get data from Consul immediately. + * + * @param key data key in Consul + * @return the value associated to the key, or null if error occurs + */ + private Response getValueImmediately(String key) { + return getValue(key, -1, -1); + } + + /** + * Get data from Consul (blocking). + * + * @param key data key in Consul + * @param index the index of data in Consul. + * @param waitTime time(second) for waiting get updated value. + * @return the value associated to the key, or null if error occurs + */ + private Response getValue(String key, long index, long waitTime) { + try { + return client.getKVValue(key, new QueryParams(waitTime, index)); + } catch (Throwable t) { + RecordLog.warn("[ConsulDataSource] Failed to get value for key: " + key, t); + } + return null; + } + +} diff --git a/sentinel-extension/sentinel-datasource-consul/src/test/java/com/alibaba/csp/sentinel/datasource/consul/ConsulDataSourceTest.java b/sentinel-extension/sentinel-datasource-consul/src/test/java/com/alibaba/csp/sentinel/datasource/consul/ConsulDataSourceTest.java new file mode 100644 index 0000000000..f5b531c668 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-consul/src/test/java/com/alibaba/csp/sentinel/datasource/consul/ConsulDataSourceTest.java @@ -0,0 +1,109 @@ +/* + * 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.consul; + +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.datasource.ReadableDataSource; +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 com.ecwid.consul.v1.ConsulClient; +import com.ecwid.consul.v1.Response; +import com.pszymczyk.consul.ConsulProcess; +import com.pszymczyk.consul.ConsulStarterBuilder; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +/** + * @author wavesZh + */ +public class ConsulDataSourceTest { + + private final String ruleKey = "sentinel.rules.flow.ruleKey"; + private final int waitTimeoutInSecond = 1; + + private ConsulProcess consul; + private ConsulClient client; + + private ReadableDataSource> consulDataSource; + + private List rules; + + @Before + public void init() { + this.consul = ConsulStarterBuilder.consulStarter() + .build() + .start(); + int port = consul.getHttpPort(); + String host = "127.0.0.1"; + client = new ConsulClient(host, port); + Converter> flowConfigParser = buildFlowConfigParser(); + String flowRulesJson = + "[{\"resource\":\"test\", \"limitApp\":\"default\", \"grade\":1, \"count\":\"0.0\", \"strategy\":0, " + + "\"refResource\":null, " + + + "\"controlBehavior\":0, \"warmUpPeriodSec\":10, \"maxQueueingTimeMs\":500, \"controller\":null}]"; + initConsulRuleData(flowRulesJson); + rules = flowConfigParser.convert(flowRulesJson); + consulDataSource = new ConsulDataSource<>(host, port, ruleKey, waitTimeoutInSecond, flowConfigParser); + FlowRuleManager.register2Property(consulDataSource.getProperty()); + } + + @After + public void clean() throws Exception { + if (consulDataSource != null) { + consulDataSource.close(); + } + if (consul != null) { + consul.close(); + } + FlowRuleManager.loadRules(new ArrayList<>()); + } + + @Test + public void testConsulDataSourceWhenInit() { + List rules = FlowRuleManager.getRules(); + Assert.assertEquals(this.rules, rules); + } + + @Test + public void testConsulDataSourceWhenUpdate() throws InterruptedException { + rules.get(0).setMaxQueueingTimeMs(new Random().nextInt()); + client.setKVValue(ruleKey, JSON.toJSONString(rules)); + TimeUnit.SECONDS.sleep(waitTimeoutInSecond); + List rules = FlowRuleManager.getRules(); + Assert.assertEquals(this.rules, rules); + } + + private Converter> buildFlowConfigParser() { + return source -> JSON.parseObject(source, new TypeReference>() {}); + } + + private void initConsulRuleData(String flowRulesJson) { + Response response = client.setKVValue(ruleKey, flowRulesJson); + Assert.assertEquals(Boolean.TRUE, response.getValue()); + } + +} diff --git a/sentinel-extension/sentinel-datasource-etcd/README.md b/sentinel-extension/sentinel-datasource-etcd/README.md new file mode 100644 index 0000000000..23d6129d4b --- /dev/null +++ b/sentinel-extension/sentinel-datasource-etcd/README.md @@ -0,0 +1,39 @@ +# Sentinel DataSource Etcd + +Sentinel DataSource Etcd provides integration with etcd so that etcd +can be the dynamic rule data source of Sentinel. The data source uses push model (watcher). + +> **NOTE**: This module requires JDK 1.8 or later. + +To use Sentinel DataSource Etcd, you should add the following dependency: + +```xml + + com.alibaba.csp + sentinel-datasource-etcd + x.y.z + +``` + +We could configure Etcd connection configuration by config file (for example `sentinel.properties`): + +``` +csp.sentinel.etcd.endpoints=http://ip1:port1,http://ip2:port2 +csp.sentinel.etcd.user=your_user +csp.sentinel.etcd.password=your_password +csp.sentinel.etcd.charset=your_charset +csp.sentinel.etcd.auth.enable=true # if ture, then open user/password or ssl check +csp.sentinel.etcd.authority=authority # ssl +``` + +Or we could configure via JVM -D args or via `SentinelConfig.setConfig(key, value)`. + +Then we can create an `EtcdDataSource` and register to rule managers. For instance: + +```java +// `rule_key` is the rule config key +ReadableDataSource> flowRuleEtcdDataSource = new EtcdDataSource<>(rule_key, (rule) -> JSON.parseArray(rule, FlowRule.class)); +FlowRuleManager.register2Property(flowRuleEtcdDataSource.getProperty()); +``` + +We've also provided an example: [sentinel-demo-etcd-datasource](https://github.com/alibaba/Sentinel/tree/master/sentinel-demo/sentinel-demo-etcd-datasource) \ No newline at end of file diff --git a/sentinel-extension/sentinel-datasource-etcd/pom.xml b/sentinel-extension/sentinel-datasource-etcd/pom.xml new file mode 100644 index 0000000000..dbf4717a07 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-etcd/pom.xml @@ -0,0 +1,63 @@ + + + + sentinel-extension + com.alibaba.csp + 1.7.2-SNAPSHOT + + 4.0.0 + + sentinel-datasource-etcd + jar + + + 1.8 + 1.8 + 0.3.0 + + + + + com.alibaba.csp + sentinel-datasource-extension + + + + io.etcd + jetcd-core + ${jetcd.version} + + + + + junit + junit + test + + + + com.alibaba + fastjson + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + + + + + + + + \ No newline at end of file diff --git a/sentinel-extension/sentinel-datasource-etcd/src/main/java/com/alibaba/csp/sentinel/datasource/etcd/EtcdConfig.java b/sentinel-extension/sentinel-datasource-etcd/src/main/java/com/alibaba/csp/sentinel/datasource/etcd/EtcdConfig.java new file mode 100644 index 0000000000..8977dfb041 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-etcd/src/main/java/com/alibaba/csp/sentinel/datasource/etcd/EtcdConfig.java @@ -0,0 +1,68 @@ +/* + * 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.etcd; + +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * Etcd connection configuration. + * + * @author lianglin + * @since 1.7.0 + */ +public final class EtcdConfig { + + public final static String END_POINTS = "csp.sentinel.etcd.endpoints"; + public final static String USER = "csp.sentinel.etcd.user"; + public final static String PASSWORD = "csp.sentinel.etcd.password"; + public final static String CHARSET = "csp.sentinel.etcd.charset"; + public final static String AUTH_ENABLE = "csp.sentinel.etcd.auth.enable"; + public final static String AUTHORITY = "csp.sentinel.etcd.authority"; + + private final static String ENABLED = "true"; + + public static String getEndPoints() { + return SentinelConfig.getConfig(END_POINTS); + } + + public static String getUser() { + return SentinelConfig.getConfig(USER); + } + + public static String getPassword() { + return SentinelConfig.getConfig(PASSWORD); + } + + public static String getCharset() { + String etcdCharset = SentinelConfig.getConfig(CHARSET); + if (StringUtil.isNotBlank(etcdCharset)) { + return etcdCharset; + } + return SentinelConfig.charset(); + } + + public static boolean isAuthEnable() { + return ENABLED.equalsIgnoreCase(SentinelConfig.getConfig(AUTH_ENABLE)); + } + + public static String getAuthority() { + return SentinelConfig.getConfig(AUTHORITY); + } + + private EtcdConfig() {} + +} diff --git a/sentinel-extension/sentinel-datasource-etcd/src/main/java/com/alibaba/csp/sentinel/datasource/etcd/EtcdDataSource.java b/sentinel-extension/sentinel-datasource-etcd/src/main/java/com/alibaba/csp/sentinel/datasource/etcd/EtcdDataSource.java new file mode 100644 index 0000000000..48e503ef97 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-etcd/src/main/java/com/alibaba/csp/sentinel/datasource/etcd/EtcdDataSource.java @@ -0,0 +1,124 @@ +/* + * 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.etcd; + +import com.alibaba.csp.sentinel.datasource.AbstractDataSource; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.log.RecordLog; + +import io.etcd.jetcd.ByteSequence; +import io.etcd.jetcd.Client; +import io.etcd.jetcd.KeyValue; +import io.etcd.jetcd.Watch; +import io.etcd.jetcd.kv.GetResponse; +import io.etcd.jetcd.watch.WatchEvent; + +import java.nio.charset.Charset; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * A read-only {@code DataSource} with Etcd backend. When the data in Etcd backend has been modified, + * Etcd will automatically push the new value so that the dynamic configuration can be real-time. + * + * @author lianglin + * @since 1.7.0 + */ +public class EtcdDataSource extends AbstractDataSource { + + private final Client client; + private Watch.Watcher watcher; + + private final String key; + private Charset charset = Charset.forName(EtcdConfig.getCharset()); + + /** + * Create an etcd data-source. The connection configuration will be retrieved from {@link EtcdConfig}. + * + * @param key config key + * @param parser data parser + */ + public EtcdDataSource(String key, Converter parser) { + super(parser); + if (!EtcdConfig.isAuthEnable()) { + this.client = Client.builder() + .endpoints(EtcdConfig.getEndPoints().split(",")).build(); + } else { + this.client = Client.builder() + .endpoints(EtcdConfig.getEndPoints().split(",")) + .user(ByteSequence.from(EtcdConfig.getUser(), charset)) + .password(ByteSequence.from(EtcdConfig.getPassword(), charset)) + .authority(EtcdConfig.getAuthority()) + .build(); + } + this.key = key; + loadInitialConfig(); + initWatcher(); + } + + private void loadInitialConfig() { + try { + T newValue = loadConfig(); + if (newValue == null) { + RecordLog.warn( + "[EtcdDataSource] Initial configuration is null, you may have to check your data source"); + } + getProperty().updateValue(newValue); + } catch (Exception ex) { + RecordLog.warn("[EtcdDataSource] Error when loading initial configuration", ex); + } + } + + private void initWatcher() { + watcher = client.getWatchClient().watch(ByteSequence.from(key, charset), (watchResponse) -> { + for (WatchEvent watchEvent : watchResponse.getEvents()) { + WatchEvent.EventType eventType = watchEvent.getEventType(); + if (eventType == WatchEvent.EventType.PUT) { + try { + T newValue = loadConfig(); + getProperty().updateValue(newValue); + } catch (Exception e) { + RecordLog.warn("[EtcdDataSource] Failed to update config", e); + } + } else if (eventType == WatchEvent.EventType.DELETE) { + RecordLog.info("[EtcdDataSource] Cleaning config for key <{0}>", key); + getProperty().updateValue(null); + } + } + }); + } + + @Override + public String readSource() throws Exception { + CompletableFuture responseFuture = client.getKVClient().get(ByteSequence.from(key, charset)); + List kvs = responseFuture.get().getKvs(); + return kvs.size() == 0 ? null : kvs.get(0).getValue().toString(charset); + } + + @Override + public void close() { + if (watcher != null) { + try { + watcher.close(); + } catch (Exception ex) { + RecordLog.info("[EtcdDataSource] Failed to close watcher", ex); + } + } + if (client != null) { + client.close(); + } + } +} diff --git a/sentinel-extension/sentinel-datasource-etcd/src/test/java/com/alibaba/csp/sentinel/datasource/etcd/EtcdDataSourceTest.java b/sentinel-extension/sentinel-datasource-etcd/src/test/java/com/alibaba/csp/sentinel/datasource/etcd/EtcdDataSourceTest.java new file mode 100644 index 0000000000..73eda249eb --- /dev/null +++ b/sentinel-extension/sentinel-datasource-etcd/src/test/java/com/alibaba/csp/sentinel/datasource/etcd/EtcdDataSourceTest.java @@ -0,0 +1,123 @@ +/* + * 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.etcd; + +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.datasource.ReadableDataSource; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.fastjson.JSON; +import io.etcd.jetcd.ByteSequence; +import io.etcd.jetcd.Client; +import io.etcd.jetcd.KV; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + + +/** + * @author lianglin + * @since 1.7.0 + */ +@Ignore(value = "Before run this test, you need to set up your etcd server.") +public class EtcdDataSourceTest { + + + private final String endPoints = "http://127.0.0.1:2379"; + + + @Before + public void setUp() { + SentinelConfig.setConfig(EtcdConfig.END_POINTS, endPoints); + FlowRuleManager.loadRules(new ArrayList<>()); + } + + @After + public void tearDown() { + SentinelConfig.setConfig(EtcdConfig.END_POINTS, ""); + FlowRuleManager.loadRules(new ArrayList<>()); + } + + @Test + public void testReadSource() throws Exception { + EtcdDataSource dataSource = new EtcdDataSource("foo", value -> value); + KV kvClient = Client.builder() + .endpoints(endPoints) + .build().getKVClient(); + + kvClient.put(ByteSequence.from("foo".getBytes()), ByteSequence.from("test".getBytes())); + Assert.assertNotNull(dataSource.readSource().equals("test")); + + kvClient.put(ByteSequence.from("foo".getBytes()), ByteSequence.from("test2".getBytes())); + Assert.assertNotNull(dataSource.getProperty().equals("test2")); + } + + @Test + public void testDynamicUpdate() throws InterruptedException { + String demo_key = "etcd_demo_key"; + ReadableDataSource> flowRuleEtcdDataSource = new EtcdDataSource<>(demo_key, (value) -> JSON.parseArray(value, FlowRule.class)); + FlowRuleManager.register2Property(flowRuleEtcdDataSource.getProperty()); + + KV kvClient = Client.builder() + .endpoints(endPoints) + .build().getKVClient(); + + final String rule1 = "[\n" + + " {\n" + + " \"resource\": \"TestResource\",\n" + + " \"controlBehavior\": 0,\n" + + " \"count\": 5.0,\n" + + " \"grade\": 1,\n" + + " \"limitApp\": \"default\",\n" + + " \"strategy\": 0\n" + + " }\n" + + "]"; + + kvClient.put(ByteSequence.from(demo_key.getBytes()), ByteSequence.from(rule1.getBytes())); + Thread.sleep(1000); + + FlowRule flowRule = FlowRuleManager.getRules().get(0); + Assert.assertTrue(flowRule.getResource().equals("TestResource")); + Assert.assertTrue(flowRule.getCount() == 5.0); + Assert.assertTrue(flowRule.getGrade() == 1); + + final String rule2 = "[\n" + + " {\n" + + " \"resource\": \"TestResource\",\n" + + " \"controlBehavior\": 0,\n" + + " \"count\": 6.0,\n" + + " \"grade\": 3,\n" + + " \"limitApp\": \"default\",\n" + + " \"strategy\": 0\n" + + " }\n" + + "]"; + + kvClient.put(ByteSequence.from(demo_key.getBytes()), ByteSequence.from(rule2.getBytes())); + Thread.sleep(1000); + + flowRule = FlowRuleManager.getRules().get(0); + Assert.assertTrue(flowRule.getResource().equals("TestResource")); + Assert.assertTrue(flowRule.getCount() == 6.0); + Assert.assertTrue(flowRule.getGrade() == 3); + + + } +} diff --git a/sentinel-extension/sentinel-datasource-extension/pom.xml b/sentinel-extension/sentinel-datasource-extension/pom.xml index 45e6d77f33..e3a0eecc66 100755 --- a/sentinel-extension/sentinel-datasource-extension/pom.xml +++ b/sentinel-extension/sentinel-datasource-extension/pom.xml @@ -6,7 +6,7 @@ com.alibaba.csp sentinel-extension - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT sentinel-datasource-extension diff --git a/sentinel-extension/sentinel-datasource-nacos/pom.xml b/sentinel-extension/sentinel-datasource-nacos/pom.xml index 4f80e984dd..292ec17e19 100644 --- a/sentinel-extension/sentinel-datasource-nacos/pom.xml +++ b/sentinel-extension/sentinel-datasource-nacos/pom.xml @@ -5,7 +5,7 @@ sentinel-extension com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 @@ -13,7 +13,7 @@ jar - 1.0.0 + 1.1.4 diff --git a/sentinel-extension/sentinel-datasource-redis/pom.xml b/sentinel-extension/sentinel-datasource-redis/pom.xml index a07831a3f0..e1c909d21a 100644 --- a/sentinel-extension/sentinel-datasource-redis/pom.xml +++ b/sentinel-extension/sentinel-datasource-redis/pom.xml @@ -5,7 +5,7 @@ sentinel-extension com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 diff --git a/sentinel-extension/sentinel-datasource-spring-cloud-config/README.md b/sentinel-extension/sentinel-datasource-spring-cloud-config/README.md new file mode 100644 index 0000000000..aa53afb884 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-spring-cloud-config/README.md @@ -0,0 +1,41 @@ +# Sentinel DataSource Spring Cloud Config + +Sentinel DataSource Spring Cloud Config provides integration with Spring Cloud Config +so that Spring Cloud Config can be the dynamic rule data source of Sentinel. + +To use Sentinel DataSource Spring Cloud Config, you should add the following dependency: + +```xml + + com.alibaba.csp + sentinel-datasource-spring-cloud-config + x.y.z + +``` + +Then you can create an `SpringCloudConfigDataSource` and register to rule managers. +For instance: + +```Java +ReadableDataSource> flowRuleDs = new SpringCloudConfigDataSource<>(ruleKey, s -> JSON.parseArray(s, FlowRule.class)); +FlowRuleManager.register2Property(flowRuleDs.getProperty()); +``` + +To notify the client that the remote config has changed, we could bind a git webhook callback with the +`com.alibaba.csp.sentinel.datasource.spring.cloud.config.SentinelRuleLocator.refresh` API. +We may refer to the the sample `com.alibaba.csp.sentinel.datasource.spring.cloud.config.test.SpringCouldDataSourceTest#refresh` in test cases. + +We offer test cases and demo in the package: `com.alibaba.csp.sentinel.datasource.spring.cloud.config.test`. +When you are running test cases, please follow the steps: + +``` +// First, start the Spring Cloud config server +com.alibaba.csp.sentinel.datasource.spring.cloud.config.server.ConfigServer + +// Second, start the Spring Cloud config client +com.alibaba.csp.sentinel.datasource.spring.cloud.config.client.ConfigClient + +// Third, run the test cases and demo +com.alibaba.csp.sentinel.datasource.spring.cloud.config.test.SentinelRuleLocatorTests +com.alibaba.csp.sentinel.datasource.spring.cloud.config.test.SpringCouldDataSourceTest +``` \ No newline at end of file diff --git a/sentinel-extension/sentinel-datasource-spring-cloud-config/pom.xml b/sentinel-extension/sentinel-datasource-spring-cloud-config/pom.xml new file mode 100644 index 0000000000..387c0bf918 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-spring-cloud-config/pom.xml @@ -0,0 +1,88 @@ + + + + sentinel-extension + com.alibaba.csp + 1.7.2-SNAPSHOT + + 4.0.0 + + sentinel-datasource-spring-cloud-config + jar + + + 2.0.0.RELEASE + + + + + + com.alibaba.csp + sentinel-datasource-extension + + + + org.springframework.cloud + spring-cloud-starter-config + ${spring.cloud.version} + + + + org.springframework.retry + spring-retry + 1.2.4.RELEASE + + + org.springframework + spring-core + + + + + + org.springframework.boot + spring-boot-starter-test + ${spring.cloud.version} + test + + + + org.springframework.cloud + spring-cloud-config-server + ${spring.cloud.version} + test + + + + org.springframework.boot + spring-boot-starter-web + ${spring.cloud.version} + test + + + + org.springframework + spring-expression + 5.1.8.RELEASE + test + + + + com.alibaba + fastjson + test + + + + junit + junit + test + + + + + + + \ No newline at end of file diff --git a/sentinel-extension/sentinel-datasource-spring-cloud-config/src/main/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/SentinelRuleLocator.java b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/main/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/SentinelRuleLocator.java new file mode 100644 index 0000000000..ca33dc5d8d --- /dev/null +++ b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/main/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/SentinelRuleLocator.java @@ -0,0 +1,294 @@ +package com.alibaba.csp.sentinel.datasource.spring.cloud.config; + +import com.alibaba.csp.sentinel.log.RecordLog; +import org.springframework.cloud.bootstrap.config.PropertySourceLocator; +import org.springframework.cloud.config.client.ConfigClientProperties; +import org.springframework.cloud.config.client.ConfigClientStateHolder; +import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.config.environment.PropertySource; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.MapPropertySource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.retry.annotation.Retryable; +import org.springframework.util.Base64Utils; +import org.springframework.util.StringUtils; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.springframework.cloud.config.client.ConfigClientProperties.*; + +/** + *

    + * {@link SentinelRuleLocator} which pull sentinel rules from remote server. + * It retrieve configurations of spring-cloud-config client configurations from {@link org.springframework.core.env.Environment} + * Such as spring.cloud.config.uri=uri, spring.cloud.config.profile=profile .... and so on. + * When pull rules successfully, save to {@link SentinelRuleStorage} for ${@link SpringCloudConfigDataSource} retrieve. + *

    + * + * @author lianglin + * @since 1.7.0 + */ +@Order(0) +public class SentinelRuleLocator implements PropertySourceLocator { + + + private RestTemplate restTemplate; + private ConfigClientProperties defaultProperties; + private org.springframework.core.env.Environment environment; + + public SentinelRuleLocator(ConfigClientProperties defaultProperties, org.springframework.core.env.Environment environment) { + this.defaultProperties = defaultProperties; + this.environment = environment; + } + + + /** + * Responsible for pull data from remote server + * + * @param environment + * @return correct data if success else a empty propertySource or null + */ + @Override + @Retryable(interceptor = "configServerRetryInterceptor") + public org.springframework.core.env.PropertySource locate( + org.springframework.core.env.Environment environment) { + ConfigClientProperties properties = this.defaultProperties.override(environment); + CompositePropertySource composite = new CompositePropertySource("configService"); + RestTemplate restTemplate = this.restTemplate == null + ? getSecureRestTemplate(properties) + : this.restTemplate; + Exception error = null; + String errorBody = null; + try { + String[] labels = new String[]{""}; + if (StringUtils.hasText(properties.getLabel())) { + labels = StringUtils + .commaDelimitedListToStringArray(properties.getLabel()); + } + String state = ConfigClientStateHolder.getState(); + // Try all the labels until one works + for (String label : labels) { + Environment result = getRemoteEnvironment(restTemplate, properties, + label.trim(), state); + if (result != null) { + log(result); + // result.getPropertySources() can be null if using xml + if (result.getPropertySources() != null) { + for (PropertySource source : result.getPropertySources()) { + @SuppressWarnings("unchecked") + Map map = (Map) source + .getSource(); + composite.addPropertySource( + new MapPropertySource(source.getName(), map)); + } + } + SentinelRuleStorage.setRulesSource(composite); + return composite; + } + } + } catch (HttpServerErrorException e) { + error = e; + if (MediaType.APPLICATION_JSON + .includes(e.getResponseHeaders().getContentType())) { + errorBody = e.getResponseBodyAsString(); + } + } catch (Exception e) { + error = e; + } + if (properties.isFailFast()) { + throw new IllegalStateException( + "Could not locate PropertySource and the fail fast property is set, failing", + error); + } + RecordLog.warn("Could not locate PropertySource: " + (errorBody == null + ? error == null ? "label not found" : error.getMessage() + : errorBody)); + return null; + + } + + public org.springframework.core.env.PropertySource refresh() { + return locate(environment); + } + + private void log(Environment result) { + + RecordLog.info(String.format( + "Located environment: name=%s, profiles=%s, label=%s, version=%s, state=%s", + result.getName(), + result.getProfiles() == null ? "" + : Arrays.asList(result.getProfiles()), + result.getLabel(), result.getVersion(), result.getState())); + + List propertySourceList = result.getPropertySources(); + if (propertySourceList != null) { + int propertyCount = 0; + for (PropertySource propertySource : propertySourceList) { + propertyCount += propertySource.getSource().size(); + } + RecordLog.info(String.format( + "Environment %s has %d property sources with %d properties.", + result.getName(), result.getPropertySources().size(), + propertyCount)); + } + + + } + + + private Environment getRemoteEnvironment(RestTemplate restTemplate, + ConfigClientProperties properties, String label, String state) { + String path = "/{name}/{profile}"; + String name = properties.getName(); + String profile = properties.getProfile(); + String token = properties.getToken(); + int noOfUrls = properties.getUri().length; + if (noOfUrls > 1) { + RecordLog.info("Multiple Config Server Urls found listed."); + } + + RecordLog.info("properties = {0},label={1}, state={2}", properties, label, state); + + Object[] args = new String[]{name, profile}; + if (StringUtils.hasText(label)) { + if (label.contains("/")) { + label = label.replace("/", "(_)"); + } + args = new String[]{name, profile, label}; + path = path + "/{label}"; + } + ResponseEntity response = null; + + for (int i = 0; i < noOfUrls; i++) { + Credentials credentials = properties.getCredentials(i); + String uri = credentials.getUri(); + String username = credentials.getUsername(); + String password = credentials.getPassword(); + + RecordLog.info("Fetching config from server at : " + uri); + + try { + HttpHeaders headers = new HttpHeaders(); + addAuthorizationToken(properties, headers, username, password); + if (StringUtils.hasText(token)) { + headers.add(TOKEN_HEADER, token); + } + if (StringUtils.hasText(state) && properties.isSendState()) { + headers.add(STATE_HEADER, state); + } + + final HttpEntity entity = new HttpEntity<>((Void) null, headers); + response = restTemplate.exchange(uri + path, HttpMethod.GET, entity, + Environment.class, args); + } catch (HttpClientErrorException e) { + if (e.getStatusCode() != HttpStatus.NOT_FOUND) { + throw e; + } + } catch (ResourceAccessException e) { + RecordLog.info("Connect Timeout Exception on Url - " + uri + + ". Will be trying the next url if available"); + if (i == noOfUrls - 1) { + throw e; + } else { + continue; + } + } + + if (response == null || response.getStatusCode() != HttpStatus.OK) { + return null; + } + + Environment result = response.getBody(); + return result; + } + + return null; + } + + + private RestTemplate getSecureRestTemplate(ConfigClientProperties client) { + SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + if (client.getRequestReadTimeout() < 0) { + throw new IllegalStateException("Invalid Value for Read Timeout set."); + } + requestFactory.setReadTimeout(client.getRequestReadTimeout()); + RestTemplate template = new RestTemplate(requestFactory); + Map headers = new HashMap<>(client.getHeaders()); + if (headers.containsKey(AUTHORIZATION)) { + // To avoid redundant addition of header + headers.remove(AUTHORIZATION); + } + if (!headers.isEmpty()) { + template.setInterceptors(Arrays.asList( + new GenericRequestHeaderInterceptor(headers))); + } + + return template; + } + + private void addAuthorizationToken(ConfigClientProperties configClientProperties, + HttpHeaders httpHeaders, String username, String password) { + String authorization = configClientProperties.getHeaders().get(AUTHORIZATION); + + if (password != null && authorization != null) { + throw new IllegalStateException( + "You must set either 'password' or 'authorization'"); + } + + if (password != null) { + byte[] token = Base64Utils.encode((username + ":" + password).getBytes()); + httpHeaders.add("Authorization", "Basic " + new String(token)); + } else if (authorization != null) { + httpHeaders.add("Authorization", authorization); + } + + } + + public void setRestTemplate(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + + public static class GenericRequestHeaderInterceptor + implements ClientHttpRequestInterceptor { + + private final Map headers; + + public GenericRequestHeaderInterceptor(Map headers) { + this.headers = headers; + } + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, + ClientHttpRequestExecution execution) throws IOException { + for (Map.Entry header : headers.entrySet()) { + request.getHeaders().add(header.getKey(), header.getValue()); + } + return execution.execute(request, body); + } + + protected Map getHeaders() { + return headers; + } + + } +} diff --git a/sentinel-extension/sentinel-datasource-spring-cloud-config/src/main/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/SentinelRuleStorage.java b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/main/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/SentinelRuleStorage.java new file mode 100644 index 0000000000..9e8884cf83 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/main/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/SentinelRuleStorage.java @@ -0,0 +1,44 @@ +/* + * 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.spring.cloud.config; + +import org.springframework.core.env.PropertySource; + +/** + * Storage data pull from spring-config-cloud server + * And notice ${@link SpringCloudConfigDataSource} update latest values + * + * @author lianglin + * @since 1.7.0 + */ +public class SentinelRuleStorage { + + public static PropertySource rulesSource; + + public static void setRulesSource(PropertySource source) { + rulesSource = source; + noticeSpringCloudDataSource(); + } + + public static String retrieveRule(String ruleKey) { + return rulesSource == null ? null : (String) rulesSource.getProperty(ruleKey); + } + + private static void noticeSpringCloudDataSource(){ + SpringCloudConfigDataSource.updateValues(); + } + +} diff --git a/sentinel-extension/sentinel-datasource-spring-cloud-config/src/main/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/SpringCloudConfigDataSource.java b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/main/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/SpringCloudConfigDataSource.java new file mode 100644 index 0000000000..093dcf88d5 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/main/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/SpringCloudConfigDataSource.java @@ -0,0 +1,111 @@ +/* + * 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.spring.cloud.config; + +import com.alibaba.csp.sentinel.datasource.AbstractDataSource; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.util.StringUtil; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + *

    A read-only {@code DataSource} with Spring Cloud Config backend.

    + *

    + * It retrieves the Spring Cloud Config data stored in {@link SentinelRuleStorage}. + * When the data in the backend has been modified, {@link SentinelRuleStorage} will + * invoke {@link SpringCloudConfigDataSource#updateValues()} to update values dynamically. + *

    + *

    + * To notify the client that the remote config has changed, users could bind a git + * webhook callback with the {@link SentinelRuleLocator#refresh()} API. + *

    + * + * @author lianglin + * @since 1.7.0 + */ +public class SpringCloudConfigDataSource extends AbstractDataSource { + + private final static Map listeners; + + static { + listeners = new ConcurrentHashMap<>(); + } + + private final String ruleKey; + + public SpringCloudConfigDataSource(final String ruleKey, Converter converter) { + super(converter); + if (StringUtil.isBlank(ruleKey)) { + throw new IllegalArgumentException(String.format("Bad argument: ruleKey=[%s]", ruleKey)); + } + + this.ruleKey = ruleKey; + loadInitialConfig(); + initListener(); + } + + private void loadInitialConfig() { + try { + T newValue = loadConfig(); + if (newValue == null) { + RecordLog.warn("[SpringCloudConfigDataSource] WARN: initial application is null, you may have to check your data source"); + } + getProperty().updateValue(newValue); + } catch (Exception ex) { + RecordLog.warn("[SpringCloudConfigDataSource] Error when loading initial application", ex); + } + } + + private void initListener() { + listeners.put(this, new SpringConfigListener(this)); + } + + @Override + public String readSource() { + return SentinelRuleStorage.retrieveRule(ruleKey); + } + + @Override + public void close() throws Exception { + listeners.remove(this); + } + + public static void updateValues() { + for (SpringConfigListener listener : listeners.values()) { + listener.listenChanged(); + } + } + + private static class SpringConfigListener { + + private SpringCloudConfigDataSource dataSource; + + public SpringConfigListener(SpringCloudConfigDataSource dataSource) { + this.dataSource = dataSource; + } + + public void listenChanged() { + try { + Object newValue = dataSource.loadConfig(); + dataSource.getProperty().updateValue(newValue); + } catch (Exception e) { + RecordLog.warn("[SpringConfigListener] load config error: ", e); + } + } + } +} diff --git a/sentinel-extension/sentinel-datasource-spring-cloud-config/src/main/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/config/DataSourceBootstrapConfiguration.java b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/main/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/config/DataSourceBootstrapConfiguration.java new file mode 100644 index 0000000000..a1c3390d09 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/main/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/config/DataSourceBootstrapConfiguration.java @@ -0,0 +1,48 @@ +/* + * 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.spring.cloud.config.config; + +import com.alibaba.csp.sentinel.datasource.spring.cloud.config.SentinelRuleLocator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.config.client.ConfigClientProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; + +/** + *

    + * Define the configuration Loaded when spring application start. + * Put it in META-INF/spring.factories, it will be auto loaded by Spring + *

    + * + * @author lianglin + * @since 1.7.0 + */ +@Configuration +public class DataSourceBootstrapConfiguration { + + @Autowired + private ConfigurableEnvironment environment; + + @Bean + public SentinelRuleLocator sentinelPropertySourceLocator(ConfigClientProperties properties) { + SentinelRuleLocator locator = new SentinelRuleLocator( + properties, environment); + return locator; + } + + +} diff --git a/sentinel-extension/sentinel-datasource-spring-cloud-config/src/main/resources/META-INF/spring.factories b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..31c250fc27 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.cloud.bootstrap.BootstrapConfiguration=\ +com.alibaba.csp.sentinel.datasource.spring.cloud.config.config.DataSourceBootstrapConfiguration diff --git a/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/SimpleSpringApplication.java b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/SimpleSpringApplication.java new file mode 100644 index 0000000000..b4a7a0bdc5 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/SimpleSpringApplication.java @@ -0,0 +1,32 @@ +/* + * 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.spring.cloud.config; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author lianglin + * @since 1.7.0 + */ +@SpringBootApplication +public abstract class SimpleSpringApplication { + public static void main(String[] args) { + SpringApplication.run(SimpleSpringApplication.class); + } + +} diff --git a/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/client/ConfigClient.java b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/client/ConfigClient.java new file mode 100644 index 0000000000..03c5ebd3d1 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/client/ConfigClient.java @@ -0,0 +1,34 @@ +/* + * 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.spring.cloud.config.client; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.PropertySource; + +/** + * @author lianglin + * @since 1.7.0 + */ +@SpringBootApplication +@ComponentScan("com.alibaba.csp.sentinel.datasource.spring.cloud.config.test") +@PropertySource("classpath:config-client-application.properties") +public class ConfigClient { + public static void main(String[] args) { + SpringApplication.run(ConfigClient.class); + } +} diff --git a/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/server/ConfigServer.java b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/server/ConfigServer.java new file mode 100644 index 0000000000..14d46fb243 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/server/ConfigServer.java @@ -0,0 +1,35 @@ +/* + * 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.spring.cloud.config.server; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.config.server.EnableConfigServer; +import org.springframework.context.annotation.PropertySource; + +/** + * @author lianglin + * @since 1.7.0 + */ +@EnableConfigServer +@SpringBootApplication +@PropertySource("classpath:config-server-application.properties") +public class ConfigServer { + public static void main(String[] args) { + SpringApplication.run(ConfigServer.class); + + } +} diff --git a/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/test/SentinelRuleLocatorTests.java b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/test/SentinelRuleLocatorTests.java new file mode 100644 index 0000000000..a6fbf73e0c --- /dev/null +++ b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/test/SentinelRuleLocatorTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2018-2019 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 + * + * https://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.spring.cloud.config.test; + +import com.alibaba.csp.sentinel.datasource.spring.cloud.config.SentinelRuleLocator; +import com.alibaba.csp.sentinel.datasource.spring.cloud.config.SentinelRuleStorage; +import com.alibaba.csp.sentinel.datasource.spring.cloud.config.config.DataSourceBootstrapConfiguration; +import com.alibaba.csp.sentinel.datasource.spring.cloud.config.server.ConfigServer; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.config.client.ConfigClientProperties; +import org.springframework.core.env.Environment; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * @author lianglin + * @since 1.7.0 + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = DataSourceBootstrapConfiguration.class, properties = { + "spring.application.name=sentinel" +}) +public class SentinelRuleLocatorTests { + + + @Autowired + private SentinelRuleLocator sentinelRulesSourceLocator; + + @Autowired + private Environment environment; + + @Test + public void testAutoLoad() { + Assert.assertTrue(sentinelRulesSourceLocator != null); + Assert.assertTrue(environment != null); + } + + + /** + * Before run this test case, please start the Config Server ${@link ConfigServer} + */ + public void testLocate() { + ConfigClientProperties configClientProperties = new ConfigClientProperties(environment); + configClientProperties.setLabel("master"); + configClientProperties.setProfile("dev"); + configClientProperties.setUri(new String[]{"http://localhost:10086/"}); + SentinelRuleLocator sentinelRulesSourceLocator = new SentinelRuleLocator(configClientProperties, environment); + sentinelRulesSourceLocator.locate(environment); + Assert.assertTrue(StringUtil.isNotBlank(SentinelRuleStorage.retrieveRule("flow_rule"))); + + } + +} diff --git a/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/test/SpringCouldDataSourceTest.java b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/test/SpringCouldDataSourceTest.java new file mode 100644 index 0000000000..589a3ba922 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/java/com/alibaba/csp/sentinel/datasource/spring/cloud/config/test/SpringCouldDataSourceTest.java @@ -0,0 +1,74 @@ +/* + * 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.spring.cloud.config.test; + +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.datasource.spring.cloud.config.SentinelRuleLocator; +import com.alibaba.csp.sentinel.datasource.spring.cloud.config.SpringCloudConfigDataSource; +import com.alibaba.csp.sentinel.datasource.spring.cloud.config.client.ConfigClient; +import com.alibaba.csp.sentinel.datasource.spring.cloud.config.server.ConfigServer; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.fastjson.JSON; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * Before test, please start ${@link ConfigServer} and ${@link ConfigClient} + * + * @author lianglin + * @since 1.7.0 + */ +@RestController +@RequestMapping(value = "/test/dataSource/") +public class SpringCouldDataSourceTest { + + + @Autowired + private SentinelRuleLocator locator; + + Converter> converter = new Converter>() { + @Override + public List convert(String source) { + return JSON.parseArray(source, FlowRule.class); + } + }; + + + @GetMapping("/get") + @ResponseBody + public List get() { + SpringCloudConfigDataSource dataSource = new SpringCloudConfigDataSource("flow_rule", converter); + FlowRuleManager.register2Property(dataSource.getProperty()); + return FlowRuleManager.getRules(); + } + + /** + * WebHook refresh config + */ + @GetMapping("/refresh") + @ResponseBody + public List refresh() { + locator.refresh(); + return FlowRuleManager.getRules(); + } +} diff --git a/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/resources/bootstrap.yml b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/resources/bootstrap.yml new file mode 100644 index 0000000000..150d37ebc4 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/resources/bootstrap.yml @@ -0,0 +1,10 @@ +spring: + application: + name: sentinel + cloud: + config: + uri: http://localhost:10086/ + profile: dev + label: master + + diff --git a/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/resources/config-client-application.properties b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/resources/config-client-application.properties new file mode 100644 index 0000000000..0528bbdef5 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/resources/config-client-application.properties @@ -0,0 +1,3 @@ +spring.application.name=sentinel +server.port=8080 + diff --git a/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/resources/config-server-application.properties b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/resources/config-server-application.properties new file mode 100644 index 0000000000..4a0b77457c --- /dev/null +++ b/sentinel-extension/sentinel-datasource-spring-cloud-config/src/test/resources/config-server-application.properties @@ -0,0 +1,4 @@ +spring.cloud.config.server.git.uri=git@github.com:linlinisme/spring-cloud-config-datasource.git +spring.cloud.config.server.git.search-paths=sentinel +server.port=10086 +spring.cloud.config.label=master diff --git a/sentinel-extension/sentinel-datasource-zookeeper/pom.xml b/sentinel-extension/sentinel-datasource-zookeeper/pom.xml index 2cd550d7ea..530c06369a 100644 --- a/sentinel-extension/sentinel-datasource-zookeeper/pom.xml +++ b/sentinel-extension/sentinel-datasource-zookeeper/pom.xml @@ -5,7 +5,7 @@ sentinel-extension com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 @@ -15,7 +15,7 @@ 1.7 1.7 - 3.4.13 + 3.4.14 4.0.1 2.12.0 diff --git a/sentinel-extension/sentinel-datasource-zookeeper/src/main/java/com/alibaba/csp/sentinel/datasource/zookeeper/ZookeeperDataSource.java b/sentinel-extension/sentinel-datasource-zookeeper/src/main/java/com/alibaba/csp/sentinel/datasource/zookeeper/ZookeeperDataSource.java index 24c3557bdd..5fb243bbc5 100644 --- a/sentinel-extension/sentinel-datasource-zookeeper/src/main/java/com/alibaba/csp/sentinel/datasource/zookeeper/ZookeeperDataSource.java +++ b/sentinel-extension/sentinel-datasource-zookeeper/src/main/java/com/alibaba/csp/sentinel/datasource/zookeeper/ZookeeperDataSource.java @@ -1,17 +1,10 @@ package com.alibaba.csp.sentinel.datasource.zookeeper; -import java.util.List; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - import com.alibaba.csp.sentinel.concurrent.NamedThreadFactory; import com.alibaba.csp.sentinel.datasource.AbstractDataSource; import com.alibaba.csp.sentinel.datasource.Converter; import com.alibaba.csp.sentinel.log.RecordLog; import com.alibaba.csp.sentinel.util.StringUtil; - import org.apache.curator.framework.AuthInfo; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.CuratorFrameworkFactory; @@ -19,8 +12,15 @@ import org.apache.curator.framework.recipes.cache.NodeCache; import org.apache.curator.framework.recipes.cache.NodeCacheListener; import org.apache.curator.retry.ExponentialBackoffRetry; -import org.apache.zookeeper.CreateMode; -import org.apache.zookeeper.data.Stat; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; /** * A read-only {@code DataSource} with ZooKeeper backend. @@ -32,9 +32,13 @@ public class ZookeeperDataSource extends AbstractDataSource { private static final int RETRY_TIMES = 3; private static final int SLEEP_TIME = 1000; + private static volatile Map zkClientMap = new HashMap<>(); + private static final Object lock = new Object(); + + private final ExecutorService pool = new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS, - new ArrayBlockingQueue(1), new NamedThreadFactory("sentinel-zookeeper-ds-update"), - new ThreadPoolExecutor.DiscardOldestPolicy()); + new ArrayBlockingQueue(1), new NamedThreadFactory("sentinel-zookeeper-ds-update"), + new ThreadPoolExecutor.DiscardOldestPolicy()); private NodeCacheListener listener; private final String path; @@ -116,16 +120,33 @@ public void nodeChanged() { } }; - if (authInfos == null || authInfos.size() == 0) { - this.zkClient = CuratorFrameworkFactory.newClient(serverAddr, new ExponentialBackoffRetry(SLEEP_TIME, RETRY_TIMES)); + String zkKey = getZkKey(serverAddr, authInfos); + if (zkClientMap.containsKey(zkKey)) { + this.zkClient = zkClientMap.get(zkKey); } else { - this.zkClient = CuratorFrameworkFactory.builder(). - connectString(serverAddr). - retryPolicy(new ExponentialBackoffRetry(SLEEP_TIME, RETRY_TIMES)). - authorization(authInfos). - build(); + synchronized (lock) { + if (!zkClientMap.containsKey(zkKey)) { + CuratorFramework zc = null; + if (authInfos == null || authInfos.size() == 0) { + zc = CuratorFrameworkFactory.newClient(serverAddr, new ExponentialBackoffRetry(SLEEP_TIME, RETRY_TIMES)); + } else { + zc = CuratorFrameworkFactory.builder(). + connectString(serverAddr). + retryPolicy(new ExponentialBackoffRetry(SLEEP_TIME, RETRY_TIMES)). + authorization(authInfos). + build(); + } + this.zkClient = zc; + this.zkClient.start(); + Map newZkClientMap = new HashMap<>(zkClientMap.size()); + newZkClientMap.putAll(zkClientMap); + newZkClientMap.put(zkKey, zc); + zkClientMap = newZkClientMap; + } else { + this.zkClient = zkClientMap.get(zkKey); + } + } } - this.zkClient.start(); this.nodeCache = new NodeCache(this.zkClient, this.path); this.nodeCache.getListenable().addListener(this.listener, this.pool); @@ -165,4 +186,31 @@ public void close() throws Exception { private String getPath(String groupId, String dataId) { return String.format("/%s/%s", groupId, dataId); } + + private String getZkKey(final String serverAddr, final List authInfos) { + if (authInfos == null || authInfos.size() == 0) { + return serverAddr; + } + StringBuilder builder = new StringBuilder(64); + builder.append(serverAddr).append(getAuthInfosKey(authInfos)); + return builder.toString(); + } + + private String getAuthInfosKey(List authInfos) { + StringBuilder builder = new StringBuilder(32); + for (AuthInfo authInfo : authInfos) { + if (authInfo == null) { + builder.append("{}"); + } else { + builder.append("{" + "sc=" + authInfo.getScheme() + ",au=" + Arrays.toString(authInfo.getAuth()) + "}"); + } + } + return builder.toString(); + } + + protected CuratorFramework getZkClient() { + return this.zkClient; + } + + } diff --git a/sentinel-extension/sentinel-datasource-zookeeper/src/test/java/com/alibaba/csp/sentinel/datasource/zookeeper/ZookeeperDataSourceTest.java b/sentinel-extension/sentinel-datasource-zookeeper/src/test/java/com/alibaba/csp/sentinel/datasource/zookeeper/ZookeeperDataSourceTest.java index 52867cc38f..f9c62e65b7 100644 --- a/sentinel-extension/sentinel-datasource-zookeeper/src/test/java/com/alibaba/csp/sentinel/datasource/zookeeper/ZookeeperDataSourceTest.java +++ b/sentinel-extension/sentinel-datasource-zookeeper/src/test/java/com/alibaba/csp/sentinel/datasource/zookeeper/ZookeeperDataSourceTest.java @@ -3,6 +3,7 @@ import com.alibaba.csp.sentinel.datasource.Converter; import com.alibaba.csp.sentinel.datasource.ReadableDataSource; import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; import com.alibaba.fastjson.JSON; @@ -18,6 +19,7 @@ import org.apache.zookeeper.data.Id; import org.apache.zookeeper.data.Stat; import org.apache.zookeeper.server.auth.DigestAuthenticationProvider; +import org.junit.Assert; import org.junit.Test; import java.util.Collections; @@ -43,16 +45,17 @@ public void testZooKeeperDataSource() throws Exception { final String path = "/sentinel-zk-ds-demo/flow-HK"; ReadableDataSource> flowRuleDataSource = new ZookeeperDataSource>(remoteAddress, path, - new Converter>() { - @Override - public List convert(String source) { - return JSON.parseObject(source, new TypeReference>() {}); - } - }); + new Converter>() { + @Override + public List convert(String source) { + return JSON.parseObject(source, new TypeReference>() { + }); + } + }); FlowRuleManager.register2Property(flowRuleDataSource.getProperty()); CuratorFramework zkClient = CuratorFrameworkFactory.newClient(remoteAddress, - new ExponentialBackoffRetry(3, 1000)); + new ExponentialBackoffRetry(3, 1000)); zkClient.start(); Stat stat = zkClient.checkExists().forPath(path); if (stat == null) { @@ -67,6 +70,9 @@ public List convert(String source) { server.stop(); } + + + @Test public void testZooKeeperDataSourceAuthorization() throws Exception { TestingServer server = new TestingServer(21812); @@ -116,21 +122,21 @@ public List convert(String source) { private void publishThenTestFor(CuratorFramework zkClient, String path, String resourceName, long count) throws Exception { FlowRule rule = new FlowRule().setResource(resourceName) - .setLimitApp("default") - .as(FlowRule.class) - .setCount(count) - .setGrade(RuleConstant.FLOW_GRADE_QPS); + .setLimitApp("default") + .as(FlowRule.class) + .setCount(count) + .setGrade(RuleConstant.FLOW_GRADE_QPS); String ruleString = JSON.toJSONString(Collections.singletonList(rule)); zkClient.setData().forPath(path, ruleString.getBytes()); await().timeout(5, TimeUnit.SECONDS) - .until(new Callable() { - @Override - public Boolean call() throws Exception { - List rules = FlowRuleManager.getRules(); - return rules != null && !rules.isEmpty(); - } - }); + .until(new Callable() { + @Override + public Boolean call() throws Exception { + List rules = FlowRuleManager.getRules(); + return rules != null && !rules.isEmpty(); + } + }); List rules = FlowRuleManager.getRules(); boolean exists = false; @@ -142,4 +148,77 @@ public Boolean call() throws Exception { } assertTrue(exists); } + + + /** + * Test whether different dataSources can share the same zkClient when the connection parameters are the same. + * @throws Exception + */ + @Test + public void testZooKeeperDataSourceSameZkClient() throws Exception { + TestingServer server = new TestingServer(21813); + server.start(); + + final String remoteAddress = server.getConnectString(); + final String flowPath = "/sentinel-zk-ds-demo/flow-HK"; + final String degradePath = "/sentinel-zk-ds-demo/degrade-HK"; + + + ZookeeperDataSource> flowRuleZkDataSource = new ZookeeperDataSource<>(remoteAddress, flowPath, + new Converter>() { + @Override + public List convert(String source) { + return JSON.parseObject(source, new TypeReference>() { + }); + } + }); + ZookeeperDataSource> degradeRuleZkDataSource = new ZookeeperDataSource<>(remoteAddress, degradePath, + new Converter>() { + @Override + public List convert(String source) { + return JSON.parseObject(source, new TypeReference>() { + }); + } + }); + + + Assert.assertTrue(flowRuleZkDataSource.getZkClient() == degradeRuleZkDataSource.getZkClient()); + + + final String groupId = "sentinel-zk-ds-demo"; + final String flowDataId = "flow-HK"; + final String degradeDataId = "degrade-HK"; + final String scheme = "digest"; + final String auth = "root:123456"; + AuthInfo authInfo = new AuthInfo(scheme, auth.getBytes()); + List authInfoList = Collections.singletonList(authInfo); + + + ZookeeperDataSource> flowRuleZkAutoDataSource = new ZookeeperDataSource>(remoteAddress, + authInfoList, groupId, flowDataId, + new Converter>() { + @Override + public List convert(String source) { + return JSON.parseObject(source, new TypeReference>() { + }); + } + }); + + ZookeeperDataSource> degradeRuleZkAutoDataSource = new ZookeeperDataSource>(remoteAddress, + authInfoList, groupId, degradeDataId, + new Converter>() { + @Override + public List convert(String source) { + return JSON.parseObject(source, new TypeReference>() { + }); + } + }); + + Assert.assertTrue(flowRuleZkAutoDataSource.getZkClient() == degradeRuleZkAutoDataSource.getZkClient()); + + server.stop(); + } + + + } \ No newline at end of file diff --git a/sentinel-extension/sentinel-parameter-flow-control/pom.xml b/sentinel-extension/sentinel-parameter-flow-control/pom.xml index 406ec2b752..ba5e9c48c3 100644 --- a/sentinel-extension/sentinel-parameter-flow-control/pom.xml +++ b/sentinel-extension/sentinel-parameter-flow-control/pom.xml @@ -5,7 +5,7 @@ sentinel-extension com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/HotParamSlotChainBuilder.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/HotParamSlotChainBuilder.java new file mode 100644 index 0000000000..baf8d8adee --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/HotParamSlotChainBuilder.java @@ -0,0 +1,52 @@ +/* + * 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.slots; + +import com.alibaba.csp.sentinel.slotchain.DefaultProcessorSlotChain; +import com.alibaba.csp.sentinel.slotchain.ProcessorSlotChain; +import com.alibaba.csp.sentinel.slotchain.SlotChainBuilder; +import com.alibaba.csp.sentinel.slots.block.authority.AuthoritySlot; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeSlot; +import com.alibaba.csp.sentinel.slots.block.flow.FlowSlot; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowSlot; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; +import com.alibaba.csp.sentinel.slots.logger.LogSlot; +import com.alibaba.csp.sentinel.slots.nodeselector.NodeSelectorSlot; +import com.alibaba.csp.sentinel.slots.statistic.StatisticSlot; +import com.alibaba.csp.sentinel.slots.system.SystemSlot; + +/** + * @author Eric Zhao + * @since 0.2.0 + */ +public class HotParamSlotChainBuilder implements SlotChainBuilder { + + @Override + public ProcessorSlotChain build() { + ProcessorSlotChain chain = new DefaultProcessorSlotChain(); + chain.addLast(new NodeSelectorSlot()); + chain.addLast(new ClusterBuilderSlot()); + chain.addLast(new LogSlot()); + chain.addLast(new StatisticSlot()); + chain.addLast(new AuthoritySlot()); + chain.addLast(new SystemSlot()); + chain.addLast(new ParamFlowSlot()); + chain.addLast(new FlowSlot()); + chain.addLast(new DegradeSlot()); + + return chain; + } +} diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowChecker.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowChecker.java index aa4598f8cc..055c0560a0 100644 --- a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowChecker.java +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowChecker.java @@ -121,7 +121,7 @@ static boolean passSingleValueCheck(ResourceWrapper resourceWrapper, ParamFlowRu static boolean passDefaultLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int acquireCount, Object value) { ParameterMetric metric = getParameterMetric(resourceWrapper); - CacheMap tokenCounters = metric == null ? null : metric.getRuleTokenCounter(rule); + CacheMap tokenCounters = metric == null ? null : metric.getRuleTokenCounter(rule); CacheMap timeCounters = metric == null ? null : metric.getRuleTimeCounter(rule); if (tokenCounters == null || timeCounters == null) { @@ -130,7 +130,7 @@ static boolean passDefaultLocalCheck(ResourceWrapper resourceWrapper, ParamFlowR // Calculate max token count (threshold) Set exclusionItems = rule.getParsedHotItems().keySet(); - int tokenCount = (int)rule.getCount(); + long tokenCount = (long)rule.getCount(); if (exclusionItems.contains(value)) { tokenCount = rule.getParsedHotItems().get(value); } @@ -139,7 +139,7 @@ static boolean passDefaultLocalCheck(ResourceWrapper resourceWrapper, ParamFlowR return false; } - int maxCount = tokenCount + rule.getBurstCount(); + long maxCount = tokenCount + rule.getBurstCount(); if (acquireCount > maxCount) { return false; } @@ -150,7 +150,7 @@ static boolean passDefaultLocalCheck(ResourceWrapper resourceWrapper, ParamFlowR AtomicLong lastAddTokenTime = timeCounters.putIfAbsent(value, new AtomicLong(currentTime)); if (lastAddTokenTime == null) { // Token never added, just replenish the tokens and consume {@code acquireCount} immediately. - tokenCounters.putIfAbsent(value, new AtomicInteger(maxCount - acquireCount)); + tokenCounters.putIfAbsent(value, new AtomicLong(maxCount - acquireCount)); return true; } @@ -158,15 +158,15 @@ static boolean passDefaultLocalCheck(ResourceWrapper resourceWrapper, ParamFlowR long passTime = currentTime - lastAddTokenTime.get(); // A simplified token bucket algorithm that will replenish the tokens only when statistic window has passed. if (passTime > rule.getDurationInSec() * 1000) { - AtomicInteger oldQps = tokenCounters.putIfAbsent(value, new AtomicInteger(maxCount - acquireCount)); + AtomicLong oldQps = tokenCounters.putIfAbsent(value, new AtomicLong(maxCount - acquireCount)); if (oldQps == null) { // Might not be accurate here. lastAddTokenTime.set(currentTime); return true; } else { - int restQps = oldQps.get(); - int toAddCount = (int)((passTime * tokenCount) / (rule.getDurationInSec() * 1000)); - int newQps = (restQps + toAddCount) > maxCount ? (maxCount - acquireCount) + long restQps = oldQps.get(); + long toAddCount = (passTime * tokenCount) / (rule.getDurationInSec() * 1000); + long newQps = toAddCount + restQps > maxCount ? (maxCount - acquireCount) : (restQps + toAddCount - acquireCount); if (newQps < 0) { @@ -179,9 +179,9 @@ static boolean passDefaultLocalCheck(ResourceWrapper resourceWrapper, ParamFlowR Thread.yield(); } } else { - AtomicInteger oldQps = tokenCounters.get(value); + AtomicLong oldQps = tokenCounters.get(value); if (oldQps != null) { - int oldQpsValue = oldQps.get(); + long oldQpsValue = oldQps.get(); if (oldQpsValue - acquireCount >= 0) { if (oldQps.compareAndSet(oldQpsValue, oldQpsValue - acquireCount)) { return true; diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParameterMetric.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParameterMetric.java index 4cb9d53fea..daa436f3e3 100644 --- a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParameterMetric.java +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParameterMetric.java @@ -51,7 +51,7 @@ public class ParameterMetric { * * @since 1.6.0 */ - private final Map> ruleTokenCounter = new HashMap<>(); + private final Map> ruleTokenCounter = new HashMap<>(); private final Map> threadCountMap = new HashMap<>(); /** @@ -61,7 +61,7 @@ public class ParameterMetric { * @return the associated token counter * @since 1.6.0 */ - public CacheMap getRuleTokenCounter(ParamFlowRule rule) { + public CacheMap getRuleTokenCounter(ParamFlowRule rule) { return ruleTokenCounter.get(rule); } @@ -98,7 +98,7 @@ public void initialize(ParamFlowRule rule) { synchronized (lock) { if (ruleTokenCounter.get(rule) == null) { long size = Math.min(BASE_PARAM_MAX_CAPACITY * rule.getDurationInSec(), TOTAL_MAX_CAPACITY); - ruleTokenCounter.put(rule, new ConcurrentLinkedHashMapWrapper(size)); + ruleTokenCounter.put(rule, new ConcurrentLinkedHashMapWrapper(size)); } } } @@ -245,7 +245,7 @@ public long getThreadCount(int index, Object value) { * * @return the token counter map */ - Map> getRuleTokenCounterMap() { + Map> getRuleTokenCounterMap() { return ruleTokenCounter; } diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowCheckerTest.java b/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowCheckerTest.java index 70c549f2b8..d7010b7ad1 100644 --- a/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowCheckerTest.java +++ b/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowCheckerTest.java @@ -160,7 +160,7 @@ public void testPassLocalCheckForCollection() throws InterruptedException { ParameterMetric metric = new ParameterMetric(); ParameterMetricStorage.getMetricsMap().put(resourceWrapper.getName(), metric); metric.getRuleTimeCounterMap().put(rule, new ConcurrentLinkedHashMapWrapper(4000)); - metric.getRuleTokenCounterMap().put(rule, new ConcurrentLinkedHashMapWrapper(4000)); + metric.getRuleTokenCounterMap().put(rule, new ConcurrentLinkedHashMapWrapper(4000)); assertTrue(ParamFlowChecker.passCheck(resourceWrapper, rule, 1, list)); assertFalse(ParamFlowChecker.passCheck(resourceWrapper, rule, 1, list)); diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowDefaultCheckerTest.java b/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowDefaultCheckerTest.java index fca4fb1c4d..581c8522fd 100644 --- a/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowDefaultCheckerTest.java +++ b/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowDefaultCheckerTest.java @@ -38,9 +38,49 @@ /** * @author jialiang.linjl + * @author Eric Zhao */ public class ParamFlowDefaultCheckerTest extends AbstractTimeBasedTest { + @Test + public void testCheckQpsWithLongIntervalAndHighThreshold() { + // This test case is intended to avoid number overflow. + final String resourceName = "testCheckQpsWithLongIntervalAndHighThreshold"; + final ResourceWrapper resourceWrapper = new StringResourceWrapper(resourceName, EntryType.IN); + int paramIdx = 0; + + // Set a large threshold. + long threshold = 25000L; + + ParamFlowRule rule = new ParamFlowRule(resourceName) + .setCount(threshold) + .setParamIdx(paramIdx); + + String valueA = "valueA"; + ParameterMetric metric = new ParameterMetric(); + ParameterMetricStorage.getMetricsMap().put(resourceWrapper.getName(), metric); + metric.getRuleTimeCounterMap().put(rule, new ConcurrentLinkedHashMapWrapper(4000)); + metric.getRuleTokenCounterMap().put(rule, + new ConcurrentLinkedHashMapWrapper(4000)); + + // We mock the time directly to avoid unstable behaviour. + setCurrentMillis(System.currentTimeMillis()); + + assertTrue(ParamFlowChecker.passSingleValueCheck(resourceWrapper, rule, 1, valueA)); + assertTrue(ParamFlowChecker.passSingleValueCheck(resourceWrapper, rule, 1, valueA)); + + // 24 hours passed. + // This can make `toAddCount` larger that Integer.MAX_VALUE. + sleep(1000 * 60 * 60 * 24); + assertTrue(ParamFlowChecker.passSingleValueCheck(resourceWrapper, rule, 1, valueA)); + assertTrue(ParamFlowChecker.passSingleValueCheck(resourceWrapper, rule, 1, valueA)); + + // 48 hours passed. + sleep(1000 * 60 * 60 * 48); + assertTrue(ParamFlowChecker.passSingleValueCheck(resourceWrapper, rule, 1, valueA)); + assertTrue(ParamFlowChecker.passSingleValueCheck(resourceWrapper, rule, 1, valueA)); + } + @Test public void testParamFlowDefaultCheckSingleQps() { final String resourceName = "testParamFlowDefaultCheckSingleQps"; @@ -59,7 +99,7 @@ public void testParamFlowDefaultCheckSingleQps() { ParameterMetricStorage.getMetricsMap().put(resourceWrapper.getName(), metric); metric.getRuleTimeCounterMap().put(rule, new ConcurrentLinkedHashMapWrapper(4000)); metric.getRuleTokenCounterMap().put(rule, - new ConcurrentLinkedHashMapWrapper(4000)); + new ConcurrentLinkedHashMapWrapper(4000)); // We mock the time directly to avoid unstable behaviour. setCurrentMillis(System.currentTimeMillis()); @@ -99,7 +139,7 @@ public void testParamFlowDefaultCheckSingleQpsWithBurst() throws InterruptedExce ParameterMetricStorage.getMetricsMap().put(resourceWrapper.getName(), metric); metric.getRuleTimeCounterMap().put(rule, new ConcurrentLinkedHashMapWrapper(4000)); metric.getRuleTokenCounterMap().put(rule, - new ConcurrentLinkedHashMapWrapper(4000)); + new ConcurrentLinkedHashMapWrapper(4000)); // We mock the time directly to avoid unstable behaviour. setCurrentMillis(System.currentTimeMillis()); @@ -169,7 +209,7 @@ public void testParamFlowDefaultCheckQpsInDifferentDuration() throws Interrupted ParameterMetricStorage.getMetricsMap().put(resourceWrapper.getName(), metric); metric.getRuleTimeCounterMap().put(rule, new ConcurrentLinkedHashMapWrapper(4000)); metric.getRuleTokenCounterMap().put(rule, - new ConcurrentLinkedHashMapWrapper(4000)); + new ConcurrentLinkedHashMapWrapper(4000)); // We mock the time directly to avoid unstable behaviour. setCurrentMillis(System.currentTimeMillis()); @@ -222,7 +262,7 @@ public void testParamFlowDefaultCheckSingleValueCheckQpsMultipleThreads() throws ParameterMetricStorage.getMetricsMap().put(resourceWrapper.getName(), metric); metric.getRuleTimeCounterMap().put(rule, new ConcurrentLinkedHashMapWrapper(4000)); metric.getRuleTokenCounterMap().put(rule, - new ConcurrentLinkedHashMapWrapper(4000)); + new ConcurrentLinkedHashMapWrapper(4000)); int threadCount = 40; final CountDownLatch waitLatch = new CountDownLatch(threadCount); diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowSlotTest.java b/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowSlotTest.java index 0d9838449e..aadef74abd 100644 --- a/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowSlotTest.java +++ b/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowSlotTest.java @@ -99,8 +99,8 @@ public void testEntryWhenParamFlowExists() throws Throwable { ParameterMetric metric = mock(ParameterMetric.class); - CacheMap map = new ConcurrentLinkedHashMapWrapper(4000); - CacheMap map2 = new ConcurrentLinkedHashMapWrapper(4000); + CacheMap map = new ConcurrentLinkedHashMapWrapper<>(4000); + CacheMap map2 = new ConcurrentLinkedHashMapWrapper<>(4000); when(metric.getRuleTimeCounter(rule)).thenReturn(map); when(metric.getRuleTokenCounter(rule)).thenReturn(map2); map.put(argToGo, new AtomicLong(TimeUtil.currentTimeMillis())); diff --git a/sentinel-transport/pom.xml b/sentinel-transport/pom.xml index e53f957103..7355d78df2 100755 --- a/sentinel-transport/pom.xml +++ b/sentinel-transport/pom.xml @@ -6,7 +6,7 @@ com.alibaba.csp sentinel-parent - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT sentinel-transport The transport module of Sentinel diff --git a/sentinel-transport/sentinel-transport-common/pom.xml b/sentinel-transport/sentinel-transport-common/pom.xml index 7206e440ce..13c9d28cfd 100755 --- a/sentinel-transport/sentinel-transport-common/pom.xml +++ b/sentinel-transport/sentinel-transport-common/pom.xml @@ -5,7 +5,7 @@ com.alibaba.csp sentinel-transport - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 jar diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/CommandHandlerProvider.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/CommandHandlerProvider.java index a39c24ba5b..00b8e5decf 100755 --- a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/CommandHandlerProvider.java +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/CommandHandlerProvider.java @@ -21,6 +21,7 @@ import java.util.ServiceLoader; import com.alibaba.csp.sentinel.command.annotation.CommandMapping; +import com.alibaba.csp.sentinel.spi.ServiceLoaderUtil; import com.alibaba.csp.sentinel.util.StringUtil; /** @@ -30,7 +31,8 @@ */ public class CommandHandlerProvider implements Iterable { - private final ServiceLoader serviceLoader = ServiceLoader.load(CommandHandler.class); + private final ServiceLoader serviceLoader = ServiceLoaderUtil.getServiceLoader( + CommandHandler.class); /** * Get all command handlers annotated with {@link CommandMapping} with command name. diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/config/TransportConfig.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/config/TransportConfig.java index 84b53cbd6d..c27625cdca 100755 --- a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/config/TransportConfig.java +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/config/TransportConfig.java @@ -29,6 +29,9 @@ public class TransportConfig { public static final String SERVER_PORT = "csp.sentinel.api.port"; public static final String HEARTBEAT_INTERVAL_MS = "csp.sentinel.heartbeat.interval.ms"; public static final String HEARTBEAT_CLIENT_IP = "csp.sentinel.heartbeat.client.ip"; + public static final String HEARTBEAT_API_PATH = "csp.sentinel.heartbeat.api.path"; + + public static final String HEARTBEAT_DEFAULT_PATH = "/registry/machine"; private static int runtimePort = -1; @@ -94,4 +97,22 @@ public static String getHeartbeatClientIp() { } return ip; } + + /** + * Get the heartbeat api path. If the machine registry path of the dashboard + * is modified, then the API path should also be consistent with the API path of the dashboard. + * + * @return the heartbeat api path + * @since 1.7.1 + */ + public static String getHeartbeatApiPath() { + String apiPath = SentinelConfig.getConfig(HEARTBEAT_API_PATH); + if (StringUtil.isBlank(apiPath)) { + return HEARTBEAT_DEFAULT_PATH; + } + if (!apiPath.startsWith("/")) { + apiPath = "/" + apiPath; + } + return apiPath; + } } diff --git a/sentinel-transport/sentinel-transport-common/src/test/java/com/alibaba/csp/sentinel/transport/config/TransportConfigTest.java b/sentinel-transport/sentinel-transport-common/src/test/java/com/alibaba/csp/sentinel/transport/config/TransportConfigTest.java index d77a88b764..a67d42852c 100644 --- a/sentinel-transport/sentinel-transport-common/src/test/java/com/alibaba/csp/sentinel/transport/config/TransportConfigTest.java +++ b/sentinel-transport/sentinel-transport-common/src/test/java/com/alibaba/csp/sentinel/transport/config/TransportConfigTest.java @@ -64,4 +64,22 @@ public void testGetHeartbeatClientIp() { SentinelConfig.setConfig(TransportConfig.HEARTBEAT_CLIENT_IP, ""); assertTrue(StringUtil.isNotEmpty(TransportConfig.getHeartbeatClientIp())); } + + @Test + public void testGetHeartbeatApiPath() { + // use default heartbeat api path + assertTrue(StringUtil.isNotEmpty(TransportConfig.getHeartbeatApiPath())); + assertEquals(TransportConfig.HEARTBEAT_DEFAULT_PATH, TransportConfig.getHeartbeatApiPath()); + + // config heartbeat api path + SentinelConfig.setConfig(TransportConfig.HEARTBEAT_API_PATH, "/demo"); + assertTrue(StringUtil.isNotEmpty(TransportConfig.getHeartbeatApiPath())); + assertEquals("/demo", TransportConfig.getHeartbeatApiPath()); + + SentinelConfig.setConfig(TransportConfig.HEARTBEAT_API_PATH, "demo/registry"); + assertEquals("/demo/registry", TransportConfig.getHeartbeatApiPath()); + + SentinelConfig.removeConfig(TransportConfig.HEARTBEAT_API_PATH); + assertEquals(TransportConfig.HEARTBEAT_DEFAULT_PATH, TransportConfig.getHeartbeatApiPath()); + } } \ No newline at end of file diff --git a/sentinel-transport/sentinel-transport-netty-http/pom.xml b/sentinel-transport/sentinel-transport-netty-http/pom.xml index 928f0db503..bedcaac3db 100755 --- a/sentinel-transport/sentinel-transport-netty-http/pom.xml +++ b/sentinel-transport/sentinel-transport-netty-http/pom.xml @@ -5,7 +5,7 @@ com.alibaba.csp sentinel-transport - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 sentinel-transport-netty-http diff --git a/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerHandler.java b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerHandler.java index 956ecad30b..36146c2fea 100755 --- a/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerHandler.java +++ b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerHandler.java @@ -221,11 +221,15 @@ private String parseTarget(String uri) { if (StringUtil.isEmpty(uri)) { return ""; } - String[] arr = uri.split("/"); - if (arr.length < 2) { - return ""; + + // Remove the / of the uri as the target(command name) + // Usually the uri is start with / + int start = uri.indexOf('/'); + if (start != -1) { + return uri.substring(start + 1); } - return arr[1]; + + return uri; } private CommandHandler getHandler(String commandName) { diff --git a/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/HttpHeartbeatSender.java b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/HttpHeartbeatSender.java index e150f266b6..be5c7fa2e4 100755 --- a/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/HttpHeartbeatSender.java +++ b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/HttpHeartbeatSender.java @@ -109,7 +109,7 @@ public boolean sendHeartbeat() throws Exception { } URIBuilder uriBuilder = new URIBuilder(); uriBuilder.setScheme("http").setHost(consoleHost).setPort(consolePort) - .setPath("/registry/machine") + .setPath(TransportConfig.getHeartbeatApiPath()) .setParameter("app", AppNameUtil.getAppName()) .setParameter("app_type", String.valueOf(SentinelConfig.getAppType())) .setParameter("v", Constants.SENTINEL_VERSION) diff --git a/sentinel-transport/sentinel-transport-netty-http/src/test/java/com/alibaba/csp/sentinel/transport/command/handler/MultipleSlashNameCommandTestHandler.java b/sentinel-transport/sentinel-transport-netty-http/src/test/java/com/alibaba/csp/sentinel/transport/command/handler/MultipleSlashNameCommandTestHandler.java new file mode 100644 index 0000000000..7baf60d825 --- /dev/null +++ b/sentinel-transport/sentinel-transport-netty-http/src/test/java/com/alibaba/csp/sentinel/transport/command/handler/MultipleSlashNameCommandTestHandler.java @@ -0,0 +1,32 @@ +/* + * 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.transport.command.handler; + +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; +import com.alibaba.csp.sentinel.command.annotation.CommandMapping; + +/** + * @author cdfive + */ +@CommandMapping(name = "aa/bb/cc", desc = "a test handler with multiple / in its name") +public class MultipleSlashNameCommandTestHandler implements CommandHandler { + @Override + public CommandResponse handle(CommandRequest request) { + return CommandResponse.ofSuccess("MultipleSlashNameCommandTestHandler result"); + } +} diff --git a/sentinel-transport/sentinel-transport-netty-http/src/test/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerHandlerTest.java b/sentinel-transport/sentinel-transport-netty-http/src/test/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerHandlerTest.java index 3102c217e2..2baf4f0564 100644 --- a/sentinel-transport/sentinel-transport-netty-http/src/test/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerHandlerTest.java +++ b/sentinel-transport/sentinel-transport-netty-http/src/test/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerHandlerTest.java @@ -23,6 +23,8 @@ import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; import com.alibaba.csp.sentinel.transport.CommandCenter; import com.alibaba.csp.sentinel.transport.command.NettyHttpCommandCenter; +import com.alibaba.csp.sentinel.transport.command.handler.MultipleSlashNameCommandTestHandler; +import com.alibaba.fastjson.JSON; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; @@ -34,6 +36,7 @@ import java.lang.reflect.Field; import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -45,7 +48,6 @@ * Test cases for {@link HttpServerHandler}. * * @author cdfive - * @date 2018-12-17 */ public class HttpServerHandlerTest { @@ -59,7 +61,7 @@ public class HttpServerHandlerTest { @BeforeClass public static void beforeClass() throws Exception { - // don't execute InitExecutor.doInit, to avoid CommandCenter SPI loaded + // Don't execute InitExecutor.doInit, to avoid CommandCenter SPI loaded Field[] declaredFields = InitExecutor.class.getDeclaredFields(); for (Field declaredField : declaredFields) { if (declaredField.getName().equals("initialized")) { @@ -68,23 +70,26 @@ public static void beforeClass() throws Exception { } } - // create NettyHttpCommandCenter to create HttpServer + // Create NettyHttpCommandCenter to create HttpServer CommandCenter commandCenter = new NettyHttpCommandCenter(); - // call beforeStart to register handlers + // Call beforeStart to register handlers commandCenter.beforeStart(); } @Before public void before() { - // the same Handlers in order as the ChannelPipeline in HttpServerInitializer + // The same Handlers in order as the ChannelPipeline in HttpServerInitializer HttpRequestDecoder httpRequestDecoder = new HttpRequestDecoder(); HttpObjectAggregator httpObjectAggregator = new HttpObjectAggregator(1024 * 1024); HttpResponseEncoder httpResponseEncoder = new HttpResponseEncoder(); HttpServerHandler httpServerHandler = new HttpServerHandler(); - // create new EmbeddedChannel every method call + // Create new EmbeddedChannel every method call embeddedChannel = new EmbeddedChannel(httpRequestDecoder, httpObjectAggregator, httpResponseEncoder, httpServerHandler); + + // Clear flow rules + FlowRuleManager.loadRules(Collections.EMPTY_LIST); } @Test @@ -92,9 +97,9 @@ public void testInvalidCommand() { String httpRequestStr = "GET / HTTP/1.1" + CRLF + "Host: localhost:8719" + CRLF + CRLF; - String body = "Invalid command"; + String expectedBody = "Invalid command"; - processError(httpRequestStr, body); + processError(httpRequestStr, expectedBody); } @Test @@ -102,44 +107,49 @@ public void testUnknownCommand() { String httpRequestStr = "GET /aaa HTTP/1.1" + CRLF + "Host: localhost:8719" + CRLF + CRLF; - String body = String.format("Unknown command \"%s\"", "aaa"); + String expectedBody = String.format("Unknown command \"%s\"", "aaa"); - processError(httpRequestStr, body); + processError(httpRequestStr, expectedBody); } + /** + * {@link com.alibaba.csp.sentinel.command.handler.VersionCommandHandler} + */ @Test public void testVersionCommand() { String httpRequestStr = "GET /version HTTP/1.1" + CRLF + "Host: localhost:8719" + CRLF + CRLF; - String body = Constants.SENTINEL_VERSION; + String expectedBody = Constants.SENTINEL_VERSION; - processSuccess(httpRequestStr, body); + processSuccess(httpRequestStr, expectedBody); } + /** + * {@link com.alibaba.csp.sentinel.command.handler.FetchActiveRuleCommandHandler} + */ @Test - public void testGetRuleCommandInvalidType() { + public void testFetchActiveRuleCommandInvalidType() { String httpRequestStr = "GET /getRules HTTP/1.1" + CRLF + "Host: localhost:8719" + CRLF + CRLF; - String body = "invalid type"; + String expectedBody = "invalid type"; - processFailed(httpRequestStr, body); + processFailed(httpRequestStr, expectedBody); } @Test - public void testGetRuleCommandFlowEmptyRule() { + public void testFetchActiveRuleCommandEmptyRule() { String httpRequestStr = "GET /getRules?type=flow HTTP/1.1" + CRLF + "Host: localhost:8719" + CRLF + CRLF; - String body = "[]"; + String expectedBody = "[]"; - processSuccess(httpRequestStr, body); + processSuccess(httpRequestStr, expectedBody); } -// FIXME byteBuf.toString can't get body response now, need to find another way -// @Test - public void testGetRuleCommandFlowSomeRules() { + @Test + public void testFetchActiveRuleCommandSomeFlowRules() { List rules = new ArrayList(); FlowRule rule1 = new FlowRule(); rule1.setResource("key"); @@ -152,60 +162,86 @@ public void testGetRuleCommandFlowSomeRules() { String httpRequestStr = "GET /getRules?type=flow HTTP/1.1" + CRLF + "Host: localhost:8719" + CRLF + CRLF; - String body = ""; - processSuccess(httpRequestStr, body); + // body json + /* + String expectedBody = "[{\"clusterMode\":false,\"controlBehavior\":0,\"count\":20.0" + + ",\"grade\":1,\"limitApp\":\"default\",\"maxQueueingTimeMs\":500" + + ",\"resource\":\"key\",\"strategy\":0,\"warmUpPeriodSec\":10}]"; + */ + String expectedBody = JSON.toJSONString(rules); + + processSuccess(httpRequestStr, expectedBody); } - private void processError(String httpRequestStr, String body) { - processError(httpRequestStr, BAD_REQUEST, body); + /** + * {@link MultipleSlashNameCommandTestHandler} + * + * Test command whose mapping path and command name contain multiple / + */ + @Test + public void testMultipleSlashNameCommand() { + String httpRequestStr = "GET /aa/bb/cc HTTP/1.1" + CRLF + + "Host: localhost:8719" + CRLF + + CRLF; + String expectedBody = "MultipleSlashNameCommandTestHandler result"; + + processSuccess(httpRequestStr, expectedBody); } - private void processError(String httpRequestStr, HttpResponseStatus status, String body) { + private void processError(String httpRequestStr, String expectedBody) { + processError(httpRequestStr, BAD_REQUEST, expectedBody); + } + + private void processError(String httpRequestStr, HttpResponseStatus status, String expectedBody) { String httpResponseStr = processResponse(httpRequestStr); - assertErrorStatusAndBody(status, body, httpResponseStr); + assertErrorStatusAndBody(status, expectedBody, httpResponseStr); } - private void processSuccess(String httpRequestStr, String body) { - process(httpRequestStr, OK, body); + private void processSuccess(String httpRequestStr, String expectedBody) { + process(httpRequestStr, OK, expectedBody); } - private void processFailed(String httpRequestStr, String body) { - process(httpRequestStr, BAD_REQUEST, body); + private void processFailed(String httpRequestStr, String expectedBody) { + process(httpRequestStr, BAD_REQUEST, expectedBody); } - private void process(String httpRequestStr, HttpResponseStatus status, String body) { + private void process(String httpRequestStr, HttpResponseStatus status, String expectedBody) { String responseStr = processResponse(httpRequestStr); - assertStatusAndBody(status, body, responseStr); + assertStatusAndBody(status, expectedBody, responseStr); } private String processResponse(String httpRequestStr) { embeddedChannel.writeInbound(Unpooled.wrappedBuffer(httpRequestStr.getBytes(SENTINEL_CHARSET))); - ByteBuf byteBuf = embeddedChannel.readOutbound(); + StringBuilder sb = new StringBuilder(); + + ByteBuf byteBuf; + while ((byteBuf = embeddedChannel.readOutbound()) != null) { + sb.append(byteBuf.toString(SENTINEL_CHARSET)); + } - String responseStr = byteBuf.toString(SENTINEL_CHARSET); - return responseStr; + return sb.toString(); } - private void assertErrorStatusAndBody(HttpResponseStatus status, String body, String httpResponseStr) { + private void assertErrorStatusAndBody(HttpResponseStatus status, String expectedBody, String httpResponseStr) { StringBuilder text = new StringBuilder(); text.append(HttpVersion.HTTP_1_1.toString()).append(' ').append(status.toString()).append(CRLF); text.append("Content-Type: text/plain; charset=").append(SENTINEL_CHARSET_NAME).append(CRLF); text.append(CRLF); - text.append(body); + text.append(expectedBody); assertEquals(text.toString(), httpResponseStr); } - private void assertStatusAndBody(HttpResponseStatus status, String body, String httpResponseStr) { + private void assertStatusAndBody(HttpResponseStatus status, String expectedBody, String httpResponseStr) { StringBuilder text = new StringBuilder(); text.append(HttpVersion.HTTP_1_1.toString()).append(' ').append(status.toString()).append(CRLF); text.append("Content-Type: text/plain; charset=").append(SENTINEL_CHARSET_NAME).append(CRLF); - text.append("content-length: " + body.length()).append(CRLF); + text.append("content-length: " + expectedBody.length()).append(CRLF); text.append("connection: close").append(CRLF); text.append(CRLF); - text.append(body); + text.append(expectedBody); assertEquals(text.toString(), httpResponseStr); } diff --git a/sentinel-transport/sentinel-transport-netty-http/src/test/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerInitializerTest.java b/sentinel-transport/sentinel-transport-netty-http/src/test/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerInitializerTest.java index ba43448c8e..68c0fe77bc 100644 --- a/sentinel-transport/sentinel-transport-netty-http/src/test/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerInitializerTest.java +++ b/sentinel-transport/sentinel-transport-netty-http/src/test/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerInitializerTest.java @@ -30,30 +30,29 @@ * Test cases for {@link HttpServerInitializer}. * * @author cdfive - * @date 2018-12-19 */ public class HttpServerInitializerTest { @Test public void testInitChannel() throws Exception { - // mock Objects + // Mock Objects HttpServerInitializer httpServerInitializer = mock(HttpServerInitializer.class); SocketChannel socketChannel = mock(SocketChannel.class); ChannelPipeline channelPipeline = mock(ChannelPipeline.class); - // mock SocketChannel#pipeline() method + // Mock SocketChannel#pipeline() method when(socketChannel.pipeline()).thenReturn(channelPipeline); // HttpServerInitializer#initChannel(SocketChannel) call real method doCallRealMethod().when(httpServerInitializer).initChannel(socketChannel); - // start test for HttpServerInitializer#initChannel(SocketChannel) + // Start test for HttpServerInitializer#initChannel(SocketChannel) httpServerInitializer.initChannel(socketChannel); - // verify 4 times calling ChannelPipeline#addLast() method + // Verify 4 times calling ChannelPipeline#addLast() method verify(channelPipeline, times(4)).addLast(any(ChannelHandler.class)); - // verify the order of calling ChannelPipeline#addLast() method + // Verify the order of calling ChannelPipeline#addLast() method InOrder inOrder = inOrder(channelPipeline); inOrder.verify(channelPipeline).addLast(any(HttpRequestDecoder.class)); inOrder.verify(channelPipeline).addLast(any(HttpObjectAggregator.class)); diff --git a/sentinel-transport/sentinel-transport-netty-http/src/test/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerTest.java b/sentinel-transport/sentinel-transport-netty-http/src/test/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerTest.java index dbb213c18a..a82d0fca5c 100644 --- a/sentinel-transport/sentinel-transport-netty-http/src/test/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerTest.java +++ b/sentinel-transport/sentinel-transport-netty-http/src/test/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerTest.java @@ -31,7 +31,6 @@ * Test cases for {@link HttpServer}. * * @author cdfive - * @date 2018-12-19 */ public class HttpServerTest { @@ -39,20 +38,20 @@ public class HttpServerTest { @BeforeClass public static void beforeClass() { - // note: clear handlerMap first, as other test case may init HttpServer.handlerMap + // Note: clear handlerMap first, as other test case may init HttpServer.handlerMap // if not, run mvn test, the next assertEquals(0, HttpServer.handlerMap.size()) may fail HttpServer.handlerMap.clear(); - // create new HttpServer + // Create new HttpServer httpServer = new HttpServer(); - // no handler in handlerMap at first + // No handler in handlerMap at first assertEquals(0, HttpServer.handlerMap.size()); } @Before public void before() { - // clear handlerMap every method call + // Clear handlerMap every method call HttpServer.handlerMap.clear(); } @@ -61,37 +60,37 @@ public void testRegisterCommand() { String commandName; CommandHandler handler; - // if commandName is null, no handler added in handlerMap + // If commandName is null, no handler added in handlerMap commandName = null; handler = new VersionCommandHandler(); httpServer.registerCommand(commandName, handler); assertEquals(0, HttpServer.handlerMap.size()); - // if commandName is "", no handler added in handlerMap + // If commandName is "", no handler added in handlerMap commandName = ""; handler = new VersionCommandHandler(); httpServer.registerCommand(commandName, handler); assertEquals(0, HttpServer.handlerMap.size()); - // if handler is null, no handler added in handlerMap + // If handler is null, no handler added in handlerMap commandName = "version"; handler = null; httpServer.registerCommand(commandName, handler); assertEquals(0, HttpServer.handlerMap.size()); - // add one handler, commandName:version, handler:VersionCommandHandler + // Add one handler, commandName:version, handler:VersionCommandHandler commandName = "version"; handler = new VersionCommandHandler(); httpServer.registerCommand(commandName, handler); assertEquals(1, HttpServer.handlerMap.size()); - // add the same name Handler, no handler added in handlerMap + // Add the same name Handler, no handler added in handlerMap commandName = "version"; handler = new VersionCommandHandler(); httpServer.registerCommand(commandName, handler); assertEquals(1, HttpServer.handlerMap.size()); - // add another handler, commandName:basicInfo, handler:BasicInfoCommandHandler + // Add another handler, commandName:basicInfo, handler:BasicInfoCommandHandler commandName = "basicInfo"; handler = new BasicInfoCommandHandler(); httpServer.registerCommand(commandName, handler); @@ -102,16 +101,16 @@ public void testRegisterCommand() { public void testRegisterCommands() { Map handlerMap = null; - // if handlerMap is null, no handler added in handlerMap + // If handlerMap is null, no handler added in handlerMap httpServer.registerCommands(handlerMap); assertEquals(0, HttpServer.handlerMap.size()); - // add handler from CommandHandlerProvider + // Add handler from CommandHandlerProvider handlerMap = CommandHandlerProvider.getInstance().namedHandlers(); httpServer.registerCommands(handlerMap); - // check same size + // Check same size assertEquals(handlerMap.size(), HttpServer.handlerMap.size()); - // check not same reference + // Check not same reference assertTrue(handlerMap != HttpServer.handlerMap); } } diff --git a/sentinel-transport/sentinel-transport-netty-http/src/test/resources/META-INF/services/com.alibaba.csp.sentinel.command.CommandHandler b/sentinel-transport/sentinel-transport-netty-http/src/test/resources/META-INF/services/com.alibaba.csp.sentinel.command.CommandHandler new file mode 100755 index 0000000000..3c64c0a2e8 --- /dev/null +++ b/sentinel-transport/sentinel-transport-netty-http/src/test/resources/META-INF/services/com.alibaba.csp.sentinel.command.CommandHandler @@ -0,0 +1 @@ +com.alibaba.csp.sentinel.transport.command.handler.MultipleSlashNameCommandTestHandler \ No newline at end of file diff --git a/sentinel-transport/sentinel-transport-simple-http/pom.xml b/sentinel-transport/sentinel-transport-simple-http/pom.xml index 5712b46343..a588e4903e 100755 --- a/sentinel-transport/sentinel-transport-simple-http/pom.xml +++ b/sentinel-transport/sentinel-transport-simple-http/pom.xml @@ -5,7 +5,7 @@ sentinel-transport com.alibaba.csp - 1.7.0-SNAPSHOT + 1.7.2-SNAPSHOT 4.0.0 diff --git a/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/command/http/HttpEventTask.java b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/command/http/HttpEventTask.java index 80df0955e7..7658955bf9 100755 --- a/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/command/http/HttpEventTask.java +++ b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/command/http/HttpEventTask.java @@ -72,10 +72,10 @@ public void run() { new OutputStreamWriter(outputStream, Charset.forName(SentinelConfig.charset()))); String line = in.readLine(); - CommandCenterLog.info("[SimpleHttpCommandCenter] socket income: " + line - + "," + socket.getInetAddress()); + CommandCenterLog.info("[SimpleHttpCommandCenter] Socket income: " + line + + ", addr: " + socket.getInetAddress()); CommandRequest request = parseRequest(line); - + if (line.length() > 4 && StringUtil.equalsIgnoreCase("POST", line.substring(0, 4))) { // Deal with post method // Now simple-http only support form-encoded post request. @@ -95,12 +95,12 @@ public void run() { parseParams(postData, request); break; } - + bodyLine = in.readLine(); if (bodyLine == null) { break; } - // Body seperator + // Body separator if (StringUtil.isEmpty(bodyLine)) { bodyNext = true; continue; @@ -113,19 +113,25 @@ public void run() { String headerName = bodyLine.substring(0, index); String header = bodyLine.substring(index + 1).trim(); if (StringUtil.equalsIgnoreCase("content-type", headerName)) { + int idx = header.indexOf(";"); + if (idx > 0) { + header = header.substring(0, idx).trim(); + } if (StringUtil.equals("application/x-www-form-urlencoded", header)) { supported = true; } else { + CommandCenterLog.warn("Content-Type not supported: " + header); // not support request break; } } else if (StringUtil.equalsIgnoreCase("content-length", headerName)) { try { - int len = new Integer(header); + int len = Integer.parseInt(header); if (len > 0) { maxLength = len; } } catch (Exception e) { + CommandCenterLog.warn("Malformed content-length header value: " + header); } } } @@ -261,7 +267,7 @@ private CommandRequest parseRequest(String line) { parseParams(parameterStr, request); return request; } - + private void parseParams(String queryString, CommandRequest request) { for (String parameter : queryString.split("&")) { if (StringUtil.isBlank(parameter)) { diff --git a/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/SimpleHttpHeartbeatSender.java b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/SimpleHttpHeartbeatSender.java index a230c125b6..605beff007 100755 --- a/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/SimpleHttpHeartbeatSender.java +++ b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/SimpleHttpHeartbeatSender.java @@ -36,7 +36,6 @@ */ public class SimpleHttpHeartbeatSender implements HeartbeatSender { - private static final String HEARTBEAT_PATH = "/registry/machine"; private static final int OK_STATUS = 200; private static final long DEFAULT_INTERVAL = 1000 * 10; @@ -66,7 +65,7 @@ public boolean sendHeartbeat() throws Exception { return false; } - SimpleHttpRequest request = new SimpleHttpRequest(addr, HEARTBEAT_PATH); + SimpleHttpRequest request = new SimpleHttpRequest(addr, TransportConfig.getHeartbeatApiPath()); request.setParams(heartBeat.generateCurrentMessage()); try { SimpleHttpResponse response = httpClient.post(request); @@ -74,7 +73,7 @@ public boolean sendHeartbeat() throws Exception { return true; } } catch (Exception e) { - RecordLog.warn("[SimpleHttpHeartbeatSender] Failed to send heartbeat to " + addr + " : ", e); + RecordLog.warn("[SimpleHttpHeartbeatSender] Failed to send heartbeat to " + addr, e); } return false; } @@ -100,7 +99,7 @@ private List getDefaultConsoleIps() { try { String ipsStr = TransportConfig.getConsoleServer(); if (StringUtil.isEmpty(ipsStr)) { - RecordLog.warn("[NettyHttpHeartbeatSender] Dashboard server address not configured"); + RecordLog.warn("[SimpleHttpHeartbeatSender] Dashboard server address not configured"); return newAddrs; }