├── .gitignore
├── .gitmodules
├── .travis.yml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── README_CN.md
├── ROADMAP.md
├── cmd
├── api
│ └── api_server.go
├── backend
│ └── backend.go
└── proxy
│ └── proxy.go
├── docker-compose.yml
├── docs-cn
├── api.md
├── apiserver.md
├── benchmark.md
├── build.md
├── cluster.md
├── images
│ ├── bm_cpu_all.jpg
│ ├── bm_cpu_io_sum.jpg
│ └── bm_net_packet.png
├── optimization.md
├── plugin.md
├── plugin_js.md
├── proxy.md
├── restful.md
├── routing.md
├── server.md
└── user-guide.md
├── docs
├── api.md
├── apiserver.md
├── benchmark.md
├── build.md
├── cluster.md
├── images
│ ├── api_2.png
│ ├── api_basics.png
│ ├── bm_cpu_all.jpg
│ ├── bm_cpu_io_sum.jpg
│ ├── bm_net_packet.png
│ ├── defaultFilters.png
│ ├── jwt_example.png
│ ├── jwt_in_auth.png
│ ├── jwt_postman.png
│ ├── postman.png
│ ├── routing.png
│ ├── server_configuration.png
│ ├── specs.png
│ └── web_ui_front_page.png
├── optimization.md
├── plugin-tutorial.md
├── plugin.md
├── plugin_js.md
├── proxy.md
├── restful.md
├── routing.md
├── server.md
├── tutorial.md
└── user-guide.md
├── entrypoint.sh
├── examples
├── api.go
├── cluster.go
├── example.go
├── jwt.json
├── jwt_a_simple_working_one.json
├── routing.go
└── server.go
├── go.mod
├── go.sum
├── grafana
└── grafana.json
├── images
├── alipay.jpg
├── arch.png
├── flow.png
├── logo.png
├── qr.jpg
└── wechat.png
├── pkg
├── client
│ ├── api.go
│ ├── client.go
│ ├── cluster.go
│ ├── plugin.go
│ ├── routing.go
│ └── server.go
├── expr
│ ├── expr.go
│ └── expr_test.go
├── filter
│ ├── cache_util.go
│ ├── const.go
│ ├── filter.go
│ └── test_help.go
├── lb
│ ├── haship.go
│ ├── haship_test.go
│ ├── lb.go
│ ├── rand.go
│ ├── rand_test.go
│ ├── roundrobin.go
│ ├── weightrobin.go
│ └── weightrobin_test.go
├── pb
│ ├── gen.sh
│ ├── metapb
│ │ ├── metapb.pb.go
│ │ └── metapb.proto
│ ├── rpcpb
│ │ ├── rpcpb.pb.go
│ │ ├── rpcpb.proto
│ │ └── services.go
│ └── validation.go
├── plugin
│ ├── builtin_base.go
│ ├── builtin_http.go
│ ├── builtin_json.go
│ ├── builtin_log.go
│ ├── builtin_redis.go
│ ├── context_adapter.go
│ ├── engine.go
│ ├── runtime.go
│ └── runtime_test.go
├── proxy
│ ├── cfg.go
│ ├── checker.go
│ ├── dispatcher.go
│ ├── dispatcher_copy_on_write.go
│ ├── dispatcher_event.go
│ ├── dispatcher_meta.go
│ ├── dispatcher_runtime.go
│ ├── errors.go
│ ├── factory.go
│ ├── filter.go
│ ├── filter_access.go
│ ├── filter_analysis.go
│ ├── filter_blacklist.go
│ ├── filter_caching.go
│ ├── filter_circuit_breaker.go
│ ├── filter_cross_domain.go
│ ├── filter_headers.go
│ ├── filter_jwt.go
│ ├── filter_prepare.go
│ ├── filter_rate_limiting.go
│ ├── filter_validation.go
│ ├── filter_whitelist.go
│ ├── filter_xforwardfor.go
│ ├── io.go
│ ├── metric.go
│ ├── multi.go
│ ├── pool.go
│ ├── proxy.go
│ ├── proxy_gc.go
│ ├── proxy_https.go
│ ├── proxy_start.go
│ ├── proxy_websocket.go
│ ├── rate_limit.go
│ └── render.go
├── route
│ ├── const.go
│ ├── lexer.go
│ ├── parser.go
│ ├── parser_test.go
│ ├── route.go
│ ├── route_test.go
│ ├── scanner.go
│ └── scanner_test.go
├── service
│ ├── errors.go
│ ├── g.go
│ ├── http.go
│ ├── http_api.go
│ ├── http_bind.go
│ ├── http_cluster.go
│ ├── http_plugin.go
│ ├── http_routing.go
│ ├── http_server.go
│ ├── http_static.go
│ ├── http_system.go
│ └── rpc_meta.go
├── store
│ ├── store.go
│ ├── store_etcd.go
│ ├── store_watch.go
│ └── txn.go
└── util
│ ├── addr.go
│ ├── analysis.go
│ ├── analysis_test.go
│ ├── barrier.go
│ ├── fasthttp_client.go
│ ├── ip_util.go
│ ├── lru.go
│ ├── metric_push.go
│ ├── time.go
│ └── version.go
└── prometheus.yml
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
2 | *.o
3 | *.a
4 | *.so
5 |
6 | # Folders
7 | _obj
8 | _test
9 | .vscode/*
10 | Library/
11 | dist/
12 |
13 | # Architecture specific extensions/prefixes
14 | *.[568vq]
15 | [568vq].out
16 |
17 | *.cgo1.go
18 | *.cgo2.c
19 | _cgo_defun.c
20 | _cgo_gotypes.go
21 | _cgo_export.*
22 |
23 | _testmain.go
24 |
25 | *.exe
26 | *.test
27 | *.prof
28 | cmd/api/api_server
29 | cmd/backend/backend
30 | cmd/proxy/proxy
31 | .idea/
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "vendor"]
2 | path = vendor
3 | url = https://github.com/fagongzi/gateway-vendor.git
4 | branch = master
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | sudo: true
3 | dist: xenial
4 |
5 | go:
6 | - 1.11
7 | - 1.12
8 | - 1.13
9 |
10 | script: go build ./...
11 |
12 | env:
13 | global:
14 | core:
15 | longpaths: true
16 | core.longpaths: true
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:latest
2 |
3 | ARG APP_ROOT=/app/manba
4 | ARG EXEC_NAME=manba-proxy
5 | ARG UID=2019
6 | ARG CMD_NAME=demo
7 | ENV CURRENT_EXEC_PATH=${APP_ROOT}/${EXEC_NAME}
8 | ENV PATH=${APP_ROOT}:$PATH
9 |
10 | WORKDIR ${APP_ROOT}
11 |
12 | ADD dist ${APP_ROOT}
13 |
14 | # Alpine Linux doesn't use pam, which means that there is no /etc/nsswitch.conf,
15 | # but Golang relies on /etc/nsswitch.conf to check the order of DNS resolving
16 | # (see https://github.com/golang/go/commit/9dee7771f561cf6aee081c0af6658cc81fac3918)
17 | # To fix this we just create /etc/nsswitch.conf and add the following line:
18 | # hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4
19 |
20 | RUN MAIN_VERSION=$(cat /etc/alpine-release | cut -d '.' -f 0-2) \
21 | && mv /etc/apk/repositories /etc/apk/repositories-bak \
22 | && { \
23 | echo "https://mirrors.aliyun.com/alpine/v${MAIN_VERSION}/main"; \
24 | echo "https://mirrors.aliyun.com/alpine/v${MAIN_VERSION}/community"; \
25 | } >> /etc/apk/repositories \
26 | && apk add --update --no-cache libcap \
27 | && addgroup -g ${UID} -S manba \
28 | && adduser -u ${UID} -S manba -G manba \
29 | && mkdir -p ${APP_ROOT}/plugins \
30 | && chown -R manba:manba ./ \
31 | && if [ -e ${CURRENT_EXEC_PATH} ]; then \
32 | setcap CAP_NET_BIND_SERVICE=+eip ${CURRENT_EXEC_PATH}; \
33 | fi \
34 | && echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf \
35 | && echo -n ${CMD_NAME} > cmd
36 |
37 | USER manba
38 |
39 | EXPOSE 80 2379 9092 9093
40 |
41 | ENTRYPOINT ["/bin/sh", "./entrypoint.sh"]
42 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | RELEASE_VERSION = $(release_version)
2 |
3 | ifeq ("$(RELEASE_VERSION)","")
4 | RELEASE_VERSION := "unknown"
5 | endif
6 |
7 | ROOT_DIR = $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))/
8 | VERSION_PATH = $(shell echo $(ROOT_DIR) | sed -e "s;${GOPATH}/src/;;g")pkg/util
9 | LD_GIT_COMMIT = -X '$(VERSION_PATH).GitCommit=`git rev-parse --short HEAD`'
10 | LD_BUILD_TIME = -X '$(VERSION_PATH).BuildTime=`date +%FT%T%z`'
11 | LD_GO_VERSION = -X '$(VERSION_PATH).GoVersion=`go version`'
12 | LD_MANBA_VERSION = -X '$(VERSION_PATH).Version=$(RELEASE_VERSION)'
13 | LD_FLAGS = -ldflags "$(LD_GIT_COMMIT) $(LD_BUILD_TIME) $(LD_GO_VERSION) $(LD_MANBA_VERSION) -w -s"
14 |
15 | GOOS = linux
16 | CGO_ENABLED = 0
17 | DIST_DIR = $(ROOT_DIR)dist/
18 |
19 | ETCD_VER = v3.3.12
20 | ETCD_DOWNLOAD_URL = https://github.com/coreos/etcd/releases/download
21 |
22 | MY_TARGET := dist_dir
23 | EXEC_NAME := manba-proxy
24 | IMAGE_NAME := manba
25 | CMD_NAME := demo
26 | ifeq ("$(MAKECMDGOALS)","docker")
27 | ifeq ("$(with)","")
28 | MY_TARGET := release download_etcd ui
29 | endif
30 | ifeq ($(findstring etcd,$(with)),etcd)
31 | MY_TARGET := $(MY_TARGET) download_etcd
32 | EXEC_NAME := etcd
33 | IMAGE_NAME = etcd
34 | CMD_NAME = etcd
35 | endif
36 | ifeq ($(findstring apiserver,$(with)),apiserver)
37 | MY_TARGET := $(MY_TARGET) apiserver ui
38 | EXEC_NAME := apiserver
39 | IMAGE_NAME = apiserver
40 | CMD_NAME = apiserver
41 | endif
42 | ifeq ($(findstring proxy,$(with)),proxy)
43 | MY_TARGET := $(MY_TARGET) proxy
44 | EXEC_NAME := manba-proxy
45 | IMAGE_NAME = proxy
46 | CMD_NAME = manba-proxy
47 | endif
48 | endif
49 |
50 | .PHONY: release
51 | release: dist_dir apiserver proxy;
52 |
53 | .PHONY: release_darwin
54 | release_darwin: darwin dist_dir apiserver proxy;
55 |
56 | .PHONY: docker
57 | docker:
58 | @$(MAKE) $(MY_TARGET)
59 | @echo ========== current docker tag is: $(RELEASE_VERSION) ==========
60 | docker build -t fagongzi/$(IMAGE_NAME):$(RELEASE_VERSION) \
61 | --build-arg EXEC_NAME="$(EXEC_NAME)" \
62 | --build-arg CMD_NAME="$(CMD_NAME)" \
63 | -f Dockerfile .
64 | docker tag fagongzi/$(IMAGE_NAME):$(RELEASE_VERSION) fagongzi/$(IMAGE_NAME)
65 |
66 | .PHONY: ui
67 | ui: ; $(info ======== compile ui:)
68 | git clone https://github.com/fagongzi/gateway-ui-vue.git $(DIST_DIR)ui
69 | cd $(DIST_DIR)ui && git checkout 3.0.0
70 |
71 | .PHONY: darwin
72 | darwin:
73 | $(eval GOOS := darwin)
74 |
75 | .PHONY: apiserver
76 | apiserver: ; $(info ======== compiled apiserver:)
77 | env CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) go build -mod vendor -a -installsuffix cgo -o $(DIST_DIR)manba-apiserver $(LD_FLAGS) $(ROOT_DIR)cmd/api/*.go
78 |
79 | .PHONY: proxy
80 | proxy: ; $(info ======== compiled proxy:)
81 | env CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) go build -mod vendor -a -installsuffix cgo -o $(DIST_DIR)manba-proxy $(LD_FLAGS) $(ROOT_DIR)cmd/proxy/*.go
82 |
83 | .PHONY: download_etcd
84 | download_etcd:
85 | curl -L $(ETCD_DOWNLOAD_URL)/$(ETCD_VER)/etcd-$(ETCD_VER)-linux-amd64.tar.gz -o /tmp/etcd-$(ETCD_VER)-linux-amd64.tar.gz
86 | tar xzvf /tmp/etcd-$(ETCD_VER)-linux-amd64.tar.gz -C $(DIST_DIR) --strip-components=1
87 | @rm -rf $(DIST_DIR)Documentation $(DIST_DIR)README*
88 |
89 | .PHONY: dist_dir
90 | dist_dir: ; $(info ======== prepare distribute dir:)
91 | mkdir -p $(DIST_DIR)
92 | @rm -rf $(DIST_DIR)*
93 | @cp entrypoint.sh $(DIST_DIR)
94 |
95 | .PHONY: clean
96 | clean: ; $(info ======== clean all:)
97 | rm -rf $(DIST_DIR)*
98 |
99 | .PHONY: help
100 | help:
101 | @echo "build release binary: \n\t\tmake release\n"
102 | @echo "build Mac OS X release binary: \n\t\tmake release_darwin\n"
103 | @echo "build docker release with etcd: \n\t\tmake docker\n"
104 | @echo "\t add 「with」 params can select what you need:\n"
105 | @echo "\t default: all, like 「make docker」\n"
106 | @echo "\t etcd: download and extract etcd and etcdctl\n"
107 | @echo "\t proxy: only compile proxy\n"
108 | @echo "\t apiserver: compile apiserver and download ui\n"
109 | @echo "clean all binary: \n\t\tmake clean\n"
110 |
111 | UNAME_S := $(shell uname -s)
112 |
113 | ifeq ($(UNAME_S),Darwin)
114 | .DEFAULT_GOAL := release_darwin
115 | else
116 | .DEFAULT_GOAL := release
117 | endif
118 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [](https://gitter.im/fagongzi/gateway?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
4 | [](https://travis-ci.org/fagongzi/gateway)
5 | [](https://goreportcard.com/report/github.com/fagongzi/gateway)
6 |
7 | Manba/[简体中文](README_CN.md)
8 | -------
9 | Manba is a restful API gateway based on HTTP, which can be used as a unified API access layer.
10 |
11 | ## Tutorial
12 | A very detailed tutorial for beginners. [Link](./docs/tutorial.md)
13 | Below are video tutorials.
14 | Basics:
15 | [](https://www.youtube.com/watch?v=2qMWmdcw7o4)
16 |
17 | Alternative bilibili.com video link: https://www.bilibili.com/video/av73432556/
18 |
19 | Routing Configuration Tutorial:
20 | [](https://www.youtube.com/watch?v=D1pI6opB_ks)
21 |
22 | Alternative bilibili.com video link: https://www.bilibili.com/video/av73432836/
23 |
24 | JWT Plugin Configuration Tutorial:
25 | [](https://www.youtube.com/watch?v=sLb16YDSlBs)
26 |
27 | Alternative bilibili.com video link: https://www.bilibili.com/video/av73433002/
28 |
29 | ## Attention
30 | Please make sure your Go version is 1.10 or above. Otherwise, **undefined "math/rand".Shuffle** error will occur when compiling. [StackOverFlow Link](https://stackoverflow.com/questions/52172794/getting-undefined-rand-shuffle-in-golang)
31 |
32 |
33 | ## Features
34 | * Traffic Control (on Server or API)
35 | * Circuit Breaker (on Server or API)
36 | * Load Balance
37 | * Service Discovery
38 | * Plugin
39 | * Routing (Divert Traffic, Duplicate Traffic)
40 | * API Aggregation
41 | * API Argument Check
42 | * API Access Control (White and Black List)
43 | * API Default Return Value
44 | * API Customized Return Value
45 | * API Result Cache
46 | * JWT Authorization
47 | * API Metric Imports Prometheus
48 | * API Retry After Failure
49 | * Backend Server Health Check
50 | * Open Management of API (GRPC、Restful)
51 | * Websocket Support
52 | * Online Data Migration Support
53 |
54 | ## Docker
55 |
56 | The following content requires reader some knowledge of Docker. You can refer to [this book][2], or check out [the official documentation][1]。
57 |
58 | ### Available Docker Images
59 | * `fagongzi/proxy`
60 |
61 | proxy component, `production ready`
62 |
63 | * `fagongzi/apiserver`
64 |
65 | apiserver component, `production ready`
66 |
67 | ### Quick start with docker-compose
68 | ```bash
69 | docker-compose up -d
70 | ```
71 |
72 | Use `http://127.0.0.1:9093/ui/index.html` to access `apiserver`
73 |
74 | Use `http://127.0.0.1` to access to your API
75 |
76 | ## Architecture
77 | 
78 |
79 | ## Web UI
80 | Available Manba Web UI Projects:
81 | * [Official](https://github.com/fagongzi/gateway-ui-vue)
82 | * [gateway_ui (v2.x ONLY)](https://github.com/archfish/gateway_ui)
83 | * [gateway_admin_ui](https://github.com/wilehos/gateway_admin_ui)
84 |
85 | ## Components
86 | Manba consists of `proxy` and `apiserver`.
87 |
88 | ### Proxy
89 | Proxy is a component which provides service to clients. Proxy is a stateless node. Multiple proxies can be deployed to handle huge traffic.
90 | [More](./docs/proxy.md).
91 |
92 | ### ApiServer
93 | ApiServer provides GRPC and Restful to manage metadata for users. ApiServer integrates official Web UI.
94 | [More](./docs/apiserver.md).
95 |
96 | ## Concepts of Manba
97 | ### Server
98 | A server is a a real backend service.
99 | [More](./docs/server.md).
100 |
101 | ### Cluster
102 | Cluster consists of servers which provide the same service. A server is chosen to handle a specific request based on a load balance strategy.
103 | [More](./docs/cluster.md).
104 |
105 | ### API
106 | API is a key concept of Manba. We can manage external APIs in Manba and their distribution rules, aggregation rules and URL matching rules.
107 | [More](./docs/api.md).
108 |
109 | ### Routing
110 | Routing is a route strategy. Cookie, Querystring, Header and Path in HTTP Request dictate traffic distribution and traffic duplication to a specific cluster. Through this feature, AB test and online traffic divertion is achieved.
111 | [More](./docs/routing.md).
112 |
113 | ## Getting Involved
114 | [More](./docs/build.md)
115 |
116 | ## WeChat
117 | 
118 |
119 | [1]: https://docs.docker.com/ "Docker Documentation"
120 | [2]: https://github.com/yeasy/docker_practice "docker_practice"
121 |
--------------------------------------------------------------------------------
/README_CN.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [](https://gitter.im/fagongzi/gateway?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
4 | [](https://travis-ci.org/fagongzi/gateway)
5 | [](https://goreportcard.com/report/github.com/fagongzi/gateway)
6 |
7 | Manba/[English](./README.md)
8 | -------
9 | Manba是一个基于HTTP协议的restful的API网关。可以作为统一的API接入层。
10 |
11 | ## 教程
12 | 如果你是一个初学者,那么这个[详细的教程](./docs/tutorial.md)非常适合你。现在只有英文版本。
13 |
14 | ## 注意
15 | 请确保你的Go版本是在1.10或者之上。用1.10之前版本的Go编译时会出现**undefined "math/rand".Shuffle**错误。[StackOverFlow链接](https://stackoverflow.com/questions/52172794/getting-undefined-rand-shuffle-in-golang)
16 |
17 | ## Features
18 | * 流量控制(Server或API级别)
19 | * 熔断(Server或API级别)
20 | * 负载均衡
21 | * 服务发现
22 | * 插件机制
23 | * 路由(分流,复制流量)
24 | * API 聚合
25 | * API 参数校验
26 | * API 访问控制(黑白名单)
27 | * API 默认返回值
28 | * API 定制返回值
29 | * API 结果Cache
30 | * JWT Authorization
31 | * API Metric导入Prometheus
32 | * API 失败重试
33 | * 后端server的健康检查
34 | * 开放管理API(GRPC、Restful)
35 | * 支持websocket
36 | * 支持在线迁移数据
37 |
38 | ## Docker
39 |
40 | 以下内容要求对docker基本操作有一定了解,可以看[这本书][2],或者直接看[官方文档][1]。
41 |
42 | ### Quick start with docker-compose
43 | ```bash
44 | docker-compose up -d
45 | ```
46 |
47 | 使用 `http://127.0.0.1:9093/ui/index.html` 访问 `apiserver`
48 |
49 | 使用 `http://127.0.0.1` 访问你的API
50 |
51 | ## 架构
52 | 
53 |
54 | ## WebUI
55 | 可用的Manba的WebUI的项目:
56 | * [官方](https://github.com/fagongzi/gateway-ui-vue)
57 | * [gateway_ui(仅适配2.x)](https://github.com/archfish/gateway_ui)
58 | * [gateway_admin_ui](https://github.com/wilehos/gateway_admin_ui)
59 |
60 | ## 组件
61 | Gateway由`proxy`, `apiserver`组成
62 |
63 | ### Proxy
64 | Proxy是Gateway对终端用户提供服务的组件,Proxy是一个无状态的节点,可以部署多个来支撑更大的流量,[更多](./docs-cn/proxy.md)。
65 |
66 | ### ApiServer
67 | ApiServer对外提供GRPC和Restful来管理元信息,ApiServer同时集成了官方的WebUI,[更多](./docs-cn/apiserver.md)。
68 |
69 | ## Manba中的概念
70 | ### Server
71 | Server是一个真实的后端服务,[更多](./docs-cn/server.md)。
72 |
73 | ### Cluster
74 | Cluster是一个逻辑概念,它由一组提供相同服务的Server组成。会依据负载均衡策略选择一个可用的Server,[更多](./docs-cn/cluster.md)。
75 |
76 | ### API
77 | API是Manba的核心概念,我们可以在Manba的中维护对外的API,以及API的分发规则,聚合规则以及URL匹配规则,[更多](./docs-cn/api.md)。
78 |
79 | ### Routing
80 | Routing是一个路由策略,根据HTTP Request中的Cookie,Querystring、Header、Path中的一些信息把流量分发到或者复制到指定的Cluster,通过这个功能,我们可以实现AB Test和线上引流,[更多](./docs-cn/routing.md)。
81 |
82 | ## 参与开发
83 | [更多](./docs-cn/build.md)
84 |
85 | ## 交流方式-微信
86 | 
87 |
88 | [1]: https://docs.docker.com/ "Docker Documentation"
89 | [2]: https://github.com/yeasy/docker_practice "docker_practice"
90 |
--------------------------------------------------------------------------------
/ROADMAP.md:
--------------------------------------------------------------------------------
1 | # Roadmap
2 | 这个文档定义了Manba的roadmap.
3 |
4 | ## Features
5 | - [x] 在线流量复制
6 | - [x] 定制化返回值
7 | - [x] API结果Cache
8 | - [x] API Server支持Restful
9 | - [x] 支持依赖聚合
10 | - [ ] 协议转换插件机制
11 |
12 | ## Plugins
13 | - [x] JWT插件
14 | - [ ] 分布式限流插件
15 | - [ ] SpringCloud协议转换插件
16 | - [ ] Dubbo协议转换插件
17 | - [ ] Grpc协议转换插件
18 |
--------------------------------------------------------------------------------
/cmd/api/api_server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "os"
7 | "os/signal"
8 | "runtime"
9 | "syscall"
10 | "time"
11 |
12 | "github.com/fagongzi/gateway/pkg/util"
13 |
14 | "github.com/coreos/etcd/clientv3"
15 | "github.com/fagongzi/gateway/pkg/pb/rpcpb"
16 | "github.com/fagongzi/gateway/pkg/service"
17 | "github.com/fagongzi/gateway/pkg/store"
18 | "github.com/fagongzi/grpcx"
19 | "github.com/fagongzi/log"
20 | "github.com/labstack/echo"
21 | "google.golang.org/grpc"
22 | )
23 |
24 | var (
25 | addr = flag.String("addr", "127.0.0.1:9092", "Addr: client grpc entrypoint")
26 | addrHTTP = flag.String("addr-http", "127.0.0.1:9093", "Addr: client http restful entrypoint")
27 | addrStore = flag.String("addr-store", "etcd://127.0.0.1:2379", "Addr: store address")
28 | addrStoreUser = flag.String("addr-store-user", "", "addr Store UserName")
29 | addrStorePwd = flag.String("addr-store-pwd", "", "addr Store Password")
30 | namespace = flag.String("namespace", "dev", "The namespace to isolation the environment.")
31 | discovery = flag.Bool("discovery", false, "Publish apiserver service via discovery.")
32 | servicePrefix = flag.String("service-prefix", "/services", "The prefix for service name.")
33 | publishLease = flag.Int64("publish-lease", 10, "Publish service lease seconds")
34 | publishTimeout = flag.Int("publish-timeout", 30, "Publish service timeout seconds")
35 | ui = flag.String("ui", "/app/manba/ui/dist", "The gateway ui dist dir.")
36 | uiPrefix = flag.String("ui-prefix", "/ui", "The gateway ui prefix path.")
37 | version = flag.Bool("version", false, "Show version info")
38 | )
39 |
40 | func main() {
41 | flag.Parse()
42 |
43 | if *version {
44 | util.PrintVersion()
45 | os.Exit(0)
46 | }
47 |
48 | log.InitLog()
49 | runtime.GOMAXPROCS(runtime.NumCPU())
50 |
51 | log.Infof("addr: %s", *addr)
52 | log.Infof("addr-store: %s", *addrStore)
53 | log.Infof("addr-store-user: %s", *addrStoreUser)
54 | log.Infof("addr-store-pwd: %s", *addrStorePwd)
55 | log.Infof("namespace: %s", *namespace)
56 | log.Infof("discovery: %v", *discovery)
57 | log.Infof("service-prefix: %s", *servicePrefix)
58 | log.Infof("publish-lease: %d", *publishLease)
59 | log.Infof("publish-timeout: %d", *publishTimeout)
60 |
61 | db, err := store.GetStoreFrom(*addrStore, fmt.Sprintf("/%s", *namespace), *addrStoreUser, *addrStorePwd)
62 | if err != nil {
63 | log.Fatalf("init store failed for %s, errors:\n%+v",
64 | *addrStore,
65 | err)
66 | }
67 |
68 | service.Init(db)
69 |
70 | var opts []grpcx.ServerOption
71 | if *discovery {
72 | opts = append(opts, grpcx.WithEtcdPublisher(db.Raw().(*clientv3.Client), *servicePrefix, *publishLease, time.Second*time.Duration(*publishTimeout)))
73 | }
74 |
75 | if *addrHTTP != "" {
76 | opts = append(opts, grpcx.WithHTTPServer(*addrHTTP, func(server *echo.Echo) {
77 | service.InitHTTPRouter(server, *ui, *uiPrefix)
78 | }))
79 | }
80 |
81 | s := grpcx.NewGRPCServer(*addr, func(svr *grpc.Server) []grpcx.Service {
82 | var services []grpcx.Service
83 | rpcpb.RegisterMetaServiceServer(svr, service.MetaService)
84 | services = append(services, grpcx.NewService(rpcpb.ServiceMeta, nil))
85 | return services
86 | }, opts...)
87 |
88 | log.Infof("api server listen at %s", *addr)
89 | go s.Start()
90 |
91 | waitStop(s)
92 | }
93 |
94 | func waitStop(s *grpcx.GRPCServer) {
95 | sc := make(chan os.Signal, 1)
96 | signal.Notify(sc,
97 | syscall.SIGHUP,
98 | syscall.SIGINT,
99 | syscall.SIGTERM,
100 | syscall.SIGQUIT)
101 |
102 | sig := <-sc
103 | s.GracefulStop()
104 | log.Infof("exit: signal=<%d>.", sig)
105 | switch sig {
106 | case syscall.SIGTERM:
107 | log.Infof("exit: bye :-).")
108 | os.Exit(0)
109 | default:
110 | log.Infof("exit: bye :-(.")
111 | os.Exit(1)
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/cmd/backend/backend.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "net/http"
7 | "os"
8 | "time"
9 |
10 | "github.com/fagongzi/util/format"
11 | "github.com/labstack/echo"
12 | md "github.com/labstack/echo/middleware"
13 | )
14 |
15 | var (
16 | addr = flag.String("addr", "127.0.0.1:9090", "addr for backend")
17 | )
18 |
19 | func main() {
20 | flag.Parse()
21 |
22 | server := echo.New()
23 | server.Use(md.Logger())
24 | server.Use(md.Gzip())
25 |
26 | server.GET("/serverinfo", func(c echo.Context) error {
27 | hostname, _ := os.Hostname()
28 | return c.String(http.StatusOK, hostname+"\n"+*addr)
29 | })
30 | server.GET("/fail", func(c echo.Context) error {
31 | sleep := c.QueryParam("sleep")
32 | if sleep != "" {
33 | time.Sleep(time.Second * time.Duration(format.MustParseStrInt(sleep)))
34 | }
35 |
36 | code := c.QueryParam("code")
37 | if code != "" {
38 | return c.String(format.MustParseStrInt(code), "OK")
39 | }
40 |
41 | return c.String(http.StatusOK, "OK")
42 | })
43 |
44 | server.GET("/check", func(c echo.Context) error {
45 | return c.String(http.StatusOK, "OK")
46 | })
47 |
48 | server.GET("/header", func(c echo.Context) error {
49 | name := c.QueryParam("name")
50 | return c.String(http.StatusOK, c.Request().Header.Get(name))
51 | })
52 |
53 | server.GET("/host", func(c echo.Context) error {
54 | return c.String(http.StatusOK, "Host in HTTP request header: "+c.Request().Host+"\nserver:"+*addr)
55 | })
56 |
57 | server.GET("/error", func(c echo.Context) error {
58 | return c.NoContent(http.StatusBadRequest)
59 | })
60 |
61 | server.GET("/v1/components/:id", func(c echo.Context) error {
62 | value := make(map[string]interface{})
63 | data := make(map[string]interface{})
64 | user := make(map[string]interface{})
65 | user["id"] = c.Param("id")
66 | user["name"] = fmt.Sprintf("v1-name-%s", c.Param("id"))
67 | data["user"] = user
68 | data["source"] = *addr
69 | data["query"] = c.QueryString()
70 |
71 | value["code"] = "0"
72 | value["data"] = data
73 | return c.JSON(http.StatusOK, value)
74 | })
75 | server.GET("/v1/users/:id", func(c echo.Context) error {
76 | user := make(map[string]interface{})
77 | user["id"] = c.Param("id")
78 | user["name"] = fmt.Sprintf("v1-name-%s", c.Param("id"))
79 | user["source"] = *addr
80 | user["query"] = c.QueryString()
81 | user["header"] = c.QueryParam(c.Request().Header.Get("header"))
82 | return c.JSON(http.StatusOK, user)
83 | })
84 | server.GET("/v1/account/:id", func(c echo.Context) error {
85 | account := make(map[string]interface{})
86 | account["id"] = c.Param("id")
87 | account["source"] = *addr
88 | account["account"] = fmt.Sprintf("v1-account-%s", c.Param("id"))
89 | account["query"] = c.QueryString()
90 | return c.JSON(http.StatusOK, account)
91 | })
92 |
93 | server.GET("/v2/users/:id", func(c echo.Context) error {
94 | user := make(map[string]interface{})
95 | user["id"] = c.Param("id")
96 | user["source"] = *addr
97 | user["name"] = fmt.Sprintf("v2-name-%s", c.Param("id"))
98 | user["query"] = c.QueryString()
99 | return c.JSON(http.StatusOK, user)
100 | })
101 | server.GET("/v2/account/:id", func(c echo.Context) error {
102 | account := make(map[string]interface{})
103 | account["id"] = c.Param("id")
104 | account["source"] = *addr
105 | account["account"] = fmt.Sprintf("v2-account-%s", c.Param("id"))
106 | account["query"] = c.QueryString()
107 | return c.JSON(http.StatusOK, account)
108 | })
109 |
110 | server.Start(*addr)
111 | }
112 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2.1'
2 | services:
3 | pushgateway:
4 | image: prom/pushgateway:v0.9.1
5 | expose:
6 | - 9091
7 | ports:
8 | - "9091:9091"
9 |
10 | prometheus:
11 | image: prom/prometheus:v2.9.2
12 | depends_on:
13 | - pushgateway
14 | volumes:
15 | - ./prometheus.yml:/etc/prometheus/prometheus.yml
16 | command:
17 | - --config.file=/etc/prometheus/prometheus.yml
18 | expose:
19 | - 9090
20 | ports:
21 | - "9090:9090"
22 |
23 | grafana:
24 | image: grafana/grafana:4.6.3
25 | depends_on:
26 | - prometheus
27 | environment:
28 | - GF_SECURITY_ADMIN_USER=${ADMIN_USER:-admin}
29 | - GF_SECURITY_ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin}
30 | - GF_USERS_ALLOW_SIGN_UP=false
31 | expose:
32 | - 3000
33 | ports:
34 | - "3000:3000"
35 |
36 | etcd:
37 | image: gcr.io/etcd-development/etcd
38 | depends_on:
39 | - prometheus
40 | expose:
41 | - 2379
42 | ports:
43 | - "2379:2379"
44 | command:
45 | - etcd
46 | - --listen-client-urls=http://0.0.0.0:2379
47 | - --advertise-client-urls=http://0.0.0.0:2379
48 |
49 | apiserver:
50 | image: fagongzi/apiserver
51 | depends_on:
52 | - etcd
53 | expose:
54 | - 9093
55 | ports:
56 | - "9093:9093"
57 | command:
58 | - manba-apiserver
59 | - --addr-http=:9093
60 | - --addr-store=etcd://etcd:2379
61 |
62 | proxy:
63 | image: fagongzi/proxy
64 | depends_on:
65 | - etcd
66 | - apiserver
67 | expose:
68 | - 80
69 | ports:
70 | - "80:80"
71 | command:
72 | - manba-proxy
73 | - --addr=:80
74 | - --addr-store=etcd://etcd:2379
75 | - --metric-job=proxy
76 | - --metric-address=pushgateway:9091
77 | - --interval-metric-sync=5
78 | - --js
79 |
--------------------------------------------------------------------------------
/docs-cn/apiserver.md:
--------------------------------------------------------------------------------
1 | ApiServer
2 | --------------
3 | ApiServer对外提供GRPC接口,用来管理Manba的元信息(Cluster、Server、Routing以及API)。
4 |
5 | # 对外开放的接口:
6 | ```
7 | // MetaService is a interface for meta manager
8 | service MetaService {
9 | rpc PutCluster (PutClusterReq) returns (PutClusterRsp) {}
10 | rpc RemoveCluster (RemoveClusterReq) returns (RemoveClusterRsp) {}
11 | rpc GetCluster (GetClusterReq) returns (GetClusterRsp) {}
12 | rpc GetClusterList (GetClusterListReq) returns (stream metapb.Cluster) {}
13 |
14 | rpc PutServer (PutServerReq) returns (PutServerRsp) {}
15 | rpc RemoveServer (RemoveServerReq) returns (RemoveServerRsp) {}
16 | rpc GetServer (GetServerReq) returns (GetServerRsp) {}
17 | rpc GetServerList (GetServerListReq) returns (stream metapb.Server) {}
18 |
19 | rpc PutAPI (PutAPIReq) returns (PutAPIRsp) {}
20 | rpc RemoveAPI (RemoveAPIReq) returns (RemoveAPIRsp) {}
21 | rpc GetAPI (GetAPIReq) returns (GetAPIRsp) {}
22 | rpc GetAPIList (GetAPIListReq) returns (stream metapb.API) {}
23 |
24 | rpc PutRouting (PutRoutingReq) returns (PutRoutingRsp) {}
25 | rpc RemoveRouting (RemoveRoutingReq) returns (RemoveRoutingRsp) {}
26 | rpc GetRouting (GetRoutingReq) returns (GetRoutingRsp) {}
27 | rpc GetRoutingList (GetRoutingListReq) returns (stream metapb.Routing) {}
28 |
29 | rpc AddBind (AddBindReq) returns (AddBindRsp) {}
30 | rpc RemoveBind (RemoveBindReq) returns (RemoveBindRsp) {}
31 | rpc RemoveClusterBind (RemoveClusterBindReq) returns (RemoveClusterBindRsp) {}
32 | rpc GetBindServers (GetBindServersReq) returns (GetBindServersRsp) {}
33 | }
34 | ```
35 | 具体的PB在项目的`pkg/pb/rpcpb`目录下
36 |
37 | # 客户端
38 | 目前Gateway支持GO的客户端,这里以Gateway的GO客户端管理元信息的例子,参见[examples](../examples)
39 |
--------------------------------------------------------------------------------
/docs-cn/cluster.md:
--------------------------------------------------------------------------------
1 | Cluster
2 | -------
3 | 在Manba中,Cluster是一个逻辑的概念。它是后端真实Server的一个逻辑组,在同一个组内的后端Server提供相同的服务。
4 |
5 | # Cluster属性
6 | 一个Cluster包含2部分信息:
7 | ## ID
8 | Cluster ID, 全局唯一。
9 |
10 | ## Name
11 | Cluster名称
12 |
13 | ## LoadBalance
14 | Cluster采取的负载均衡算法。
15 |
--------------------------------------------------------------------------------
/docs-cn/images/bm_cpu_all.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs-cn/images/bm_cpu_all.jpg
--------------------------------------------------------------------------------
/docs-cn/images/bm_cpu_io_sum.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs-cn/images/bm_cpu_io_sum.jpg
--------------------------------------------------------------------------------
/docs-cn/images/bm_net_packet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs-cn/images/bm_net_packet.png
--------------------------------------------------------------------------------
/docs-cn/optimization.md:
--------------------------------------------------------------------------------
1 | ## Optimization
2 | Manba网关在使用的时候有一些地方需要做一些优化,以便达到更好的性能。这里给出一些优化配置的意见。
3 |
4 | ### Docker
5 | 在使用docker启动`Proxy`镜像的时候,由于默认的镜像的`somaxconn`是默认值`128`,docker 启动的时候加上如下参数:
6 | ```bash
7 | docker run -d --rm --sysctl net.core.somaxconn=你期望的值 fagongzi/proxy
8 | ```
9 |
10 | ### Keepalive
11 | Proxy默认回启用Keepalive来保持和后端服务的链接,这里有2个重要的启动参数:
12 | * limit-conn-keepalive 链接最大Keepalive保持的时间
13 | * limit-conn-idle 链接最大的空闲时间,超过这个时间,Proxy会主动关闭这个连接
14 |
15 | 建议配置: `后端服务的Keepalive时间 > limit-conn-keepalive > 2*limit-conn-idle`
16 |
17 | ### limit-dispatch
18 | 这个参数是用来设置`聚合`请求的工作协程池的个数,当系统中有`聚合`请求,可以适当调大这个值来提升`聚合`请求的吞吐。
19 |
20 | ### limit-copy
21 | 这个参数是用来设置`Copy流量`流量的工作协程池的个数,当系统中有`Copy流量`流量的场景,可以适当调大这个值来提升`Copy流量`的吞吐。
22 |
23 | ### limit-conn
24 | 这个参数是用来控制`Proxy`与每个后端建立的最大链接数,如果发现某个后端Server的并发非常高,延迟较大,有可能时间花费了等待空闲链接的操作上,可以适当调大这个值。
25 |
26 | ### limit-caching
27 | 这个参数控制单个`Proxy`使用多少内存来做缓存,如果`Proxy`中的API开启了缓存,那么可以通过这个参数控制缓存大小。
28 |
--------------------------------------------------------------------------------
/docs-cn/plugin.md:
--------------------------------------------------------------------------------
1 | Filter plugin
2 | --------------
3 | Manba中的很多功能都是使用Filter来实现的,用户的大部分功能需求都可以使用Filter来解决。所以Filter被设计成Plugin机制,借助于Go1.8的plugin机制,可以很好的扩展Manba。
4 |
5 | # Request处理流程
6 | request -> filter预处理 -> 转发请求 -> filter后置处理 -> 响应客户端
7 |
8 | 整个逻辑处理符合以下规则:
9 |
10 | * filter预处理返回错误,流程立即终止,并且使用filter返回的状态码响应客户端
11 | * filter后置处理返回错误,使用filter返回的状态码响应客户端
12 | * 转发请求,后端返回的状态码`>=500`,调用filter的错误处理接口
13 |
14 | # Filter接口定义
15 | ```golang
16 | // Filter filter interface
17 | type Filter interface {
18 | Name() string
19 | Init(cfg string) error
20 |
21 | Pre(c Context) (statusCode int, err error)
22 | Post(c Context) (statusCode int, err error)
23 | PostErr(c Context)
24 | }
25 |
26 | // Context filter context
27 | type Context interface {
28 | StartAt() time.Time
29 | EndAt() time.Time
30 |
31 | OriginRequest() *fasthttp.RequestCtx
32 | ForwardRequest() *fasthttp.Request
33 | Response() *fasthttp.Response
34 |
35 | API() *metapb.API
36 | DispatchNode() *metapb.DispatchNode
37 | Server() *metapb.Server
38 | Analysis() *util.Analysis
39 |
40 | SetAttr(key string, value interface{})
41 | GetAttr(key string) interface{}
42 | }
43 |
44 | // BaseFilter base filter support default implemention
45 | type BaseFilter struct{}
46 |
47 | // Pre execute before proxy
48 | func (f BaseFilter) Pre(c Context) (statusCode int, err error) {
49 | return http.StatusOK, nil
50 | }
51 |
52 | // Post execute after proxy
53 | func (f BaseFilter) Post(c Context) (statusCode int, err error) {
54 | return http.StatusOK, nil
55 | }
56 |
57 | // PostErr execute proxy has errors
58 | func (f BaseFilter) PostErr(c Context) {
59 |
60 | }
61 | ```
62 |
63 | 这些相关的定义都在`github.com/fagongzi/manba/pkg/filter`包中,每一个Filter都需要导入。其中的`Context`的上下文接口,提供了Filter和Manba交互的能力;`BaseFilter`定义了默认行为。
64 |
65 | # Manba加载Filter插件机制
66 | ```golang
67 | func newExternalFilter(filterSpec *conf.FilterSpec) (filter.Filter, error) {
68 | p, err := plugin.Open(filterSpec.ExternalPluginFile)
69 | if err != nil {
70 | return nil, err
71 | }
72 |
73 | s, err := p.Lookup("NewExternalFilter")
74 | if err != nil {
75 | return nil, err
76 | }
77 |
78 | sf := s.(func() (filter.Filter, error))
79 | return sf()
80 | }
81 | ```
82 |
83 | 每一个外部的Filter插件,对外提供`NewExternalFilter`,返回一个`filter.Filter`实现,或者错误。
84 |
85 | # Go1.8 Plugin的问题
86 | 当编写的自定义插件的时候,有一个问题涉及到Go1.8的一个[Bug](https://github.com/golang/go/issues/19233)。所以编写的自定义插件必须在`Manba的Project`下编译的插件才能被正确加载。
87 |
88 | # Go1.9.2以上版本
89 | 支持插件项目独立目录,但是不能有自己的vender目录,否则加载的时候一样会出现1.8的问题。
90 |
91 | # 自定义插件例子
92 | 可以参考这个例子实现自己的插件
93 | [参考JWT插件](https://github.com/fagongzi/jwt-plugin)
94 |
95 | # 启动自定义插件
96 | `Proxy`组件有一个`--filter`选项来指定Manba使用的插件以及顺序。默认情况下Manba使用一下的内置插件顺序:`--filter WHITELIST --filter WHITELIST --filter ANALYSIS --filter RATE-LIMITING --filter CIRCUIT-BREAKER --filter HTTP-ACCESS --filter HEADER --filter XFORWARD --filter VALIDATION`。例如我们开发好了一个插件JWT,并且编译成为jwt.so文件,可以加上启动参数加载插件:`--filter WHITELIST --filter WHITELIST --filter ANALYSIS --filter RATE-LIMITING --filter CIRCUIT-BREAKER --filter HTTP-ACCESS --filter HEADER --filter XFORWARD --filter VALIDATION --filter JWT:/plugins/jwt.so:/plugins/jwt.json`,自定义插件的格式:`名称:插件文件:插件配置`
97 |
--------------------------------------------------------------------------------
/docs-cn/proxy.md:
--------------------------------------------------------------------------------
1 | Proxy
2 | --------------
3 | Proxy是一个独立的进程,对外提供网关的代理服务,Proxy是无状态的,可以水平扩容,提升服务能力。
4 |
5 | # 职责
6 | Proxy的负责接收和响应客户端的http请求,可以作为后端服务的统一接入层。
7 |
8 | # Proxy的处理请求的流程
9 | 
--------------------------------------------------------------------------------
/docs-cn/routing.md:
--------------------------------------------------------------------------------
1 | Routing
2 | ------
3 | 在Manba中,Routing代表一个路由,利用路由我们可以实现我们的AB Test以及线上导流等高级特性。
4 |
5 | # Routing属性
6 | ## ID
7 | Routing ID,唯一标识
8 |
9 | ## Name
10 | Routing Name,路由名称
11 |
12 | ## clusterID
13 | 流量路由到哪一个Cluster
14 |
15 | ## apiID
16 | 针对哪一个API设置路由
17 |
18 | ## Condition(可选)
19 | 路由条件,当满足这些条件,则Manba执行这个路由。路由条件可以设置`cookie`、`querystring`、`header`、`json body`,`path value`中的参数的表达式。不配置,匹配所有流量。
20 |
21 | ## RoutingStrategy
22 | 路由策略,目前支持`Split`分发。分发是指:把满足条件的请求按照比例转发到目标Cluster,剩余比例的流量按照正常流程进入API匹配阶段,流向原有的Cluster。
23 |
24 | ## TrafficRate
25 | 路由流量的比例,例如设置为50,那么50%的流量会根据`RoutingStrategy`进行路由。
26 |
27 | ## Status
28 | 路由的状态,只有`UP`状态才会生效。
29 |
--------------------------------------------------------------------------------
/docs-cn/server.md:
--------------------------------------------------------------------------------
1 | Server
2 | ------
3 | 在Manba中,一个Server对应一个真实存在的后端Server。
4 |
5 | # Server属性
6 | ## ID
7 | Server ID,唯一标识。
8 |
9 | ## Addr
10 | Server地址,格式为:"IP:PORT"。
11 |
12 | ## Protocol
13 | Server的接口协议,目前支持HTTP。
14 |
15 | ## Weight
16 | Weight 服务器的权重(当该服务器所属的集群负载方式是权重轮询时则需要配置)
17 |
18 | ## MaxQPS
19 | Server能够支持的最大QPS,用于流控。Manba采用令牌桶算法,根据QPS限制流量,保护后端Server被压垮。
20 |
21 | ## HealthCheck(可选)
22 | Server的健康检查机制,目前支持HTTP的协议检查,支持检查返回状态码以及返回内容。如果没有设置,认为这个Server的健康检查交给外部,Manba永久认为这个Server是健康的。
23 |
24 | ## CircuitBreaker(可选)
25 | 熔断器,设置后端Server的熔断规则。熔断器分为3个状态:
26 |
27 | * Open
28 |
29 | Open状态,正常状态,Manba放入全部流量。当Manba发现失败的请求比例达到了设置的规则,熔断器会把状态切换到Close状态
30 |
31 | * Half
32 |
33 | Half状态,尝试恢复的状态。在这个状态下,Manba会尝试放入一定比例的流量,然后观察这些流量的请求的情况,如果达到预期就把状态转换为Open状态,如果没有达到预期,恢复成Close状态
34 |
35 | * Close
36 |
37 | Close状态,在这个状态下,Manba禁止任何流量进入这个后端Server,在达到指定的阈值时间后,Manba自动尝试切换到Half状态,尝试恢复。
38 |
--------------------------------------------------------------------------------
/docs/apiserver.md:
--------------------------------------------------------------------------------
1 | ApiServer
2 | --------------
3 | ApiServer provides GRPC APIs to manage metadata of Gateway which is Cluster,Server, Routing, and API info.
4 |
5 | # APIs Exposed
6 | ```
7 | // MetaService is a interface for meta manager
8 | service MetaService {
9 | rpc PutCluster (PutClusterReq) returns (PutClusterRsp) {}
10 | rpc RemoveCluster (RemoveClusterReq) returns (RemoveClusterRsp) {}
11 | rpc GetCluster (GetClusterReq) returns (GetClusterRsp) {}
12 | rpc GetClusterList (GetClusterListReq) returns (stream metapb.Cluster) {}
13 |
14 | rpc PutServer (PutServerReq) returns (PutServerRsp) {}
15 | rpc RemoveServer (RemoveServerReq) returns (RemoveServerRsp) {}
16 | rpc GetServer (GetServerReq) returns (GetServerRsp) {}
17 | rpc GetServerList (GetServerListReq) returns (stream metapb.Server) {}
18 |
19 | rpc PutAPI (PutAPIReq) returns (PutAPIRsp) {}
20 | rpc RemoveAPI (RemoveAPIReq) returns (RemoveAPIRsp) {}
21 | rpc GetAPI (GetAPIReq) returns (GetAPIRsp) {}
22 | rpc GetAPIList (GetAPIListReq) returns (stream metapb.API) {}
23 |
24 | rpc PutRouting (PutRoutingReq) returns (PutRoutingRsp) {}
25 | rpc RemoveRouting (RemoveRoutingReq) returns (RemoveRoutingRsp) {}
26 | rpc GetRouting (GetRoutingReq) returns (GetRoutingRsp) {}
27 | rpc GetRoutingList (GetRoutingListReq) returns (stream metapb.Routing) {}
28 |
29 | rpc AddBind (AddBindReq) returns (AddBindRsp) {}
30 | rpc RemoveBind (RemoveBindReq) returns (RemoveBindRsp) {}
31 | rpc RemoveClusterBind (RemoveClusterBindReq) returns (RemoveClusterBindRsp) {}
32 | rpc GetBindServers (GetBindServersReq) returns (GetBindServersRsp) {}
33 | }
34 | ```
35 | The PB is under `pkg/pb/rpcpb`.
36 |
37 | # Client
38 | For the moment, Gateway supports Go client. Here are [examples](../examples) of Go clients of Gateway which manage metadata.
39 |
--------------------------------------------------------------------------------
/docs/cluster.md:
--------------------------------------------------------------------------------
1 | Cluster
2 | -------
3 | In Gateway, Cluster is a logical concept. It is a logical collection of backend servers which provide the same service.
4 |
5 | # Cluster Attributes
6 | A Cluster has two fields.
7 | ## ID
8 | Cluster ID, unique identifier
9 |
10 | ## Name
11 | Cluster Name
12 |
13 | ## LoadBalance
14 | The load balance algorithm used by Cluster
--------------------------------------------------------------------------------
/docs/images/api_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/api_2.png
--------------------------------------------------------------------------------
/docs/images/api_basics.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/api_basics.png
--------------------------------------------------------------------------------
/docs/images/bm_cpu_all.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/bm_cpu_all.jpg
--------------------------------------------------------------------------------
/docs/images/bm_cpu_io_sum.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/bm_cpu_io_sum.jpg
--------------------------------------------------------------------------------
/docs/images/bm_net_packet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/bm_net_packet.png
--------------------------------------------------------------------------------
/docs/images/defaultFilters.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/defaultFilters.png
--------------------------------------------------------------------------------
/docs/images/jwt_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/jwt_example.png
--------------------------------------------------------------------------------
/docs/images/jwt_in_auth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/jwt_in_auth.png
--------------------------------------------------------------------------------
/docs/images/jwt_postman.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/jwt_postman.png
--------------------------------------------------------------------------------
/docs/images/postman.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/postman.png
--------------------------------------------------------------------------------
/docs/images/routing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/routing.png
--------------------------------------------------------------------------------
/docs/images/server_configuration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/server_configuration.png
--------------------------------------------------------------------------------
/docs/images/specs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/specs.png
--------------------------------------------------------------------------------
/docs/images/web_ui_front_page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/docs/images/web_ui_front_page.png
--------------------------------------------------------------------------------
/docs/optimization.md:
--------------------------------------------------------------------------------
1 | ## Optimization
2 | There are some places where the Gateway Gateway needs to be optimized to achieve better performance. Here are some suggestions for optimizing the configuration.
3 |
4 | ### Docker
5 | When using docker to start the `Proxy` image, since the default image `somaxconn` is the default value of `128`, the docker starts with the following parameters:
6 | ```bash
7 | Docker run -d --rm --sysctl net.core.somaxconn=The value you expect fagongzi/proxy
8 | ```
9 |
10 | ### Keepalive
11 | Proxy defaults to enabling Keepalive to maintain links to backend services. There are 2 important startup parameters:
12 | * limit-conn-keepalive link maximum keepalive time
13 | * limit-conn-idle The maximum idle time of the link. After this time, the Proxy will actively close the connection.
14 |
15 | Recommended configuration: `Keepalive time of backend service> limit-conn-keepalive > 2*limit-conn-idle`
16 |
17 | ### limit-dispatch
18 | This parameter is used to set the number of working coroutine pools for the ʻaggregation` request. When there is an 'aggregation' request in the system, this value can be appropriately adjusted to improve the throughput of the `aggregation` request.
19 |
20 | ### limit-copy
21 | This parameter is used to set the number of working coordination pools of `Copy traffic` traffic. When there is a scene of `Copy traffic` traffic in the system, this value can be adjusted appropriately to improve the throughput of `Copy traffic`.
22 |
23 | ### limit-conn
24 | This parameter is used to control the maximum number of links that `Proxy` can establish with each backend. If it finds that the backend of a backend server is very high and the delay is large, it may take time to wait for the operation of the idle link. Increase this value.
25 |
26 | ### limit-caching
27 | This parameter controls how much memory a single `Proxy` uses for caching. If the API in `Proxy` has caching enabled, then this parameter can be used to control the cache size.
--------------------------------------------------------------------------------
/docs/plugin-tutorial.md:
--------------------------------------------------------------------------------
1 | # Tutorial
2 | This tutorial aims to teach you how to make your own plugins.
3 | If you just started dealing with Gateway and want to add a plugin to fulfill your deployment needs, then congradulations, you have come to the right place!
4 | I have gone through a lot of pain trying to figure out how to write my JWT plugin. And thanks to the in-time replies of the maintainer and other members of the community, I finally made it. I wrapped up my weary and yet fruitful journey and published this tutorial in the hope that future newcomers have a detailed reference to look up.
5 | If you encounter any problem going through the tutorial, feel free to create an issue or send me an email to this address **brucewangno1@qq.com** with the subject "Issues with Gateway Tutorial."
6 |
7 | ## JWT Plugin Example
8 | Before we dive into anything, I should tell you that **pkg/proxy/filter_jwt.go** is a properly working one with methods **Pre()** and **Post()**. You may modify it to suit your needs.
9 | **examples/jwt_a_simple_working_one.json**, as its name suggests, is a simple working configuration which has one functionality - to check if the JWT is in the Redis server.
10 | Add an JWT entry to your Redis server by **redis-cli**:
11 | ```redis
12 | SET prefix_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.G9-vbFqv8vkn91T028YcVOVId-wUF9kG5wIc2UgXxe4 true
13 | ```
14 | **prefix_** is in consistency with that in the JSON configuration file.
15 |
16 | Start your proxy server with a command like the following:
17 | ```shell
18 | ./proxy --addr=127.0.0.1:80 --addr-rpc=127.0.0.1:9091 --addr-store=etcd://127.0.0.1:2379 --namespace=test --filter JWT --jwt "/some/path/leading/to/jwt_a_simple_working_one.json"
19 | ```
20 | 
21 | "this_is_jwt_secret" which appears in the JSON file is in the signature field in this JWT example.
22 | 
23 | Before we mock an API request from the client side, we have to go to the UI page and add this entry to take this all into effect. FYI, this makes **c.API().AuthFilter** in the **Pre()** method in **pkg/proxy/filter_jwt.go** **JWT** which matches **f.Name()**.
24 |
25 | 
26 | Alright, alright, alright. This picture is self-explanatory.
27 |
28 | ## Customize JWT Plugin
29 | Since we are developing a JWT plugin to authenticate external requests, most of us have already had some working knowledge of JWT. If not, [this official introduction](https://jwt.io/introduction/) is pretty informative. For Chinese developers, [this link from Ruan Yifeng's blog](http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html) is an excellent concise alternative.
30 |
31 | ### Source Code Walk-through
32 | **pkg/filter/filter.go** is pretty much an empty abstract from which **\*Filters** structs in **pkg/proxy/filter_\*.go** inherit the **BaseFilter** struct. For example, struct **ValidationFilter** only implementes methods **Init()**, **Name()**, and **Pre()**. These methods in **BaseFilter** are overridden and others remain available.
33 | For JWT plugins, you should name your file like **filter_my_plugin.go** under **pkg/proxy/** and refer to **filter_plugin.go** to write your own.
34 | **\*.so** plugin as mentioned in the [docs/plugin.md](plugin.md), which involves **cgo** is really troublesome and thus highly discouraged.
35 | After you have finished your plugin file, a plugin JSON configuration file is needed. For more information, please reference to [this JSON configuration file example](https://github.com/fagongzi/jwt-plugin).
36 | For your JWT plugin to take effect, you need to pass options **--filter JWT** and **--jwt yourJSONConfigurationFilePath**. This is because neither **filter_jwt.go** nor your plugin is in **defaultFilters** in **pkg/proxy/proxy.go**.
37 | An example JWT JSON configuration file can be found in [**examples/jwt.json**](../examples/jwt.json).
38 | 
39 | If one of the **defaultFilters** is expected to be used, please specify it by **--filter** like **--filter WHITELIST** when launching **proxy** because if there is **--filter**, **defaultFilters** gets discarded.
40 | 
41 |
42 |
--------------------------------------------------------------------------------
/docs/plugin.md:
--------------------------------------------------------------------------------
1 | Filter plugin
2 | --------------
3 | Most features of Gateway are implemented through Filter. Most of users' functional requirements are implemented through Filter. Filter is thus implemented as a plugin thanks to Go 1.8's plugin mechanism to scale Gateway well.
4 |
5 | # Handling Procedures of Request
6 | request -> filter preprocess -> redirect request -> filter postprocess -> respond client
7 |
8 | All the logic processing follows the rules below:
9 |
10 | * When filter preprocessing returns error, the procedure aborts immediately and uses the returned status code of filter to respond to client.
11 | * When filter postprocessing returns error, the returned status code of filter is used to respond to client.
12 | * When the status code of the response of redirected requests is `>=500`, filter's error handling API is called.
13 |
14 | # Filter API Definition
15 | ```golang
16 | // Filter filter interface
17 | type Filter interface {
18 | Name() string
19 | Init(cfg string) error
20 |
21 | Pre(c Context) (statusCode int, err error)
22 | Post(c Context) (statusCode int, err error)
23 | PostErr(c Context)
24 | }
25 |
26 | // Context filter context
27 | type Context interface {
28 | StartAt() time.Time
29 | EndAt() time.Time
30 |
31 | OriginRequest() *fasthttp.RequestCtx
32 | ForwardRequest() *fasthttp.Request
33 | Response() *fasthttp.Response
34 |
35 | API() *metapb.API
36 | DispatchNode() *metapb.DispatchNode
37 | Server() *metapb.Server
38 | Analysis() *util.Analysis
39 |
40 | SetAttr(key string, value interface{})
41 | GetAttr(key string) interface{}
42 | }
43 |
44 | // BaseFilter base filter support default implemention
45 | type BaseFilter struct{}
46 |
47 | // Pre execute before proxy
48 | func (f BaseFilter) Pre(c Context) (statusCode int, err error) {
49 | return http.StatusOK, nil
50 | }
51 |
52 | // Post execute after proxy
53 | func (f BaseFilter) Post(c Context) (statusCode int, err error) {
54 | return http.StatusOK, nil
55 | }
56 |
57 | // PostErr execute proxy has errors
58 | func (f BaseFilter) PostErr(c Context) {
59 |
60 | }
61 | ```
62 |
63 | Relevant definitions are in `github.com/fagongzi/gateway/pkg/filter`. Each filter needs to be imported. `Context` API provides the ability of interactions between Filter and Gateway. `BaseFilter` defines the default.
64 |
65 | # The Mechanism of Gateway Loading Filter Plugin
66 | ```golang
67 | func newExternalFilter(filterSpec *conf.FilterSpec) (filter.Filter, error) {
68 | p, err := plugin.Open(filterSpec.ExternalPluginFile)
69 | if err != nil {
70 | return nil, err
71 | }
72 |
73 | s, err := p.Lookup("NewExternalFilter")
74 | if err != nil {
75 | return nil, err
76 | }
77 |
78 | sf := s.(func() (filter.Filter, error))
79 | return sf()
80 | }
81 | ```
82 |
83 | Every external Filter plugin exposes `NewExternalFilter`. It returns a `filter.Filter` or an error.
84 |
85 | # A Problem of Go1.8 Plugin
86 | When writing customized plugin, there is a problem concerning a Go 1.8 [bug](https://github.com/golang/go/issues/19233). It can be avoided by compiling the customized plugin under `Gateway/Project` in order to make the plugin load correctly.
87 |
88 | # For Go 1.9.2 and Above
89 | Independent directories of plugins are supported. However, it can not have its own vender directory, otherwise, the same problem arises as with Go 1.8
90 |
91 | # A Customized Plugin Example
92 | You can refer to this example to make your own plugin
93 | [JWT Plugin Reference](https://github.com/fagongzi/jwt-plugin)
94 |
95 | # Start A Customized Plugin Example
96 | `Proxy` component has a `--filter` option to designate plugins used by Gateway and its order. By default, the order of built-in plugins of Gateway:`--filter WHITELIST --filter WHITELIST --filter ANALYSIS --filter RATE-LIMITING --filter CIRCUIT-BREAKER --filter HTTP-ACCESS --filter HEADER --filter XFORWARD --filter VALIDATION`. For instance, suppose we have a JWT plugin ready and it is compiled as a jwt.so file, options to start can be `--filter WHITELIST --filter WHITELIST --filter ANALYSIS --filter RATE-LIMITING --filter CIRCUIT-BREAKER --filter HTTP-ACCESS --filter HEADER --filter XFORWARD --filter VALIDATION --filter JWT:/plugins/jwt.so:/plugins/jwt.json`. The format of a customized plugin is `Name:Plugin File:Plugin Configuration`
--------------------------------------------------------------------------------
/docs/proxy.md:
--------------------------------------------------------------------------------
1 | Proxy
2 | --------------
3 | Proxy is an independent process which provides gateway proxy service. Proxy is stateless and able to scale horizontally to enhance service.
4 |
5 | # Responsibility
6 | Proxy accepts and responds to HTTP requests from clients. It can be the unified access layer of backend services.
7 |
8 | # Request Handling Procedure of Proxy
9 | 
--------------------------------------------------------------------------------
/docs/routing.md:
--------------------------------------------------------------------------------
1 | Routing
2 | ------
3 | A Routing represents a router. Through routing, we can implement AB test, traffic direction and other advanced features.
4 |
5 | # Routing Attributes
6 | ## ID
7 | Unique Identifier
8 |
9 | ## Name
10 | Routing Name
11 |
12 | ## clusterID
13 | The cluster to which the routing traffic goes.
14 |
15 | ## apiID
16 | The API for which the routing is
17 |
18 | ## Condition (Optional)
19 | Routing Condition. When the condition is met, Gateway executes this routing strategy. The routing condition can set the arguement expressions of `cookie`、`querystring`、`header`、`json body`,`path value`. If not set, all traffic is matched.
20 |
21 | ## RoutingStrategy
22 | Currently support `Split`, which refers to redirecting a certain percentage of eligible requests to the target cluster and direct the rest to the API matching phase and then to the original cluster destination.
23 |
24 | ## TrafficRate
25 | If set to 50, 50% of traffic is being routed according to `RoutingStrategy`.
26 |
27 | ## Status
28 | Routing is valid only if status is `UP`.
--------------------------------------------------------------------------------
/docs/server.md:
--------------------------------------------------------------------------------
1 | Server
2 | ------
3 | In Gateway, a server refers to a real backend server.
4 |
5 | # Server Attributes
6 | ## ID
7 | Unique Identifier
8 |
9 | ## Addr
10 | Format: "IP:PORT"
11 |
12 | ## Protocol
13 | API Protocol. Currently only support HTTP
14 |
15 | ## Weight
16 | Valid only if the load balance strategy is Weighted Round Robin
17 |
18 | ## MaxQPS
19 | Maximum QPS supported by server. Used to Control Traffic. Gateway uses the Token Bucket Algorithm, restricting traffic by MaxQPS, thus protecting backend servers from overload.
20 |
21 | ## HealthCheck (Optional)
22 | Health check mechanism, currently supporting HTTP check, response status code and response body. If not set, the server's health check becomes external responsibility and Gateway always assumes that this server is healthy.
23 |
24 | ## CircuitBreaker (Optional)
25 | Backend server circuit break status:
26 |
27 | * Open
28 |
29 | Normal. All traffic in. When Gateway find the failed requests to all requests ratio reach a certain threshold, CircuitBreaker switches from Open to Close.
30 |
31 | * Half
32 |
33 | Attempt to recover. Gateway tries to direct a certain percentage of traffic to the server and observe the result. If the expectation is met, CircuitBreaker switches to Open. If not, Close.
34 |
35 | * Close
36 |
37 | Gateway does not direct any traffic to this backend server. When the time threshold is reached, Gateway automatically tries to recover by switching to Half.
--------------------------------------------------------------------------------
/docs/tutorial.md:
--------------------------------------------------------------------------------
1 | # Tutorial
2 | If you have not dealt much with HTTP gateway before and want to deploy one in your company or just play with one, then congradulations, you have come to the right place!
3 | This tutorial is very user-friendly. It aims to assist first-timers to have a hands-on experience without going through the pain of searching, asking and wondering.
4 | This was tested on a **MacOS** environment.
5 | If you encounter any problem going through the tutorial, feel free to create an issue or send me an email to this address **brucewangno1@qq.com** with the subject "Issues with Gateway Tutorial."
6 |
7 | ## ETCD Setup
8 | ETCD is a distributed key-value storage required to store **Gateway configurations**.
9 | You need an ETCD server or a cluster of them running.
10 | Follow the directions on [this page](https://github.com/etcd-io/etcd/blob/master/Documentation/op-guide/container.md#docker).
11 |
12 | ## Gateway Setup
13 | ### Download this project
14 | Under **$GOPATH/src/github.com/fagongzi**, run
15 | ```shell
16 | git clone https://github.com/fagongzi/gateway.git
17 | ```
18 |
19 | ### Compile this project
20 | Make sure your Go version is **1.10** or above. Otherwise error will occurs.
21 | In the root directory of this project, there is a **Makefile** which is responsible to generate the executables and the static Web UI. Under the root directory of this project, run
22 | ```shell
23 | make
24 | ```
25 | to generate executables **apiserver** and **proxy** , found under directory **dist**; run
26 | ```shell
27 | make ui
28 | ```
29 | to generate the UI directory under dist, which is the Web UI needed by **apiserver**.
30 |
31 | ## Gateway Service Online
32 | ### Run executables
33 | So now there is a ETCD or a cluster of ETCD Docker containers running.
34 | Under directory **dist**, run the following two command lines in two separate terminal tabs in a termial (**iTerm** highly recommended)
35 | ```shell
36 | sudo ./proxy --addr=127.0.0.1:80 --addr-rpc=127.0.0.1:9091 --addr-store=etcd://127.0.0.1:2379 --namespace=test
37 | ./apiserver --addr=127.0.0.1:9091 --addr-store=etcd://127.0.0.1:2379 --discovery --namespace=test -ui=ui/dist
38 | ```
39 | to start proxy and apiserver.
40 |
41 | ## Backend Mock Service
42 | ### Start three servers
43 | Under the directory **cmd/backend**, there is the file **backend.go**. This is a backend mock service. It have many simple APIs like the one returning the hostname, ip and port of the server.
44 | Start up 3 terminal tabs and run the following commands in these 3 tabs, respectively.
45 | ```shell
46 | go run backend.go --addr=localhost:9000
47 | go run backend.go --addr=localhost:9001
48 | go run backend.go --addr=localhost:9002
49 | ```
50 | Alright, alright, alright. You now have three servers which provide the same service and will later form a cluster by configurations on the Web UI.
51 |
52 | ## Gateway Configuration
53 | There are two ways to configure Gateway which is responsible for stuff like redirecting traffic. The first one is through Web UI and the second is through GRPC.
54 |
55 | ### Through the Web UI
56 | Web UI is at http://localhost:9093/ui/index.html#/home. Please do not try to access http://localhost:9093. Instead of the Web UI showing up, you get
57 | ```json
58 | {"message":"Not Found"}
59 | ```
60 | A rookie mistake. In the future, this issue might be resolved.
61 |
62 | #### Web UI front page:
63 | 
64 | 1. Click on "Cluster" on the side bar and add a cluster. Let's name it "Cluster Server Info". For now the only load balance algorithm you could choose is "Round Robin".
65 | 2. Click on "Server" on the side bar and add the three servers you just started. QPS stands for Requests Per Second. 1000 is enough for our tutorial. /check is used by a health check mechanism.
66 | 
67 | 3. Click on "API" on the side bar and add an API.
68 | 
69 | The retry strategy is to retry sending a request to a cluster after 10ms if it failed last time.
70 | 
71 |
72 | The configuration is all set up.
73 |
74 | ## Postman
75 | ### Download Postman
76 | Click on [the link](https://www.getpostman.com/downloads/) to open the download page.
77 |
78 | ### Create a HTTP Get request
79 | Remember to add the **Host** field in **Headers**. Otherwise it will not work.
80 | 
81 |
82 | ### Click On "Send"
83 | And voila! You have something like
84 | ```html
85 | Johns-MacBook-Pro.local
86 | localhost:9002
87 | ```
88 | Notice that almost every time you click on **Send**, server address changes. This is because of the Round Robin load balance strategy.
89 |
90 | ## Bonus Round
91 | ### Routing Strategy
92 | 1. Create three more servers with ip:ports: "localhost:9003", "localhost:9004", "localhost:9005".
93 | 2. Create a cluster called "Cluster Server Info Backup"
94 | 3. Click on "Routing" on the side bar and add a routing. Our **Split** routing strategy reroutes 60% of all the API traffic initially bound to **Cluster Server Info** to our designated cluster **Cluster Server Info Backup**.
95 | 
96 | If you keep clicking on "Send" enough times, you will find that approximately 60% of all your HTTP Get requests goes to the second cluster.
97 |
--------------------------------------------------------------------------------
/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 |
5 | start_etcd() {
6 | etcd $ETCD_OPTS &
7 | }
8 |
9 | DEFAULT_IP="0.0.0.0"
10 |
11 | start_apiserver() {
12 | manba-apiserver --addr=${DEFAULT_IP}:9092 --addr-http=${DEFAULT_IP}:9093 --discovery $API_SERVER_OPTS &
13 | }
14 |
15 | INPUT_CMD=$@
16 | CMD=`cat cmd`
17 | if [ "$INPUT_CMD" = "" ]
18 | then
19 | INPUT_CMD=${CMD}
20 | fi
21 |
22 | DEFAULT_EXEC="manba-proxy --addr=${DEFAULT_IP}:80 --log-level=$MANBA_LOG_LEVEL $GW_PROXY_OPTS"
23 | if [ "${INPUT_CMD}" = 'demo' ]
24 | then
25 | start_etcd
26 | sleep 3
27 | start_apiserver
28 | sleep 1
29 | EXEC=$DEFAULT_EXEC
30 | fi
31 |
32 | if [ "${INPUT_CMD}" = 'proxy' ]
33 | then
34 | EXEC=$DEFAULT_EXEC
35 | fi
36 |
37 | if [ "${INPUT_CMD}" = 'apiserver' ]
38 | then
39 | EXEC="apiserver --addr=${DEFAULT_IP}:9092 --addr-http=${DEFAULT_IP}:9093 --discovery $API_SERVER_OPTS"
40 | fi
41 |
42 | if [ "${INPUT_CMD}" = 'etcd' ]
43 | then
44 | EXEC="etcd $ETCD_OPTS"
45 | fi
46 |
47 | if [ ! -z "${INPUT_CMD}" ] && [ -z "$EXEC" ]
48 | then
49 | EXEC=${INPUT_CMD}
50 | fi
51 |
52 | exec $EXEC
53 |
--------------------------------------------------------------------------------
/examples/cluster.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/fagongzi/gateway/pkg/pb/metapb"
7 | )
8 |
9 | func createCluster() error {
10 | c, err := getClient()
11 | if err != nil {
12 | return err
13 | }
14 |
15 | id, err := c.NewClusterBuilder().Name("cluster-01").Loadbalance(metapb.RoundRobin).Commit()
16 | if err != nil {
17 | return err
18 | }
19 |
20 | fmt.Printf("cluster id is: %d", id)
21 | return nil
22 | }
23 |
24 | func deleteCluster(id uint64) error {
25 | c, err := getClient()
26 | if err != nil {
27 | return err
28 | }
29 |
30 | return c.RemoveCluster(id)
31 | }
32 |
33 | func updateCluster(id uint64) error {
34 | c, err := getClient()
35 | if err != nil {
36 | return err
37 | }
38 |
39 | cluster, err := c.GetCluster(id)
40 | if err != nil {
41 | return err
42 | }
43 |
44 | // 修改名称
45 | _, err = c.NewClusterBuilder().Use(*cluster).Name("cluster-1").Commit()
46 | if err != nil {
47 | return err
48 | }
49 |
50 | fmt.Printf("cluster %d name is updated", id)
51 | return nil
52 | }
53 |
--------------------------------------------------------------------------------
/examples/example.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/fagongzi/gateway/pkg/client"
7 | )
8 |
9 | func main() {
10 |
11 | }
12 |
13 | // 如果你的api server使用了"--discovery"参数启动
14 | func getClientWithDiscovery() (client.Client, error) {
15 | return client.NewClientWithEtcdDiscovery("/services",
16 | time.Second*10,
17 | "127.0.0.1:2379")
18 | }
19 |
20 | // 如果你的api server没有使用"--discovery"参数启动
21 | func getClient() (client.Client, error) {
22 | return client.NewClient(time.Second*10,
23 | "127.0.0.1:9092")
24 | }
25 |
--------------------------------------------------------------------------------
/examples/jwt.json:
--------------------------------------------------------------------------------
1 | {
2 | "secret": "jwt secret",
3 | "method": "jwt signing method, [HS256|HS384|HS512]",
4 | "tokenLookup": "token lookup, [header|query|cookie:Authorization]",
5 | "authSchema": "jwt schema, [Bearer]",
6 | "renewTokenHeaderName": "the header name for new token in the response header",
7 | "csrfHeaderName": "the header name for CSRFToken",
8 | "redis": {
9 | "addr": "127.0.0.1:6379",
10 | "maxActive": "max connections, int",
11 | "maxIdle": "max idle connections, int",
12 | "idleTimeout": "idle timeout seconds, int"
13 | },
14 | "actions": [
15 | {
16 | "method": "token_in_redis",
17 | "params": {
18 | "prefix": "the prefix of token in the redis"
19 | }
20 | },
21 | {
22 | "method": "token_and_csrf_in_redis",
23 | "params": {
24 | "prefix": "the prefix of token in the redis",
25 | "csrf_white_method":"GET,OPTION",
26 | "csrf_white_path":"/testinfo,/testdesc"
27 | }
28 | },
29 | {
30 | "method": "renew_by_redis",
31 | "params": {
32 | "prefix": "the prefix of token in the redis",
33 | "ttl": "ttl seconds, int"
34 | }
35 | },
36 | {
37 | "method": "renew_by_raw",
38 | "params": {
39 | "ttl": "ttl seconds, required if the renew is true, int"
40 | }
41 | },
42 | {
43 | "method": "fetch_to_header",
44 | "params": {
45 | "prefix": "prefix added to field set to header",
46 | "fields": [
47 | "f1",
48 | "f2"
49 | ]
50 | }
51 | },
52 | {
53 | "method": "fetch_to_cookie",
54 | "params": {
55 | "prefix": "prefix added to field set to cookie",
56 | "fields": [
57 | "f1",
58 | "f2"
59 | ]
60 | }
61 | }
62 | ]
63 | }
--------------------------------------------------------------------------------
/examples/jwt_a_simple_working_one.json:
--------------------------------------------------------------------------------
1 | {
2 | "secret": "this_is_jwt_secret",
3 | "method": "HS256",
4 | "tokenLookup": "header:Authorization",
5 | "authSchema": "Bearer",
6 | "renewTokenHeaderName": "",
7 | "csrfHeaderName": "_csrf",
8 | "redis": {
9 | "addr": "127.0.0.1:6379",
10 | "maxActive": 1000,
11 | "maxIdle": 1000,
12 | "idleTimeout": 100
13 | },
14 | "actions": [
15 | {
16 | "method": "token_in_redis",
17 | "params": {
18 | "prefix": "prefix_"
19 | }
20 | }
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/examples/routing.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/fagongzi/gateway/pkg/pb/metapb"
5 | )
6 |
7 | func createRouting() error {
8 | c, err := getClient()
9 | if err != nil {
10 | return err
11 | }
12 |
13 | rb := c.NewRoutingBuilder()
14 | // 必选项
15 | // 下线
16 | rb.Down()
17 | // 上线
18 | rb.Up()
19 | // 拆分10%的流量到cluster 2,剩余90%的流量按照原有规则转发
20 | rb.TrafficRate(10)
21 | rb.To(2)
22 | rb.Strategy(metapb.Split)
23 |
24 | // 可选项
25 | // 目标请求必须包含 v 的query string,且必须是v1,那么就是把v1的流量导流10%到cluster 2
26 | param := metapb.Parameter{
27 | Name: "v",
28 | Source: metapb.QueryString,
29 | }
30 | rb.AddCondition(param, metapb.CMPEQ, "v1")
31 |
32 | _, err = rb.Commit()
33 | return err
34 | }
35 |
36 | func updateRouting(id uint64) error {
37 | c, err := getClient()
38 | if err != nil {
39 | return err
40 | }
41 |
42 | routing, err := c.GetRouting(id)
43 | if err != nil {
44 | return err
45 | }
46 |
47 | rb := c.NewRoutingBuilder().Use(*routing)
48 | // 必选项
49 | // 下线
50 | rb.Down()
51 | // 上线
52 | rb.Up()
53 | // 拆分10%的流量到cluster 2,剩余90%的流量按照原有规则转发
54 | rb.TrafficRate(10)
55 | rb.To(2)
56 | rb.Strategy(metapb.Split)
57 |
58 | // 可选项
59 | // 目标请求必须包含 v 的query string,且必须是v1,那么就是把v1的流量导流10%到cluster 2
60 | param := metapb.Parameter{
61 | Name: "v",
62 | Source: metapb.QueryString,
63 | }
64 | rb.AddCondition(param, metapb.CMPEQ, "v1")
65 |
66 | _, err = rb.Commit()
67 | return err
68 | }
69 |
--------------------------------------------------------------------------------
/examples/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "time"
6 | )
7 |
8 | func createServer() error {
9 | c, err := getClient()
10 | if err != nil {
11 | return err
12 | }
13 |
14 | sb := c.NewServerBuilder()
15 | // 必选项
16 | sb.Addr("127.0.0.1:8080").HTTPBackend().MaxQPS(100)
17 |
18 | // 健康检查,可选项
19 | // 每个10秒钟检查一次,每次检查的超时时间30秒,即30秒后端Server没有返回认为后端不健康
20 | sb.CheckHTTPCode("/check/path", time.Second*10, time.Second*30)
21 |
22 | // 熔断器,可选项
23 | // 统计周期1秒钟
24 | sb.CircuitBreakerCheckPeriod(time.Second)
25 | // 在Close状态60秒后自动转到Half状态
26 | sb.CircuitBreakerCloseToHalfTimeout(time.Second * 60)
27 | // Half状态下,允许10%的流量流入后端
28 | sb.CircuitBreakerHalfTrafficRate(10)
29 | // 在Half状态,1秒内有2%的请求失败了,转换到Close状态
30 | sb.CircuitBreakerHalfToCloseCondition(2)
31 | // 在Half状态,1秒内有90%的请求成功了,转换到Open状态
32 | sb.CircuitBreakerHalfToOpenCondition(90)
33 |
34 | id, err := sb.Commit()
35 | if err != nil {
36 | return err
37 | }
38 |
39 | fmt.Printf("server id is: %d", id)
40 |
41 | // 把这个server加入到cluster 1
42 | c.AddBind(1, id)
43 |
44 | // 把这个server从cluster 1 移除
45 | c.RemoveBind(1, id)
46 |
47 | // 加入到cluster 2
48 | c.AddBind(2, id)
49 | return nil
50 | }
51 |
52 | func updateServer(id uint64) error {
53 | c, err := getClient()
54 | if err != nil {
55 | return err
56 | }
57 |
58 | svr, err := c.GetServer(id)
59 | if err != nil {
60 | return err
61 | }
62 |
63 | sb := c.NewServerBuilder()
64 | sb.Use(*svr)
65 |
66 | // 修改你想要修改的字段
67 | sb.MaxQPS(1000)
68 | sb.NoCircuitBreaker() // 删除熔断器
69 | sb.NoHeathCheck() // 删除健康检查
70 |
71 | _, err = sb.Commit()
72 | return err
73 | }
74 |
75 | func deleteServer(id uint64) error {
76 | c, err := getClient()
77 | if err != nil {
78 | return err
79 | }
80 |
81 | return c.RemoveServer(id)
82 | }
83 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/fagongzi/gateway
2 |
3 | require (
4 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect
5 | github.com/boltdb/bolt v1.3.1 // indirect
6 | github.com/buger/jsonparser v0.0.0-20180318095312-2cac668e8456
7 | github.com/coreos/bbolt v1.3.0 // indirect
8 | github.com/coreos/etcd v3.3.12+incompatible
9 | github.com/coreos/go-semver v0.2.0 // indirect
10 | github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142 // indirect
11 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect
12 | github.com/dgrijalva/jwt-go v0.0.0-20180308231308-06ea1031745c
13 | github.com/fagongzi/goetty v0.0.0-20180427060148-8f06d410550f
14 | github.com/fagongzi/grpcx v0.0.0-20190226052515-f1ec50ae76bf
15 | github.com/fagongzi/log v0.0.0-20170831135209-9a647df25e0e
16 | github.com/fagongzi/util v0.0.0-20180330021808-4acf02da76a9
17 | github.com/garyburd/redigo v0.0.0-20180228092057-a69d19351219
18 | github.com/ghodss/yaml v1.0.0 // indirect
19 | github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415
20 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
21 | github.com/golang/groupcache v0.0.0-20181024230925-c65c006176ff // indirect
22 | github.com/golang/protobuf v0.0.0-20180430185241-b4deda0973fb
23 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect
24 | github.com/gorilla/websocket v0.0.0-20180816221803-3ff3320c2a17
25 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 // indirect
26 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
27 | github.com/grpc-ecosystem/grpc-gateway v1.6.2 // indirect
28 | github.com/jonboulle/clockwork v0.1.0 // indirect
29 | github.com/juju/ratelimit v1.0.1
30 | github.com/koding/websocketproxy v0.0.0-20180716164433-0fa3f994f6e7
31 | github.com/labstack/echo v0.0.0-20180412143600-6d227dfea4d2
32 | github.com/labstack/gommon v0.0.0-20180613044413-d6898124de91 // indirect
33 | github.com/mattn/go-colorable v0.0.0-20170801030607-167de6bfdfba // indirect
34 | github.com/mattn/go-isatty v0.0.0-20170925053441-0360b2af4f38 // indirect
35 | github.com/matttproud/golang_protobuf_extensions v0.0.0-20160424113007-c12348ce28de // indirect
36 | github.com/pkg/errors v0.8.1 // indirect
37 | github.com/prometheus/client_golang v0.0.0-20160817154824-c5b7fccd2042
38 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 // indirect
39 | github.com/prometheus/common v0.0.0-20180518154759-7600349dcfe1
40 | github.com/prometheus/procfs v0.0.0-20180705121852-ae68e2d4c00f // indirect
41 | github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d
42 | github.com/sirupsen/logrus v1.2.0 // indirect
43 | github.com/soheilhy/cmux v0.0.0-20180129155001-e09e9389d85d
44 | github.com/stretchr/testify v1.2.2
45 | github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 // indirect
46 | github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 // indirect
47 | github.com/valyala/fasthttp v1.2.0
48 | github.com/valyala/fastrand v1.0.0
49 | github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 // indirect
50 | github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 // indirect
51 | go.uber.org/atomic v1.3.2 // indirect
52 | go.uber.org/multierr v1.1.0 // indirect
53 | go.uber.org/zap v1.9.1 // indirect
54 | golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3
55 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
56 | golang.org/x/text v0.3.2 // indirect
57 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 // indirect
58 | google.golang.org/genproto v0.0.0-20180716172848-2731d4fa720b // indirect
59 | google.golang.org/grpc v0.0.0-20180619221905-168a6198bcb0
60 | gopkg.in/sourcemap.v1 v1.0.5 // indirect
61 | gopkg.in/yaml.v2 v2.2.2 // indirect
62 | )
63 |
64 | go 1.13
65 |
--------------------------------------------------------------------------------
/images/alipay.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/images/alipay.jpg
--------------------------------------------------------------------------------
/images/arch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/images/arch.png
--------------------------------------------------------------------------------
/images/flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/images/flow.png
--------------------------------------------------------------------------------
/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/images/logo.png
--------------------------------------------------------------------------------
/images/qr.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/images/qr.jpg
--------------------------------------------------------------------------------
/images/wechat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fagongzi/manba/2b39aeed1c7805e5b8e5ae8e5a0b482f539141e0/images/wechat.png
--------------------------------------------------------------------------------
/pkg/client/cluster.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "github.com/fagongzi/gateway/pkg/pb"
5 | "github.com/fagongzi/gateway/pkg/pb/metapb"
6 | "github.com/fagongzi/gateway/pkg/pb/rpcpb"
7 | )
8 |
9 | // ClusterBuilder cluster builder
10 | type ClusterBuilder struct {
11 | c *client
12 | value metapb.Cluster
13 | }
14 |
15 | // NewClusterBuilder return a cluster build
16 | func (c *client) NewClusterBuilder() *ClusterBuilder {
17 | return &ClusterBuilder{
18 | c: c,
19 | value: metapb.Cluster{},
20 | }
21 | }
22 |
23 | // Use use a cluster
24 | func (cb *ClusterBuilder) Use(value metapb.Cluster) *ClusterBuilder {
25 | cb.value = value
26 | return cb
27 | }
28 |
29 | // Name set a name
30 | func (cb *ClusterBuilder) Name(name string) *ClusterBuilder {
31 | cb.value.Name = name
32 | return cb
33 | }
34 |
35 | // Loadbalance set a loadbalance
36 | func (cb *ClusterBuilder) Loadbalance(lb metapb.LoadBalance) *ClusterBuilder {
37 | cb.value.LoadBalance = lb
38 | return cb
39 | }
40 |
41 | // Commit commit
42 | func (cb *ClusterBuilder) Commit() (uint64, error) {
43 | err := pb.ValidateCluster(&cb.value)
44 | if err != nil {
45 | return 0, err
46 | }
47 |
48 | return cb.c.putCluster(cb.value)
49 | }
50 |
51 | // Build build
52 | func (cb *ClusterBuilder) Build() (*rpcpb.PutClusterReq, error) {
53 | err := pb.ValidateCluster(&cb.value)
54 | if err != nil {
55 | return nil, err
56 | }
57 |
58 | return &rpcpb.PutClusterReq{
59 | Cluster: cb.value,
60 | }, nil
61 | }
62 |
--------------------------------------------------------------------------------
/pkg/client/plugin.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "github.com/fagongzi/gateway/pkg/pb"
5 | "github.com/fagongzi/gateway/pkg/pb/metapb"
6 | "github.com/fagongzi/gateway/pkg/pb/rpcpb"
7 | )
8 |
9 | // PluginBuilder plugin builder
10 | type PluginBuilder struct {
11 | c *client
12 | value metapb.Plugin
13 | }
14 |
15 | // NewPluginBuilder return a plugin build
16 | func (c *client) NewPluginBuilder() *PluginBuilder {
17 | return &PluginBuilder{
18 | c: c,
19 | value: metapb.Plugin{
20 | Type: metapb.JavaScript,
21 | },
22 | }
23 | }
24 |
25 | // Use use a plugin
26 | func (sb *PluginBuilder) Use(value metapb.Plugin) *PluginBuilder {
27 | sb.value = value
28 | return sb
29 | }
30 |
31 | // Name set plugin name
32 | func (sb *PluginBuilder) Name(name string) *PluginBuilder {
33 | sb.value.Name = name
34 | return sb
35 | }
36 |
37 | // Version set plugin version
38 | func (sb *PluginBuilder) Version(version int64) *PluginBuilder {
39 | sb.value.Version = version
40 | return sb
41 | }
42 |
43 | // Author set plugin author
44 | func (sb *PluginBuilder) Author(author, email string) *PluginBuilder {
45 | sb.value.Author = author
46 | sb.value.Email = email
47 | return sb
48 | }
49 |
50 | // Script set plugin script
51 | func (sb *PluginBuilder) Script(content, cfg []byte) *PluginBuilder {
52 | sb.value.Content = content
53 | sb.value.Cfg = cfg
54 | return sb
55 | }
56 |
57 | // Commit commit
58 | func (sb *PluginBuilder) Commit() (uint64, error) {
59 | err := pb.ValidatePlugin(&sb.value)
60 | if err != nil {
61 | return 0, err
62 | }
63 |
64 | return sb.c.putPlugin(sb.value)
65 | }
66 |
67 | // Build build
68 | func (sb *PluginBuilder) Build() (*rpcpb.PutPluginReq, error) {
69 | err := pb.ValidatePlugin(&sb.value)
70 | if err != nil {
71 | return nil, err
72 | }
73 |
74 | return &rpcpb.PutPluginReq{
75 | Plugin: sb.value,
76 | }, nil
77 | }
78 |
--------------------------------------------------------------------------------
/pkg/client/routing.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "github.com/fagongzi/gateway/pkg/pb"
5 | "github.com/fagongzi/gateway/pkg/pb/metapb"
6 | "github.com/fagongzi/gateway/pkg/pb/rpcpb"
7 | )
8 |
9 | // RoutingBuilder routing builder
10 | type RoutingBuilder struct {
11 | c *client
12 | value metapb.Routing
13 | }
14 |
15 | // NewRoutingBuilder return a routing build
16 | func (c *client) NewRoutingBuilder() *RoutingBuilder {
17 | return &RoutingBuilder{
18 | c: c,
19 | value: metapb.Routing{},
20 | }
21 | }
22 |
23 | // Use use a cluster
24 | func (rb *RoutingBuilder) Use(value metapb.Routing) *RoutingBuilder {
25 | rb.value = value
26 | return rb
27 | }
28 |
29 | // To routing to
30 | func (rb *RoutingBuilder) To(clusterID uint64) *RoutingBuilder {
31 | rb.value.ClusterID = clusterID
32 | return rb
33 | }
34 |
35 | // AddCondition add condition
36 | func (rb *RoutingBuilder) AddCondition(param metapb.Parameter, op metapb.CMP, expect string) *RoutingBuilder {
37 | rb.value.Conditions = append(rb.value.Conditions, metapb.Condition{
38 | Parameter: param,
39 | Cmp: op,
40 | Expect: expect,
41 | })
42 | return rb
43 | }
44 |
45 | // TrafficRate set traffic rate for this routing
46 | func (rb *RoutingBuilder) TrafficRate(rate int) *RoutingBuilder {
47 | rb.value.TrafficRate = int32(rate)
48 | return rb
49 | }
50 |
51 | // Strategy set strategy for this routing
52 | func (rb *RoutingBuilder) Strategy(strategy metapb.RoutingStrategy) *RoutingBuilder {
53 | rb.value.Strategy = strategy
54 | return rb
55 | }
56 |
57 | // Up up this routing
58 | func (rb *RoutingBuilder) Up() *RoutingBuilder {
59 | rb.value.Status = metapb.Up
60 | return rb
61 | }
62 |
63 | // Down down this routing
64 | func (rb *RoutingBuilder) Down() *RoutingBuilder {
65 | rb.value.Status = metapb.Down
66 | return rb
67 | }
68 |
69 | // Name routing name
70 | func (rb *RoutingBuilder) Name(name string) *RoutingBuilder {
71 | rb.value.Name = name
72 | return rb
73 | }
74 |
75 | // API set routing API
76 | func (rb *RoutingBuilder) API(api uint64) *RoutingBuilder {
77 | rb.value.API = api
78 | return rb
79 | }
80 |
81 | // Commit commit
82 | func (rb *RoutingBuilder) Commit() (uint64, error) {
83 | err := pb.ValidateRouting(&rb.value)
84 | if err != nil {
85 | return 0, err
86 | }
87 |
88 | return rb.c.putRouting(rb.value)
89 | }
90 |
91 | // Build build
92 | func (rb *RoutingBuilder) Build() (*rpcpb.PutRoutingReq, error) {
93 | err := pb.ValidateRouting(&rb.value)
94 | if err != nil {
95 | return nil, err
96 | }
97 |
98 | return &rpcpb.PutRoutingReq{
99 | Routing: rb.value,
100 | }, nil
101 | }
102 |
--------------------------------------------------------------------------------
/pkg/client/server.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/fagongzi/gateway/pkg/pb"
7 | "github.com/fagongzi/gateway/pkg/pb/metapb"
8 | "github.com/fagongzi/gateway/pkg/pb/rpcpb"
9 | )
10 |
11 | // ServerBuilder server builder
12 | type ServerBuilder struct {
13 | c *client
14 | value metapb.Server
15 | }
16 |
17 | // NewServerBuilder return a server build
18 | func (c *client) NewServerBuilder() *ServerBuilder {
19 | return &ServerBuilder{
20 | c: c,
21 | value: metapb.Server{},
22 | }
23 | }
24 |
25 | // Use use a server
26 | func (sb *ServerBuilder) Use(value metapb.Server) *ServerBuilder {
27 | sb.value = value
28 | return sb
29 | }
30 |
31 | // NoHeathCheck no heath check
32 | func (sb *ServerBuilder) NoHeathCheck() *ServerBuilder {
33 | sb.value.HeathCheck = nil
34 | return sb
35 | }
36 |
37 | // CheckHTTPCode use a heath check
38 | func (sb *ServerBuilder) CheckHTTPCode(path string, interval time.Duration, timeout time.Duration) *ServerBuilder {
39 | if sb.value.HeathCheck == nil {
40 | sb.value.HeathCheck = &metapb.HeathCheck{}
41 |
42 | }
43 |
44 | sb.value.HeathCheck.Path = path
45 | sb.value.HeathCheck.Body = ""
46 | sb.value.HeathCheck.CheckInterval = int64(interval)
47 | sb.value.HeathCheck.Timeout = int64(timeout)
48 | return sb
49 | }
50 |
51 | // CheckHTTPBody use a heath check
52 | func (sb *ServerBuilder) CheckHTTPBody(path, body string, interval time.Duration, timeout time.Duration) *ServerBuilder {
53 | if sb.value.HeathCheck == nil {
54 | sb.value.HeathCheck = &metapb.HeathCheck{}
55 |
56 | }
57 |
58 | sb.value.HeathCheck.Path = path
59 | sb.value.HeathCheck.Body = body
60 | sb.value.HeathCheck.CheckInterval = int64(interval)
61 | sb.value.HeathCheck.Timeout = int64(timeout)
62 | return sb
63 | }
64 |
65 | // Addr set addr
66 | func (sb *ServerBuilder) Addr(addr string) *ServerBuilder {
67 | sb.value.Addr = addr
68 | return sb
69 | }
70 |
71 | // HTTPBackend set backend is http backend
72 | func (sb *ServerBuilder) HTTPBackend() *ServerBuilder {
73 | sb.value.Protocol = metapb.HTTP
74 | return sb
75 | }
76 |
77 | // MaxQPS set max qps
78 | func (sb *ServerBuilder) MaxQPS(max int64) *ServerBuilder {
79 | sb.value.MaxQPS = max
80 | return sb
81 | }
82 |
83 | // Weight set robin weight
84 | func (sb *ServerBuilder) Weight(weight int64) *ServerBuilder {
85 | sb.value.Weight = weight
86 | return sb
87 | }
88 |
89 | // NoCircuitBreaker no circuit breaker
90 | func (sb *ServerBuilder) NoCircuitBreaker() *ServerBuilder {
91 | sb.value.CircuitBreaker = nil
92 | return sb
93 | }
94 |
95 | // CircuitBreakerCheckPeriod set circuit breaker period
96 | func (sb *ServerBuilder) CircuitBreakerCheckPeriod(checkPeriod time.Duration) *ServerBuilder {
97 | if sb.value.CircuitBreaker == nil {
98 | sb.value.CircuitBreaker = &metapb.CircuitBreaker{}
99 | }
100 |
101 | sb.value.CircuitBreaker.RateCheckPeriod = int64(checkPeriod)
102 | return sb
103 | }
104 |
105 | // CircuitBreakerHalfTrafficRate set circuit breaker traffic in half status
106 | func (sb *ServerBuilder) CircuitBreakerHalfTrafficRate(rate int) *ServerBuilder {
107 | if sb.value.CircuitBreaker == nil {
108 | sb.value.CircuitBreaker = &metapb.CircuitBreaker{}
109 | }
110 |
111 | sb.value.CircuitBreaker.HalfTrafficRate = int32(rate)
112 | return sb
113 | }
114 |
115 | // CircuitBreakerCloseToHalfTimeout set circuit breaker timeout that close status convert to half
116 | func (sb *ServerBuilder) CircuitBreakerCloseToHalfTimeout(timeout time.Duration) *ServerBuilder {
117 | if sb.value.CircuitBreaker == nil {
118 | sb.value.CircuitBreaker = &metapb.CircuitBreaker{}
119 | }
120 |
121 | sb.value.CircuitBreaker.CloseTimeout = int64(timeout)
122 | return sb
123 | }
124 |
125 | // CircuitBreakerHalfToCloseCondition set circuit breaker condition of half convert to close
126 | func (sb *ServerBuilder) CircuitBreakerHalfToCloseCondition(failureRate int) *ServerBuilder {
127 | if sb.value.CircuitBreaker == nil {
128 | sb.value.CircuitBreaker = &metapb.CircuitBreaker{}
129 | }
130 |
131 | sb.value.CircuitBreaker.FailureRateToClose = int32(failureRate)
132 | return sb
133 | }
134 |
135 | // CircuitBreakerHalfToOpenCondition set circuit breaker condition of half convert to open
136 | func (sb *ServerBuilder) CircuitBreakerHalfToOpenCondition(succeedRate int) *ServerBuilder {
137 | if sb.value.CircuitBreaker == nil {
138 | sb.value.CircuitBreaker = &metapb.CircuitBreaker{}
139 | }
140 |
141 | sb.value.CircuitBreaker.SucceedRateToOpen = int32(succeedRate)
142 | return sb
143 | }
144 |
145 | // Commit commit
146 | func (sb *ServerBuilder) Commit() (uint64, error) {
147 | err := pb.ValidateServer(&sb.value)
148 | if err != nil {
149 | return 0, err
150 | }
151 |
152 | return sb.c.putServer(sb.value)
153 | }
154 |
155 | // Build build
156 | func (sb *ServerBuilder) Build() (*rpcpb.PutServerReq, error) {
157 | err := pb.ValidateServer(&sb.value)
158 | if err != nil {
159 | return nil, err
160 | }
161 |
162 | return &rpcpb.PutServerReq{
163 | Server: sb.value,
164 | }, nil
165 | }
166 |
--------------------------------------------------------------------------------
/pkg/expr/expr_test.go:
--------------------------------------------------------------------------------
1 | package expr
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/valyala/fasthttp"
7 | )
8 |
9 | func TestParse(t *testing.T) {
10 | value := []byte("abc$(")
11 | _, err := Parse(value)
12 | if err == nil {
13 | t.Errorf("expect syntax error: %+v", err)
14 | }
15 |
16 | value = []byte("abc$(abc)")
17 | _, err = Parse(value)
18 | if err == nil {
19 | t.Errorf("expect syntax error: %+v", err)
20 | }
21 |
22 | value = []byte("abc$(origin.)")
23 | _, err = Parse(value)
24 | if err == nil {
25 | t.Errorf("expect syntax error: %+v", err)
26 | }
27 |
28 | value = []byte("abc$(origin.path)(")
29 | _, err = Parse(value)
30 | if err == nil {
31 | t.Errorf("expect syntax error: %+v", err)
32 | }
33 |
34 | value = []byte("abc$(origin.path))")
35 | _, err = Parse(value)
36 | if err == nil {
37 | t.Errorf("expect syntax error: %+v", err)
38 | }
39 |
40 | value = []byte("abc")
41 | exprs, err := Parse(value)
42 | if err != nil {
43 | t.Errorf("parse const error: %+v", err)
44 | }
45 | if len(exprs) != 1 || exprs[0].Name() != "const-expr" {
46 | t.Errorf("parse const error")
47 | }
48 |
49 | value = []byte("$(origin.path)")
50 | exprs, err = Parse(value)
51 | if err != nil {
52 | t.Errorf("parse origin-path error: %+v", err)
53 | }
54 | if len(exprs) != 1 || exprs[0].Name() != "origin-path-expr" {
55 | t.Errorf("parse origin-path error, %+v", exprs)
56 | }
57 |
58 | value = []byte("$(origin.query)")
59 | exprs, err = Parse(value)
60 | if err != nil {
61 | t.Errorf("parse origin-query error: %+v", err)
62 | }
63 | if len(exprs) != 1 || exprs[0].Name() != "origin-query-expr" {
64 | t.Errorf("parse origin-query error, %+v", exprs)
65 | }
66 |
67 | value = []byte("$(origin.query.abc)")
68 | exprs, err = Parse(value)
69 | if err != nil {
70 | t.Errorf("parse origin-query-param error: %+v", err)
71 | }
72 | if len(exprs) != 1 || exprs[0].Name() != "origin-query-param-expr" {
73 | t.Errorf("parse origin-query-param error, %+v", exprs)
74 | }
75 |
76 | value = []byte("$(origin.cookie.abc)")
77 | exprs, err = Parse(value)
78 | if err != nil {
79 | t.Errorf("parse origin-cookie error: %+v", err)
80 | }
81 | if len(exprs) != 1 || exprs[0].Name() != "origin-cookie-expr" {
82 | t.Errorf("parse origin-cookie error, %+v", exprs)
83 | }
84 |
85 | value = []byte("$(origin.header.abc)")
86 | exprs, err = Parse(value)
87 | if err != nil {
88 | t.Errorf("parse origin-header error: %+v", err)
89 | }
90 | if len(exprs) != 1 || exprs[0].Name() != "origin-header-expr" {
91 | t.Errorf("parse origin-header error, %+v", exprs)
92 | }
93 |
94 | value = []byte("$(origin.body.abc.abc.abc)")
95 | exprs, err = Parse(value)
96 | if err != nil {
97 | t.Errorf("parse origin-body error: %+v", err)
98 | }
99 | if len(exprs) != 1 || exprs[0].Name() != "origin-body-expr" {
100 | t.Errorf("parse origin-body error, %+v", exprs)
101 | }
102 |
103 | value = []byte("$(depend.abc.abc.abc)")
104 | exprs, err = Parse(value)
105 | if err != nil {
106 | t.Errorf("parse depend error: %+v", err)
107 | }
108 | if len(exprs) != 1 || exprs[0].Name() != "depend-expr" {
109 | t.Errorf("parse depend error, %+v", exprs)
110 | }
111 |
112 | value = []byte("$(param.abc)")
113 | exprs, err = Parse(value)
114 | if err != nil {
115 | t.Errorf("parse param error: %+v", err)
116 | }
117 | if len(exprs) != 1 || exprs[0].Name() != "param-expr" {
118 | t.Errorf("parse param error, %+v", exprs)
119 | }
120 |
121 | value = []byte("/$(origin.path)?id=$(param.abc)&value=$(origin.body.abc.abc)&value2=$(depend.abc.abc.abc)&value3=$(origin.header.abc)&value4=4")
122 | exprs, err = Parse(value)
123 | if err != nil {
124 | t.Errorf("parse param error: %+v", err)
125 | }
126 | if len(exprs) != 11 {
127 | t.Errorf("expect 11 expers but %d", len(exprs))
128 | }
129 | }
130 |
131 | func TestExec(t *testing.T) {
132 | exprs, err := Parse([]byte("$(origin.query.names)"))
133 | if err != nil {
134 | t.Errorf("expect syntax error: %+v", err)
135 | }
136 |
137 | req := fasthttp.AcquireRequest()
138 | req.SetRequestURI("http://127.0.0.1/path?names=abc&names=abc2")
139 | value := Exec(&Ctx{
140 | Origin: req,
141 | }, exprs...)
142 |
143 | if string(value) != "abc" {
144 | t.Errorf("expect but %s", value)
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/pkg/filter/cache_util.go:
--------------------------------------------------------------------------------
1 | package filter
2 |
3 | import (
4 | "github.com/fagongzi/goetty"
5 | "github.com/valyala/fasthttp"
6 | )
7 |
8 | // NewCachedValue returns a cached value
9 | func NewCachedValue(resp *fasthttp.Response) *goetty.ByteBuf {
10 | buf := goetty.NewByteBuf(128)
11 | buf.WriteInt(0)
12 | n := 0
13 | resp.Header.VisitAll(func(key, value []byte) {
14 | buf.WriteInt(len(key))
15 | buf.Write(key)
16 | buf.WriteInt(len(value))
17 | buf.Write(value)
18 | n++
19 | })
20 | buf.WriteInt(len(resp.Body()))
21 | buf.Write(resp.Body())
22 |
23 | goetty.Int2BytesTo(n, buf.RawBuf())
24 | return buf
25 | }
26 |
27 | // ReadCachedValueTo read cached value to response
28 | func ReadCachedValueTo(buf *goetty.ByteBuf, resp *fasthttp.Response) {
29 | headers, _ := buf.ReadInt()
30 | for i := 0; i < headers; i++ {
31 | resp.Header.SetBytesKV(readBytes(buf), readBytes(buf))
32 | }
33 |
34 | resp.SetBody(readBytes(buf))
35 | }
36 |
37 | func readBytes(buf *goetty.ByteBuf) []byte {
38 | n, _ := buf.ReadInt()
39 | _, value, _ := buf.ReadBytes(n)
40 | return value
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/filter/const.go:
--------------------------------------------------------------------------------
1 | package filter
2 |
3 | const (
4 | // AttrClientRealIP real client ip
5 | AttrClientRealIP = "__internal_real_ip__"
6 | // AttrUsingCachingValue using cached value to response
7 | AttrUsingCachingValue = "__internal_using_cache_value__"
8 | // AttrUsingResponse using response to response
9 | AttrUsingResponse = "__internal_using_response__"
10 |
11 | // BreakFilterChainCode break filter chain code
12 | BreakFilterChainCode = -1
13 | )
14 |
15 | // StringValue returns the attr value
16 | func StringValue(attr string, c Context) string {
17 | return c.GetAttr(attr).(string)
18 | }
19 |
--------------------------------------------------------------------------------
/pkg/filter/filter.go:
--------------------------------------------------------------------------------
1 | package filter
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/fagongzi/gateway/pkg/pb/metapb"
7 | "github.com/fagongzi/gateway/pkg/util"
8 | "github.com/valyala/fasthttp"
9 | )
10 |
11 | // Context filter context
12 | type Context interface {
13 | StartAt() time.Time
14 | EndAt() time.Time
15 |
16 | OriginRequest() *fasthttp.RequestCtx
17 | ForwardRequest() *fasthttp.Request
18 | Response() *fasthttp.Response
19 |
20 | API() *metapb.API
21 | DispatchNode() *metapb.DispatchNode
22 | Server() *metapb.Server
23 | Analysis() *util.Analysis
24 |
25 | SetAttr(key string, value interface{})
26 | GetAttr(key string) interface{}
27 | }
28 |
29 | // Filter filter interface
30 | type Filter interface {
31 | Name() string
32 | Init(cfg string) error
33 |
34 | Pre(c Context) (statusCode int, err error)
35 | Post(c Context) (statusCode int, err error)
36 | PostErr(c Context, code int, err error)
37 | }
38 |
39 | // BaseFilter base filter support default implemention
40 | type BaseFilter struct{}
41 |
42 | // Init init filter
43 | func (f BaseFilter) Init(cfg string) error {
44 | return nil
45 | }
46 |
47 | // Pre execute before proxy
48 | func (f BaseFilter) Pre(c Context) (statusCode int, err error) {
49 | return fasthttp.StatusOK, nil
50 | }
51 |
52 | // Post execute after proxy
53 | func (f BaseFilter) Post(c Context) (statusCode int, err error) {
54 | return fasthttp.StatusOK, nil
55 | }
56 |
57 | // PostErr execute proxy has errors
58 | func (f BaseFilter) PostErr(c Context, code int, err error) {
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/pkg/filter/test_help.go:
--------------------------------------------------------------------------------
1 | package filter
2 |
3 | import (
4 | "sync"
5 | "time"
6 |
7 | "github.com/fagongzi/gateway/pkg/pb/metapb"
8 | "github.com/fagongzi/gateway/pkg/util"
9 | "github.com/valyala/fasthttp"
10 | )
11 |
12 | // TestContext the context for test
13 | type TestContext struct {
14 | Attr sync.Map
15 | StartAtValue, EndAtValue time.Time
16 | OriginValue *fasthttp.RequestCtx
17 | ForwardValue *fasthttp.Request
18 | ResponseValue *fasthttp.Response
19 | APIValue *metapb.API
20 | NodeValue *metapb.DispatchNode
21 | ServerValue *metapb.Server
22 | AnalysisValue *util.Analysis
23 | }
24 |
25 | // StartAt returns StartAt value
26 | func (ctx *TestContext) StartAt() time.Time {
27 | return ctx.StartAtValue
28 | }
29 |
30 | // EndAt returns EndAt value
31 | func (ctx *TestContext) EndAt() time.Time {
32 | return ctx.EndAtValue
33 | }
34 |
35 | // OriginRequest returns OriginRequest value
36 | func (ctx *TestContext) OriginRequest() *fasthttp.RequestCtx {
37 | return ctx.OriginValue
38 | }
39 |
40 | // ForwardRequest returns ForwardRequest value
41 | func (ctx *TestContext) ForwardRequest() *fasthttp.Request {
42 | return ctx.ForwardValue
43 | }
44 |
45 | // Response returns Response value
46 | func (ctx *TestContext) Response() *fasthttp.Response {
47 | return ctx.ResponseValue
48 | }
49 |
50 | // API returns API value
51 | func (ctx *TestContext) API() *metapb.API {
52 | return ctx.APIValue
53 | }
54 |
55 | // DispatchNode returns DispatchNode value
56 | func (ctx *TestContext) DispatchNode() *metapb.DispatchNode {
57 | return ctx.NodeValue
58 | }
59 |
60 | // Server returns Server value
61 | func (ctx *TestContext) Server() *metapb.Server {
62 | return ctx.ServerValue
63 | }
64 |
65 | // Analysis returns Analysis value
66 | func (ctx *TestContext) Analysis() *util.Analysis {
67 | return ctx.AnalysisValue
68 | }
69 |
70 | // SetAttr set attr
71 | func (ctx *TestContext) SetAttr(key string, value interface{}) {
72 | ctx.Attr.Store(key, value)
73 | }
74 |
75 | // GetAttr get attr value
76 | func (ctx *TestContext) GetAttr(key string) interface{} {
77 | value, _ := ctx.Attr.Load(key)
78 | return value
79 | }
80 |
--------------------------------------------------------------------------------
/pkg/lb/haship.go:
--------------------------------------------------------------------------------
1 | package lb
2 |
3 | import (
4 | "hash/fnv"
5 |
6 | "github.com/valyala/fasthttp"
7 |
8 | "github.com/fagongzi/gateway/pkg/pb/metapb"
9 | "github.com/fagongzi/gateway/pkg/util"
10 | )
11 |
12 | // HashIPBalance is hash IP loadBalance impl
13 | type HashIPBalance struct {
14 | }
15 |
16 | // NewHashIPBalance create a HashIPBalance
17 | func NewHashIPBalance() LoadBalance {
18 | lb := HashIPBalance{}
19 | return lb
20 | }
21 |
22 | // Select select a server from servers using HashIPBalance
23 | func (haship HashIPBalance) Select(ctx *fasthttp.RequestCtx, servers []metapb.Server) uint64 {
24 | l := len(servers)
25 | if 0 >= l {
26 | return 0
27 | }
28 | hash := fnv.New32a()
29 | // key is client ip
30 | key := util.ClientIP(ctx)
31 | hash.Write([]byte(key))
32 | serve := servers[hash.Sum32()%uint32(l)]
33 | return serve.ID
34 | }
35 |
--------------------------------------------------------------------------------
/pkg/lb/haship_test.go:
--------------------------------------------------------------------------------
1 | package lb
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/fagongzi/gateway/pkg/pb/metapb"
7 | "github.com/valyala/fasthttp"
8 | )
9 |
10 | var (
11 | Servers = []metapb.Server{
12 | metapb.Server{
13 | ID: 1,
14 | Weight: 10,
15 | },
16 | metapb.Server{
17 | ID: 2,
18 | Weight: 20,
19 | },
20 | metapb.Server{
21 | ID: 3,
22 | Weight: 40,
23 | },
24 | metapb.Server{
25 | ID: 5,
26 | Weight: 50,
27 | },
28 | metapb.Server{
29 | ID: 19,
30 | Weight: 20,
31 | },
32 | }
33 | )
34 |
35 | func Test_HashIPBalance(t *testing.T) {
36 | lb := NewHashIPBalance()
37 | reqCtx := &fasthttp.RequestCtx{}
38 | reqCtx.Request.Header.Add("X-Forwarded-For", "192.168.0.5")
39 | for i := 0; i < 66; i++ {
40 | id := lb.Select(reqCtx, Servers)
41 | if id < 1 {
42 | t.Errorf("Test_HashIPBalance is error=%d", id)
43 | }
44 | t.Logf("id=%d", id)
45 | }
46 | }
47 |
48 | func Benchmark_HashIPBalance(b *testing.B) {
49 | lb := NewHashIPBalance()
50 | reqCtx := &fasthttp.RequestCtx{}
51 | reqCtx.Request.Header.Add("X-Forwarded-For", "192.168.0.5")
52 | for i := 0; i < b.N; i++ {
53 | id := lb.Select(reqCtx, Servers)
54 | if id < 1 {
55 | b.Errorf("Test_HashIPBalance is error=%d", id)
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/pkg/lb/lb.go:
--------------------------------------------------------------------------------
1 | package lb
2 |
3 | import (
4 | "github.com/fagongzi/gateway/pkg/pb/metapb"
5 | "github.com/valyala/fasthttp"
6 | )
7 |
8 | var (
9 | supportLbs = []metapb.LoadBalance{metapb.RoundRobin}
10 | )
11 |
12 | var (
13 | // LBS map loadBalance name and process function
14 | LBS = map[metapb.LoadBalance]func() LoadBalance{
15 | metapb.RoundRobin: NewRoundRobin,
16 | metapb.WightRobin: NewWeightRobin,
17 | metapb.IPHash: NewHashIPBalance,
18 | metapb.Rand: NewRandBalance,
19 | }
20 | )
21 |
22 | // LoadBalance loadBalance interface returns selected server's id
23 | type LoadBalance interface {
24 | Select(ctx *fasthttp.RequestCtx, servers []metapb.Server) uint64
25 | }
26 |
27 | // GetSupportLBS return supported loadBalances
28 | func GetSupportLBS() []metapb.LoadBalance {
29 | return supportLbs
30 | }
31 |
32 | // NewLoadBalance create a LoadBalance,if LoadBalance function is not supported
33 | // it will return NewRoundRobin
34 | func NewLoadBalance(name metapb.LoadBalance) LoadBalance {
35 | if l, ok := LBS[name]; ok {
36 | return l()
37 | }
38 | return NewRoundRobin()
39 | }
40 |
--------------------------------------------------------------------------------
/pkg/lb/rand.go:
--------------------------------------------------------------------------------
1 | package lb
2 |
3 | import (
4 | "github.com/valyala/fasthttp"
5 | "github.com/valyala/fastrand"
6 |
7 | "github.com/fagongzi/gateway/pkg/pb/metapb"
8 | )
9 |
10 | // RandBalance is rand loadBalance impl
11 | type RandBalance struct {
12 | }
13 |
14 | // NewRandBalance create a RandBalance
15 | func NewRandBalance() LoadBalance {
16 | lb := RandBalance{}
17 | return lb
18 | }
19 |
20 | // Select select a server from servers using fastrand
21 | func (rb RandBalance) Select(ctx *fasthttp.RequestCtx, servers []metapb.Server) uint64 {
22 | l := len(servers)
23 | if 0 >= l {
24 | return 0
25 | }
26 | server := servers[fastrand.Uint32n(uint32(l))]
27 | return server.ID
28 | }
29 |
--------------------------------------------------------------------------------
/pkg/lb/rand_test.go:
--------------------------------------------------------------------------------
1 | package lb
2 |
3 | import (
4 | "github.com/valyala/fasthttp"
5 | "testing"
6 | )
7 |
8 | func Test_RandBalance(t *testing.T) {
9 | lb := NewRandBalance()
10 | reqCtx := &fasthttp.RequestCtx{}
11 | for i := 0; i < 66; i++ {
12 | id := lb.Select(reqCtx, Servers)
13 | if id < 1 {
14 | t.Errorf("Test_HashIPBalance is error=%d", id)
15 | }
16 | t.Logf("id=%d", id)
17 | }
18 | }
19 |
20 | func Benchmark_RandBalance(b *testing.B) {
21 | lb := NewRandBalance()
22 | reqCtx := &fasthttp.RequestCtx{}
23 | for i := 0; i < b.N; i++ {
24 | id := lb.Select(reqCtx, Servers)
25 | if id < 1 {
26 | b.Errorf("Test_HashIPBalance is error=%d", id)
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/lb/roundrobin.go:
--------------------------------------------------------------------------------
1 | package lb
2 |
3 | import (
4 | "sync/atomic"
5 |
6 | "github.com/fagongzi/gateway/pkg/pb/metapb"
7 | "github.com/valyala/fasthttp"
8 | )
9 |
10 | // RoundRobin round robin loadBalance impl
11 | type RoundRobin struct {
12 | ops *uint64
13 | }
14 |
15 | // NewRoundRobin create a RoundRobin
16 | func NewRoundRobin() LoadBalance {
17 | var ops uint64
18 | ops = 0
19 |
20 | return RoundRobin{
21 | ops: &ops,
22 | }
23 | }
24 |
25 | // Select select a server from servers using RoundRobin
26 | func (rr RoundRobin) Select(req *fasthttp.RequestCtx, servers []metapb.Server) uint64 {
27 | l := uint64(len(servers))
28 |
29 | if 0 >= l {
30 | return 0
31 | }
32 |
33 | target := servers[int(atomic.AddUint64(rr.ops, 1)%l)]
34 | return target.ID
35 | }
36 |
--------------------------------------------------------------------------------
/pkg/lb/weightrobin.go:
--------------------------------------------------------------------------------
1 | package lb
2 |
3 | import (
4 | "github.com/fagongzi/gateway/pkg/pb/metapb"
5 | "github.com/valyala/fasthttp"
6 | )
7 |
8 | // WeightRobin weight robin loadBalance impl
9 | type WeightRobin struct {
10 | opts map[uint64]*weightRobin
11 | }
12 |
13 | // weightRobin used to save the weight info of server
14 | type weightRobin struct {
15 | effectiveWeight int64
16 | currentWeight int64
17 | }
18 |
19 | // NewWeightRobin create a WeightRobin
20 | func NewWeightRobin() LoadBalance {
21 | return &WeightRobin{
22 | opts: make(map[uint64]*weightRobin, 1024),
23 | }
24 | }
25 |
26 | // Select select a server from servers using WeightRobin
27 | func (w *WeightRobin) Select(req *fasthttp.RequestCtx, servers []metapb.Server) (best uint64) {
28 | var total int64
29 | l := len(servers)
30 | if 0 >= l {
31 | return 0
32 | }
33 |
34 | for i := l - 1; i >= 0; i-- {
35 | svr := servers[i]
36 |
37 | id := svr.ID
38 | if _, ok := w.opts[id]; !ok {
39 | w.opts[id] = &weightRobin{
40 | effectiveWeight: svr.Weight,
41 | }
42 | }
43 |
44 | wt := w.opts[id]
45 | wt.currentWeight += wt.effectiveWeight
46 | total += wt.effectiveWeight
47 |
48 | if wt.effectiveWeight < svr.Weight {
49 | wt.effectiveWeight++
50 | }
51 |
52 | if best == 0 || w.opts[uint64(best)] == nil || wt.currentWeight > w.opts[best].currentWeight {
53 | best = id
54 | }
55 | }
56 |
57 | if best == 0 {
58 | return 0
59 | }
60 |
61 | w.opts[best].currentWeight -= total
62 |
63 | return best
64 | }
65 |
--------------------------------------------------------------------------------
/pkg/lb/weightrobin_test.go:
--------------------------------------------------------------------------------
1 | package lb
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/fagongzi/gateway/pkg/pb/metapb"
7 | "github.com/valyala/fasthttp"
8 | )
9 |
10 | func TestWeightRobin_Select(t *testing.T) {
11 | var values []metapb.Server
12 | values = append(values, metapb.Server{
13 | ID: 1,
14 | Weight: 20,
15 | })
16 |
17 | values = append(values, metapb.Server{
18 | ID: 2,
19 | Weight: 10,
20 | })
21 |
22 | values = append(values, metapb.Server{
23 | ID: 3,
24 | Weight: 35,
25 | })
26 |
27 | values = append(values, metapb.Server{
28 | ID: 4,
29 | Weight: 5,
30 | })
31 |
32 | type fields struct {
33 | opts map[uint64]*weightRobin
34 | }
35 | type args struct {
36 | req *fasthttp.RequestCtx
37 | servers []metapb.Server
38 | }
39 | tests := []struct {
40 | name string
41 | fields fields
42 | args args
43 | wantBest []int
44 | }{
45 | {
46 | name: "test_case_1",
47 | fields: struct{ opts map[uint64]*weightRobin }{opts: make(map[uint64]*weightRobin, 50)},
48 | args: struct {
49 | req *fasthttp.RequestCtx
50 | servers []metapb.Server
51 | }{req: nil, servers: values},
52 | wantBest: []int{20, 10, 35, 5},
53 | },
54 | }
55 | for _, tt := range tests {
56 | var res = make(map[uint64]int)
57 | t.Run(tt.name, func(t *testing.T) {
58 | w := &WeightRobin{
59 | opts: tt.fields.opts,
60 | }
61 | for i := 0; i < 70; i++ {
62 | res[w.Select(tt.args.req, tt.args.servers)]++
63 | }
64 | })
65 | for k, v := range res {
66 | if tt.wantBest[k-1] != v {
67 | t.Errorf("WeightRobin.Select() = %v, want %v", res, tt.wantBest)
68 | }
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/pkg/pb/gen.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Generate all gateway protobuf bindings.
4 | # Run from repository root.
5 | #
6 | set -e
7 |
8 | # directories containing protos to be built
9 | DIRS="./metapb ./rpcpb"
10 |
11 | GOGOPROTO_ROOT="${GOPATH}/src/github.com/gogo/protobuf"
12 | GOGOPROTO_PATH="${GOGOPROTO_ROOT}:${GOGOPROTO_ROOT}/protobuf"
13 |
14 | GATEWAY_PB_PATH="${GOPATH}/src/github.com/fagongzi/gateway/pkg/pb"
15 |
16 |
17 | for dir in ${DIRS}; do
18 | pushd ${dir}
19 | protoc --gofast_out=plugins=grpc,import_prefix=github.com/fagongzi/gateway/pkg/pb/:. -I=.:"${GOGOPROTO_PATH}":"${GATEWAY_PB_PATH}":"${GOPATH}/src" *.proto
20 | sed -i.bak -E "s/github\.com\/fagongzi\/gateway\/pkg\/pb\/(gogoproto|github\.com|golang\.org|google\.golang\.org)/\1/g" *.pb.go
21 | sed -i.bak -E 's/github\.com\/fagongzi\/gateway\/pkg\/pb\/(errors|fmt|io)/\1/g' *.pb.go
22 | sed -i.bak -E 's/import _ \"gogoproto\"//g' *.pb.go
23 | sed -i.bak -E 's/import fmt \"fmt\"//g' *.pb.go
24 | sed -i.bak -E 's/import math \"github.com\/fagongzi\/gateway\/pkg\/pb\/math\"//g' *.pb.go
25 | rm -f *.bak
26 | goimports -w *.pb.go
27 | popd
28 | done
29 |
--------------------------------------------------------------------------------
/pkg/pb/rpcpb/services.go:
--------------------------------------------------------------------------------
1 | package rpcpb
2 |
3 | const (
4 | // ServiceMeta meta service name
5 | ServiceMeta = "gateway-service-meta"
6 | )
7 |
--------------------------------------------------------------------------------
/pkg/pb/validation.go:
--------------------------------------------------------------------------------
1 | package pb
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 |
7 | "github.com/fagongzi/gateway/pkg/expr"
8 | "github.com/fagongzi/gateway/pkg/pb/metapb"
9 | "github.com/fagongzi/gateway/pkg/plugin"
10 | )
11 |
12 | // ValidateRouting validate routing
13 | func ValidateRouting(value *metapb.Routing) error {
14 | if value.API == 0 {
15 | return fmt.Errorf("missing api")
16 | }
17 |
18 | if value.ClusterID == 0 {
19 | return fmt.Errorf("missing cluster")
20 | }
21 |
22 | if value.Name == "" {
23 | return fmt.Errorf("missing name")
24 | }
25 |
26 | if value.TrafficRate <= 0 || value.TrafficRate > 100 {
27 | return fmt.Errorf("error traffic rate: %d", value.TrafficRate)
28 | }
29 |
30 | return nil
31 | }
32 |
33 | // ValidateCluster validate cluster
34 | func ValidateCluster(value *metapb.Cluster) error {
35 | if value.Name == "" {
36 | return fmt.Errorf("missing name")
37 | }
38 |
39 | return nil
40 | }
41 |
42 | // ValidateServer validate server
43 | func ValidateServer(value *metapb.Server) error {
44 | if value.Addr == "" {
45 | return fmt.Errorf("missing server address")
46 | }
47 |
48 | if value.MaxQPS == 0 {
49 | return fmt.Errorf("missing server max qps")
50 | }
51 |
52 | return nil
53 | }
54 |
55 | // ValidateAPI validate api
56 | func ValidateAPI(value *metapb.API) error {
57 | if value.Name == "" {
58 | return fmt.Errorf("missing api name")
59 | }
60 |
61 | if value.URLPattern == "" {
62 | return fmt.Errorf("missing URLPattern")
63 | }
64 |
65 | for _, n := range value.Nodes {
66 | if n.URLRewrite != "" {
67 | _, err := expr.Parse([]byte(n.URLRewrite))
68 | if err != nil {
69 | return err
70 | }
71 | }
72 |
73 | for _, v := range n.Validations {
74 | for _, r := range v.Rules {
75 | if r.RuleType == metapb.RuleRegexp {
76 | _, err := regexp.Compile(r.Expression)
77 | if err != nil {
78 | return err
79 | }
80 | }
81 | }
82 | }
83 | }
84 |
85 | return nil
86 | }
87 |
88 | // ValidatePlugin validate plugin
89 | func ValidatePlugin(value *metapb.Plugin) error {
90 | if value.Name == "" {
91 | return fmt.Errorf("missing plugin name")
92 | }
93 |
94 | if value.Version == 0 {
95 | return fmt.Errorf("missing plugin version")
96 | }
97 |
98 | if len(value.Content) == 0 {
99 | return fmt.Errorf("missing plugin content")
100 | }
101 |
102 | _, err := plugin.NewRuntime(value)
103 | if err != nil {
104 | return err
105 | }
106 |
107 | return nil
108 | }
109 |
--------------------------------------------------------------------------------
/pkg/plugin/builtin_base.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | const (
4 | httpModuleName = "http"
5 | jsonModuleName = "json"
6 | logModuleName = "log"
7 | redisModuleName = "redis"
8 | )
9 |
10 | var (
11 | httpModule = newHTTPModule()
12 | jsonModule = &JSONModule{}
13 | logModule = &LogModule{}
14 | )
15 |
--------------------------------------------------------------------------------
/pkg/plugin/builtin_http.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "bytes"
5 | "io/ioutil"
6 | "net/http"
7 | "time"
8 |
9 | "github.com/valyala/fasthttp"
10 | )
11 |
12 | // HTTPResult result
13 | type HTTPResult struct {
14 | rsp *http.Response
15 | err error
16 | body string
17 | }
18 |
19 | func newHTTPResult(rsp *http.Response, err error) *HTTPResult {
20 | if rsp != nil {
21 | defer rsp.Body.Close()
22 |
23 | data, err := ioutil.ReadAll(rsp.Body)
24 | if err != nil {
25 | return &HTTPResult{
26 | err: err,
27 | }
28 | }
29 |
30 | return &HTTPResult{
31 | err: err,
32 | body: string(data),
33 | rsp: rsp,
34 | }
35 | }
36 |
37 | return &HTTPResult{
38 | err: err,
39 | rsp: rsp,
40 | }
41 | }
42 |
43 | // HasError returns true if has a error
44 | func (res *HTTPResult) HasError() bool {
45 | return res.err != nil
46 | }
47 |
48 | // Error returns error
49 | func (res *HTTPResult) Error() string {
50 | if res.err != nil {
51 | return res.err.Error()
52 | }
53 |
54 | return ""
55 | }
56 |
57 | // StatusCode returns status code
58 | func (res *HTTPResult) StatusCode() int {
59 | if res.HasError() {
60 | return 0
61 | }
62 |
63 | return res.rsp.StatusCode
64 | }
65 |
66 | // Header returns http response header
67 | func (res *HTTPResult) Header() map[string][]string {
68 | headers := make(map[string][]string)
69 | if res.HasError() {
70 | return headers
71 | }
72 |
73 | for key, values := range res.rsp.Header {
74 | headers[key] = values
75 | }
76 |
77 | return headers
78 | }
79 |
80 | // Cookie returns http response cookie
81 | func (res *HTTPResult) Cookie() []*http.Cookie {
82 | if res.HasError() {
83 | return nil
84 | }
85 |
86 | return res.rsp.Cookies()
87 | }
88 |
89 | // Body returns http response body
90 | func (res *HTTPResult) Body() string {
91 | if res.HasError() {
92 | return ""
93 | }
94 |
95 | return res.body
96 | }
97 |
98 | // HTTPModule http module
99 | type HTTPModule struct {
100 | client *http.Client
101 | }
102 |
103 | func newHTTPModule() *HTTPModule {
104 | client := &http.Client{}
105 | *client = *http.DefaultClient
106 | client.Timeout = time.Second * 30
107 |
108 | return &HTTPModule{
109 | client: client,
110 | }
111 | }
112 |
113 | // NewHTTPResponse returns http response
114 | func (h *HTTPModule) NewHTTPResponse() *FastHTTPResponseAdapter {
115 | return newFastHTTPResponseAdapter(fasthttp.AcquireResponse())
116 | }
117 |
118 | // Get go get
119 | func (h *HTTPModule) Get(url string) *HTTPResult {
120 | rsp, err := h.client.Get(url)
121 | return newHTTPResult(rsp, err)
122 | }
123 |
124 | // Post do post
125 | func (h *HTTPModule) Post(url string, body string, header map[string][]string) *HTTPResult {
126 | return h.do("POST", url, body, header)
127 | }
128 |
129 | // PostJSON do post
130 | func (h *HTTPModule) PostJSON(url string, body string, header map[string][]string) *HTTPResult {
131 | header["Content-Type"] = []string{"application/json"}
132 | return h.Post(url, body, header)
133 | }
134 |
135 | // Put do put
136 | func (h *HTTPModule) Put(url string, body string, header map[string][]string) *HTTPResult {
137 | return h.do("PUT", url, body, header)
138 | }
139 |
140 | // PutJSON do put json
141 | func (h *HTTPModule) PutJSON(url string, body string, header map[string][]string) *HTTPResult {
142 | header["Content-Type"] = []string{"application/json"}
143 | return h.Put(url, body, header)
144 | }
145 |
146 | // Delete do delete
147 | func (h *HTTPModule) Delete(url string, body string, header map[string][]string) *HTTPResult {
148 | return h.do("DELETE", url, body, header)
149 | }
150 |
151 | // DeleteJSON do delete json
152 | func (h *HTTPModule) DeleteJSON(url string, body string, header map[string][]string) *HTTPResult {
153 | header["Content-Type"] = []string{"application/json"}
154 | return h.Delete(url, body, header)
155 | }
156 |
157 | func (h *HTTPModule) do(method string, url string, body string, header map[string][]string) *HTTPResult {
158 | r := bytes.NewReader([]byte(body))
159 | req, err := http.NewRequest(method, url, r)
160 | if err != nil {
161 | return newHTTPResult(nil, err)
162 | }
163 |
164 | for key, values := range header {
165 | for _, value := range values {
166 | req.Header.Add(key, value)
167 | }
168 | }
169 |
170 | rsp, err := h.client.Do(req)
171 | return newHTTPResult(rsp, err)
172 | }
173 |
--------------------------------------------------------------------------------
/pkg/plugin/builtin_json.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "encoding/json"
5 | )
6 |
7 | // JSONModule json builtin
8 | type JSONModule struct {
9 | }
10 |
11 | // Stringify returns json string
12 | func (j *JSONModule) Stringify(value interface{}) string {
13 | v, _ := json.Marshal(value)
14 | return string(v)
15 | }
16 |
17 | // Parse parse a string to json
18 | func (j *JSONModule) Parse(value string) map[string]interface{} {
19 | obj := make(map[string]interface{})
20 | json.Unmarshal([]byte(value), &obj)
21 | return obj
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/plugin/builtin_log.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "github.com/fagongzi/log"
5 | )
6 |
7 | // LogModule log module
8 | type LogModule struct {
9 | }
10 |
11 | // Info info
12 | func (l *LogModule) Info(v ...interface{}) {
13 | log.Info(v...)
14 | }
15 |
16 | // Infof infof
17 | func (l *LogModule) Infof(format string, v ...interface{}) {
18 | log.Infof(format, v...)
19 | }
20 |
21 | // Debug debug
22 | func (l *LogModule) Debug(v ...interface{}) {
23 | log.Debug(v...)
24 | }
25 |
26 | // Debugf debugf
27 | func (l *LogModule) Debugf(format string, v ...interface{}) {
28 | log.Debugf(format, v...)
29 | }
30 |
31 | // Warn warn
32 | func (l *LogModule) Warn(v ...interface{}) {
33 | log.Warning(v...)
34 | }
35 |
36 | // Warnf warnf
37 | func (l *LogModule) Warnf(format string, v ...interface{}) {
38 | log.Warningf(format, v...)
39 | }
40 |
41 | // Warning warning
42 | func (l *LogModule) Warning(v ...interface{}) {
43 | log.Warning(v...)
44 | }
45 |
46 | // Warningf warningf
47 | func (l *LogModule) Warningf(format string, v ...interface{}) {
48 | log.Warningf(format, v...)
49 | }
50 |
51 | // Error error
52 | func (l *LogModule) Error(v ...interface{}) {
53 | log.Error(v...)
54 | }
55 |
56 | // Errorf errorf
57 | func (l *LogModule) Errorf(format string, v ...interface{}) {
58 | log.Errorf(format, v...)
59 | }
60 |
61 | // Fatal fatal
62 | func (l *LogModule) Fatal(v ...interface{}) {
63 | log.Fatal(v...)
64 | }
65 |
66 | // Fatalf fatalf
67 | func (l *LogModule) Fatalf(format string, v ...interface{}) {
68 | log.Fatalf(format, v...)
69 | }
70 |
--------------------------------------------------------------------------------
/pkg/plugin/builtin_redis.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/garyburd/redigo/redis"
7 | )
8 |
9 | // RedisModule redis module
10 | type RedisModule struct {
11 | rt *Runtime
12 | }
13 |
14 | // CreateRedis create redis
15 | func (m *RedisModule) CreateRedis(cfg map[string]interface{}) *RedisOp {
16 | p := &redis.Pool{
17 | MaxActive: int(cfg["maxActive"].(int64)),
18 | MaxIdle: int(cfg["maxIdle"].(int64)),
19 | IdleTimeout: time.Second * time.Duration(int(cfg["idleTimeout"].(int64))),
20 | Dial: func() (redis.Conn, error) {
21 | return redis.Dial("tcp",
22 | cfg["addr"].(string),
23 | redis.DialWriteTimeout(time.Second*10))
24 | },
25 | }
26 |
27 | m.rt.addCloser(p)
28 |
29 | conn := p.Get()
30 | _, err := conn.Do("PING")
31 | if err != nil {
32 | conn.Close()
33 | return &RedisOp{
34 | err: err,
35 | }
36 | }
37 |
38 | conn.Close()
39 | return &RedisOp{
40 | pool: p,
41 | }
42 | }
43 |
44 | // RedisOp redis
45 | type RedisOp struct {
46 | err error
47 | pool *redis.Pool
48 | }
49 |
50 | // Do do redis cmd
51 | func (r *RedisOp) Do(cmd string, args ...interface{}) *CmdResp {
52 | if r.err != nil {
53 | return &CmdResp{
54 | err: r.err,
55 | }
56 | }
57 |
58 | conn := r.pool.Get()
59 | rsp, err := conn.Do(cmd, args...)
60 | if err != nil {
61 | conn.Close()
62 | return &CmdResp{
63 | err: err,
64 | }
65 | }
66 |
67 | conn.Close()
68 | return &CmdResp{
69 | rsp: rsp,
70 | }
71 | }
72 |
73 | // CmdResp redis cmd resp
74 | type CmdResp struct {
75 | err error
76 | rsp interface{}
77 | }
78 |
79 | // HasError returns has error
80 | func (r *CmdResp) HasError() bool {
81 | return r.err != nil
82 | }
83 |
84 | // Error returns error
85 | func (r *CmdResp) Error() string {
86 | if r.err != nil {
87 | return r.err.Error()
88 | }
89 |
90 | return ""
91 | }
92 |
93 | // StringValue returns string value
94 | func (r *CmdResp) StringValue() string {
95 | if r.HasError() {
96 | return ""
97 | }
98 |
99 | value, _ := redis.String(r.rsp, nil)
100 | return value
101 | }
102 |
103 | // StringsValue returns strings value
104 | func (r *CmdResp) StringsValue() []string {
105 | if r.HasError() {
106 | return nil
107 | }
108 |
109 | value, _ := redis.Strings(r.rsp, nil)
110 | return value
111 | }
112 |
113 | // StringMapValue returns string map value
114 | func (r *CmdResp) StringMapValue() map[string]string {
115 | if r.HasError() {
116 | return make(map[string]string)
117 | }
118 |
119 | value, _ := redis.StringMap(r.rsp, nil)
120 | return value
121 | }
122 |
123 | // IntValue returns int value
124 | func (r *CmdResp) IntValue() int {
125 | if r.HasError() {
126 | return 0
127 | }
128 |
129 | value, _ := redis.Int(r.rsp, nil)
130 | return value
131 | }
132 |
133 | // IntsValue returns ints value
134 | func (r *CmdResp) IntsValue() []int {
135 | if r.HasError() {
136 | return nil
137 | }
138 |
139 | value, _ := redis.Ints(r.rsp, nil)
140 | return value
141 | }
142 |
143 | // IntMapValue returns int map value
144 | func (r *CmdResp) IntMapValue() map[string]int {
145 | if r.HasError() {
146 | return make(map[string]int)
147 | }
148 |
149 | value, _ := redis.IntMap(r.rsp, nil)
150 | return value
151 | }
152 |
153 | // Int64Value returns int64 value
154 | func (r *CmdResp) Int64Value() int64 {
155 | if r.HasError() {
156 | return 0
157 | }
158 |
159 | value, _ := redis.Int64(r.rsp, nil)
160 | return value
161 | }
162 |
163 | // Int64sValue returns int64s value
164 | func (r *CmdResp) Int64sValue() []int64 {
165 | if r.HasError() {
166 | return nil
167 | }
168 |
169 | value, _ := redis.Int64s(r.rsp, nil)
170 | return value
171 | }
172 |
173 | // Int64MapValue returns int64 map value
174 | func (r *CmdResp) Int64MapValue() map[string]int64 {
175 | if r.HasError() {
176 | return make(map[string]int64)
177 | }
178 |
179 | value, _ := redis.Int64Map(r.rsp, nil)
180 | return value
181 | }
182 |
--------------------------------------------------------------------------------
/pkg/plugin/engine.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "time"
7 |
8 | "github.com/fagongzi/gateway/pkg/filter"
9 | "github.com/fagongzi/gateway/pkg/pb/metapb"
10 | "github.com/fagongzi/log"
11 | )
12 |
13 | // Engine plugin engine
14 | type Engine struct {
15 | filter.BaseFilter
16 |
17 | name string
18 | enable bool
19 | applied []*Runtime
20 | lastActive time.Time
21 | }
22 |
23 | // NewEngine returns a plugin engine
24 | func NewEngine(enable bool, name string) *Engine {
25 | return &Engine{
26 | enable: enable,
27 | name: name,
28 | }
29 | }
30 |
31 | // LastActive returns the time that last used
32 | func (eng *Engine) LastActive() time.Time {
33 | return eng.lastActive
34 | }
35 |
36 | // Destroy destory all applied plugins
37 | func (eng *Engine) Destroy() {
38 | for _, rt := range eng.applied {
39 | rt.destroy()
40 | }
41 | }
42 |
43 | // UpdatePlugin update plugin
44 | func (eng *Engine) UpdatePlugin(plugin *metapb.Plugin) error {
45 | target := -1
46 | for idx, rt := range eng.applied {
47 | if rt.meta.ID == plugin.ID {
48 | target = idx
49 | break
50 | }
51 | }
52 |
53 | if target == -1 {
54 | return fmt.Errorf("plugin not found")
55 | }
56 |
57 | rt, err := NewRuntime(plugin)
58 | if err != nil {
59 | return err
60 | }
61 |
62 | eng.applied[target] = rt
63 | return nil
64 | }
65 |
66 | // ApplyPlugins apply plugins
67 | func (eng *Engine) ApplyPlugins(plugins ...*metapb.Plugin) error {
68 | var applied []*Runtime
69 | for idx, plugin := range plugins {
70 | rt, err := NewRuntime(plugin)
71 | if err != nil {
72 | return err
73 | }
74 |
75 | applied = append(applied, rt)
76 | log.Infof("plugin: %d/%s:%d applied with index %d",
77 | plugin.ID,
78 | plugin.Name,
79 | plugin.Version,
80 | idx)
81 | }
82 |
83 | eng.applied = applied
84 | return nil
85 | }
86 |
87 | // Name returns filter name
88 | func (eng *Engine) Name() string {
89 | return eng.name
90 | }
91 |
92 | // Init returns error if init failed
93 | func (eng *Engine) Init(cfg string) error {
94 | return nil
95 | }
96 |
97 | // Pre filter pre method
98 | func (eng *Engine) Pre(c filter.Context) (int, error) {
99 | if !eng.enable {
100 | return eng.BaseFilter.Pre(c)
101 | }
102 |
103 | eng.lastActive = time.Now()
104 |
105 | if len(eng.applied) == 0 {
106 | return eng.BaseFilter.Pre(c)
107 | }
108 |
109 | rc := acquireContext()
110 | rc.delegate = c
111 | for _, rt := range eng.applied {
112 | statusCode, err := rt.Pre(rc)
113 | if nil != err {
114 | releaseContext(rc)
115 | return statusCode, err
116 | }
117 |
118 | if statusCode == filter.BreakFilterChainCode {
119 | releaseContext(rc)
120 | return statusCode, err
121 | }
122 | }
123 |
124 | releaseContext(rc)
125 | return http.StatusOK, nil
126 | }
127 |
128 | // Post filter post method
129 | func (eng *Engine) Post(c filter.Context) (int, error) {
130 | if !eng.enable {
131 | return eng.BaseFilter.Post(c)
132 | }
133 |
134 | eng.lastActive = time.Now()
135 |
136 | if len(eng.applied) == 0 {
137 | return eng.BaseFilter.Post(c)
138 | }
139 |
140 | rc := acquireContext()
141 | rc.delegate = c
142 |
143 | l := len(eng.applied)
144 | for i := l - 1; i >= 0; i-- {
145 | rt := eng.applied[i]
146 |
147 | statusCode, err := rt.Post(rc)
148 | if nil != err {
149 | releaseContext(rc)
150 | return statusCode, err
151 | }
152 |
153 | if statusCode == filter.BreakFilterChainCode {
154 | releaseContext(rc)
155 | return statusCode, err
156 | }
157 | }
158 |
159 | releaseContext(rc)
160 | return http.StatusOK, nil
161 | }
162 |
163 | // PostErr filter post error method
164 | func (eng *Engine) PostErr(c filter.Context, code int, err error) {
165 | if !eng.enable {
166 | eng.BaseFilter.PostErr(c, code, err)
167 | return
168 | }
169 |
170 | eng.lastActive = time.Now()
171 |
172 | if len(eng.applied) == 0 {
173 | eng.BaseFilter.PostErr(c, code, err)
174 | return
175 | }
176 |
177 | rc := acquireContext()
178 | rc.delegate = c
179 |
180 | l := len(eng.applied)
181 | for i := l - 1; i >= 0; i-- {
182 | eng.applied[i].PostErr(rc, code, err)
183 | }
184 |
185 | releaseContext(rc)
186 | }
187 |
--------------------------------------------------------------------------------
/pkg/proxy/cfg.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "time"
7 |
8 | "github.com/fagongzi/gateway/pkg/util"
9 | )
10 |
11 | // Option proxy option
12 | type Option struct {
13 | LimitCountDispatchWorker uint64
14 | LimitCountCopyWorker uint64
15 | LimitCountHeathCheckWorker int
16 | LimitCountConn int
17 | LimitIntervalHeathCheck time.Duration
18 | LimitDurationConnKeepalive time.Duration
19 | LimitDurationConnIdle time.Duration
20 | LimitTimeoutWrite time.Duration
21 | LimitTimeoutRead time.Duration
22 | LimitBufferRead int
23 | LimitBufferWrite int
24 | LimitBytesBody int
25 | LimitBytesCaching uint64
26 |
27 | JWTCfgFile string
28 | CrossCfgFile string
29 |
30 | EnableWebSocket bool
31 | EnableJSPlugin bool
32 | DisableHeaderNameNormalizing bool
33 | }
34 |
35 | // Cfg proxy config
36 | type Cfg struct {
37 | Addr string
38 | AddrHTTPS string
39 | DefaultTLSCert string
40 | DefaultTLSKey string
41 | AddrRPC string
42 | AddrStore string
43 | AddrStoreUserName string
44 | AddrStorePwd string
45 | AddrPPROF string
46 | Namespace string
47 | TTLProxy int64
48 | Filers []*FilterSpec
49 |
50 | Option *Option
51 | Metric *util.MetricCfg
52 | }
53 |
54 | // AddFilter add a filter
55 | func (c *Cfg) AddFilter(filter *FilterSpec) {
56 | c.Filers = append(c.Filers, filter)
57 | }
58 |
59 | // FilterSpec filter spec
60 | type FilterSpec struct {
61 | Name string `json:"name"`
62 | External bool `json:"external,omitempty"`
63 | ExternalPluginFile string `json:"externalPluginFile,omitempty"`
64 | ExternalCfg string `json:"externalCfg,omitempty"`
65 | }
66 |
67 | // ParseFilter returns a filter
68 | func ParseFilter(filter string) (*FilterSpec, error) {
69 | specs := strings.Split(filter, ":")
70 |
71 | switch len(specs) {
72 | case 1:
73 | return &FilterSpec{Name: specs[0]}, nil
74 | case 2:
75 | return &FilterSpec{
76 | Name: specs[0],
77 | External: true,
78 | ExternalPluginFile: specs[1]}, nil
79 | case 3:
80 | return &FilterSpec{
81 | Name: specs[0],
82 | External: true,
83 | ExternalPluginFile: specs[1],
84 | ExternalCfg: specs[2]}, nil
85 | default:
86 | return nil, fmt.Errorf("error format: %s", filter)
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/pkg/proxy/checker.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | "github.com/fagongzi/gateway/pkg/pb/metapb"
8 | "github.com/fagongzi/gateway/pkg/store"
9 | "github.com/fagongzi/gateway/pkg/util"
10 | "github.com/fagongzi/log"
11 | "github.com/valyala/fasthttp"
12 | )
13 |
14 | func (r *dispatcher) readyToHeathChecker() {
15 | for i := 0; i < r.cnf.Option.LimitCountHeathCheckWorker; i++ {
16 | r.runner.RunCancelableTask(func(ctx context.Context) {
17 | log.Infof("start server check worker")
18 |
19 | for {
20 | select {
21 | case <-ctx.Done():
22 | return
23 | case id := <-r.checkerC:
24 | r.check(id)
25 | }
26 | }
27 | })
28 | }
29 | }
30 |
31 | func (r *dispatcher) addToCheck(svr *serverRuntime) {
32 | svr.circuit = metapb.Open
33 | if svr.meta.HeathCheck != nil {
34 | svr.useCheckDuration = time.Duration(svr.meta.HeathCheck.CheckInterval)
35 | }
36 | svr.heathTimeout.Stop()
37 | r.checkerC <- svr.meta.ID
38 | }
39 |
40 | func (r *dispatcher) heathCheckTimeout(arg interface{}) {
41 | id := arg.(uint64)
42 | if _, ok := r.servers[id]; ok {
43 | r.checkerC <- id
44 | }
45 | }
46 |
47 | func (r *dispatcher) check(id uint64) {
48 | svr, ok := r.servers[id]
49 | if !ok {
50 | return
51 | }
52 |
53 | defer func() {
54 | if svr.meta.HeathCheck != nil {
55 | if svr.useCheckDuration > r.cnf.Option.LimitIntervalHeathCheck {
56 | svr.useCheckDuration = r.cnf.Option.LimitIntervalHeathCheck
57 | }
58 |
59 | if svr.useCheckDuration == 0 {
60 | svr.useCheckDuration = time.Duration(svr.meta.HeathCheck.CheckInterval)
61 | }
62 |
63 | svr.heathTimeout, _ = r.tw.Schedule(svr.useCheckDuration, r.heathCheckTimeout, id)
64 | }
65 | }()
66 |
67 | status := metapb.Unknown
68 | prev := r.getServerStatus(svr.meta.ID)
69 |
70 | if svr.meta.HeathCheck == nil {
71 | log.Warnf("server <%d> heath check not setting", svr.meta.ID)
72 | r.watchEventC <- &store.Evt{
73 | Src: eventSrcStatusChanged,
74 | Type: eventTypeStatusChanged,
75 | Value: statusChanged{
76 | meta: *svr.meta,
77 | status: metapb.Up,
78 | },
79 | }
80 | return
81 | }
82 |
83 | if r.doCheck(svr) {
84 | status = metapb.Up
85 | } else {
86 | status = metapb.Down
87 | }
88 |
89 | if prev != status {
90 | r.watchEventC <- &store.Evt{
91 | Src: eventSrcStatusChanged,
92 | Type: eventTypeStatusChanged,
93 | Value: statusChanged{
94 | meta: *svr.meta,
95 | status: status,
96 | },
97 | }
98 | }
99 | }
100 |
101 | func (r *dispatcher) doCheck(svr *serverRuntime) bool {
102 | req := fasthttp.AcquireRequest()
103 | defer fasthttp.ReleaseRequest(req)
104 |
105 | req.SetRequestURI(svr.getCheckURL())
106 |
107 | opt := util.DefaultHTTPOption()
108 | *opt = *globalHTTPOptions
109 | opt.ReadTimeout = time.Duration(svr.meta.HeathCheck.Timeout)
110 |
111 | resp, err := r.httpClient.Do(req, svr.meta.Addr, opt)
112 | defer fasthttp.ReleaseResponse(resp)
113 | if err != nil {
114 | log.Warnf("server <%d, %s, %d> check failed, errors:\n%+v",
115 | svr.meta.ID,
116 | svr.getCheckURL(),
117 | svr.checkFailCount+1,
118 | err)
119 | svr.fail()
120 | return false
121 | }
122 |
123 | if fasthttp.StatusOK != resp.StatusCode() {
124 | log.Warnf("server <%d, %s, %d, %d> check failed",
125 | svr.meta.ID,
126 | svr.getCheckURL(),
127 | resp.StatusCode(),
128 | svr.checkFailCount+1)
129 | svr.fail()
130 | return false
131 | }
132 |
133 | if svr.meta.HeathCheck.Body != "" &&
134 | svr.meta.HeathCheck.Body != string(resp.Body()) {
135 | log.Warnf("server <%s, %s, %d> check failed, body <%s>, expect <%s>",
136 | svr.meta.Addr,
137 | svr.getCheckURL(),
138 | svr.checkFailCount+1,
139 | resp.Body(),
140 | svr.meta.HeathCheck.Body)
141 | svr.fail()
142 | return false
143 | }
144 |
145 | svr.reset()
146 | return true
147 | }
148 |
--------------------------------------------------------------------------------
/pkg/proxy/dispatcher_copy_on_write.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "github.com/fagongzi/gateway/pkg/pb/metapb"
5 | "github.com/fagongzi/gateway/pkg/route"
6 | )
7 |
8 | func (r *dispatcher) copyServers(exclude uint64) map[uint64]*serverRuntime {
9 | values := make(map[uint64]*serverRuntime)
10 | for key, value := range r.servers {
11 | if key != exclude {
12 | values[key] = value.clone()
13 | }
14 | }
15 | return values
16 | }
17 |
18 | func (r *dispatcher) copyClusters(exclude uint64) map[uint64]*clusterRuntime {
19 | values := make(map[uint64]*clusterRuntime)
20 | for key, value := range r.clusters {
21 | if key != exclude {
22 | values[key] = value.clone()
23 | }
24 |
25 | }
26 | return values
27 | }
28 |
29 | func (r *dispatcher) copyRoutings(exclude uint64) map[uint64]*routingRuntime {
30 | values := make(map[uint64]*routingRuntime)
31 | for key, value := range r.routings {
32 | if key != exclude {
33 | values[key] = value.clone()
34 | }
35 | }
36 | return values
37 | }
38 |
39 | func (r *dispatcher) copyAPIs(exclude uint64, excludeToRoute uint64) (*route.Route, map[uint64]*apiRuntime) {
40 | route := route.NewRoute()
41 | values := make(map[uint64]*apiRuntime)
42 | for key, value := range r.apis {
43 | if key != exclude {
44 | values[key] = value.clone()
45 | if key != excludeToRoute && value.isUp() {
46 | route.Add(values[key].meta)
47 | }
48 | }
49 | }
50 |
51 | return route, values
52 | }
53 |
54 | func (r *dispatcher) copyBinds(exclude metapb.Bind) map[uint64]*binds {
55 | // remove server from all cluster
56 | removedServer := exclude.ClusterID == 0
57 |
58 | values := make(map[uint64]*binds)
59 | for key, bindsInfo := range r.binds {
60 | if removedServer {
61 | exclude.ClusterID = key
62 | }
63 |
64 | newBindsInfo := &binds{}
65 | for _, info := range bindsInfo.servers {
66 | if info.svrID != exclude.ServerID || exclude.ClusterID != key {
67 | newBindsInfo.servers = append(newBindsInfo.servers, &bindInfo{
68 | svrID: info.svrID,
69 | status: info.status,
70 | })
71 | }
72 | }
73 |
74 | for _, info := range bindsInfo.actives {
75 | if info.ID != exclude.ServerID || exclude.ClusterID != key {
76 | newBindsInfo.actives = append(newBindsInfo.actives, info)
77 | }
78 | }
79 |
80 | values[key] = newBindsInfo
81 | }
82 |
83 | return values
84 | }
85 |
--------------------------------------------------------------------------------
/pkg/proxy/errors.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | var (
8 | // ErrPrefixRequestCancel user cancel request error
9 | ErrPrefixRequestCancel = "request canceled"
10 | // ErrNoServer no server
11 | ErrNoServer = errors.New("has no server")
12 | // ErrRewriteNotMatch rewrite not match request url
13 | ErrRewriteNotMatch = errors.New("rewrite not match request url")
14 | )
15 |
--------------------------------------------------------------------------------
/pkg/proxy/factory.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "errors"
5 | "plugin"
6 | "strings"
7 |
8 | "github.com/fagongzi/gateway/pkg/filter"
9 | )
10 |
11 | var (
12 | // ErrUnknownFilter unknown filter error
13 | ErrUnknownFilter = errors.New("unknown filter")
14 | )
15 |
16 | const (
17 | // FilterPrepare prepare filter
18 | FilterPrepare = "PREPARE"
19 | // FilterHTTPAccess access log filter
20 | FilterHTTPAccess = "HTTP-ACCESS"
21 | // FilterHeader header filter
22 | FilterHeader = "HEADER" // process header fiter
23 | // FilterXForward xforward fiter
24 | FilterXForward = "XFORWARD"
25 | // FilterBlackList blacklist filter
26 | FilterBlackList = "BLACKLIST"
27 | // FilterWhiteList whitelist filter
28 | FilterWhiteList = "WHITELIST"
29 | // FilterAnalysis analysis filter
30 | FilterAnalysis = "ANALYSIS"
31 | // FilterRateLimiting limit filter
32 | FilterRateLimiting = "RATE-LIMITING"
33 | // FilterCircuitBreake circuit breake filter
34 | FilterCircuitBreake = "CIRCUIT-BREAKER"
35 | // FilterValidation validation request filter
36 | FilterValidation = "VALIDATION"
37 | // FilterCaching caching filter
38 | FilterCaching = "CACHING"
39 | // FilterJWT jwt filter
40 | FilterJWT = "JWT"
41 | // FilterCross cross filter
42 | FilterCross = "CROSS"
43 | // FilterJSPlugin js plugin engine
44 | FilterJSPlugin = "JS-ENGINE"
45 | )
46 |
47 | func (p *Proxy) newFilter(filterSpec *FilterSpec) (filter.Filter, error) {
48 | if filterSpec.External {
49 | return newExternalFilter(filterSpec)
50 | }
51 |
52 | input := strings.ToUpper(filterSpec.Name)
53 |
54 | switch input {
55 | case FilterPrepare:
56 | return newPrepareFilter(), nil
57 | case FilterHTTPAccess:
58 | return newAccessFilter(), nil
59 | case FilterHeader:
60 | return newHeadersFilter(), nil
61 | case FilterXForward:
62 | return newXForwardForFilter(), nil
63 | case FilterAnalysis:
64 | return newAnalysisFilter(), nil
65 | case FilterBlackList:
66 | return newBlackListFilter(), nil
67 | case FilterWhiteList:
68 | return newWhiteListFilter(), nil
69 | case FilterRateLimiting:
70 | return newRateLimitingFilter(), nil
71 | case FilterCircuitBreake:
72 | return newCircuitBreakeFilter(), nil
73 | case FilterValidation:
74 | return newValidationFilter(), nil
75 | case FilterCaching:
76 | return newCachingFilter(p.cfg.Option.LimitBytesCaching, p.dispatcher.tw), nil
77 | case FilterJWT:
78 | return newJWTFilter(p.cfg.Option.JWTCfgFile)
79 | case FilterCross:
80 | return newCrossDomainFilter(p.cfg.Option.CrossCfgFile)
81 | case FilterJSPlugin:
82 | return p.jsEngine, nil
83 | default:
84 | return nil, ErrUnknownFilter
85 | }
86 | }
87 |
88 | func newExternalFilter(filterSpec *FilterSpec) (filter.Filter, error) {
89 | p, err := plugin.Open(filterSpec.ExternalPluginFile)
90 | if err != nil {
91 | return nil, err
92 | }
93 |
94 | s, err := p.Lookup("NewExternalFilter")
95 | if err != nil {
96 | return nil, err
97 | }
98 |
99 | sf := s.(func() (filter.Filter, error))
100 | return sf()
101 | }
102 |
--------------------------------------------------------------------------------
/pkg/proxy/filter.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "net/http"
5 | "time"
6 |
7 | "github.com/fagongzi/gateway/pkg/filter"
8 | "github.com/fagongzi/gateway/pkg/pb/metapb"
9 | "github.com/fagongzi/gateway/pkg/util"
10 | "github.com/fagongzi/log"
11 | "github.com/valyala/fasthttp"
12 | )
13 |
14 | func (f *Proxy) doPreFilters(requestTag string, c filter.Context, filters ...filter.Filter) (filterName string, statusCode int, err error) {
15 | for _, f := range filters {
16 | filterName = f.Name()
17 |
18 | statusCode, err = f.Pre(c)
19 | if nil != err {
20 | return filterName, statusCode, err
21 | }
22 |
23 | if statusCode == filter.BreakFilterChainCode {
24 | log.Debugf("%s: break pre filter chain by filter %s",
25 | requestTag,
26 | filterName)
27 | return filterName, statusCode, err
28 | }
29 | }
30 |
31 | return "", http.StatusOK, nil
32 | }
33 |
34 | func (f *Proxy) doPostFilters(requestTag string, c filter.Context, filters ...filter.Filter) (filterName string, statusCode int, err error) {
35 | l := len(filters)
36 | for i := l - 1; i >= 0; i-- {
37 | f := filters[i]
38 | statusCode, err = f.Post(c)
39 | if nil != err {
40 | return filterName, statusCode, err
41 | }
42 |
43 | if statusCode == filter.BreakFilterChainCode {
44 | log.Debugf("%s: break post filter chain by filter %s",
45 | requestTag,
46 | filterName)
47 | return filterName, statusCode, err
48 | }
49 | }
50 |
51 | return "", http.StatusOK, nil
52 | }
53 |
54 | func (f *Proxy) doPostErrFilters(c filter.Context, code int, err error, filters ...filter.Filter) {
55 | l := len(filters)
56 | for i := l - 1; i >= 0; i-- {
57 | f := filters[i]
58 | f.PostErr(c, code, err)
59 | }
60 | }
61 |
62 | type proxyContext struct {
63 | startAt time.Time
64 | endAt time.Time
65 | result *dispatchNode
66 | forwardReq *fasthttp.Request
67 | originCtx *fasthttp.RequestCtx
68 | rt *dispatcher
69 |
70 | attrs map[string]interface{}
71 | }
72 |
73 | func (c *proxyContext) init(rt *dispatcher, originCtx *fasthttp.RequestCtx, forwardReq *fasthttp.Request, result *dispatchNode) {
74 | c.result = result
75 | c.originCtx = originCtx
76 | c.forwardReq = forwardReq
77 | c.rt = rt
78 | c.startAt = time.Now()
79 | c.attrs = make(map[string]interface{})
80 | }
81 |
82 | func (c *proxyContext) reset() {
83 | if c.forwardReq != nil {
84 | fasthttp.ReleaseRequest(c.forwardReq)
85 | }
86 | *c = emptyContext
87 | }
88 |
89 | func (c *proxyContext) SetAttr(key string, value interface{}) {
90 | c.attrs[key] = value
91 | }
92 |
93 | func (c *proxyContext) GetAttr(key string) interface{} {
94 | return c.attrs[key]
95 | }
96 |
97 | func (c *proxyContext) StartAt() time.Time {
98 | return c.startAt
99 | }
100 |
101 | func (c *proxyContext) EndAt() time.Time {
102 | return c.endAt
103 | }
104 |
105 | func (c *proxyContext) DispatchNode() *metapb.DispatchNode {
106 | return c.result.node.meta
107 | }
108 |
109 | func (c *proxyContext) API() *metapb.API {
110 | return c.result.api.meta
111 | }
112 |
113 | func (c *proxyContext) Server() *metapb.Server {
114 | return c.result.dest.meta
115 | }
116 |
117 | func (c *proxyContext) ForwardRequest() *fasthttp.Request {
118 | return c.forwardReq
119 | }
120 |
121 | func (c *proxyContext) Response() *fasthttp.Response {
122 | return c.result.res
123 | }
124 |
125 | func (c *proxyContext) OriginRequest() *fasthttp.RequestCtx {
126 | return c.originCtx
127 | }
128 |
129 | func (c *proxyContext) Analysis() *util.Analysis {
130 | return c.rt.analysiser
131 | }
132 |
133 | func (c *proxyContext) setEndAt(endAt time.Time) {
134 | c.endAt = endAt
135 | }
136 |
137 | func (c *proxyContext) validateRequest() bool {
138 | return c.result.node.validate(c.ForwardRequest())
139 | }
140 |
141 | func (c *proxyContext) allowWithBlacklist(ip string) bool {
142 | return c.result.api.allowWithBlacklist(ip)
143 | }
144 |
145 | func (c *proxyContext) allowWithWhitelist(ip string) bool {
146 | return c.result.api.allowWithWhitelist(ip)
147 | }
148 |
149 | func (c *proxyContext) circuitResourceID() uint64 {
150 | if c.result.api.cb != nil {
151 | return c.result.api.id
152 | }
153 |
154 | return c.result.dest.id
155 | }
156 |
157 | func (c *proxyContext) rateLimiter() *rateLimiter {
158 | if c.result.api.limiter != nil {
159 | return c.result.api.limiter
160 | }
161 |
162 | return c.result.dest.limiter
163 | }
164 |
165 | func (c *proxyContext) circuitBreaker() (*metapb.CircuitBreaker, *util.RateBarrier) {
166 | if c.result.api.cb != nil {
167 | return c.result.api.cb, c.result.api.barrier
168 | }
169 |
170 | return c.result.dest.cb, c.result.dest.barrier
171 | }
172 |
173 | func (c *proxyContext) circuitStatus() metapb.CircuitStatus {
174 | if c.result.api.cb != nil {
175 | return c.result.api.getCircuitStatus()
176 | }
177 |
178 | return c.result.dest.getCircuitStatus()
179 | }
180 |
181 | func (c *proxyContext) changeCircuitStatusToClose() {
182 | if c.result.api.cb != nil {
183 | c.result.api.circuitToClose()
184 | }
185 |
186 | c.result.dest.circuitToClose()
187 | }
188 |
189 | func (c *proxyContext) changeCircuitStatusToOpen() {
190 | if c.result.api.cb != nil {
191 | c.result.api.circuitToOpen()
192 | }
193 |
194 | c.result.dest.circuitToOpen()
195 | }
196 |
--------------------------------------------------------------------------------
/pkg/proxy/filter_access.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "github.com/fagongzi/gateway/pkg/filter"
5 | "github.com/fagongzi/log"
6 | )
7 |
8 | // AccessFilter record the http access log
9 | // log format: $remoteip "$method $path" $code "$agent" $svr $cost
10 | type AccessFilter struct {
11 | filter.BaseFilter
12 | }
13 |
14 | func newAccessFilter() filter.Filter {
15 | return &AccessFilter{}
16 | }
17 |
18 | // Init init filter
19 | func (f *AccessFilter) Init(cfg string) error {
20 | return nil
21 | }
22 |
23 | // Name return name of this filter
24 | func (f *AccessFilter) Name() string {
25 | return FilterHTTPAccess
26 | }
27 |
28 | // Post execute after proxy
29 | func (f *AccessFilter) Post(c filter.Context) (statusCode int, err error) {
30 | cost := c.EndAt().Sub(c.StartAt())
31 |
32 | if log.InfoEnabled() {
33 | log.Infof("filter: %s %s \"%s\" %d \"%s\" %s %s",
34 | filter.StringValue(filter.AttrClientRealIP, c),
35 | c.OriginRequest().Method(),
36 | c.ForwardRequest().RequestURI(),
37 | c.Response().StatusCode(),
38 | c.OriginRequest().UserAgent(),
39 | c.Server().Addr,
40 | cost)
41 | }
42 |
43 | return f.BaseFilter.Post(c)
44 | }
45 |
--------------------------------------------------------------------------------
/pkg/proxy/filter_analysis.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "github.com/fagongzi/gateway/pkg/filter"
5 | )
6 |
7 | // AnalysisFilter analysis filter
8 | type AnalysisFilter struct {
9 | filter.BaseFilter
10 | }
11 |
12 | func newAnalysisFilter() filter.Filter {
13 | return &AnalysisFilter{}
14 | }
15 |
16 | // Init init filter
17 | func (f *AnalysisFilter) Init(cfg string) error {
18 | return nil
19 | }
20 |
21 | // Name return name of this filter
22 | func (f *AnalysisFilter) Name() string {
23 | return FilterAnalysis
24 | }
25 |
26 | // Pre execute before proxy
27 | func (f *AnalysisFilter) Pre(c filter.Context) (statusCode int, err error) {
28 | // TODO: avoid lock overhead in every request
29 | c.Analysis().Request(c.(*proxyContext).circuitResourceID())
30 | return f.BaseFilter.Pre(c)
31 | }
32 |
33 | // Post execute after proxy
34 | func (f *AnalysisFilter) Post(c filter.Context) (statusCode int, err error) {
35 | c.Analysis().Response(c.(*proxyContext).circuitResourceID(), c.EndAt().Sub(c.StartAt()).Nanoseconds())
36 | return f.BaseFilter.Post(c)
37 | }
38 |
39 | // PostErr execute proxy has errors
40 | func (f *AnalysisFilter) PostErr(c filter.Context, code int, err error) {
41 | c.Analysis().Failure(c.(*proxyContext).circuitResourceID())
42 | }
43 |
--------------------------------------------------------------------------------
/pkg/proxy/filter_blacklist.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/fagongzi/gateway/pkg/filter"
7 | "github.com/valyala/fasthttp"
8 | )
9 |
10 | var (
11 | // ErrBlacklist target ip in black list
12 | ErrBlacklist = errors.New("Err, target ip in black list")
13 | )
14 |
15 | // BlackListFilter blacklist filter
16 | type BlackListFilter struct {
17 | filter.BaseFilter
18 | }
19 |
20 | func newBlackListFilter() filter.Filter {
21 | return &BlackListFilter{}
22 | }
23 |
24 | // Init init filter
25 | func (f *BlackListFilter) Init(cfg string) error {
26 | return nil
27 | }
28 |
29 | // Name return name of this filter
30 | func (f *BlackListFilter) Name() string {
31 | return FilterBlackList
32 | }
33 |
34 | // Pre execute before proxy
35 | func (f *BlackListFilter) Pre(c filter.Context) (statusCode int, err error) {
36 | if !c.(*proxyContext).allowWithBlacklist(filter.StringValue(filter.AttrClientRealIP, c)) {
37 | return fasthttp.StatusForbidden, ErrBlacklist
38 | }
39 |
40 | return f.BaseFilter.Pre(c)
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/proxy/filter_caching.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "strings"
5 | "sync"
6 | "time"
7 |
8 | "github.com/fagongzi/gateway/pkg/filter"
9 | "github.com/fagongzi/gateway/pkg/pb/metapb"
10 | "github.com/fagongzi/gateway/pkg/util"
11 | "github.com/fagongzi/goetty"
12 | "github.com/fagongzi/util/hack"
13 | "github.com/valyala/fasthttp"
14 | )
15 |
16 | var (
17 | cachePool sync.Pool
18 | )
19 |
20 | // CachingFilter cache api result
21 | type CachingFilter struct {
22 | filter.BaseFilter
23 |
24 | tw *goetty.TimeoutWheel
25 | cache *util.Cache
26 | }
27 |
28 | func newCachingFilter(maxBytes uint64, tw *goetty.TimeoutWheel) filter.Filter {
29 | f := &CachingFilter{
30 | tw: tw,
31 | }
32 |
33 | f.cache = util.NewLRUCache(maxBytes, f.onEvicted)
34 | return f
35 | }
36 |
37 | // Name return name of this filter
38 | func (f *CachingFilter) Name() string {
39 | return FilterCaching
40 | }
41 |
42 | // Pre execute before proxy
43 | func (f *CachingFilter) Pre(c filter.Context) (statusCode int, err error) {
44 | if c.DispatchNode().Cache == nil {
45 | return f.BaseFilter.Post(c)
46 | }
47 |
48 | matches, id := getCachingID(c)
49 | if !matches {
50 | return f.BaseFilter.Post(c)
51 | }
52 |
53 | if value, ok := f.cache.Get(id); ok {
54 | c.SetAttr(filter.AttrUsingCachingValue, value)
55 | }
56 |
57 | return f.BaseFilter.Post(c)
58 | }
59 |
60 | // Post execute after proxy
61 | func (f *CachingFilter) Post(c filter.Context) (statusCode int, err error) {
62 | if c.DispatchNode().Cache == nil {
63 | return f.BaseFilter.Post(c)
64 | }
65 |
66 | matches, id := getCachingID(c)
67 | if !matches {
68 | return f.BaseFilter.Post(c)
69 | }
70 |
71 | f.cache.Add(id, genCachedValue(c))
72 | if c.DispatchNode().Cache.Deadline > 0 {
73 | f.tw.Schedule(time.Duration(c.DispatchNode().Cache.Deadline),
74 | f.removeCache, id)
75 | }
76 |
77 | return f.BaseFilter.Post(c)
78 | }
79 |
80 | func (f *CachingFilter) removeCache(id interface{}) {
81 | f.cache.Remove(id)
82 | }
83 |
84 | func (f *CachingFilter) onEvicted(key util.Key, value *goetty.ByteBuf) {
85 | f.tw.Schedule(time.Second*10, f.doReleaseCacheBuf, value)
86 | }
87 |
88 | func (f *CachingFilter) doReleaseCacheBuf(arg interface{}) {
89 | arg.(*goetty.ByteBuf).Release()
90 | }
91 |
92 | func getCachingID(c filter.Context) (bool, string) {
93 | req := c.ForwardRequest()
94 | if len(c.DispatchNode().Cache.Conditions) == 0 {
95 | return true, getID(req, c.DispatchNode().Cache.Keys)
96 | }
97 |
98 | matches := true
99 | for _, cond := range c.DispatchNode().Cache.Conditions {
100 | matches = conditionsMatches(&cond, req)
101 | if !matches {
102 | break
103 | }
104 | }
105 |
106 | if !matches {
107 | return false, ""
108 | }
109 |
110 | return matches, getID(req, c.DispatchNode().Cache.Keys)
111 | }
112 |
113 | func getID(req *fasthttp.Request, keys []metapb.Parameter) string {
114 | size := len(keys)
115 | if size == 0 {
116 | return hack.SliceToString(req.RequestURI())
117 | }
118 |
119 | ids := make([]string, size+1, size+1)
120 | ids[0] = hack.SliceToString(req.RequestURI())
121 | for idx, param := range keys {
122 | ids[idx+1] = paramValue(¶m, req)
123 | }
124 |
125 | return strings.Join(ids, "-")
126 | }
127 |
128 | func genCachedValue(c filter.Context) *goetty.ByteBuf {
129 | return filter.NewCachedValue(c.Response())
130 | }
131 |
--------------------------------------------------------------------------------
/pkg/proxy/filter_circuit_breaker.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "errors"
5 | "net/http"
6 | "strings"
7 | "time"
8 |
9 | "github.com/fagongzi/gateway/pkg/filter"
10 | "github.com/fagongzi/gateway/pkg/pb/metapb"
11 | )
12 |
13 | var (
14 | // ErrCircuitClose resource is in circuit close
15 | ErrCircuitClose = errors.New("resource is in circuit close")
16 | // ErrCircuitHalfLimited resource is in circuit half, traffic limit
17 | ErrCircuitHalfLimited = errors.New("resource is in circuit half, traffic limit")
18 | )
19 |
20 | // CircuitBreakeFilter CircuitBreakeFilter
21 | type CircuitBreakeFilter struct {
22 | filter.BaseFilter
23 | }
24 |
25 | func newCircuitBreakeFilter() filter.Filter {
26 | return &CircuitBreakeFilter{}
27 | }
28 |
29 | // Init init filter
30 | func (f *CircuitBreakeFilter) Init(cfg string) error {
31 | return nil
32 | }
33 |
34 | // Name return name of this filter
35 | func (f *CircuitBreakeFilter) Name() string {
36 | return FilterCircuitBreake
37 | }
38 |
39 | // Pre execute before proxy
40 | func (f *CircuitBreakeFilter) Pre(c filter.Context) (statusCode int, err error) {
41 | pc := c.(*proxyContext)
42 | cb, barrier := pc.circuitBreaker()
43 | if cb == nil {
44 | return f.BaseFilter.Pre(c)
45 | }
46 |
47 | protectedResourceStatus := pc.circuitStatus()
48 | protectedResource := pc.circuitResourceID()
49 |
50 | switch protectedResourceStatus {
51 | case metapb.Open:
52 | if c.Analysis().GetRecentlyRequestFailureRate(protectedResource, time.Duration(cb.RateCheckPeriod)) >= int(cb.FailureRateToClose) {
53 | pc.changeCircuitStatusToClose()
54 | c.Analysis().Reject(protectedResource)
55 | return http.StatusServiceUnavailable, ErrCircuitClose
56 | }
57 |
58 | return http.StatusOK, nil
59 | case metapb.Half:
60 | if barrier.Allow() {
61 | return f.BaseFilter.Pre(c)
62 | }
63 |
64 | c.Analysis().Reject(protectedResource)
65 | return http.StatusServiceUnavailable, ErrCircuitHalfLimited
66 | default:
67 | c.Analysis().Reject(protectedResource)
68 | return http.StatusServiceUnavailable, ErrCircuitClose
69 | }
70 | }
71 |
72 | // Post execute after proxy
73 | func (f *CircuitBreakeFilter) Post(c filter.Context) (statusCode int, err error) {
74 | pc := c.(*proxyContext)
75 | cb, _ := pc.circuitBreaker()
76 | if cb == nil {
77 | return f.BaseFilter.Post(c)
78 | }
79 |
80 | protectedResourceStatus := pc.circuitStatus()
81 | protectedResource := pc.circuitResourceID()
82 |
83 | if protectedResourceStatus == metapb.Half &&
84 | c.Analysis().GetRecentlyRequestSuccessedRate(protectedResource, time.Duration(cb.RateCheckPeriod)) >= int(cb.SucceedRateToOpen) {
85 | pc.changeCircuitStatusToOpen()
86 | }
87 |
88 | return f.BaseFilter.Post(c)
89 | }
90 |
91 | // PostErr execute proxy has errors
92 | func (f *CircuitBreakeFilter) PostErr(c filter.Context, code int, err error) {
93 | // ignore user cancel
94 | if nil != err && strings.HasPrefix(err.Error(), ErrPrefixRequestCancel) {
95 | f.BaseFilter.PostErr(c, code, err)
96 | return
97 | }
98 |
99 | pc := c.(*proxyContext)
100 | cb, _ := pc.circuitBreaker()
101 | if cb == nil {
102 | f.BaseFilter.PostErr(c, code, err)
103 | return
104 | }
105 |
106 | protectedResourceStatus := pc.circuitStatus()
107 | protectedResource := pc.circuitResourceID()
108 |
109 | if protectedResourceStatus == metapb.Half &&
110 | c.Analysis().GetRecentlyRequestFailureRate(protectedResource, time.Duration(cb.RateCheckPeriod)) >= int(cb.FailureRateToClose) {
111 | pc.changeCircuitStatusToClose()
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/pkg/proxy/filter_cross_domain.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "io/ioutil"
7 |
8 | "github.com/fagongzi/gateway/pkg/filter"
9 | "github.com/valyala/fasthttp"
10 | )
11 |
12 | var (
13 | options = []byte("OPTIONS")
14 | )
15 |
16 | // CrossCfg cross cfg
17 | type CrossCfg struct {
18 | Headers []CrossHeader `json:"headers"`
19 | }
20 |
21 | // CrossHeader cross header
22 | type CrossHeader struct {
23 | Name string `json:"name"`
24 | Value string `json:"value"`
25 | }
26 |
27 | // CrossDomainFilter cross domain
28 | type CrossDomainFilter struct {
29 | filter.BaseFilter
30 | cfg CrossCfg
31 | }
32 |
33 | func newCrossDomainFilter(file string) (filter.Filter, error) {
34 | f := &CrossDomainFilter{}
35 |
36 | err := f.parseCfg(file)
37 | if err != nil {
38 | return nil, err
39 | }
40 |
41 | return f, nil
42 | }
43 |
44 | func (f *CrossDomainFilter) parseCfg(file string) error {
45 | data, err := ioutil.ReadFile(file)
46 | if err != nil {
47 | return err
48 | }
49 |
50 | err = json.Unmarshal(data, &f.cfg)
51 | if err != nil {
52 | return err
53 | }
54 |
55 | return nil
56 | }
57 |
58 | // Name return name of this filter
59 | func (f *CrossDomainFilter) Name() string {
60 | return FilterCross
61 | }
62 |
63 | // Pre execute before proxy
64 | func (f *CrossDomainFilter) Pre(c filter.Context) (statusCode int, err error) {
65 | if bytes.Compare(c.OriginRequest().Method(), options) != 0 {
66 | return f.BaseFilter.Pre(c)
67 | }
68 |
69 | resp := fasthttp.AcquireResponse()
70 | for _, h := range f.cfg.Headers {
71 | resp.Header.Add(h.Name, h.Value)
72 | }
73 |
74 | c.SetAttr(filter.AttrUsingResponse, resp)
75 | return filter.BreakFilterChainCode, nil
76 | }
77 |
--------------------------------------------------------------------------------
/pkg/proxy/filter_headers.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "github.com/fagongzi/gateway/pkg/filter"
5 | )
6 |
7 | // Hop-by-hop headers. These are removed when sent to the backend.
8 | // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
9 | var hopHeaders = []string{
10 | //"Connection",
11 | "Keep-Alive",
12 | "Proxy-Authenticate",
13 | "Proxy-Authorization",
14 | "Te",
15 | "Trailers",
16 | "Transfer-Encoding",
17 | }
18 |
19 | // HeadersFilter HeadersFilter
20 | type HeadersFilter struct {
21 | filter.BaseFilter
22 | }
23 |
24 | func newHeadersFilter() filter.Filter {
25 | return &HeadersFilter{}
26 | }
27 |
28 | // Init init filter
29 | func (f *HeadersFilter) Init(cfg string) error {
30 | return nil
31 | }
32 |
33 | // Name return name of this filter
34 | func (f *HeadersFilter) Name() string {
35 | return FilterHeader
36 | }
37 |
38 | // Pre execute before proxy
39 | func (f *HeadersFilter) Pre(c filter.Context) (statusCode int, err error) {
40 | for _, h := range hopHeaders {
41 | c.ForwardRequest().Header.Del(h)
42 | }
43 |
44 | c.ForwardRequest().Header.SetHost(c.Server().Addr)
45 | return f.BaseFilter.Pre(c)
46 | }
47 |
48 | // Post execute after proxy
49 | func (f *HeadersFilter) Post(c filter.Context) (statusCode int, err error) {
50 | for _, h := range hopHeaders {
51 | c.Response().Header.Del(h)
52 | }
53 |
54 | // 需要合并处理的,不做header的复制,由proxy做合并
55 | if len(c.API().Nodes) == 1 {
56 | c.OriginRequest().Response.Header.Reset()
57 | c.Response().Header.CopyTo(&c.OriginRequest().Response.Header)
58 | }
59 |
60 | return f.BaseFilter.Post(c)
61 | }
62 |
--------------------------------------------------------------------------------
/pkg/proxy/filter_prepare.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "github.com/fagongzi/gateway/pkg/filter"
5 | "github.com/fagongzi/gateway/pkg/util"
6 | )
7 |
8 | // PrepareFilter Must be in the first of the filter chain,
9 | // used to get some public information into the context,
10 | // to avoid subsequent filters to do duplicate things.
11 | type PrepareFilter struct {
12 | filter.BaseFilter
13 | }
14 |
15 | func newPrepareFilter() filter.Filter {
16 | return &PrepareFilter{}
17 | }
18 |
19 | // Init init filter
20 | func (f *PrepareFilter) Init(cfg string) error {
21 | return nil
22 | }
23 |
24 | // Name return name of this filter
25 | func (f *PrepareFilter) Name() string {
26 | return FilterPrepare
27 | }
28 |
29 | // Pre execute before proxy
30 | func (f *PrepareFilter) Pre(c filter.Context) (statusCode int, err error) {
31 | c.SetAttr(filter.AttrClientRealIP, util.ClientIP(c.OriginRequest()))
32 | return f.BaseFilter.Pre(c)
33 | }
34 |
--------------------------------------------------------------------------------
/pkg/proxy/filter_rate_limiting.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "errors"
5 | "net/http"
6 |
7 | "github.com/fagongzi/gateway/pkg/filter"
8 | )
9 |
10 | var (
11 | errOverLimit = errors.New("too many requests")
12 | )
13 |
14 | // RateLimitingFilter RateLimitingFilter
15 | type RateLimitingFilter struct {
16 | filter.BaseFilter
17 | }
18 |
19 | func newRateLimitingFilter() filter.Filter {
20 | return &RateLimitingFilter{}
21 | }
22 |
23 | // Init init filter
24 | func (f *RateLimitingFilter) Init(cfg string) error {
25 | return nil
26 | }
27 |
28 | // Name return name of this filter
29 | func (f *RateLimitingFilter) Name() string {
30 | return FilterRateLimiting
31 | }
32 |
33 | // Pre execute before proxy
34 | func (f *RateLimitingFilter) Pre(c filter.Context) (statusCode int, err error) {
35 | if !c.(*proxyContext).rateLimiter().do(1) {
36 | return http.StatusTooManyRequests, errOverLimit
37 | }
38 |
39 | return f.BaseFilter.Pre(c)
40 | }
41 |
--------------------------------------------------------------------------------
/pkg/proxy/filter_validation.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/fagongzi/gateway/pkg/filter"
7 | "github.com/valyala/fasthttp"
8 | )
9 |
10 | var (
11 | // ErrValidationFailure validation failure
12 | ErrValidationFailure = errors.New("request validation failure")
13 | )
14 |
15 | // ValidationFilter validation request
16 | type ValidationFilter struct {
17 | filter.BaseFilter
18 | }
19 |
20 | func newValidationFilter() filter.Filter {
21 | return &ValidationFilter{}
22 | }
23 |
24 | // Init init filter
25 | func (f *ValidationFilter) Init(cfg string) error {
26 | return nil
27 | }
28 |
29 | // Name return name of this filter
30 | func (f *ValidationFilter) Name() string {
31 | return FilterValidation
32 | }
33 |
34 | // Pre pre filter, before proxy reuqest
35 | func (f *ValidationFilter) Pre(c filter.Context) (statusCode int, err error) {
36 | if c.(*proxyContext).validateRequest() {
37 | return f.BaseFilter.Pre(c)
38 | }
39 |
40 | return fasthttp.StatusBadRequest, ErrValidationFailure
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/proxy/filter_whitelist.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/fagongzi/gateway/pkg/filter"
7 | "github.com/valyala/fasthttp"
8 | )
9 |
10 | var (
11 | // ErrWhitelist target ip not in in white list
12 | ErrWhitelist = errors.New("Err, target ip not in in white list")
13 | )
14 |
15 | // WhiteListFilter whitelist filter
16 | type WhiteListFilter struct {
17 | filter.BaseFilter
18 | }
19 |
20 | func newWhiteListFilter() filter.Filter {
21 | return &WhiteListFilter{}
22 | }
23 |
24 | // Init init filter
25 | func (f *WhiteListFilter) Init(cfg string) error {
26 | return nil
27 | }
28 |
29 | // Name return name of this filter
30 | func (f *WhiteListFilter) Name() string {
31 | return FilterWhiteList
32 | }
33 |
34 | // Pre execute before proxy
35 | func (f *WhiteListFilter) Pre(c filter.Context) (statusCode int, err error) {
36 | if !c.(*proxyContext).allowWithWhitelist(filter.StringValue(filter.AttrClientRealIP, c)) {
37 | return fasthttp.StatusForbidden, ErrWhitelist
38 | }
39 |
40 | return f.BaseFilter.Pre(c)
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/proxy/filter_xforwardfor.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "bytes"
5 |
6 | "github.com/fagongzi/gateway/pkg/filter"
7 | "github.com/fagongzi/util/hack"
8 | )
9 |
10 | var (
11 | headerName = []byte("X-Forwarded-For")
12 | )
13 |
14 | // XForwardForFilter XForwardForFilter
15 | type XForwardForFilter struct {
16 | filter.BaseFilter
17 | }
18 |
19 | func newXForwardForFilter() filter.Filter {
20 | return &XForwardForFilter{}
21 | }
22 |
23 | // Init init filter
24 | func (f *XForwardForFilter) Init(cfg string) error {
25 | return nil
26 | }
27 |
28 | // Name return name of this filter
29 | func (f *XForwardForFilter) Name() string {
30 | return FilterXForward
31 | }
32 |
33 | // Pre execute before proxy
34 | func (f *XForwardForFilter) Pre(c filter.Context) (statusCode int, err error) {
35 | prevForward := c.OriginRequest().Request.Header.PeekBytes(headerName)
36 | if len(prevForward) == 0 {
37 | c.ForwardRequest().Header.SetBytesKV(headerName, hack.StringToSlice(c.OriginRequest().RemoteIP().String()))
38 | } else {
39 | var buf bytes.Buffer
40 | buf.Write(prevForward)
41 | buf.WriteByte(',')
42 | buf.WriteByte(' ')
43 | buf.WriteString(c.OriginRequest().RemoteIP().String())
44 | c.ForwardRequest().Header.SetBytesKV(headerName, buf.Bytes())
45 | }
46 |
47 | return f.BaseFilter.Pre(c)
48 | }
49 |
--------------------------------------------------------------------------------
/pkg/proxy/io.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "io"
5 | "net/http"
6 | "sync"
7 | "time"
8 |
9 | "github.com/valyala/fasthttp"
10 | )
11 |
12 | type writeFlusher interface {
13 | io.Writer
14 | http.Flusher
15 | }
16 |
17 | type maxLatencyWriter struct {
18 | dst writeFlusher
19 | latency time.Duration
20 |
21 | lk sync.Mutex // protects Write + Flush
22 | done chan bool
23 | }
24 |
25 | func (m *maxLatencyWriter) Write(p []byte) (int, error) {
26 | m.lk.Lock()
27 | defer m.lk.Unlock()
28 | return m.dst.Write(p)
29 | }
30 |
31 | func (m *maxLatencyWriter) flushLoop() {
32 | t := time.NewTicker(m.latency)
33 | defer t.Stop()
34 | for {
35 | select {
36 | case <-m.done:
37 | if onExitFlushLoop != nil {
38 | onExitFlushLoop()
39 | }
40 | return
41 | case <-t.C:
42 | m.lk.Lock()
43 | m.dst.Flush()
44 | m.lk.Unlock()
45 | }
46 | }
47 | }
48 |
49 | func (m *maxLatencyWriter) stop() { m.done <- true }
50 |
51 | // onExitFlushLoop is a callback set by tests to detect the state of the
52 | // flushLoop() goroutine.
53 | var onExitFlushLoop func()
54 |
55 | func copyHeader(dst, src http.Header) {
56 | for k, vv := range src {
57 | for _, v := range vv {
58 | dst.Add(k, v)
59 | }
60 | }
61 | }
62 |
63 | type requestCanceler interface {
64 | CancelRequest(*http.Request)
65 | }
66 |
67 | type runOnFirstRead struct {
68 | io.Reader
69 |
70 | fn func() // Run before first Read, then set to nil
71 | }
72 |
73 | func (c *runOnFirstRead) Read(bs []byte) (int, error) {
74 | if c.fn != nil {
75 | c.fn()
76 | c.fn = nil
77 | }
78 | return c.Reader.Read(bs)
79 | }
80 |
81 | func copyRequest(req *fasthttp.Request) *fasthttp.Request {
82 | newreq := fasthttp.AcquireRequest()
83 | newreq.Reset()
84 | req.CopyTo(newreq)
85 | return newreq
86 | }
87 |
--------------------------------------------------------------------------------
/pkg/proxy/metric.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/prometheus/client_golang/prometheus"
7 | "github.com/valyala/fasthttp"
8 | )
9 |
10 | const (
11 | typeRequestAll = "all"
12 | typeRequestFail = "fail"
13 | typeRequestSucceed = "succeed"
14 | typeRequestLimit = "limit"
15 | typeRequestReject = "reject"
16 | )
17 |
18 | var (
19 | apiRequestCounterVec = prometheus.NewCounterVec(
20 | prometheus.CounterOpts{
21 | Namespace: "gateway",
22 | Subsystem: "proxy",
23 | Name: "api_request_total",
24 | Help: "Total number of request made.",
25 | }, []string{"name", "type"})
26 |
27 | apiResponseHistogramVec = prometheus.NewHistogramVec(
28 | prometheus.HistogramOpts{
29 | Namespace: "gateway",
30 | Subsystem: "proxy",
31 | Name: "api_response_duration_seconds",
32 | Help: "Bucketed histogram of api response time duration",
33 | Buckets: prometheus.ExponentialBuckets(0.0005, 2.0, 20),
34 | }, []string{"name"})
35 | )
36 |
37 | func init() {
38 | prometheus.Register(apiRequestCounterVec)
39 | prometheus.Register(apiResponseHistogramVec)
40 | }
41 |
42 | func (p *Proxy) postRequest(api *apiRuntime, dispatches []*dispatchNode, startAt time.Time) {
43 | doMetrics := true
44 | for _, dn := range dispatches {
45 | if doMetrics &&
46 | (dn.err == ErrCircuitClose || dn.err == ErrBlacklist || dn.err == ErrWhitelist) {
47 | incrRequestReject(api.meta.Name)
48 | doMetrics = false
49 | } else if doMetrics && dn.err == ErrCircuitHalfLimited {
50 | incrRequestLimit(api.meta.Name)
51 | doMetrics = false
52 | } else if doMetrics && dn.err != nil {
53 | incrRequestFailed(api.meta.Name)
54 | doMetrics = false
55 | } else if doMetrics && dn.code >= fasthttp.StatusBadRequest {
56 | incrRequestFailed(api.meta.Name)
57 | doMetrics = false
58 | }
59 |
60 | releaseDispathNode(dn)
61 | }
62 |
63 | if doMetrics {
64 | incrRequestSucceed(api.meta.Name)
65 | observeAPIResponse(api.meta.Name, startAt)
66 | }
67 | }
68 |
69 | func incrRequest(name string) {
70 | apiRequestCounterVec.WithLabelValues(name, typeRequestAll).Inc()
71 | }
72 |
73 | func incrRequestFailed(name string) {
74 | apiRequestCounterVec.WithLabelValues(name, typeRequestFail).Inc()
75 | }
76 |
77 | func incrRequestSucceed(name string) {
78 | apiRequestCounterVec.WithLabelValues(name, typeRequestSucceed).Inc()
79 | }
80 |
81 | func incrRequestLimit(name string) {
82 | apiRequestCounterVec.WithLabelValues(name, typeRequestLimit).Inc()
83 | }
84 |
85 | func incrRequestReject(name string) {
86 | apiRequestCounterVec.WithLabelValues(name, typeRequestReject).Inc()
87 | }
88 |
89 | func observeAPIResponse(name string, startAt time.Time) {
90 | now := time.Now()
91 | apiResponseHistogramVec.WithLabelValues(name).Observe(now.Sub(startAt).Seconds())
92 | }
93 |
--------------------------------------------------------------------------------
/pkg/proxy/multi.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "sync"
5 |
6 | "github.com/buger/jsonparser"
7 | "github.com/fagongzi/log"
8 | "github.com/fagongzi/util/hack"
9 | )
10 |
11 | type multiContext struct {
12 | sync.RWMutex
13 | data []byte
14 | }
15 |
16 | func (c *multiContext) reset() {
17 | c.init()
18 | }
19 |
20 | func (c *multiContext) init() {
21 | c.data = emptyObject
22 | }
23 |
24 | func (c *multiContext) completePart(attr string, data []byte) {
25 | c.Lock()
26 | if len(data) > 0 && attr != "" {
27 | c.data, _ = jsonparser.Set(c.data, data, attr)
28 | }
29 | c.Unlock()
30 | }
31 |
32 | func (c *multiContext) getAttr(paths ...string) string {
33 | c.RLock()
34 | value, _, _, err := jsonparser.Get(c.data, paths...)
35 | c.RUnlock()
36 | if err != nil {
37 | log.Errorf("extract %+v failed, errors:\n%+v", paths, err)
38 | return ""
39 | }
40 |
41 | return hack.SliceToString(value)
42 | }
43 |
--------------------------------------------------------------------------------
/pkg/proxy/pool.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "sync"
5 |
6 | "github.com/fagongzi/gateway/pkg/expr"
7 | "github.com/fagongzi/goetty"
8 | )
9 |
10 | var (
11 | renderPool sync.Pool
12 | contextPool sync.Pool
13 | dispatchNodePool sync.Pool
14 | multiContextPool sync.Pool
15 | wgPool sync.Pool
16 | exprCtxPool sync.Pool
17 | bytesPool = goetty.NewSyncPool(2, 1024*1024*5, 2)
18 |
19 | emptyRender = render{}
20 | emptyContext = proxyContext{}
21 | emptyDispathNode = dispatchNode{}
22 | )
23 |
24 | func acquireWG() *sync.WaitGroup {
25 | v := wgPool.Get()
26 | if v == nil {
27 | return &sync.WaitGroup{}
28 | }
29 |
30 | return v.(*sync.WaitGroup)
31 | }
32 |
33 | func releaseWG(value *sync.WaitGroup) {
34 | if value != nil {
35 | wgPool.Put(value)
36 | }
37 | }
38 |
39 | func acquireMultiContext() *multiContext {
40 | v := multiContextPool.Get()
41 | if v == nil {
42 | return &multiContext{}
43 | }
44 |
45 | return v.(*multiContext)
46 | }
47 |
48 | func releaseMultiContext(value *multiContext) {
49 | if value != nil {
50 | value.reset()
51 | multiContextPool.Put(value)
52 | }
53 | }
54 |
55 | func acquireDispathNode() *dispatchNode {
56 | v := dispatchNodePool.Get()
57 | if v == nil {
58 | return &dispatchNode{}
59 | }
60 |
61 | return v.(*dispatchNode)
62 | }
63 |
64 | func releaseDispathNode(value *dispatchNode) {
65 | if value != nil {
66 | value.reset()
67 | dispatchNodePool.Put(value)
68 | }
69 | }
70 |
71 | func acquireContext() *proxyContext {
72 | v := contextPool.Get()
73 | if v == nil {
74 | return &proxyContext{}
75 | }
76 |
77 | return v.(*proxyContext)
78 | }
79 |
80 | func releaseContext(value *proxyContext) {
81 | if value != nil {
82 | value.reset()
83 | contextPool.Put(value)
84 | }
85 | }
86 |
87 | func acquireRender() *render {
88 | v := renderPool.Get()
89 | if v == nil {
90 | return &render{}
91 | }
92 |
93 | return v.(*render)
94 | }
95 |
96 | func releaseRender(value *render) {
97 | if value != nil {
98 | value.reset()
99 | renderPool.Put(value)
100 | }
101 | }
102 |
103 | func acquireExprCtx() *expr.Ctx {
104 | v := exprCtxPool.Get()
105 | if v == nil {
106 | return &expr.Ctx{
107 | Params: make(map[string][]byte),
108 | }
109 | }
110 |
111 | return v.(*expr.Ctx)
112 | }
113 |
114 | func releaseExprCtx(value *expr.Ctx) {
115 | if value != nil {
116 | value.Reset()
117 | exprCtxPool.Put(value)
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/pkg/proxy/proxy_gc.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | "github.com/fagongzi/gateway/pkg/plugin"
8 | "github.com/fagongzi/log"
9 | )
10 |
11 | func (p *Proxy) addGCJSEngine(value *plugin.Engine) {
12 | p.Lock()
13 | defer p.Unlock()
14 |
15 | p.gcJSEngines = append(p.gcJSEngines, value)
16 | }
17 |
18 | func (p *Proxy) readyToGCJSEngine() {
19 | _, err := p.runner.RunCancelableTask(func(ctx context.Context) {
20 | t := time.NewTicker(time.Minute)
21 | defer t.Stop()
22 |
23 | for {
24 | select {
25 | case <-ctx.Done():
26 | log.Info("stop: gc js engine stopped")
27 | t.Stop()
28 | return
29 | case <-t.C:
30 | now := time.Now()
31 | p.Lock()
32 | var values []*plugin.Engine
33 | for _, eng := range p.gcJSEngines {
34 | if now.Sub(eng.LastActive()) > time.Hour {
35 | go eng.Destroy()
36 | } else {
37 | values = append(values, eng)
38 | }
39 | }
40 | p.gcJSEngines = values
41 | p.Unlock()
42 | }
43 | }
44 | })
45 | if err != nil {
46 | log.Fatalf("start gc js engine failed, errors:\n%+v", err)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/pkg/proxy/proxy_https.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "crypto/tls"
5 | "io/ioutil"
6 | "net/http"
7 |
8 | "github.com/fagongzi/gateway/pkg/pb/metapb"
9 | "github.com/fagongzi/log"
10 | "github.com/valyala/fasthttp"
11 | )
12 |
13 | func (p *Proxy) enableHTTPS() bool {
14 | return p.cfg.DefaultTLSCert != "" && p.cfg.DefaultTLSKey != "" && p.cfg.AddrHTTPS != ""
15 | }
16 |
17 | func (p *Proxy) appendCertsEmbed(server *fasthttp.Server, certData []byte, keyData []byte) {
18 | for _, api := range p.dispatcher.apis {
19 | if metapb.Up == api.meta.GetStatus() && api.meta.GetUseTLS() {
20 | server.AppendCertEmbed(api.meta.TlsEmbedCert.CertData, api.meta.TlsEmbedCert.KeyData)
21 | }
22 | }
23 | server.AppendCertEmbed(certData, keyData)
24 | }
25 |
26 | func (p *Proxy) configTLSConfig(server *http.Server, certData []byte, keyData []byte) {
27 | certs := make([]tls.Certificate, 0)
28 | for _, api := range p.dispatcher.apis {
29 | if metapb.Up == api.meta.GetStatus() && api.meta.GetUseTLS() {
30 | cert, err := tls.X509KeyPair(api.meta.TlsEmbedCert.CertData, api.meta.TlsEmbedCert.KeyData)
31 | if err != nil {
32 | log.Errorf("api %s has invalid TLS certs", api.meta.Name)
33 | continue
34 | }
35 | certs = append(certs, cert)
36 | }
37 | }
38 | cert, _ := tls.X509KeyPair(certData, keyData)
39 | certs = append(certs, cert)
40 | server.TLSConfig.Certificates = certs
41 | }
42 |
43 | func (p *Proxy) mustParseDefaultTLSCert() ([]byte, []byte) {
44 | certData, err := ioutil.ReadFile(p.cfg.DefaultTLSCert)
45 | if err != nil {
46 | log.Fatalf("parse https cert failed with %+v", err)
47 | }
48 | keyData, err := ioutil.ReadFile(p.cfg.DefaultTLSKey)
49 | if err != nil {
50 | log.Fatalf("parse https cert failed with %+v", err)
51 | }
52 | _, err = tls.X509KeyPair(certData, keyData)
53 | if err != nil {
54 | log.Fatalf("parse https cert failed with %+v", err)
55 | }
56 | return certData, keyData
57 | }
58 |
--------------------------------------------------------------------------------
/pkg/proxy/proxy_websocket.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "net/http"
7 | "net/url"
8 |
9 | "github.com/fagongzi/log"
10 | "github.com/fagongzi/util/hack"
11 | "github.com/gorilla/websocket"
12 | "github.com/koding/websocketproxy"
13 | "github.com/valyala/fasthttp"
14 | )
15 |
16 | const (
17 | websocketRspKey = "__ws_rsp"
18 | )
19 | var wsHeaders = map[string]bool{
20 | "Origin": true,
21 | "Sec-WebSocket-Protocol": true,
22 | "Sec-Websocket-Protocol": true,
23 | "Cookie": true,
24 | "Sec-WebSocket-Version": true,
25 | "Sec-Websocket-Version": true,
26 | "Sec-WebSocket-Key": true,
27 | "Sec-Websocket-Key": true,
28 | "Sec-Websocket-Extensions": true,
29 | "Connection": true,
30 | "Upgrade": true,
31 | "Set-Cookie": true,
32 | "Sec-WebSocket-Extensions": true,
33 | "Sec-WebSocket-Accept": true,
34 | }
35 | // ServeHTTP http reverse handler by http
36 | func (p *Proxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
37 | if p.isStopped() {
38 | rw.WriteHeader(fasthttp.StatusServiceUnavailable)
39 | return
40 | }
41 |
42 | var buf bytes.Buffer
43 | buf.WriteByte(charLeft)
44 | buf.Write(hack.StringToSlice(req.Method))
45 | buf.WriteByte(charRight)
46 | buf.Write(hack.StringToSlice(req.RequestURI))
47 | requestTag := hack.SliceToString(buf.Bytes())
48 |
49 | if req.Method != "GET" {
50 | rw.WriteHeader(fasthttp.StatusMethodNotAllowed)
51 | return
52 | }
53 |
54 | ctx := &fasthttp.RequestCtx{}
55 | for k, vs := range req.Header {
56 | for _, v := range vs {
57 | ctx.Request.Header.Add(k, v)
58 | }
59 | }
60 | ctx.Request.SetRequestURI(req.RequestURI)
61 |
62 | api, dispatches, exprCtx := p.dispatcher.dispatch(ctx, requestTag)
63 | if len(dispatches) <= 0 &&
64 | (nil == api || api.meta.DefaultValue == nil) {
65 | rw.WriteHeader(fasthttp.StatusNotFound)
66 | releaseExprCtx(exprCtx)
67 | return
68 | }
69 |
70 | if len(dispatches) != 1 {
71 | log.Fatalf("websocket not support dispatch to multi backend server")
72 | }
73 |
74 | if !api.isWebSocket() {
75 | log.Fatalf("normal http request must use fasthttp")
76 | }
77 |
78 | dispatches[0].ctx = ctx
79 | p.doProxy(dispatches[0], func(c *proxyContext) {
80 | c.SetAttr(websocketRspKey, rw)
81 | })
82 | dispatches[0].release()
83 | releaseExprCtx(exprCtx)
84 | }
85 |
86 | func (p *Proxy) onWebsocket(c *proxyContext, addr string) (*fasthttp.Response, error) {
87 | resp := fasthttp.AcquireResponse()
88 |
89 | var r http.Request
90 | r.Method = "GET"
91 | r.Proto = "HTTP/1.1"
92 | r.ProtoMajor = 1
93 | r.ProtoMinor = 1
94 | r.RequestURI = string(c.forwardReq.RequestURI())
95 | r.Host = string(c.forwardReq.Host())
96 |
97 | hdr := make(http.Header)
98 | c.forwardReq.Header.VisitAll(func(k, v []byte) {
99 | sk := string(k)
100 | sv := string(v)
101 | switch sk {
102 | case "Transfer-Encoding":
103 | r.TransferEncoding = append(r.TransferEncoding, sv)
104 | default:
105 | hdr.Set(sk, sv)
106 | }
107 | })
108 | r.Header = hdr
109 | r.URL, _ = url.ParseRequestURI(r.RequestURI)
110 |
111 | wp := &websocketproxy.WebsocketProxy{
112 | Upgrader: &websocket.Upgrader{
113 | ReadBufferSize: c.result.httpOption().ReadBufferSize,
114 | WriteBufferSize: c.result.httpOption().WriteBufferSize,
115 | CheckOrigin: func(r *http.Request) bool {
116 | return true
117 | },
118 | },
119 | Director: func(incoming *http.Request, out http.Header) {
120 | out.Set("Origin", fmt.Sprintf("http://%s", addr))
121 | for key, vals := range incoming.Header {
122 | if _, ok := wsHeaders[key]; ok {
123 | continue
124 | }
125 | for _, val := range vals {
126 | out.Set(key, val)
127 |
128 | }
129 | }
130 | },
131 | Backend: func(r *http.Request) *url.URL {
132 | u, _ := url.Parse(fmt.Sprintf("ws://%s%s", addr, r.RequestURI))
133 | return u
134 | },
135 | }
136 |
137 | wp.ServeHTTP(c.GetAttr(websocketRspKey).(http.ResponseWriter), &r)
138 | return resp, nil
139 | }
140 |
--------------------------------------------------------------------------------
/pkg/proxy/rate_limit.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/fagongzi/gateway/pkg/pb/metapb"
7 | "github.com/juju/ratelimit"
8 | )
9 |
10 | type rateLimiter struct {
11 | limiter *ratelimit.Bucket
12 | option metapb.RateLimitOption
13 | }
14 |
15 | func newRateLimiter(max int64, option metapb.RateLimitOption) *rateLimiter {
16 | return &rateLimiter{
17 | limiter: ratelimit.NewBucket(time.Second/time.Duration(max), max),
18 | option: option,
19 | }
20 | }
21 |
22 | func (l *rateLimiter) do(count int64) bool {
23 | if l.option == metapb.Wait {
24 | l.limiter.Wait(count)
25 | return true
26 | }
27 |
28 | return l.limiter.TakeAvailable(count) > 0
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/route/const.go:
--------------------------------------------------------------------------------
1 | package route
2 |
3 | const (
4 | eoi byte = 0x1A
5 | slash = byte('/')
6 | lParen = byte('(')
7 | rParen = byte(')')
8 | vertical = byte('|')
9 | colon = byte(':')
10 | )
11 |
12 | const (
13 | tokenEOF = iota
14 | tokenUnknown
15 | tokenSlash
16 | tokenLParen
17 | tokenRParen
18 | tokenVertical
19 | tokenColon
20 | )
21 |
22 | var (
23 | slashValue = []byte("/")
24 | numberValue = []byte("number")
25 | stringValue = []byte("string")
26 | enumValue = []byte("enum")
27 |
28 | matchAll = []byte("*")
29 | )
30 |
31 | type nodeType int
32 |
33 | const (
34 | slashType = nodeType(5)
35 | stringType = nodeType(4)
36 | constType = nodeType(3)
37 | enumType = nodeType(2)
38 | numberType = nodeType(1)
39 | )
40 |
--------------------------------------------------------------------------------
/pkg/route/lexer.go:
--------------------------------------------------------------------------------
1 | package route
2 |
3 | type lexer interface {
4 | Next() byte
5 | Current() byte
6 | NextToken()
7 | Token() int
8 | TokenIndex() int
9 | ScanString() []byte
10 | }
11 |
--------------------------------------------------------------------------------
/pkg/route/scanner.go:
--------------------------------------------------------------------------------
1 | package route
2 |
3 | type scanner struct {
4 | len int
5 | input []byte
6 |
7 | token int
8 | bp int
9 | sp int
10 | ch byte
11 | }
12 |
13 | func newScanner(input []byte) lexer {
14 | scan := &scanner{
15 | len: len(input),
16 | input: input,
17 | bp: -1,
18 | sp: 0,
19 | }
20 |
21 | scan.Next()
22 | return scan
23 | }
24 |
25 | func (scan *scanner) Next() byte {
26 | scan.bp++
27 |
28 | if scan.bp < scan.len {
29 | scan.ch = scan.input[scan.bp]
30 | } else {
31 | scan.ch = eoi
32 | }
33 |
34 | return scan.ch
35 | }
36 |
37 | func (scan *scanner) NextToken() {
38 | for {
39 | switch scan.ch {
40 | case '/':
41 | scan.token = tokenSlash
42 | scan.Next()
43 | return
44 | case '(':
45 | scan.token = tokenLParen
46 | scan.Next()
47 | return
48 | case '|':
49 | scan.token = tokenVertical
50 | scan.Next()
51 | return
52 | case ':':
53 | scan.token = tokenColon
54 | scan.Next()
55 | return
56 | case ')':
57 | scan.token = tokenRParen
58 | scan.Next()
59 | return
60 | case eoi:
61 | scan.token = tokenEOF
62 | scan.Next()
63 | return
64 | }
65 |
66 | scan.Next()
67 | }
68 | }
69 |
70 | func (scan *scanner) Current() byte {
71 | return scan.ch
72 | }
73 |
74 | func (scan *scanner) Token() int {
75 | return scan.token
76 | }
77 |
78 | func (scan *scanner) TokenIndex() int {
79 | return scan.bp - 1
80 | }
81 |
82 | func (scan *scanner) ScanString() []byte {
83 | value := scan.input[scan.sp : scan.bp-1]
84 | scan.sp = scan.bp
85 | return value
86 | }
87 |
--------------------------------------------------------------------------------
/pkg/route/scanner_test.go:
--------------------------------------------------------------------------------
1 | package route
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestNext(t *testing.T) {
8 | input := []byte("0")
9 | s := newScanner(input)
10 |
11 | ch := s.Current()
12 | if ch != '0' {
13 | t.Errorf("ch expect 0 but %c", ch)
14 | }
15 |
16 | s.Next()
17 | ch = s.Current()
18 | if ch != eoi {
19 | t.Errorf("ch expect eoi but %c", ch)
20 | }
21 | }
22 |
23 | func TestNextTokenAndScanString(t *testing.T) {
24 | input := []byte("0/1(2|3:4)")
25 | s := newScanner(input)
26 |
27 | s.NextToken()
28 | token := s.Token()
29 | if token != tokenSlash {
30 | t.Errorf("token expect / but %d", token)
31 | }
32 | value := string(s.ScanString())
33 | if value != "0" {
34 | t.Errorf("scanstring expect 0 but %s", value)
35 | }
36 |
37 | s.NextToken()
38 | token = s.Token()
39 | if token != tokenLParen {
40 | t.Errorf("token expect ( but %d", token)
41 | }
42 | value = string(s.ScanString())
43 | if value != "1" {
44 | t.Errorf("scanstring expect 1 but %s", value)
45 | }
46 |
47 | s.NextToken()
48 | token = s.Token()
49 | if token != tokenVertical {
50 | t.Errorf("token expect | but %d", token)
51 | }
52 | value = string(s.ScanString())
53 | if value != "2" {
54 | t.Errorf("scanstring expect 2 but %s", value)
55 | }
56 |
57 | s.NextToken()
58 | token = s.Token()
59 | if token != tokenColon {
60 | t.Errorf("token expect : but %d", token)
61 | }
62 | value = string(s.ScanString())
63 | if value != "3" {
64 | t.Errorf("scanstring expect 3 but %s", value)
65 | }
66 |
67 | s.NextToken()
68 | token = s.Token()
69 | if token != tokenRParen {
70 | t.Errorf("token expect ) but %d", token)
71 | }
72 | value = string(s.ScanString())
73 | if value != "4" {
74 | t.Errorf("scanstring expect 4 but %s", value)
75 | }
76 |
77 | s.NextToken()
78 | token = s.Token()
79 | if token != tokenEOF {
80 | t.Errorf("token expect eof but %d", token)
81 | }
82 | value = string(s.ScanString())
83 | if value != "" {
84 | t.Errorf("scanstring expect empty but %s", value)
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/pkg/service/errors.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | var (
8 | errRPCCancel = errors.New("rpc cancel")
9 | )
10 |
--------------------------------------------------------------------------------
/pkg/service/g.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "github.com/fagongzi/gateway/pkg/pb/rpcpb"
5 | "github.com/fagongzi/gateway/pkg/store"
6 | )
7 |
8 | var (
9 | // MetaService global service
10 | MetaService rpcpb.MetaServiceServer
11 | // Store global store db
12 | Store store.Store
13 | )
14 |
15 | // Init init service package
16 | func Init(db store.Store) {
17 | Store = db
18 | MetaService = newMetaService(db)
19 | }
20 |
--------------------------------------------------------------------------------
/pkg/service/http.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/fagongzi/util/format"
7 | "github.com/labstack/echo"
8 | )
9 |
10 | const (
11 | apiVersion = "/v1"
12 | )
13 |
14 | // InitHTTPRouter init http router
15 | func InitHTTPRouter(server *echo.Echo, ui, uiPrefix string) {
16 | versionGroup := server.Group(apiVersion)
17 | initClusterRouter(versionGroup)
18 | initServerRouter(versionGroup)
19 | initBindRouter(versionGroup)
20 | initRoutingRouter(versionGroup)
21 | initAPIRouter(versionGroup)
22 | initPluginRouter(versionGroup)
23 | initSystemRouter(versionGroup)
24 | initStatic(server, ui, uiPrefix)
25 | }
26 |
27 | type limitQuery struct {
28 | limit int64
29 | afterID uint64
30 | }
31 |
32 | func idParamFactory(ctx echo.Context) (interface{}, error) {
33 | value := ctx.Param("id")
34 | if value == "" {
35 | return nil, fmt.Errorf("missing id path value")
36 | }
37 |
38 | id, err := format.ParseStrUInt64(value)
39 | if err != nil {
40 | return nil, err
41 | }
42 |
43 | return id, nil
44 | }
45 |
46 | func limitQueryFactory(ctx echo.Context) (interface{}, error) {
47 | query := &limitQuery{
48 | limit: limit,
49 | }
50 |
51 | value := ctx.QueryParam("limit")
52 | if value != "" {
53 | l, err := format.ParseStrInt64(value)
54 | if err != nil {
55 | return nil, err
56 | }
57 | query.limit = l
58 | }
59 |
60 | value = ctx.QueryParam("after")
61 | if value != "" {
62 | l, err := format.ParseStrUInt64(value)
63 | if err != nil {
64 | return nil, err
65 | }
66 | query.afterID = l
67 | }
68 |
69 | return query, nil
70 | }
71 |
72 | func emptyParamFactory(ctx echo.Context) (interface{}, error) {
73 | return nil, nil
74 | }
75 |
--------------------------------------------------------------------------------
/pkg/service/http_api.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "github.com/fagongzi/gateway/pkg/pb/metapb"
5 | "github.com/fagongzi/grpcx"
6 | "github.com/fagongzi/log"
7 | "github.com/labstack/echo"
8 | )
9 |
10 | func initAPIRouter(server *echo.Group) {
11 | server.GET("/apis/:id",
12 | grpcx.NewGetHTTPHandle(idParamFactory, getAPIHandler))
13 | server.DELETE("/apis/:id",
14 | grpcx.NewGetHTTPHandle(idParamFactory, deleteAPIHandler))
15 | server.PUT("/apis",
16 | grpcx.NewJSONBodyHTTPHandle(putAPIFactory, postAPIHandler))
17 | server.GET("/apis",
18 | grpcx.NewGetHTTPHandle(limitQueryFactory, listAPIHandler))
19 | }
20 |
21 | func postAPIHandler(value interface{}) (*grpcx.JSONResult, error) {
22 | id, err := Store.PutAPI(value.(*metapb.API))
23 | if err != nil {
24 | log.Errorf("api-api-put: req %+v, errors:%+v", value, err)
25 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
26 | }
27 |
28 | return &grpcx.JSONResult{Data: id}, nil
29 | }
30 |
31 | func deleteAPIHandler(value interface{}) (*grpcx.JSONResult, error) {
32 | err := Store.RemoveAPI(value.(uint64))
33 | if err != nil {
34 | log.Errorf("api-api-delete: req %+v, errors:%+v", value, err)
35 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
36 | }
37 |
38 | return &grpcx.JSONResult{}, nil
39 | }
40 |
41 | func getAPIHandler(value interface{}) (*grpcx.JSONResult, error) {
42 | value, err := Store.GetAPI(value.(uint64))
43 | if err != nil {
44 | log.Errorf("api-api-get: req %+v, errors:%+v", value, err)
45 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
46 | }
47 |
48 | return &grpcx.JSONResult{Data: value}, nil
49 | }
50 |
51 | func listAPIHandler(value interface{}) (*grpcx.JSONResult, error) {
52 | query := value.(*limitQuery)
53 | var values []*metapb.API
54 |
55 | err := Store.GetAPIs(limit, func(data interface{}) error {
56 | v := data.(*metapb.API)
57 | if int64(len(values)) < query.limit && v.ID > query.afterID {
58 | values = append(values, v)
59 | }
60 | return nil
61 | })
62 | if err != nil {
63 | log.Errorf("api-api-list-get: req %+v, errors:%+v", value, err)
64 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
65 | }
66 |
67 | return &grpcx.JSONResult{Data: values}, nil
68 | }
69 |
70 | func putAPIFactory() interface{} {
71 | return &metapb.API{}
72 | }
73 |
--------------------------------------------------------------------------------
/pkg/service/http_bind.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "github.com/fagongzi/gateway/pkg/pb/metapb"
5 | "github.com/fagongzi/grpcx"
6 | "github.com/fagongzi/log"
7 | "github.com/labstack/echo"
8 | )
9 |
10 | func initBindRouter(server *echo.Group) {
11 | server.DELETE("/binds",
12 | grpcx.NewJSONBodyHTTPHandle(bindFactory, deleteBindHandler))
13 |
14 | server.PUT("/binds",
15 | grpcx.NewJSONBodyHTTPHandle(bindFactory, postBindHandler))
16 | }
17 |
18 | func postBindHandler(value interface{}) (*grpcx.JSONResult, error) {
19 | err := Store.AddBind(value.(*metapb.Bind))
20 | if err != nil {
21 | log.Errorf("api-bind-put: req %+v, errors:%+v", value, err)
22 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
23 | }
24 |
25 | return &grpcx.JSONResult{}, nil
26 | }
27 |
28 | func deleteBindHandler(value interface{}) (*grpcx.JSONResult, error) {
29 | err := Store.RemoveBind(value.(*metapb.Bind))
30 | if err != nil {
31 | log.Errorf("api-bind-delete: req %+v, errors:%+v", value, err)
32 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
33 | }
34 |
35 | return &grpcx.JSONResult{}, nil
36 | }
37 |
38 | func bindFactory() interface{} {
39 | return &metapb.Bind{}
40 | }
41 |
--------------------------------------------------------------------------------
/pkg/service/http_cluster.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "github.com/fagongzi/gateway/pkg/pb/metapb"
5 | "github.com/fagongzi/grpcx"
6 | "github.com/fagongzi/log"
7 | "github.com/labstack/echo"
8 | )
9 |
10 | func initClusterRouter(server *echo.Group) {
11 | server.GET("/clusters/:id",
12 | grpcx.NewGetHTTPHandle(idParamFactory, getClusterHandler))
13 | server.GET("/clusters/:id/binds",
14 | grpcx.NewGetHTTPHandle(idParamFactory, bindsClusterHandler))
15 | server.DELETE("/clusters/:id",
16 | grpcx.NewGetHTTPHandle(idParamFactory, deleteClusterHandler))
17 | server.DELETE("/clusters/:id/binds",
18 | grpcx.NewGetHTTPHandle(idParamFactory, deleteClusterBindsHandler))
19 | server.PUT("/clusters",
20 | grpcx.NewJSONBodyHTTPHandle(putClusterFactory, postClusterHandler))
21 | server.GET("/clusters",
22 | grpcx.NewGetHTTPHandle(limitQueryFactory, listClusterHandler))
23 | }
24 |
25 | func postClusterHandler(value interface{}) (*grpcx.JSONResult, error) {
26 | id, err := Store.PutCluster(value.(*metapb.Cluster))
27 | if err != nil {
28 | log.Errorf("api-cluster-put: req %+v, errors:%+v", value, err)
29 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
30 | }
31 |
32 | return &grpcx.JSONResult{Data: id}, nil
33 | }
34 |
35 | func deleteClusterHandler(value interface{}) (*grpcx.JSONResult, error) {
36 | err := Store.RemoveCluster(value.(uint64))
37 | if err != nil {
38 | log.Errorf("api-cluster-delete: req %+v, errors:%+v", value, err)
39 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
40 | }
41 |
42 | return &grpcx.JSONResult{}, nil
43 | }
44 |
45 | func deleteClusterBindsHandler(value interface{}) (*grpcx.JSONResult, error) {
46 | err := Store.RemoveClusterBind(value.(uint64))
47 | if err != nil {
48 | log.Errorf("api-cluster-binds-delete: req %+v, errors:%+v", value, err)
49 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
50 | }
51 |
52 | return &grpcx.JSONResult{}, nil
53 | }
54 |
55 | func getClusterHandler(value interface{}) (*grpcx.JSONResult, error) {
56 | value, err := Store.GetCluster(value.(uint64))
57 | if err != nil {
58 | log.Errorf("api-cluster-get: req %+v, errors:%+v", value, err)
59 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
60 | }
61 |
62 | return &grpcx.JSONResult{Data: value}, nil
63 | }
64 |
65 | func bindsClusterHandler(value interface{}) (*grpcx.JSONResult, error) {
66 | values, err := Store.GetBindServers(value.(uint64))
67 | if err != nil {
68 | log.Errorf("api-cluster-binds-get: req %+v, errors:%+v", value, err)
69 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
70 | }
71 |
72 | return &grpcx.JSONResult{Data: values}, nil
73 | }
74 |
75 | func listClusterHandler(value interface{}) (*grpcx.JSONResult, error) {
76 | query := value.(*limitQuery)
77 | var values []*metapb.Cluster
78 |
79 | err := Store.GetClusters(limit, func(data interface{}) error {
80 | v := data.(*metapb.Cluster)
81 | if int64(len(values)) < query.limit && v.ID > query.afterID {
82 | values = append(values, v)
83 | }
84 | return nil
85 | })
86 | if err != nil {
87 | log.Errorf("api-cluster-list-get: req %+v, errors:%+v", value, err)
88 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
89 | }
90 |
91 | return &grpcx.JSONResult{Data: values}, nil
92 | }
93 |
94 | func putClusterFactory() interface{} {
95 | return &metapb.Cluster{}
96 | }
97 |
--------------------------------------------------------------------------------
/pkg/service/http_plugin.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "github.com/fagongzi/gateway/pkg/pb/metapb"
5 | "github.com/fagongzi/grpcx"
6 | "github.com/fagongzi/log"
7 | "github.com/labstack/echo"
8 | )
9 |
10 | func initPluginRouter(server *echo.Group) {
11 | server.GET("/plugins/:id",
12 | grpcx.NewGetHTTPHandle(idParamFactory, getPluginHandler))
13 | server.DELETE("/plugins/:id",
14 | grpcx.NewGetHTTPHandle(idParamFactory, deletePluginHandler))
15 | server.PUT("/plugins",
16 | grpcx.NewJSONBodyHTTPHandle(putPluginFactory, postPluginHandler))
17 | server.GET("/plugins",
18 | grpcx.NewGetHTTPHandle(limitQueryFactory, listPluginHandler))
19 | server.PUT("/plugins/apply",
20 | grpcx.NewJSONBodyHTTPHandle(putPluginAppliedFactory, putPluginAppliedHandler))
21 | server.GET("/plugins/apply",
22 | grpcx.NewGetHTTPHandle(emptyParamFactory, getPluginAppliedHandler))
23 | }
24 |
25 | func getPluginAppliedHandler(value interface{}) (*grpcx.JSONResult, error) {
26 | value, err := Store.GetAppliedPlugins()
27 | if err != nil {
28 | log.Errorf("api-plugin-get-applied: req %+v, errors:%+v", value, err)
29 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
30 | }
31 |
32 | return &grpcx.JSONResult{Data: value}, nil
33 | }
34 |
35 | func putPluginAppliedHandler(value interface{}) (*grpcx.JSONResult, error) {
36 | err := Store.ApplyPlugins(value.(*metapb.AppliedPlugins))
37 | if err != nil {
38 | log.Errorf("api-plugin-put-applied: req %+v, errors:%+v", value, err)
39 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
40 | }
41 |
42 | return &grpcx.JSONResult{}, nil
43 | }
44 |
45 | func postPluginHandler(value interface{}) (*grpcx.JSONResult, error) {
46 | id, err := Store.PutPlugin(value.(*metapb.Plugin))
47 | if err != nil {
48 | log.Errorf("api-plugin-put: req %+v, errors:%+v", value, err)
49 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
50 | }
51 |
52 | return &grpcx.JSONResult{Data: id}, nil
53 | }
54 |
55 | func deletePluginHandler(value interface{}) (*grpcx.JSONResult, error) {
56 | err := Store.RemovePlugin(value.(uint64))
57 | if err != nil {
58 | log.Errorf("api-plugin-delete: req %+v, errors:%+v", value, err)
59 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
60 | }
61 |
62 | return &grpcx.JSONResult{}, nil
63 | }
64 |
65 | func getPluginHandler(value interface{}) (*grpcx.JSONResult, error) {
66 | value, err := Store.GetPlugin(value.(uint64))
67 | if err != nil {
68 | log.Errorf("api-plugin-get: req %+v, errors:%+v", value, err)
69 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
70 | }
71 |
72 | return &grpcx.JSONResult{Data: value}, nil
73 | }
74 |
75 | func listPluginHandler(value interface{}) (*grpcx.JSONResult, error) {
76 | query := value.(*limitQuery)
77 | var values []*metapb.Plugin
78 |
79 | err := Store.GetPlugins(limit, func(data interface{}) error {
80 | v := data.(*metapb.Plugin)
81 | if int64(len(values)) < query.limit && v.ID > query.afterID {
82 | values = append(values, v)
83 | }
84 | return nil
85 | })
86 | if err != nil {
87 | log.Errorf("api-plugin-list-get: req %+v, errors:%+v", value, err)
88 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
89 | }
90 |
91 | return &grpcx.JSONResult{Data: values}, nil
92 | }
93 |
94 | func putPluginFactory() interface{} {
95 | return &metapb.Plugin{}
96 | }
97 |
98 | func putPluginAppliedFactory() interface{} {
99 | return &metapb.AppliedPlugins{}
100 | }
101 |
--------------------------------------------------------------------------------
/pkg/service/http_routing.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "github.com/fagongzi/gateway/pkg/pb/metapb"
5 | "github.com/fagongzi/grpcx"
6 | "github.com/fagongzi/log"
7 | "github.com/labstack/echo"
8 | )
9 |
10 | func initRoutingRouter(server *echo.Group) {
11 | server.GET("/routings/:id",
12 | grpcx.NewGetHTTPHandle(idParamFactory, getRoutingHandler))
13 | server.DELETE("/routings/:id",
14 | grpcx.NewGetHTTPHandle(idParamFactory, deleteRoutingHandler))
15 | server.PUT("/routings",
16 | grpcx.NewJSONBodyHTTPHandle(putRoutingFactory, postRoutingHandler))
17 | server.GET("/routings",
18 | grpcx.NewGetHTTPHandle(limitQueryFactory, listRoutingHandler))
19 | }
20 |
21 | func postRoutingHandler(value interface{}) (*grpcx.JSONResult, error) {
22 | id, err := Store.PutRouting(value.(*metapb.Routing))
23 | if err != nil {
24 | log.Errorf("api-routing-put: req %+v, errors:%+v", value, err)
25 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
26 | }
27 |
28 | return &grpcx.JSONResult{Data: id}, nil
29 | }
30 |
31 | func deleteRoutingHandler(value interface{}) (*grpcx.JSONResult, error) {
32 | err := Store.RemoveRouting(value.(uint64))
33 | if err != nil {
34 | log.Errorf("api-routing-delete: req %+v, errors:%+v", value, err)
35 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
36 | }
37 |
38 | return &grpcx.JSONResult{}, nil
39 | }
40 |
41 | func getRoutingHandler(value interface{}) (*grpcx.JSONResult, error) {
42 | value, err := Store.GetRouting(value.(uint64))
43 | if err != nil {
44 | log.Errorf("api-routing-delete: req %+v, errors:%+v", value, err)
45 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
46 | }
47 |
48 | return &grpcx.JSONResult{Data: value}, nil
49 | }
50 |
51 | func putRoutingFactory() interface{} {
52 | return &metapb.Routing{}
53 | }
54 |
55 | func listRoutingHandler(value interface{}) (*grpcx.JSONResult, error) {
56 | query := value.(*limitQuery)
57 | var values []*metapb.Routing
58 |
59 | err := Store.GetRoutings(limit, func(data interface{}) error {
60 | v := data.(*metapb.Routing)
61 | if int64(len(values)) < query.limit && v.ID > query.afterID {
62 | values = append(values, v)
63 | }
64 | return nil
65 | })
66 | if err != nil {
67 | log.Errorf("api-routing-list-get: req %+v, errors:%+v", value, err)
68 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
69 | }
70 |
71 | return &grpcx.JSONResult{Data: values}, nil
72 | }
73 |
--------------------------------------------------------------------------------
/pkg/service/http_server.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "github.com/fagongzi/gateway/pkg/pb/metapb"
5 | "github.com/fagongzi/grpcx"
6 | "github.com/fagongzi/log"
7 | "github.com/labstack/echo"
8 | )
9 |
10 | func initServerRouter(server *echo.Group) {
11 | server.GET("/servers/:id",
12 | grpcx.NewGetHTTPHandle(idParamFactory, getServerHandler))
13 | server.DELETE("/servers/:id",
14 | grpcx.NewGetHTTPHandle(idParamFactory, deleteServerHandler))
15 | server.PUT("/servers",
16 | grpcx.NewJSONBodyHTTPHandle(putServerFactory, postServerHandler))
17 | server.GET("/servers",
18 | grpcx.NewGetHTTPHandle(limitQueryFactory, listServerHandler))
19 | }
20 |
21 | func postServerHandler(value interface{}) (*grpcx.JSONResult, error) {
22 | id, err := Store.PutServer(value.(*metapb.Server))
23 | if err != nil {
24 | log.Errorf("api-server-put: req %+v, errors:%+v", value, err)
25 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
26 | }
27 |
28 | return &grpcx.JSONResult{Data: id}, nil
29 | }
30 |
31 | func deleteServerHandler(value interface{}) (*grpcx.JSONResult, error) {
32 | err := Store.RemoveServer(value.(uint64))
33 | if err != nil {
34 | log.Errorf("api-server-delete: req %+v, errors:%+v", value, err)
35 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
36 | }
37 |
38 | return &grpcx.JSONResult{}, nil
39 | }
40 |
41 | func getServerHandler(value interface{}) (*grpcx.JSONResult, error) {
42 | value, err := Store.GetServer(value.(uint64))
43 | if err != nil {
44 | log.Errorf("api-server-get: req %+v, errors:%+v", value, err)
45 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
46 | }
47 |
48 | return &grpcx.JSONResult{Data: value}, nil
49 | }
50 |
51 | func listServerHandler(value interface{}) (*grpcx.JSONResult, error) {
52 | query := value.(*limitQuery)
53 | var values []*metapb.Server
54 |
55 | err := Store.GetServers(limit, func(data interface{}) error {
56 | v := data.(*metapb.Server)
57 | if int64(len(values)) < query.limit && v.ID > query.afterID {
58 | values = append(values, v)
59 | }
60 | return nil
61 | })
62 | if err != nil {
63 | log.Errorf("api-server-list-get: req %+v, errors:%+v", value, err)
64 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
65 | }
66 |
67 | return &grpcx.JSONResult{Data: values}, nil
68 | }
69 |
70 | func putServerFactory() interface{} {
71 | return &metapb.Server{}
72 | }
73 |
--------------------------------------------------------------------------------
/pkg/service/http_static.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/labstack/echo"
7 | )
8 |
9 | func initStatic(server *echo.Echo, ui, uiPrefix string) {
10 | server.Static(uiPrefix, ui)
11 | server.Static("static", fmt.Sprintf("%s/static", ui))
12 | }
13 |
--------------------------------------------------------------------------------
/pkg/service/http_system.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "github.com/fagongzi/grpcx"
5 | "github.com/fagongzi/log"
6 | "github.com/labstack/echo"
7 | )
8 |
9 | type backup struct {
10 | ToAddr string `json:"toAddr"`
11 | }
12 |
13 | func initSystemRouter(server *echo.Group) {
14 | server.GET("/system",
15 | grpcx.NewGetHTTPHandle(emptyParamFactory, getSystemHandler))
16 |
17 | server.POST("/system/backup",
18 | grpcx.NewJSONBodyHTTPHandle(backupFactory, postBackupHandler))
19 | }
20 |
21 | func getSystemHandler(value interface{}) (*grpcx.JSONResult, error) {
22 | info, err := Store.System()
23 | if err != nil {
24 | log.Errorf("api-system-get: errors:%+v", err)
25 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
26 | }
27 |
28 | return &grpcx.JSONResult{Data: info}, nil
29 | }
30 |
31 | func postBackupHandler(value interface{}) (*grpcx.JSONResult, error) {
32 | err := Store.BackupTo(value.(*backup).ToAddr)
33 | if err != nil {
34 | log.Errorf("api-system-backup: errors:%+v", err)
35 | return &grpcx.JSONResult{Code: -1, Data: err.Error()}, nil
36 | }
37 |
38 | return &grpcx.JSONResult{}, nil
39 | }
40 |
41 | func backupFactory() interface{} {
42 | return &backup{}
43 | }
44 |
--------------------------------------------------------------------------------
/pkg/store/store.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import (
4 | "fmt"
5 | "net/url"
6 | "strings"
7 | "time"
8 |
9 | "github.com/fagongzi/gateway/pkg/pb/metapb"
10 | "github.com/fagongzi/gateway/pkg/pb/rpcpb"
11 | "github.com/fagongzi/gateway/pkg/util"
12 | )
13 |
14 | var (
15 | // TICKER ticket
16 | TICKER = time.Second * 3
17 | // TTL timeout
18 | TTL = int64(5)
19 | )
20 |
21 | var (
22 | supportSchema = make(map[string]func(string, string, BasicAuth) (Store, error))
23 | )
24 |
25 | // EvtType event type
26 | type EvtType int
27 |
28 | // EvtSrc event src
29 | type EvtSrc int
30 |
31 | // BasicAuth basic auth
32 | type BasicAuth struct {
33 | userName string
34 | password string
35 | }
36 |
37 | const (
38 | // EventTypeNew event type new
39 | EventTypeNew = EvtType(0)
40 | // EventTypeUpdate event type update
41 | EventTypeUpdate = EvtType(1)
42 | // EventTypeDelete event type delete
43 | EventTypeDelete = EvtType(2)
44 | )
45 |
46 | const (
47 | // EventSrcCluster cluster event
48 | EventSrcCluster = EvtSrc(0)
49 | // EventSrcServer server event
50 | EventSrcServer = EvtSrc(1)
51 | // EventSrcBind bind event
52 | EventSrcBind = EvtSrc(2)
53 | // EventSrcAPI api event
54 | EventSrcAPI = EvtSrc(3)
55 | // EventSrcRouting routing event
56 | EventSrcRouting = EvtSrc(4)
57 | // EventSrcProxy routing event
58 | EventSrcProxy = EvtSrc(5)
59 | // EventSrcPlugin plugin event
60 | EventSrcPlugin = EvtSrc(6)
61 | // EventSrcApplyPlugin apply plugin event
62 | EventSrcApplyPlugin = EvtSrc(7)
63 | )
64 |
65 | // Evt event
66 | type Evt struct {
67 | Src EvtSrc
68 | Type EvtType
69 | Key string
70 | Value interface{}
71 | }
72 |
73 | func init() {
74 | supportSchema["etcd"] = getEtcdStoreFrom
75 | }
76 |
77 | // GetStoreFrom returns a store implemention, if not support returns error
78 | func GetStoreFrom(registryAddr, prefix string, userName string, password string) (Store, error) {
79 | u, err := url.Parse(registryAddr)
80 | if err != nil {
81 | panic(fmt.Sprintf("parse registry addr failed, errors:%+v", err))
82 | }
83 |
84 | schema := strings.ToLower(u.Scheme)
85 | fn, ok := supportSchema[schema]
86 | if ok {
87 | return fn(u.Host, prefix, BasicAuth{userName: userName, password: password})
88 | }
89 |
90 | return nil, fmt.Errorf("not support: %s", registryAddr)
91 | }
92 |
93 | func getEtcdStoreFrom(addr, prefix string, basicAuth BasicAuth) (Store, error) {
94 | var addrs []string
95 | values := strings.Split(addr, ",")
96 |
97 | for _, value := range values {
98 | addrs = append(addrs, fmt.Sprintf("http://%s", value))
99 | }
100 |
101 | return NewEtcdStore(addrs, prefix, basicAuth)
102 | }
103 |
104 | // Store store interface
105 | type Store interface {
106 | Raw() interface{}
107 |
108 | AddBind(bind *metapb.Bind) error
109 | RemoveBind(bind *metapb.Bind) error
110 | RemoveClusterBind(id uint64) error
111 | GetBindServers(id uint64) ([]uint64, error)
112 |
113 | PutCluster(cluster *metapb.Cluster) (uint64, error)
114 | RemoveCluster(id uint64) error
115 | GetClusters(limit int64, fn func(interface{}) error) error
116 | GetCluster(id uint64) (*metapb.Cluster, error)
117 |
118 | PutServer(svr *metapb.Server) (uint64, error)
119 | RemoveServer(id uint64) error
120 | GetServers(limit int64, fn func(interface{}) error) error
121 | GetServer(id uint64) (*metapb.Server, error)
122 |
123 | PutAPI(api *metapb.API) (uint64, error)
124 | RemoveAPI(id uint64) error
125 | GetAPIs(limit int64, fn func(interface{}) error) error
126 | GetAPI(id uint64) (*metapb.API, error)
127 |
128 | PutRouting(routing *metapb.Routing) (uint64, error)
129 | RemoveRouting(id uint64) error
130 | GetRoutings(limit int64, fn func(interface{}) error) error
131 | GetRouting(id uint64) (*metapb.Routing, error)
132 |
133 | PutPlugin(plugin *metapb.Plugin) (uint64, error)
134 | RemovePlugin(id uint64) error
135 | GetPlugins(limit int64, fn func(interface{}) error) error
136 | GetPlugin(id uint64) (*metapb.Plugin, error)
137 | ApplyPlugins(applied *metapb.AppliedPlugins) error
138 | GetAppliedPlugins() (*metapb.AppliedPlugins, error)
139 |
140 | RegistryProxy(proxy *metapb.Proxy, ttl int64) error
141 | GetProxies(limit int64, fn func(*metapb.Proxy) error) error
142 |
143 | Watch(evtCh chan *Evt, stopCh chan bool) error
144 |
145 | Clean() error
146 | SetID(id uint64) error
147 | BackupTo(to string) error
148 | Batch(batch *rpcpb.BatchReq) (*rpcpb.BatchRsp, error)
149 | System() (*metapb.System, error)
150 | }
151 |
152 | func getKey(prefix string, id uint64) string {
153 | return fmt.Sprintf("%s/%020d", prefix, id)
154 | }
155 |
156 | func getAddrKey(prefix string, addr string) string {
157 | return fmt.Sprintf("%s/%s", prefix, util.GetAddrFormat(addr))
158 | }
159 |
--------------------------------------------------------------------------------
/pkg/store/txn.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/coreos/etcd/clientv3"
7 | "github.com/fagongzi/log"
8 | "golang.org/x/net/context"
9 | )
10 |
11 | // slowLogTxn wraps etcd transaction and log slow one.
12 | type slowLogTxn struct {
13 | clientv3.Txn
14 | cancel context.CancelFunc
15 | }
16 |
17 | func newSlowLogTxn(client *clientv3.Client) clientv3.Txn {
18 | ctx, cancel := context.WithTimeout(client.Ctx(), DefaultRequestTimeout)
19 | return &slowLogTxn{
20 | Txn: client.Txn(ctx),
21 | cancel: cancel,
22 | }
23 | }
24 |
25 | func (t *slowLogTxn) If(cs ...clientv3.Cmp) clientv3.Txn {
26 | return &slowLogTxn{
27 | Txn: t.Txn.If(cs...),
28 | cancel: t.cancel,
29 | }
30 | }
31 |
32 | func (t *slowLogTxn) Then(ops ...clientv3.Op) clientv3.Txn {
33 | return &slowLogTxn{
34 | Txn: t.Txn.Then(ops...),
35 | cancel: t.cancel,
36 | }
37 | }
38 |
39 | // Commit implements Txn Commit interface.
40 | func (t *slowLogTxn) Commit() (*clientv3.TxnResponse, error) {
41 | start := time.Now()
42 | resp, err := t.Txn.Commit()
43 | t.cancel()
44 |
45 | cost := time.Now().Sub(start)
46 | if cost > DefaultSlowRequestTime {
47 | log.Warn("slow: txn runs too slow, resp=<%v> cost=<%s> errors:\n %+v",
48 | resp,
49 | cost,
50 | err)
51 | }
52 |
53 | return resp, err
54 | }
55 |
56 | func (e *EtcdStore) txn() clientv3.Txn {
57 | return newSlowLogTxn(e.rawClient)
58 | }
59 |
--------------------------------------------------------------------------------
/pkg/util/addr.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | const (
8 | // MinAddrFormat min addr format
9 | MinAddrFormat = "000000000000000000000"
10 | // MaxAddrFormat max addr format
11 | MaxAddrFormat = "255.255.255.255:99999"
12 | )
13 |
14 | // GetAddrFormat returns addr format for sort, padding left by 0
15 | func GetAddrFormat(addr string) string {
16 | return fmt.Sprintf("%021s", addr)
17 | }
18 |
19 | // GetAddrNextFormat returns next addr format for sort, padding left by 0
20 | func GetAddrNextFormat(addr string) string {
21 | return fmt.Sprintf("%s%c", addr[:len(addr)-1], addr[len(addr)-1]+1)
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/util/analysis_test.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 | "testing"
7 | "time"
8 |
9 | "github.com/fagongzi/goetty"
10 | "github.com/stretchr/testify/assert"
11 | )
12 |
13 | func mlen(m *sync.Map) int {
14 | c := 0
15 | m.Range(func(key, value interface{}) bool {
16 | c++
17 | return true
18 | })
19 |
20 | return c
21 | }
22 |
23 | func TestAddTarget(t *testing.T) {
24 | key := uint64(1)
25 | tw := goetty.NewTimeoutWheel(goetty.WithTickInterval(time.Millisecond * 10))
26 | ans := NewAnalysis(tw)
27 | ans.AddTarget(key, time.Millisecond*10)
28 |
29 | assert.Equal(t, 1, mlen(&ans.points),
30 | fmt.Sprintf("expect 1 points but %d", mlen(&ans.points)))
31 |
32 | assert.Equal(t, 1, mlen(&ans.recentlyPoints),
33 | fmt.Sprintf("expect 1 recently points but %d", mlen(&ans.recentlyPoints)))
34 |
35 | m, _ := ans.recentlyPoints.Load(key)
36 | assert.Equal(t, 1, mlen(m.(*sync.Map)),
37 | fmt.Sprintf("expect 1 recently points but %d", mlen(m.(*sync.Map))))
38 | }
39 |
40 | func TestRemoveTarget(t *testing.T) {
41 | key := uint64(1)
42 | tw := goetty.NewTimeoutWheel(goetty.WithTickInterval(time.Millisecond * 10))
43 | ans := NewAnalysis(tw)
44 | ans.AddTarget(key, time.Millisecond*10)
45 | ans.RemoveTarget(key)
46 |
47 | assert.Equal(t, 0, mlen(&ans.points),
48 | fmt.Sprintf("expect 0 points but %d", mlen(&ans.points)))
49 |
50 | assert.Equal(t, 0, mlen(&ans.recentlyPoints),
51 | fmt.Sprintf("expect 0 recently points but %d", mlen(&ans.recentlyPoints)))
52 | }
53 |
--------------------------------------------------------------------------------
/pkg/util/barrier.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "math/rand"
5 | "sync"
6 | "sync/atomic"
7 | )
8 |
9 | var (
10 | lock sync.Mutex
11 | randSources = make(map[int][]int)
12 | )
13 |
14 | func createRandSourceByBase(base int) []int {
15 | lock.Lock()
16 | defer lock.Unlock()
17 |
18 | if value, ok := randSources[base]; ok {
19 | return value
20 | }
21 |
22 | value := make([]int, base, base)
23 | for i := 0; i < base; i++ {
24 | value[i] = i
25 | }
26 |
27 | rand.Shuffle(base, func(i, j int) {
28 | value[i], value[j] = value[j], value[i]
29 | })
30 | randSources[base] = value
31 | return value
32 | }
33 |
34 | // RateBarrier rand barrier
35 | type RateBarrier struct {
36 | source []int
37 | op uint64
38 | rate int
39 | base int
40 | }
41 |
42 | // NewRateBarrier returns a barrier based by 100
43 | func NewRateBarrier(rate int) *RateBarrier {
44 | return NewRateBarrierBase(rate, 100)
45 | }
46 |
47 | // NewRateBarrierBase returns a barrier with base
48 | func NewRateBarrierBase(rate, base int) *RateBarrier {
49 | return &RateBarrier{
50 | source: createRandSourceByBase(base),
51 | rate: rate,
52 | base: base,
53 | }
54 | }
55 |
56 | // Allow returns true if allowed
57 | func (b *RateBarrier) Allow() bool {
58 | return b.source[int(atomic.AddUint64(&b.op, 1))%b.base] < b.rate
59 | }
60 |
--------------------------------------------------------------------------------
/pkg/util/ip_util.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/valyala/fasthttp"
7 | )
8 |
9 | // ClientIP returns the real client IP
10 | func ClientIP(ctx *fasthttp.RequestCtx) string {
11 | clientIP := string(ctx.Request.Header.Peek("X-Forwarded-For"))
12 | if index := strings.IndexByte(clientIP, ','); index >= 0 {
13 | clientIP = clientIP[0:index]
14 | }
15 | clientIP = strings.TrimSpace(clientIP)
16 | if len(clientIP) > 0 {
17 | return clientIP
18 | }
19 | clientIP = strings.TrimSpace(string(ctx.Request.Header.Peek("X-Real-Ip")))
20 | if len(clientIP) > 0 {
21 | return clientIP
22 | }
23 | return ctx.RemoteIP().String()
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/util/lru.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "container/list"
5 | "sync"
6 |
7 | "github.com/fagongzi/goetty"
8 | )
9 |
10 | // Cache is an LRU cache. It is not safe for concurrent access.
11 | type Cache struct {
12 | sync.RWMutex
13 |
14 | // MaxBytes is the maximum bytes of cache entries before
15 | // an item is evicted. Zero means no limit.
16 | MaxBytes uint64
17 | current uint64
18 |
19 | // OnEvicted optionally specificies a callback function to be
20 | // executed when an entry is purged from the cache.
21 | OnEvicted func(key Key, value *goetty.ByteBuf)
22 |
23 | ll *list.List
24 | cache map[interface{}]*list.Element
25 | }
26 |
27 | // A Key may be any value that is comparable. See http://golang.org/ref/spec#Comparison_operators
28 | type Key interface{}
29 |
30 | type entry struct {
31 | key Key
32 | value *goetty.ByteBuf
33 | }
34 |
35 | // NewLRUCache creates a new Cache.
36 | // If maxBytes is zero, the cache has no limit and it's assumed
37 | // that eviction is done by the caller.
38 | func NewLRUCache(maxBytes uint64, evictedFunc func(key Key, value *goetty.ByteBuf)) *Cache {
39 | return &Cache{
40 | MaxBytes: maxBytes,
41 | ll: list.New(),
42 | cache: make(map[interface{}]*list.Element),
43 | OnEvicted: evictedFunc,
44 | }
45 | }
46 |
47 | // Add adds a value to the cache.
48 | func (c *Cache) Add(key Key, value *goetty.ByteBuf) {
49 | c.Lock()
50 |
51 | if c.cache == nil {
52 | c.cache = make(map[interface{}]*list.Element)
53 | c.ll = list.New()
54 | }
55 | if ee, ok := c.cache[key]; ok {
56 | c.ll.MoveToFront(ee)
57 |
58 | entry := ee.Value.(*entry)
59 | c.current -= uint64(value.Readable())
60 | c.current += uint64(value.Readable())
61 | entry.value = value
62 | c.Unlock()
63 | return
64 | }
65 |
66 | c.current += uint64(value.Readable())
67 | ele := c.ll.PushFront(&entry{key, value})
68 | c.cache[key] = ele
69 | if c.MaxBytes != 0 && c.current > c.MaxBytes {
70 | c.removeOldest()
71 | }
72 | c.Unlock()
73 | }
74 |
75 | // Get looks up a key's value from the cache.
76 | func (c *Cache) Get(key Key) (value *goetty.ByteBuf, ok bool) {
77 | c.RLock()
78 |
79 | if c.cache == nil {
80 | c.RUnlock()
81 | return
82 | }
83 |
84 | if ele, hit := c.cache[key]; hit {
85 | c.ll.MoveToFront(ele)
86 | c.RUnlock()
87 | return ele.Value.(*entry).value, true
88 | }
89 |
90 | c.RUnlock()
91 | return
92 | }
93 |
94 | // Remove removes the provided key from the cache.
95 | func (c *Cache) Remove(key Key) {
96 | c.Lock()
97 |
98 | if c.cache == nil {
99 | c.Unlock()
100 | return
101 | }
102 | if ele, hit := c.cache[key]; hit {
103 | c.removeElement(ele)
104 | }
105 |
106 | c.Unlock()
107 | }
108 |
109 | func (c *Cache) removeOldest() {
110 | if c.cache == nil {
111 | return
112 | }
113 | ele := c.ll.Back()
114 | if ele != nil {
115 | c.removeElement(ele)
116 | }
117 | }
118 |
119 | func (c *Cache) removeElement(e *list.Element) {
120 | c.ll.Remove(e)
121 | kv := e.Value.(*entry)
122 | delete(c.cache, kv.key)
123 | c.current -= uint64(kv.value.Readable())
124 | if c.OnEvicted != nil {
125 | c.OnEvicted(kv.key, kv.value)
126 | }
127 | }
128 |
129 | // Len returns the number of items in the cache.
130 | func (c *Cache) Len() int {
131 | c.RLock()
132 | if c.cache == nil {
133 | c.RUnlock()
134 | return 0
135 | }
136 | value := c.ll.Len()
137 | c.RUnlock()
138 | return value
139 | }
140 |
141 | // Clear purges all stored items from the cache.
142 | func (c *Cache) Clear() {
143 | c.Lock()
144 | if c.OnEvicted != nil {
145 | for _, e := range c.cache {
146 | kv := e.Value.(*entry)
147 | c.OnEvicted(kv.key, kv.value)
148 | }
149 | }
150 | c.ll = nil
151 | c.cache = nil
152 | c.current = 0
153 | c.Unlock()
154 | }
155 |
--------------------------------------------------------------------------------
/pkg/util/metric_push.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "fmt"
7 | "io/ioutil"
8 | "net/http"
9 | "net/url"
10 | "os"
11 | "strings"
12 | "time"
13 |
14 | "github.com/fagongzi/log"
15 | "github.com/fagongzi/util/task"
16 | "github.com/prometheus/client_golang/prometheus"
17 | "github.com/prometheus/common/expfmt"
18 | "github.com/prometheus/common/model"
19 | )
20 |
21 | const contentTypeHeader = "Content-Type"
22 |
23 | var (
24 | client = &http.Client{}
25 | defaultTimeout = time.Second * 15
26 | )
27 |
28 | // MetricCfg is the metric configuration.
29 | type MetricCfg struct {
30 | Job string
31 | Instance string
32 | Address string
33 | DurationSync time.Duration
34 | }
35 |
36 | // NewMetricCfg returns metric cfg
37 | func NewMetricCfg(job, instance, address string, durationSync time.Duration) *MetricCfg {
38 | return &MetricCfg{
39 | Job: job,
40 | Instance: instance,
41 | Address: address,
42 | DurationSync: durationSync,
43 | }
44 | }
45 |
46 | // StartMetricsPush start a push client
47 | func StartMetricsPush(runner *task.Runner, cfg *MetricCfg) {
48 | if nil == cfg || cfg.DurationSync == 0 || len(cfg.Address) == 0 {
49 | log.Info("metric: disable prometheus push client")
50 | return
51 | }
52 |
53 | *client = *http.DefaultClient
54 | client.Timeout = defaultTimeout
55 |
56 | log.Info("metric: start prometheus push client")
57 | runner.RunCancelableTask(func(ctx context.Context) {
58 | t := time.NewTicker(cfg.DurationSync)
59 | defer t.Stop()
60 |
61 | for {
62 | select {
63 | case <-ctx.Done():
64 | log.Info("stop: prometheus push client stopped")
65 | t.Stop()
66 | return
67 | case <-t.C:
68 | err := doPush(cfg.Job, instanceGroupingKey(cfg.Instance), cfg.Address, prometheus.DefaultGatherer, "PUT")
69 | if err != nil {
70 | log.Errorf("metric: could not push metrics to prometheus pushgateway: errors:\n%+v", err)
71 | }
72 | }
73 | }
74 | })
75 | }
76 |
77 | // instanceGroupingKey returns a label map with the only entry
78 | // {instance=""}. If instance is empty, use hostname instead.
79 | func instanceGroupingKey(instance string) map[string]string {
80 | if instance == "" {
81 | var err error
82 | if instance, err = os.Hostname(); err != nil {
83 | instance = "unknown"
84 | }
85 | }
86 | return map[string]string{"instance": instance}
87 | }
88 |
89 | func doPush(job string, grouping map[string]string, pushURL string, g prometheus.Gatherer, method string) error {
90 | if !strings.Contains(pushURL, "://") {
91 | pushURL = "http://" + pushURL
92 | }
93 | if strings.HasSuffix(pushURL, "/") {
94 | pushURL = pushURL[:len(pushURL)-1]
95 | }
96 |
97 | if strings.Contains(job, "/") {
98 | return fmt.Errorf("job contains '/': %s", job)
99 | }
100 | urlComponents := []string{url.QueryEscape(job)}
101 | for ln, lv := range grouping {
102 | if !model.LabelName(ln).IsValid() {
103 | return fmt.Errorf("grouping label has invalid name: %s", ln)
104 | }
105 | if strings.Contains(lv, "/") {
106 | return fmt.Errorf("value of grouping label %s contains '/': %s", ln, lv)
107 | }
108 | urlComponents = append(urlComponents, ln, lv)
109 | }
110 | pushURL = fmt.Sprintf("%s/metrics/job/%s", pushURL, strings.Join(urlComponents, "/"))
111 |
112 | mfs, err := g.Gather()
113 | if err != nil {
114 | return err
115 | }
116 | buf := &bytes.Buffer{}
117 | enc := expfmt.NewEncoder(buf, expfmt.FmtProtoDelim)
118 | // Check for pre-existing grouping labels:
119 | for _, mf := range mfs {
120 | for _, m := range mf.GetMetric() {
121 | for _, l := range m.GetLabel() {
122 | if l.GetName() == "job" {
123 | return fmt.Errorf("pushed metric %s (%s) already contains a job label", mf.GetName(), m)
124 | }
125 | if _, ok := grouping[l.GetName()]; ok {
126 | return fmt.Errorf(
127 | "pushed metric %s (%s) already contains grouping label %s",
128 | mf.GetName(), m, l.GetName(),
129 | )
130 | }
131 | }
132 | }
133 | enc.Encode(mf)
134 | }
135 | req, err := http.NewRequest(method, pushURL, buf)
136 | if err != nil {
137 | return err
138 | }
139 | req.Header.Set(contentTypeHeader, string(expfmt.FmtProtoDelim))
140 | resp, err := client.Do(req)
141 | if err != nil {
142 | return err
143 | }
144 | defer resp.Body.Close()
145 | if resp.StatusCode != 202 {
146 | body, _ := ioutil.ReadAll(resp.Body) // Ignore any further error as this is for an error message only.
147 | return fmt.Errorf("unexpected status code %d while pushing to %s: %s", resp.StatusCode, pushURL, body)
148 | }
149 | return nil
150 | }
151 |
--------------------------------------------------------------------------------
/pkg/util/time.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | // NowWithMillisecond returns timestamp with millisecond
8 | func NowWithMillisecond() int64 {
9 | return time.Now().UnixNano() / int64(time.Millisecond)
10 | }
11 |
--------------------------------------------------------------------------------
/pkg/util/version.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | // set on build time
8 | var (
9 | GitCommit = ""
10 | BuildTime = ""
11 | GoVersion = ""
12 | Version = ""
13 | )
14 |
15 | // PrintVersion Print out version information
16 | func PrintVersion() {
17 | fmt.Println("Version : ", Version)
18 | fmt.Println("GitCommit: ", GitCommit)
19 | fmt.Println("BuildTime: ", BuildTime)
20 | fmt.Println("GoVersion: ", GoVersion)
21 | }
22 |
--------------------------------------------------------------------------------
/prometheus.yml:
--------------------------------------------------------------------------------
1 | global:
2 | scrape_interval: 15s
3 | scrape_timeout: 10s
4 | evaluation_interval: 15s
5 | alerting:
6 | alertmanagers:
7 | - static_configs:
8 | - targets: []
9 | scheme: http
10 | timeout: 10s
11 | scrape_configs:
12 | - job_name: prometheus
13 | honor_timestamps: true
14 | scrape_interval: 15s
15 | scrape_timeout: 10s
16 | metrics_path: /metrics
17 | scheme: http
18 | static_configs:
19 | - targets:
20 | - localhost:9090
21 | - pushgateway:9091
--------------------------------------------------------------------------------