diff --git a/fizz-bootstrap/pom.xml b/fizz-bootstrap/pom.xml index 1df3a90..670c4aa 100644 --- a/fizz-bootstrap/pom.xml +++ b/fizz-bootstrap/pom.xml @@ -6,7 +6,7 @@ fizz-gateway-community com.fizzgate - 2.6.6 + 2.6.7-SNAPSHOT ../pom.xml @@ -32,7 +32,7 @@ 1.15 2.11.1 2.8.9 - 2.0.53.Final + 2.0.54.Final 2.2.9.RELEASE 1.30 Moore-SR13--> diff --git a/fizz-common/pom.xml b/fizz-common/pom.xml index b0a7f37..092eb35 100644 --- a/fizz-common/pom.xml +++ b/fizz-common/pom.xml @@ -5,7 +5,7 @@ fizz-gateway-community com.fizzgate - 2.6.6 + 2.6.7-SNAPSHOT ../pom.xml 4.0.0 @@ -17,6 +17,16 @@ + + com.github.seancfoley + ipaddress + + + + ognl + ognl + + org.openjdk.jol jol-core diff --git a/fizz-common/src/main/java/we/spring/http/server/reactive/ext/FizzServerHttpRequestDecorator.java b/fizz-common/src/main/java/we/spring/http/server/reactive/ext/FizzServerHttpRequestDecorator.java index a38b5d4..083fb13 100644 --- a/fizz-common/src/main/java/we/spring/http/server/reactive/ext/FizzServerHttpRequestDecorator.java +++ b/fizz-common/src/main/java/we/spring/http/server/reactive/ext/FizzServerHttpRequestDecorator.java @@ -68,7 +68,10 @@ public class FizzServerHttpRequestDecorator extends ServerHttpRequestDecorator { public FizzServerHttpRequestDecorator(ServerHttpRequest delegate) { super(delegate); this.delegate = (AbstractServerHttpRequest) delegate; - nativeRequest = this.delegate.getNativeRequest(); + try { + nativeRequest = this.delegate.getNativeRequest(); + } catch (IllegalStateException e) { + } } @Override @@ -120,6 +123,9 @@ public class FizzServerHttpRequestDecorator extends ServerHttpRequestDecorator { } private MultiValueMap initCookies() { + if (nativeRequest == null) { + return null; + } MultiValueMap cookies = new LinkedMultiValueMap<>(); for (CharSequence name : nativeRequest.cookies().keySet()) { for (Cookie cookie : nativeRequest.cookies().get(name)) { diff --git a/fizz-common/src/test/java/we/OgnlTests.java b/fizz-common/src/test/java/we/OgnlTests.java new file mode 100644 index 0000000..c371400 --- /dev/null +++ b/fizz-common/src/test/java/we/OgnlTests.java @@ -0,0 +1,47 @@ +package we; + +import ognl.Ognl; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class OgnlTests { + + @Test + void testGet() throws Exception { + + Root root = new Root(); + + Map query = new HashMap<>(); + query.put("version", "v2"); + query.put("userId", 1234563); + query.put("age", 25); + + root.put("query", query); + + Map client = new HashMap<>(); + client.put("ip", "10.2.3.4"); + client.put("ip2", "10.2.3.88"); + root.put("client", client); + + Boolean result = (Boolean) Ognl.getValue("checkIp(client.ip2) && (query.version == 'v2' || query.age < 20) and query.age in (22,25,30) && client.ip=='10.2.3.4'", root); + + System.out.println(result); + assertEquals(true, result); + } +} + +class Root extends HashMap { + + public Root() { + } + + public boolean checkIp(String ip) { + System.out.println(ip); + return ip.equals("10.2.3.88"); + } + +} \ No newline at end of file diff --git a/fizz-core/pom.xml b/fizz-core/pom.xml index 77191de..f1798b4 100644 --- a/fizz-core/pom.xml +++ b/fizz-core/pom.xml @@ -5,7 +5,7 @@ fizz-gateway-community com.fizzgate - 2.6.6 + 2.6.7-SNAPSHOT ../pom.xml 4.0.0 diff --git a/fizz-core/src/main/java/we/filter/AggregateFilter.java b/fizz-core/src/main/java/we/filter/AggregateFilter.java index a87536f..804ce5a 100644 --- a/fizz-core/src/main/java/we/filter/AggregateFilter.java +++ b/fizz-core/src/main/java/we/filter/AggregateFilter.java @@ -83,6 +83,7 @@ public class AggregateFilter implements WebFilter { String serviceId = WebUtils.getBackendService(exchange); if (serviceId == null) { return chain.filter(exchange); + } else if (WebUtils.ignorePlugin(exchange) && WebUtils.getRoute(exchange).type == ApiConfig.Type.SERVICE_AGGREGATE) { } else { byte act = WebUtils.getApiConfigType(exchange); if (act == ApiConfig.Type.UNDEFINED) { diff --git a/fizz-core/src/main/java/we/filter/FlowControlFilter.java b/fizz-core/src/main/java/we/filter/FlowControlFilter.java index a666412..e66a6d4 100644 --- a/fizz-core/src/main/java/we/filter/FlowControlFilter.java +++ b/fizz-core/src/main/java/we/filter/FlowControlFilter.java @@ -168,7 +168,7 @@ public class FlowControlFilter extends FizzWebFilter { long currentTimeMillis = System.currentTimeMillis(); String blockedResourceId = result.getBlockedResourceId(); if (BlockType.CIRCUIT_BREAK == result.getBlockType()) { - fizzMonitorService.sendAlarm(service, path, FizzMonitorService.CIRCUIT_BREAK_ALARM, null, currentTimeMillis); + fizzMonitorService.alarm(service, path, FizzMonitorService.CIRCUIT_BREAK_ALARM, null); // log.info("{} trigger {} circuit breaker limit", traceId, blockedResourceId, LogService.BIZ_ID, traceId); log.info("{} trigger {} circuit breaker limit", traceId, blockedResourceId); @@ -200,11 +200,11 @@ public class FlowControlFilter extends FizzWebFilter { } else { if (BlockType.CONCURRENT_REQUEST == result.getBlockType()) { - fizzMonitorService.sendAlarm(service, path, FizzMonitorService.RATE_LIMIT_ALARM, concurrents, currentTimeMillis); + fizzMonitorService.alarm(service, path, FizzMonitorService.RATE_LIMIT_ALARM, concurrents); // log.info("{} exceed {} flow limit, blocked by maximum concurrent requests", traceId, blockedResourceId, LogService.BIZ_ID, traceId); log.info("{} exceed {} flow limit, blocked by maximum concurrent requests", traceId, blockedResourceId); } else { - fizzMonitorService.sendAlarm(service, path, FizzMonitorService.RATE_LIMIT_ALARM, qps, currentTimeMillis); + fizzMonitorService.alarm(service, path, FizzMonitorService.RATE_LIMIT_ALARM, qps); // log.info("{} exceed {} flow limit, blocked by maximum QPS", traceId, blockedResourceId, LogService.BIZ_ID, traceId); log.info("{} exceed {} flow limit, blocked by maximum QPS", traceId, blockedResourceId); } @@ -246,11 +246,11 @@ public class FlowControlFilter extends FizzWebFilter { cb.transit(CircuitBreaker.State.RESUME_DETECTIVE, CircuitBreaker.State.OPEN, currentTimeSlot, flowStat); } if (statusCode == HttpStatus.GATEWAY_TIMEOUT) { - fizzMonitorService.sendAlarm(finalService, finalPath, FizzMonitorService.TIMEOUT_ALARM, t.getMessage(), start); + fizzMonitorService.alarm(finalService, finalPath, FizzMonitorService.TIMEOUT_ALARM, t.getMessage()); } else if (statusCode.is5xxServerError()) { - fizzMonitorService.sendAlarm(finalService, finalPath, FizzMonitorService.ERROR_ALARM, String.valueOf(statusCode.value()), start); + fizzMonitorService.alarm(finalService, finalPath, FizzMonitorService.ERROR_ALARM, String.valueOf(statusCode.value())); } else if (s == SignalType.ON_ERROR && t != null) { - fizzMonitorService.sendAlarm(finalService, finalPath, FizzMonitorService.ERROR_ALARM, t.getMessage(), start); + fizzMonitorService.alarm(finalService, finalPath, FizzMonitorService.ERROR_ALARM, t.getMessage()); } } else { flowStat.addRequestRT(resourceConfigs, currentTimeSlot, rt, true, statusCode); diff --git a/fizz-core/src/main/java/we/monitor/FizzMonitorService.java b/fizz-core/src/main/java/we/monitor/FizzMonitorService.java index 6d50ae7..38a76cf 100644 --- a/fizz-core/src/main/java/we/monitor/FizzMonitorService.java +++ b/fizz-core/src/main/java/we/monitor/FizzMonitorService.java @@ -21,32 +21,50 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.ReactiveStringRedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import we.config.AggregateRedisConfig; +import we.config.SchedConfig; import we.util.Consts; +import we.util.DateTimeUtils; +import we.util.JacksonUtils; import we.util.ThreadContext; import javax.annotation.Resource; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; /** * @author hongqiaowei */ @Service -public class FizzMonitorService { +public class FizzMonitorService extends SchedConfig { - private static final Logger LOGGER = LoggerFactory.getLogger("monitor"); + private static final Logger MONITOR_LOGGER = LoggerFactory.getLogger("monitor"); + private static final Logger LOGGER = LoggerFactory.getLogger(FizzMonitorService.class); public static final byte ERROR_ALARM = 1; public static final byte TIMEOUT_ALARM = 2; public static final byte RATE_LIMIT_ALARM = 3; public static final byte CIRCUIT_BREAK_ALARM = 4; - private static final String _service = "\"service\":"; - private static final String _path = "\"path\":"; - private static final String _type = "\"type\":"; - private static final String _desc = "\"desc\":"; - private static final String _timestamp = "\"timestamp\":"; + private static class Alarm { + + public String service; + public String path; + public int type; + public String desc; + public long timestamp; + public int reqs = 0; + public long start; + + @Override + public String toString() { + return JacksonUtils.writeValueAsString(this); + } + } @Value("${fizz.monitor.alarm.enable:true}") private boolean alarmEnable; @@ -54,38 +72,102 @@ public class FizzMonitorService { @Value("${fizz.monitor.alarm.dest:redis}") private String dest; - @Value("${fizz.monitor.alarm.queue:fizz_alarm_channel}") + @Value("${fizz.monitor.alarm.queue:fizz_alarm_channel_new}") private String queue; @Resource(name = AggregateRedisConfig.AGGREGATE_REACTIVE_REDIS_TEMPLATE) private ReactiveStringRedisTemplate rt; - public void sendAlarm(String service, String path, byte type, String desc, long timestamp) { + private Map + > + > + threadTimeWinAlarmMap = new HashMap<>(); + + public void alarm(String service, String path, byte type, String desc) { if (alarmEnable) { - StringBuilder b = ThreadContext.getStringBuilder(); - b.append(Consts.S.LEFT_BRACE); - b.append(_service); toJsonStrVal(b, service); b.append(Consts.S.COMMA); - b.append(_path); toJsonStrVal(b, path); b.append(Consts.S.COMMA); - b.append(_type); b.append(type); b.append(Consts.S.COMMA); + long tid = Thread.currentThread().getId(); + Map> timeWinAlarmMap = threadTimeWinAlarmMap.get(tid); + if (timeWinAlarmMap == null) { + timeWinAlarmMap = new LinkedHashMap>(4, 1) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > 2; + } + }; + threadTimeWinAlarmMap.put(tid, timeWinAlarmMap); + } - if (desc != null) { - b.append(_desc); toJsonStrVal(b, desc); b.append(Consts.S.COMMA); - } + long currentTimeWinStart = DateTimeUtils.get10sTimeWinStart(1); + Map alarmMap = timeWinAlarmMap.computeIfAbsent(currentTimeWinStart, k -> new HashMap<>(128)); - b.append(_timestamp) .append(timestamp); - b.append(Consts.S.RIGHT_BRACE); - String msg = b.toString(); - if (Consts.KAFKA.equals(dest)) { - // LOGGER.warn(msg, LogService.HANDLE_STGY, LogService.toKF(queue)); - LOGGER.info(msg); - } else { - rt.convertAndSend(queue, msg).subscribe(); + String key = ThreadContext.getStringBuilder().append(service).append(path).append(type).toString(); + Alarm alarm = alarmMap.get(key); + if (alarm == null) { + alarm = new Alarm(); + alarm.service = service; + alarm.path = path; + alarm.type = type; + alarmMap.put(key, alarm); + } + alarm.desc = desc; + alarm.timestamp = System.currentTimeMillis(); + alarm.reqs++; + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("update alarm: {} at {}", alarm, DateTimeUtils.convert(alarm.timestamp, Consts.DP.DP19)); } } } - private static void toJsonStrVal(StringBuilder b, String value) { - b.append(Consts.S.DOUBLE_QUOTE).append(value).append(Consts.S.DOUBLE_QUOTE); + @Scheduled(cron = "${fizz.monitor.alarm.sched.cron:2/10 * * * * ?}") + public void sched() { + long prevTimeWinStart = DateTimeUtils.get10sTimeWinStart(2); + Map alarmMap = ThreadContext.getHashMap(); + threadTimeWinAlarmMap.forEach( + (t, timeWinAlarmMap) -> { + Map alarmMap0 = timeWinAlarmMap.get(prevTimeWinStart); + if (alarmMap0 != null) { + alarmMap0.forEach( + (spt, alarm) -> { + Alarm a = alarmMap.get(spt); + if (a == null) { + alarm.start = prevTimeWinStart; + alarmMap.put(spt, alarm); + } else { + a.reqs = a.reqs + alarm.reqs; + if (alarm.timestamp > a.timestamp) { + a.timestamp = alarm.timestamp; + a.desc = alarm.desc; + } + } + } + ); + } + } + ); + if (alarmMap.isEmpty()) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("no alarm in {} window", DateTimeUtils.convert(prevTimeWinStart, Consts.DP.DP19)); + } + } else { + alarmMap.forEach( + (spt, alarm) -> { + String msg = alarm.toString(); + if (Consts.KAFKA.equals(dest)) { + MONITOR_LOGGER.info(msg); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("send alarm {} which belong to {} window to topic", msg, DateTimeUtils.convert(alarm.start, Consts.DP.DP19)); + } + } else { + rt.convertAndSend(queue, msg).subscribe(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("send alarm {} which belong to {} window to channel {}", msg, DateTimeUtils.convert(alarm.start, Consts.DP.DP19), queue); + } + } + } + ); + } } } diff --git a/fizz-core/src/main/java/we/plugin/FizzPluginFilterChain.java b/fizz-core/src/main/java/we/plugin/FizzPluginFilterChain.java index 6700a4b..07289f3 100644 --- a/fizz-core/src/main/java/we/plugin/FizzPluginFilterChain.java +++ b/fizz-core/src/main/java/we/plugin/FizzPluginFilterChain.java @@ -44,6 +44,10 @@ public final class FizzPluginFilterChain { } public static Mono next(ServerWebExchange exchange) { + if (WebUtils.ignorePlugin(exchange)) { + WebFilterChain chain = exchange.getAttribute(WEB_FILTER_CHAIN); + return chain.filter(exchange); + } Iterator it = exchange.getAttribute(pluginConfigsIt); Route route = WebUtils.getRoute(exchange); if (it == null || route.pluginConfigsChange) { diff --git a/fizz-core/src/main/java/we/plugin/auth/ApiConfig.java b/fizz-core/src/main/java/we/plugin/auth/ApiConfig.java index b12fd84..9cebb86 100644 --- a/fizz-core/src/main/java/we/plugin/auth/ApiConfig.java +++ b/fizz-core/src/main/java/we/plugin/auth/ApiConfig.java @@ -47,6 +47,7 @@ public class ApiConfig { static final byte REVERSE_PROXY = 3; static final byte CALLBACK = 4; static final byte DUBBO = 5; + static final byte DIRECT_RESPONSE = 6; } public static final String ALL_METHOD = "AM"; @@ -226,6 +227,7 @@ public class ApiConfig { Route r = new Route().dedicatedLine( this.dedicatedLine) .type( this.type) .method( request.getMethod()) + .path( this.path) .registryCenter( this.registryCenter) .backendService( this.backendService) .backendPath( this.backendPath) diff --git a/fizz-core/src/main/java/we/plugin/stat/AccessStatSchedConfig.java b/fizz-core/src/main/java/we/plugin/stat/AccessStatSchedConfig.java index 6884a40..20b4945 100644 --- a/fizz-core/src/main/java/we/plugin/stat/AccessStatSchedConfig.java +++ b/fizz-core/src/main/java/we/plugin/stat/AccessStatSchedConfig.java @@ -1,84 +1,84 @@ -///* -// * Copyright (C) 2021 the original author or authors. -// * -// * This program is free software: you can redistribute it and/or modify -// * it under the terms of the GNU General Public License as published by -// * the Free Software Foundation, either version 3 of the License, or -// * any later version. -// * -// * This program is distributed in the hope that it will be useful, -// * but WITHOUT ANY WARRANTY; without even the implied warranty of -// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// * GNU General Public License for more details. -// * -// * You should have received a copy of the GNU General Public License -// * along with this program. If not, see . -// */ -// -//package we.plugin.stat; -// -//import org.slf4j.Logger; -//import org.slf4j.LoggerFactory; -//import org.springframework.context.annotation.Configuration; -//import org.springframework.data.redis.core.ReactiveStringRedisTemplate; -//import org.springframework.scheduling.annotation.Scheduled; -//import we.config.AggregateRedisConfig; -//import we.config.SchedConfig; -//import we.util.Consts; -//import we.util.DateTimeUtils; -//import we.util.StringUtils; -// -//import javax.annotation.Resource; -//import java.util.Map; -// -///** -// * @author hongqiaowei -// */ -// -//@Configuration -//public class AccessStatSchedConfig extends SchedConfig { -// -// private static final Logger LOGGER = LoggerFactory.getLogger(AccessStatSchedConfig.class); -// -// private static final Logger STAT_LOGGER = LoggerFactory.getLogger("stat"); -// -// @Resource(name = AggregateRedisConfig.AGGREGATE_REACTIVE_REDIS_TEMPLATE) -// private ReactiveStringRedisTemplate rt; -// -// @Resource -// private StatPluginFilterProperties statPluginFilterProperties; -// -// @Resource -// private StatPluginFilter statPluginFilter; -// -// @Scheduled(cron = "${fizz-access-stat-sched.cron:2/10 * * * * ?}") -// public void sched() { -// long prevTimeWinStart = DateTimeUtils.get10sTimeWinStart(2); -// Map accessStatMap = statPluginFilter.getAccessStat(prevTimeWinStart); -// -// if (accessStatMap.isEmpty()) { -// if (LOGGER.isDebugEnabled()) { -// LOGGER.debug("no access stat in {} window", DateTimeUtils.convert(prevTimeWinStart, Consts.DP.DP19)); -// } -// } else { -// accessStatMap.forEach( -// (smp, accessStat) -> { -// String msg = accessStat.toString(); -// String topic = statPluginFilterProperties.getFizzAccessStatTopic(); -// if (StringUtils.isBlank(topic)) { -// String channel = statPluginFilterProperties.getFizzAccessStatChannel(); -// rt.convertAndSend(channel, msg).subscribe(); -// if (LOGGER.isDebugEnabled()) { -// LOGGER.debug("send access stat {} which belong to {} window to channel {}", msg, DateTimeUtils.convert(accessStat.start, Consts.DP.DP19), channel); -// } -// } else { -// STAT_LOGGER.info(msg); -// if (LOGGER.isDebugEnabled()) { -// LOGGER.debug("send access stat {} which belong to {} window to topic", msg, DateTimeUtils.convert(accessStat.start, Consts.DP.DP19)); -// } -// } -// } -// ); -// } -// } -//} +/* + * Copyright (C) 2021 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package we.plugin.stat; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.ReactiveStringRedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import we.config.AggregateRedisConfig; +import we.config.SchedConfig; +import we.util.Consts; +import we.util.DateTimeUtils; +import we.util.StringUtils; + +import javax.annotation.Resource; +import java.util.Map; + +/** + * @author hongqiaowei + */ + +@Configuration +public class AccessStatSchedConfig extends SchedConfig { + + private static final Logger LOGGER = LoggerFactory.getLogger(AccessStatSchedConfig.class); + + private static final Logger STAT_LOGGER = LoggerFactory.getLogger("stat"); + + @Resource(name = AggregateRedisConfig.AGGREGATE_REACTIVE_REDIS_TEMPLATE) + private ReactiveStringRedisTemplate rt; + + @Resource + private StatPluginFilterProperties statPluginFilterProperties; + + @Resource + private StatPluginFilter statPluginFilter; + + @Scheduled(cron = "${fizz-access-stat-sched.cron:2/10 * * * * ?}") + public void sched() { + long prevTimeWinStart = DateTimeUtils.get10sTimeWinStart(2); + Map accessStatMap = statPluginFilter.getAccessStat(prevTimeWinStart); + + if (accessStatMap.isEmpty()) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("no access stat in {} window", DateTimeUtils.convert(prevTimeWinStart, Consts.DP.DP19)); + } + } else { + accessStatMap.forEach( + (smp, accessStat) -> { + String msg = accessStat.toString(); + String topic = statPluginFilterProperties.getFizzAccessStatTopic(); + if (StringUtils.isBlank(topic)) { + String channel = statPluginFilterProperties.getFizzAccessStatChannel(); + rt.convertAndSend(channel, msg).subscribe(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("send access stat {} which belong to {} window to channel {}", msg, DateTimeUtils.convert(accessStat.start, Consts.DP.DP19), channel); + } + } else { + STAT_LOGGER.info(msg); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("send access stat {} which belong to {} window to topic", msg, DateTimeUtils.convert(accessStat.start, Consts.DP.DP19)); + } + } + } + ); + } + } +} diff --git a/fizz-core/src/main/java/we/plugin/stat/StatPluginFilter.java b/fizz-core/src/main/java/we/plugin/stat/StatPluginFilter.java index b021700..d668efa 100644 --- a/fizz-core/src/main/java/we/plugin/stat/StatPluginFilter.java +++ b/fizz-core/src/main/java/we/plugin/stat/StatPluginFilter.java @@ -17,107 +17,107 @@ package we.plugin.stat; -import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.data.redis.core.ReactiveStringRedisTemplate; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; -import we.config.AggregateRedisConfig; import we.plugin.PluginFilter; -import we.plugin.auth.GatewayGroupService; import we.util.Consts; +import we.util.DateTimeUtils; import we.util.ThreadContext; import we.util.WebUtils; import javax.annotation.Resource; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; /** * @author hongqiaowei - * @apiNote unstable. */ @Component(StatPluginFilter.STAT_PLUGIN_FILTER) public class StatPluginFilter extends PluginFilter { - private static final Logger log = LoggerFactory.getLogger("stat"); + private static final Logger LOGGER = LoggerFactory.getLogger(StatPluginFilter.class); public static final String STAT_PLUGIN_FILTER = "statPlugin"; - private static final String ip = "\"ip\":"; - - private static final String gatewayGroup = "\"gatewayGroup\":"; - - private static final String service = "\"service\":"; - - private static final String appid = "\"appid\":"; - - private static final String apiMethod = "\"apiMethod\":"; - - private static final String apiPath = "\"apiPath\":"; - - private static final String reqTime = "\"reqTime\":"; - @Resource private StatPluginFilterProperties statPluginFilterProperties; - @Resource(name = AggregateRedisConfig.AGGREGATE_REACTIVE_REDIS_TEMPLATE) - private ReactiveStringRedisTemplate rt; - - @Resource - private GatewayGroupService gatewayGroupService; + private Map + > + > + threadTimeWinAccessStatMap = new HashMap<>(); @Override public Mono doFilter(ServerWebExchange exchange, Map config, String fixedConfig) { if (statPluginFilterProperties.isStatOpen()) { - StringBuilder b = ThreadContext.getStringBuilder(); - b.append(Consts.S.LEFT_BRACE); - b.append(ip); toJsonStringValue(b, WebUtils.getOriginIp(exchange)); b.append(Consts.S.COMMA); - b.append(gatewayGroup); toJsonStringValue(b, currentGatewayGroups()); b.append(Consts.S.COMMA); - b.append(service); toJsonStringValue(b, WebUtils.getClientService(exchange)); b.append(Consts.S.COMMA); + long tid = Thread.currentThread().getId(); + Map> timeWinAccessStatMap = threadTimeWinAccessStatMap.get(tid); + if (timeWinAccessStatMap == null) { + timeWinAccessStatMap = new LinkedHashMap>(4, 1) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > 2; + } + }; + threadTimeWinAccessStatMap.put(tid, timeWinAccessStatMap); + } - String appId = WebUtils.getAppId(exchange); - if (appId != null) { - b.append(appid); toJsonStringValue(b, appId); b.append(Consts.S.COMMA); - } + long currentTimeWinStart = DateTimeUtils.get10sTimeWinStart(1); + Map accessStatMap = timeWinAccessStatMap.computeIfAbsent(currentTimeWinStart, k -> new HashMap<>(128)); - b.append(apiMethod); toJsonStringValue(b, exchange.getRequest().getMethodValue()); b.append(Consts.S.COMMA); - b.append(apiPath); toJsonStringValue(b, WebUtils.getClientReqPath(exchange)); b.append(Consts.S.COMMA); - b.append(reqTime) .append(System.currentTimeMillis()); - b.append(Consts.S.RIGHT_BRACE); - - if (StringUtils.isBlank(statPluginFilterProperties.getFizzAccessStatTopic())) { - rt.convertAndSend(statPluginFilterProperties.getFizzAccessStatChannel(), b.toString()).subscribe(); - } else { - // log.warn(b.toString(), LogService.HANDLE_STGY, LogService.toKF(statPluginFilterProperties.getFizzAccessStatTopic())); // for internal use - log.info(b.toString()); + String service = WebUtils.getClientService(exchange); + String method = exchange.getRequest().getMethodValue(); + String path = WebUtils.getClientReqPath(exchange); + String key = ThreadContext.getStringBuilder().append(service).append(method).append(path).toString(); + AccessStat accessStat = accessStatMap.get(key); + if (accessStat == null) { + accessStat = new AccessStat(); + accessStat.service = service; + accessStat.apiMethod = method; + accessStat.apiPath = path; + accessStatMap.put(key, accessStat); + } + accessStat.reqTime = System.currentTimeMillis(); + accessStat.reqs++; + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("update access stat: {}, which request at {}", accessStat, DateTimeUtils.convert(accessStat.reqTime, Consts.DP.DP19)); } } return WebUtils.transmitSuccessFilterResultAndEmptyMono(exchange, STAT_PLUGIN_FILTER, null); } - private String currentGatewayGroups() { - int sz = gatewayGroupService.currentGatewayGroupSet.size(); - if (sz == 1) { - return gatewayGroupService.currentGatewayGroupSet.iterator().next(); - } - StringBuilder b = ThreadContext.getStringBuilder(ThreadContext.sb0); - byte i = 0; - for (String g : gatewayGroupService.currentGatewayGroupSet) { - b.append(g); - i++; - if (i < sz) { - b.append(Consts.S.COMMA); - } - } - return b.toString(); - } - - private static void toJsonStringValue(StringBuilder b, String value) { - b.append(Consts.S.DOUBLE_QUOTE).append(value).append(Consts.S.DOUBLE_QUOTE); + public Map getAccessStat(long timeWinStart) { + Map result = ThreadContext.getHashMap(); + threadTimeWinAccessStatMap.forEach( + (t, timeWinAccessStatMap) -> { + Map accessStatMap = timeWinAccessStatMap.get(timeWinStart); + if (accessStatMap != null) { + accessStatMap.forEach( + (smp, accessStat) -> { + AccessStat as = result.get(smp); + if (as == null) { + accessStat.start = timeWinStart; + result.put(smp, accessStat); + } else { + as.reqs = as.reqs + accessStat.reqs; + if (accessStat.reqTime > as.reqTime) { + as.reqTime = accessStat.reqTime; + } + } + } + ); + } + } + ); + return result; } } diff --git a/fizz-core/src/main/java/we/plugin/stat/StatPluginFilterProperties.java b/fizz-core/src/main/java/we/plugin/stat/StatPluginFilterProperties.java index cd8b471..3697196 100644 --- a/fizz-core/src/main/java/we/plugin/stat/StatPluginFilterProperties.java +++ b/fizz-core/src/main/java/we/plugin/stat/StatPluginFilterProperties.java @@ -37,7 +37,7 @@ public class StatPluginFilterProperties { @Value("${stat.open:false}") private boolean statOpen = false; - @Value("${stat.channel:fizz_access_stat}") + @Value("${stat.channel:fizz_access_stat_new}") private String fizzAccessStatChannel; @Value("${stat.topic:}") diff --git a/fizz-core/src/main/java/we/proxy/Route.java b/fizz-core/src/main/java/we/proxy/Route.java index 269e339..52773b5 100644 --- a/fizz-core/src/main/java/we/proxy/Route.java +++ b/fizz-core/src/main/java/we/proxy/Route.java @@ -18,6 +18,7 @@ package we.proxy; import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import we.plugin.PluginConfig; import we.util.Consts; import we.util.JacksonUtils; @@ -36,6 +37,8 @@ public class Route { public HttpMethod method; + public String path; + public String registryCenter; public String backendService; @@ -68,6 +71,10 @@ public class Route { public long retryInterval = 0; + public MediaType contentType; + + public String body; + public Route dedicatedLine(boolean b) { dedicatedLine = b; return this; @@ -83,6 +90,11 @@ public class Route { return this; } + public Route path(String p) { + path = p; + return this; + } + public Route registryCenter(String rc) { registryCenter = rc; return this; @@ -150,6 +162,16 @@ public class Route { return this; } + public Route contentType(MediaType type) { + contentType = type; + return this; + } + + public Route body(String b) { + body = b; + return this; + } + @Deprecated public String getBackendPathQuery() { if (query != null) { diff --git a/fizz-core/src/main/java/we/service_registry/RegistryCenterService.java b/fizz-core/src/main/java/we/service_registry/RegistryCenterService.java index 670eb3a..f6c7555 100644 --- a/fizz-core/src/main/java/we/service_registry/RegistryCenterService.java +++ b/fizz-core/src/main/java/we/service_registry/RegistryCenterService.java @@ -28,10 +28,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import we.config.AggregateRedisConfig; import we.config.SystemConfig; -import we.util.Consts; -import we.util.JacksonUtils; -import we.util.Result; -import we.util.ThreadContext; +import we.util.*; import javax.annotation.Resource; import java.util.Collections; @@ -182,7 +179,11 @@ public class RegistryCenterService implements ApplicationListener fizz-gateway-community com.fizzgate - 2.6.6 + 2.6.7-SNAPSHOT ../pom.xml 4.0.0 diff --git a/fizz-plugin/src/main/java/we/plugin/grayrelease/GrayReleasePlugin.java b/fizz-plugin/src/main/java/we/plugin/grayrelease/GrayReleasePlugin.java new file mode 100644 index 0000000..ea175aa --- /dev/null +++ b/fizz-plugin/src/main/java/we/plugin/grayrelease/GrayReleasePlugin.java @@ -0,0 +1,495 @@ +/* + * Copyright (C) 2021 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package we.plugin.grayrelease; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.interfaces.Claim; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.fasterxml.jackson.core.type.TypeReference; +import inet.ipaddr.AddressStringException; +import inet.ipaddr.IPAddress; +import inet.ipaddr.IPAddressSeqRange; +import inet.ipaddr.IPAddressString; +import ognl.Ognl; +import ognl.OgnlException; +import org.apache.logging.log4j.ThreadContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.*; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import org.springframework.util.MultiValueMap; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; +import we.plugin.FizzPluginFilterChain; +import we.plugin.auth.ApiConfig; +import we.plugin.requestbody.RequestBodyPlugin; +import we.proxy.Route; +import we.spring.web.server.ext.FizzServerWebExchangeDecorator; +import we.util.*; + +import java.nio.charset.StandardCharsets; +import java.util.*; + +/** + * @author hongqiaowei + */ + +@Component(GrayReleasePlugin.GRAY_RELEASE_PLUGIN) +public class GrayReleasePlugin extends RequestBodyPlugin { + + private static final Logger LOGGER = LoggerFactory.getLogger(GrayReleasePlugin.class); + + public static final String GRAY_RELEASE_PLUGIN = "GrayReleasePlugin"; + + private static final String triggerCondition = "triggerCondition"; + private static final String routeType = "routeType"; + private static final String routeConfig = "routeConfig"; + private static final String routeConfigMap = "routeConfigMap"; + private static final String method = "method"; + private static final String path = "path"; + private static final String contentType = "contentType"; + private static final String body = "body"; + private static final String form = "form"; + private static final String cookie = "cookie"; + private static final String header = "header"; + private static final String query = "query"; + private static final String client = "client"; + private static final String ip = "ip"; + + private static class OgnlRoot extends HashMap { + + public double random() { + return Math.random(); + } + + public boolean exist(String key) { + String[] keys = StringUtils.split(key, Consts.S.DOT); + Map m = this; + int keyLen = keys.length; + for (int i = 0; i < keyLen; i++) { + String k = keys[i]; + if (m.containsKey(k)) { + Object obj = m.get(k); + if (obj instanceof Map) { + m = (Map) obj; + } else if (i + 1 != keyLen) { + return false; + } + } else { + return false; + } + } + return true; + } + + public boolean matches(String key, String regex) throws OgnlException { + String value = (String) Ognl.getValue(key, this); + if (value == null) { + return false; + } + return value.matches(regex); + } + + public String jwtClaim(String name) { + Map headerMap = (Map) get(GrayReleasePlugin.header); + if (headerMap == null) { + return null; + } else { + String token = (String) headerMap.get(HttpHeaders.AUTHORIZATION.toLowerCase()); + if (StringUtils.isBlank(token)) { + return null; + } else if (token.length() > 7 && token.substring(0, 7).equalsIgnoreCase("Bearer ")) { + token = token.substring(7); + } + DecodedJWT jwt = JWT.decode(token); + Claim claim = jwt.getClaim(name); + if (claim == null) { + return null; + } + return claim.asString(); + } + } + + public boolean clientIpInRange(String range) throws AddressStringException { + Map cli = (Map) get(client); + if (cli == null) { + return false; + } else { + String pi = (String) cli.get(ip); + if (pi == null) { + return false; + } else { + return ipInRange(pi, range); + } + } + } + + public boolean clientIpInRange(String rangeStartIp, String rangeEndIp) throws AddressStringException { + Map cli = (Map) get(client); + if (cli == null) { + return false; + } else { + String pi = (String) cli.get(ip); + if (pi == null) { + return false; + } else { + return ipInRange(pi, rangeStartIp, rangeEndIp); + } + } + } + + public boolean ipInRange(String ip, String range) throws AddressStringException { + IPAddress ipAddress = new IPAddressString(ip).toAddress(); + IPAddress rangeAddress = new IPAddressString(range).getAddress(); + return rangeAddress.contains(ipAddress); + } + + public boolean ipInRange(String ip, String rangeStartIp, String rangeEndIp) throws AddressStringException { + IPAddress startIPAddress = new IPAddressString(rangeStartIp).getAddress(); + IPAddress endIPAddress = new IPAddressString(rangeEndIp).getAddress(); + IPAddressSeqRange ipRange = startIPAddress.spanWithRange(endIPAddress); + IPAddress ipAddress = new IPAddressString(ip).toAddress(); + return ipRange.contains(ipAddress); + } + + public String toString() { + return JacksonUtils.writeValueAsString(this); + } + } + + @Override + public Mono doFilter(ServerWebExchange exchange, Map config) { + String traceId = WebUtils.getTraceId(exchange); + ThreadContext.put(Consts.TRACE_ID, traceId); + String tc = (String) config.get(triggerCondition); + Object ognlRoot = request2ognlContext(exchange); + Boolean conditionMatch = false; + try { + conditionMatch = (Boolean) Ognl.getValue(tc, ognlRoot); + } catch (OgnlException e) { + LOGGER.error("calc condition expression {} with context {}", tc, ognlRoot, e); + throw new RuntimeException(e); + } + if (conditionMatch) { + Route route = WebUtils.getRoute(exchange); + changeRoute(exchange, route, config); + if (route.type == ApiConfig.Type.DIRECT_RESPONSE) { + HttpHeaders hdrs = new HttpHeaders(); + hdrs.setContentType(route.contentType); + return WebUtils.response(exchange, HttpStatus.OK, hdrs, route.body); + } else { + exchange.getAttributes().put(WebUtils.IGNORE_PLUGIN, Consts.S.EMPTY); + } + } + return FizzPluginFilterChain.next(exchange); + } + + private Object request2ognlContext(ServerWebExchange exchange) { + OgnlRoot ognlRoot = new OgnlRoot(); + ServerHttpRequest request = exchange.getRequest(); + + ognlRoot.put(method, request.getMethodValue().toLowerCase()); + ognlRoot.put(path, WebUtils.getClientReqPath(exchange)); + + MultiValueMap queryParams = request.getQueryParams(); + if (!queryParams.isEmpty()) { + Map queryMap = new HashMap<>(); + queryParams.forEach( + (name, values) -> { + if (CollectionUtils.isEmpty(values)) { + queryMap.put(name, null); + } else if (values.size() > 1) { + queryMap.put(name, values); + } else { + queryMap.put(name, values.get(0)); + } + } + ); + ognlRoot.put(query, queryMap); + } + + HttpHeaders headers = request.getHeaders(); + if (!headers.isEmpty()) { + Map headerMap = new HashMap<>(); + headers.forEach( + (nm, values) -> { + String name = nm.toLowerCase(); + if (CollectionUtils.isEmpty(values)) { + headerMap.put(name, null); + } else if (values.size() > 1) { + headerMap.put(name, values); + } else { + headerMap.put(name, values.get(0)); + } + } + ); + ognlRoot.put(header, headerMap); + } + + MultiValueMap cookies = request.getCookies(); + if (!CollectionUtils.isEmpty(cookies)) { + Map cookieMap = new HashMap<>(); + cookies.forEach( + (name, values) -> { + if (CollectionUtils.isEmpty(values)) { + cookieMap.put(name, null); + } else if (values.size() > 1) { + List lst = new ArrayList<>(values.size()); + for (HttpCookie value : values) { + lst.add(value.getValue()); + } + cookieMap.put(name, lst); + } else { + cookieMap.put(name, values.get(0).getValue()); + } + } + ); + ognlRoot.put(cookie, cookieMap); + } + + MediaType reqContentType = request.getHeaders().getContentType(); + if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(reqContentType)) { + exchange.getFormData() + .map( + formData -> { + if (formData == FizzServerWebExchangeDecorator.EMPTY_FORM_DATA) { + return null; + } else { + Map formMap = new HashMap<>(); + formData.forEach( + (name, values) -> { + if (CollectionUtils.isEmpty(values)) { + formMap.put(name, null); + } else if (values.size() > 1) { + formMap.put(name, values); + } else { + formMap.put(name, values.get(0)); + } + } + ); + ognlRoot.put(form, formMap); + return formMap; + } + } + ) + .subscribe(); + } else if (MediaType.APPLICATION_JSON.isCompatibleWith(reqContentType)) { + request.getBody() + .single() + .map( + bodyDataBuffer -> { + if (bodyDataBuffer == NettyDataBufferUtils.EMPTY_DATA_BUFFER) { + return ReactorUtils.NULL; + } else { + String json = bodyDataBuffer.toString(StandardCharsets.UTF_8).trim(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("request {} body: {}", request.getId(), json); + } + if (json.charAt(0) == Consts.S.LEFT_SQUARE_BRACKET) { + List bodyMap = JacksonUtils.readValue(json, new TypeReference>(){}); + ognlRoot.put(body, bodyMap); + } else { + Map bodyMap = JacksonUtils.readValue(json, new TypeReference>(){}); + ognlRoot.put(body, bodyMap); + } + return ReactorUtils.NULL; + } + } + ) + .subscribe(); + } + + String originIp = WebUtils.getOriginIp(exchange); + if (originIp != null) { + Map clientMap = new HashMap<>(); + clientMap.put(ip, originIp); + ognlRoot.put(client, clientMap); + } + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("request {} ognl root: {}", request.getId(), JacksonUtils.writeValueAsString(ognlRoot)); + } + + return ognlRoot; + } + + private void changeRoute(ServerWebExchange exchange, Route route, Map pluginConfig) { + byte rt = ((Integer) pluginConfig.get(routeType)).byteValue(); + route.type = rt; + Map newRouteConfig = (Map) pluginConfig.get(routeConfigMap); + if (newRouteConfig == null) { + newRouteConfig = routeConfig2map((String) pluginConfig.get(routeConfig)); + pluginConfig.put(routeConfigMap, newRouteConfig); + pluginConfig.remove(routeConfig); + } + if (rt == ApiConfig.Type.SERVICE_DISCOVERY) { + changeServiceDiscoveryRoute(exchange, route, newRouteConfig); + } else if (rt == ApiConfig.Type.REVERSE_PROXY) { + changeReverseProxyRoute(exchange, pluginConfig, route, newRouteConfig); + } else if (rt == ApiConfig.Type.SERVICE_AGGREGATE) { + changeAggregateRoute(exchange, route, newRouteConfig); + } else { + String ct = (String) pluginConfig.get(contentType); + String b = (String) pluginConfig.get(body); + route.contentType(MediaType.valueOf(ct)) + .body(b); + } + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("route is changed: {}", route); + } + } + + private void changeServiceDiscoveryRoute(ServerWebExchange exchange, Route route, Map newRouteConfig) { + String type = newRouteConfig.get("type"); + String service = newRouteConfig.get("serviceName"); + if (StringUtils.isNotBlank(service)) { + route.backendService = service.trim(); + } + String timeout = newRouteConfig.get("timeout"); + if (StringUtils.isNotBlank(timeout)) { + route.timeout(Long.parseLong(timeout.trim())); + } + if (type.equals("http")) { + String registry = newRouteConfig.get("registry"); + if (StringUtils.isNotBlank(registry)) { + route.registryCenter = registry.trim(); + } + String method = newRouteConfig.get("methodName"); + if (StringUtils.isNotBlank(method)) { + route.method(HttpMethod.resolve(method.trim())); + } + String path = newRouteConfig.get("path"); + if (StringUtils.isNotBlank(path)) { + route.backendPath = UrlTransformUtils.transform(route.path, path.trim(), WebUtils.getClientReqPath(exchange)); + } + String qry = newRouteConfig.get("query"); + if (StringUtils.isNotBlank(qry)) { + route.query = qry.trim(); + } + String retryCount = newRouteConfig.get("retryCount"); + if (StringUtils.isNotBlank(retryCount)) { + route.retryCount(Integer.parseInt(retryCount.trim())); + } + String retryInterval = newRouteConfig.get("retryInterval"); + if (StringUtils.isNotBlank(retryInterval)) { + route.retryInterval(Long.parseLong(retryInterval.trim())); + } + } else { + route.type = ApiConfig.Type.DUBBO; + String method = newRouteConfig.get("methodName"); + if (StringUtils.isNotBlank(method)) { + route.rpcMethod(method.trim()); + } + String version = newRouteConfig.get("version"); + if (StringUtils.isNotBlank(version)) { + route.rpcVersion(version.trim()); + } + String group = newRouteConfig.get("group"); + if (StringUtils.isNotBlank(group)) { + route.rpcGroup(group.trim()); + } + String paramTypes = newRouteConfig.get("paramTypes"); + if (StringUtils.isNotBlank(paramTypes)) { + route.rpcParamTypes(paramTypes.trim()); + } + } + } + + private void changeReverseProxyRoute(ServerWebExchange exchange, Map pluginConfig, Route route, Map newRouteConfig) { + List httpHostPorts = (List) pluginConfig.get("httpHostPorts"); + if (httpHostPorts == null) { + String httpHostPortStr = newRouteConfig.get("serviceName"); + if (StringUtils.isBlank(httpHostPortStr)) { + httpHostPorts = WebUtils.getApiConfig(exchange).httpHostPorts; + } else { + String[] httpHostPortArr = StringUtils.split(httpHostPortStr, Consts.S.COMMA); + for (int i = 0; i < httpHostPortArr.length; i++) { + httpHostPortArr[i] = httpHostPortArr[i].trim(); + } + httpHostPorts = Arrays.asList(httpHostPortArr); + } + pluginConfig.put("httpHostPorts", httpHostPorts); + newRouteConfig.remove("serviceName"); + } + int counter = (int) pluginConfig.getOrDefault("counter", -1); + counter++; + if (counter < 0) { + counter = Math.abs(counter); + } + String hostPort = httpHostPorts.get( + counter % httpHostPorts.size() + ); + route.nextHttpHostPort(hostPort); + pluginConfig.put("counter", counter); + + String method = newRouteConfig.get("methodName"); + if (StringUtils.isNotBlank(method)) { + route.method(HttpMethod.resolve(method.trim())); + } + + String path = newRouteConfig.get("path"); + if (StringUtils.isNotBlank(path)) { + route.backendPath = UrlTransformUtils.transform(route.path, path.trim(), WebUtils.getClientReqPath(exchange)); + } + + String qry = newRouteConfig.get("query"); + if (StringUtils.isNotBlank(qry)) { + route.query = qry.trim(); + } + + String timeout = newRouteConfig.get("timeout"); + if (StringUtils.isNotBlank(timeout)) { + route.timeout(Long.parseLong(timeout.trim())); + } + + String retryCount = newRouteConfig.get("retryCount"); + if (StringUtils.isNotBlank(retryCount)) { + route.retryCount(Integer.parseInt(retryCount.trim())); + } + + String retryInterval = newRouteConfig.get("retryInterval"); + if (StringUtils.isNotBlank(retryInterval)) { + route.retryInterval(Long.parseLong(retryInterval.trim())); + } + } + + private void changeAggregateRoute(ServerWebExchange exchange, Route route, Map newRouteConfig) { + String service = newRouteConfig.get("serviceName"); + if (StringUtils.isNotBlank(service)) { + route.backendService = service.trim(); + WebUtils.setBackendService(exchange, route.backendService); + } + String path = newRouteConfig.get("path"); + if (StringUtils.isNotBlank(path)) { + route.backendPath = UrlTransformUtils.transform(route.path, path.trim(), WebUtils.getClientReqPath(exchange)); + WebUtils.setBackendPath(exchange, route.backendPath); + } + } + + private Map routeConfig2map(String config) { + Map result = new HashMap<>(); + String[] lines = StringUtils.split(config, Consts.S.LF); + for (String line : lines) { + int colonIdx = line.indexOf(Consts.S.COLON); + result.put(line.substring(0, colonIdx).trim(), line.substring(colonIdx + 1).trim()); + } + return result; + } +} diff --git a/fizz-plugin/src/test/java/we/plugin/grayrelease/GrayReleasePluginTests.java b/fizz-plugin/src/test/java/we/plugin/grayrelease/GrayReleasePluginTests.java new file mode 100644 index 0000000..ee0fe0b --- /dev/null +++ b/fizz-plugin/src/test/java/we/plugin/grayrelease/GrayReleasePluginTests.java @@ -0,0 +1,198 @@ +package we.plugin.grayrelease; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.test.web.reactive.server.WebTestClient; +import reactor.core.publisher.Mono; +import we.filter.AggregateFilter; +import we.filter.FilterResult; +import we.fizz.ConfigLoader; +import we.plugin.FizzPluginFilterChain; +import we.plugin.auth.ApiConfig; +import we.proxy.Route; +import we.util.Consts; +import we.util.JacksonUtils; +import we.util.ReflectionUtils; +import we.util.WebUtils; + +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class GrayReleasePluginTests { + + /** + * service discovery backend + */ + @Test + public void simpleTest() { + final Route[] changedRoute = new Route[1]; + WebTestClient client = WebTestClient.bindToWebHandler( + exchange -> { + ServerHttpResponse r = exchange.getResponse(); + r.setStatusCode(HttpStatus.OK); + r.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE); + return r.writeWith(Mono.just(r.bufferFactory().wrap("this is web handler response".getBytes()))); + } + ) + .webFilter( + (exchange, chain) -> { + + GrayReleasePlugin grayReleasePlugin = new GrayReleasePlugin(); + Map config = new HashMap<>(); + config.put("triggerCondition", " method == 'post' " + + " and matches('path','^/apath/x*') " + + " and clientIpInRange('11.238.145.180', '11.238.145.182') " + + " and exist('body.tools.gun') "); + config.put("routeType", Integer.parseInt(String.valueOf(ApiConfig.Type.SERVICE_DISCOVERY))); + config.put("routeConfig", + "type : http \n " + + "serviceName : bservice \n " + + "path : /bpath/{$1} "); + + // exchange.getAttributes().put("pcsit@", Collections.emptyIterator()); + Route route = new Route().path("/apath/**"); + changedRoute[0] = route; + exchange.getAttributes().put(WebUtils.ROUTE, route); + exchange.getAttributes().put(WebUtils.IGNORE_PLUGIN, Consts.S.EMPTY); + exchange.getAttributes().put(FizzPluginFilterChain.WEB_FILTER_CHAIN, chain); + exchange.getAttributes().put("oi@", "11.238.145.181"); + + return grayReleasePlugin.filter(exchange, config); + } + ) + .build(); + + client.post() + .uri("/proxy/aservice/apath/xxx") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue("{\"user\":\"henry\",\"tools\":{\"gun\":\"ak\"}}") + .exchange() + .expectBody(String.class).value( + v -> { + // System.err.println("body:\n" + v); + } + ); + + Assertions.assertEquals("bservice", changedRoute[0].backendService); + Assertions.assertEquals("/bpath/xxx", changedRoute[0].backendPath); + } + + @Test + public void reverseProxyBackendTest() { + + final Route[] changedRoute = new Route[1]; + + Map config = new HashMap<>(); + config.put("triggerCondition", " method == 'get' "); + config.put("routeType", Integer.parseInt(String.valueOf(ApiConfig.Type.REVERSE_PROXY))); + config.put("routeConfig", + "serviceName : http://1.2.3.4:8080,http://1.2.3.5:8080 \n " + + "path : /a/b/c \n" + + "query : name1=value1&name2=value2 "); + + WebTestClient client = WebTestClient.bindToWebHandler( + exchange -> { + ServerHttpResponse r = exchange.getResponse(); + r.setStatusCode(HttpStatus.OK); + r.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE); + return r.writeWith(Mono.just(r.bufferFactory().wrap("this is web handler response".getBytes()))); + } + ) + .webFilter( + (exchange, chain) -> { + + GrayReleasePlugin grayReleasePlugin = new GrayReleasePlugin(); + + Route route = new Route().path("/apath/**"); + changedRoute[0] = route; + exchange.getAttributes().put(WebUtils.ROUTE, route); + exchange.getAttributes().put(WebUtils.IGNORE_PLUGIN, Consts.S.EMPTY); + exchange.getAttributes().put(FizzPluginFilterChain.WEB_FILTER_CHAIN, chain); + exchange.getAttributes().put("oi@", "11.238.145.181"); + + return grayReleasePlugin.filter(exchange, config); + } + ) + .build(); + + client.get() + .uri("/proxy/aservice/apath/xxx") + .exchange(); + Assertions.assertEquals("/a/b/c?name1=value1&name2=value2", changedRoute[0].getBackendPathQuery()); + Assertions.assertEquals("http://1.2.3.4:8080", changedRoute[0].nextHttpHostPort); + + client.get() + .uri("/proxy/aservice/apath/xxx") + .exchange(); + Assertions.assertEquals("http://1.2.3.5:8080", changedRoute[0].nextHttpHostPort); + + client.get() + .uri("/proxy/aservice/apath/xxx") + .exchange(); + Assertions.assertEquals("http://1.2.3.4:8080", changedRoute[0].nextHttpHostPort); + } + + @Test + public void aggregateBackendTest() { + AggregateFilter aggregateFilter = new AggregateFilter(); + ConfigLoader configLoader = mock(ConfigLoader.class); + when( + configLoader.matchAggregateResource("GET", "/_proxytest/bservice/bpath/xxx") + ) + .thenReturn(null); + ReflectionUtils.set(aggregateFilter, "configLoader", configLoader); + + WebTestClient client = WebTestClient.bindToWebHandler( + exchange -> { + ServerHttpResponse r = exchange.getResponse(); + r.setStatusCode(HttpStatus.OK); + return r.writeWith(Mono.just(r.bufferFactory().wrap("this is web handler response".getBytes()))); + } + ) + .webFilter( + (exchange, chain) -> { + + GrayReleasePlugin grayReleasePlugin = new GrayReleasePlugin(); + Map config = new HashMap<>(); + config.put("triggerCondition", " method == 'get' "); + config.put("routeType", Integer.parseInt(String.valueOf(ApiConfig.Type.SERVICE_AGGREGATE))); + config.put("routeConfig", + "type : http \n " + + "serviceName : bservice \n " + + "path : /bpath/{$1} "); + + Route route = new Route().path("/apath/**"); + exchange.getAttributes().put(WebUtils.ROUTE, route); + exchange.getAttributes().put(WebUtils.IGNORE_PLUGIN, Consts.S.EMPTY); + exchange.getAttributes().put(FizzPluginFilterChain.WEB_FILTER_CHAIN, chain); + exchange.getAttributes().put("oi@", "11.238.145.181"); + + Map filterContext = new HashMap<>(); + exchange.getAttributes().put(WebUtils.FILTER_CONTEXT, filterContext); + filterContext.put(WebUtils.PREV_FILTER_RESULT, FilterResult.SUCCESS("x")); + + return grayReleasePlugin.filter(exchange, config); + }, + aggregateFilter + ) + .build(); + + client.get() + .uri("/_proxytest/aservice/apath/xxx") + .exchange() + .expectBody(String.class).value( + v -> { + Map bodyMap = JacksonUtils.readValue(v, new TypeReference>(){}); + Assertions.assertEquals(bodyMap.get("message"), "API not found in aggregation: /_proxytest/bservice/bpath/xxx"); + } + ); + } +} diff --git a/fizz-plugin/src/test/resources/log4j2-test.xml b/fizz-plugin/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000..b59feb3 --- /dev/null +++ b/fizz-plugin/src/test/resources/log4j2-test.xml @@ -0,0 +1,18 @@ + + + + + fizz-plugin + + + + + + + + + + + + + diff --git a/fizz-spring-boot-starter/pom.xml b/fizz-spring-boot-starter/pom.xml index 54ddfdf..add1ba9 100644 --- a/fizz-spring-boot-starter/pom.xml +++ b/fizz-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ fizz-gateway-community com.fizzgate - 2.6.6 + 2.6.7-SNAPSHOT ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index 1909d90..1a56713 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ Dysprosium-SR25 5.3.7.RELEASE 2.2.7.RELEASE - 4.1.78.Final + 4.1.79.Final 4.4.15 2.17.2 1.7.36 @@ -22,7 +22,7 @@ 0.8.2 0.9.11 2.11.1 - 2.0.53.Final + 2.0.54.Final 2.2.9.RELEASE 1.30 Moore-SR13 @@ -38,7 +38,7 @@ fizz-gateway-community ${project.artifactId} fizz gateway community - 2.6.6 + 2.6.7-SNAPSHOT pom fizz-common @@ -70,6 +70,18 @@ + + com.github.seancfoley + ipaddress + 5.3.4 + + + + ognl + ognl + 3.3.3 + + org.openjdk.jol jol-core