You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by ju...@apache.org on 2020/06/17 09:36:09 UTC
[incubator-apisix-dashboard] 01/01: Revert "feat: remove go (#1)"
This is an automated email from the ASF dual-hosted git repository.
juzhiyuan pushed a commit to branch apisix-feat-revert
in repository https://gitbox.apache.org/repos/asf/incubator-apisix-dashboard.git
commit 4fef39ca53164d0f16e2a8e44a4c3a152aaa1f15
Author: juzhiyuan <jj...@gmail.com>
AuthorDate: Wed Jun 17 17:34:54 2020 +0800
Revert "feat: remove go (#1)"
This reverts commit ce18fc38ba220674fd56793e4f4255fc37eff39a.
---
.github/workflows/api_ut.yml | 18 +
README.md => README-dashboard.md | 0
README.md | 47 +-
api/Dockerfile | 49 ++
api/README.md | 22 +
api/build.sh | 30 +
api/conf.json | 19 +
api/conf/conf.go | 104 +++
api/conf/conf.json | 19 +
api/conf/mysql.go | 46 +
api/docker-compose.yml | 9 +
api/errno/error.go | 98 +++
api/filter/cors.go | 33 +
api/filter/logging.go | 84 ++
api/filter/recover.go | 120 +++
api/filter/request_id.go | 42 +
api/go.mod | 15 +
api/go.sum | 89 ++
api/log/log.go | 147 ++++
api/main.go | 65 ++
api/route/healthz.go | 37 +
api/route/plugin.go | 58 ++
api/route/route.go | 242 ++++++
api/route/ssl.go | 153 ++++
api/script/db/schema.sql | 31 +
api/service/base.go | 48 ++
api/service/plugin.go | 61 ++
api/service/route.go | 561 ++++++++++++
api/service/route_test.go | 169 ++++
api/service/ssl.go | 300 +++++++
api/utils/copy.go | 41 +
api/utils/http.go | 113 +++
compose/README.md | 56 ++
compose/apisix_conf/config.yaml | 137 +++
compose/docker-compose.yml | 124 +++
compose/grafana_conf/config/grafana.ini | 756 ++++++++++++++++
.../dashboards/apisix_http_prometheus.json | 956 +++++++++++++++++++++
.../grafana_conf/provisioning/dashboards/all.yaml | 11 +
.../grafana_conf/provisioning/datasources/all.yaml | 9 +
compose/manager_conf/build.sh | 20 +
compose/pics/grafana_1.png | Bin 0 -> 57816 bytes
compose/pics/grafana_2.png | Bin 0 -> 127932 bytes
compose/pics/grafana_3.png | Bin 0 -> 103384 bytes
compose/pics/grafana_4.png | Bin 0 -> 82558 bytes
compose/pics/grafana_5.png | Bin 0 -> 105366 bytes
compose/pics/grafana_6.png | Bin 0 -> 179132 bytes
compose/pics/login.png | Bin 0 -> 84958 bytes
compose/prometheus_conf/prometheus.yml | 23 +
48 files changed, 4921 insertions(+), 41 deletions(-)
diff --git a/.github/workflows/api_ut.yml b/.github/workflows/api_ut.yml
new file mode 100644
index 0000000..2b3c4bb
--- /dev/null
+++ b/.github/workflows/api_ut.yml
@@ -0,0 +1,18 @@
+name: API unit test
+
+on:
+ push:
+ branches:
+ - master
+ - manager
+ pull_request:
+ branches:
+ - master
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: use docker-compose in api
+ run: cd ./api && docker-compose up
\ No newline at end of file
diff --git a/README.md b/README-dashboard.md
similarity index 100%
copy from README.md
copy to README-dashboard.md
diff --git a/README.md b/README.md
index 74f8748..8b13b91 100644
--- a/README.md
+++ b/README.md
@@ -1,47 +1,12 @@
-# READMD for Dashboard
+# Apache APISIX Dashboard
-This project is initialized with [Ant Design Pro](https://pro.ant.design). Follow is the quick guide for how to use.
+Dashboard for [Apache APISIX](https://github.com/apache/incubator-apisix-dashboard)
-## Environment Prepare
+## Deploy with Docker
-1. Make sure you have `Node.js` installed on your machine.
-2. Install [yarn](https://yarnpkg.com/).
-3. Install `node_modules`:
-
-```bash
-$ yarn
-```
-
-### Start project
-
-```bash
-yarn start:no-mock
-```
-
-### Build project
-
-```bash
-yarn build
-```
-
-### Check code style
-
-```bash
-yarn lint
-```
-
-You can also use script to auto fix some lint error:
-
-```bash
-yarn lint:fix
-```
-
-### Test code
-
-```bash
-yarn test
-```
+Please refer to [Deploy with Docker README](./compose/README.md)
## More
-You can view full document on our [official website](https://pro.ant.design). And welcome any feedback in our [github](https://github.com/ant-design/ant-design-pro).
+1. More infomation about the frontend Dashboard, please refer to [README for Dashboard](./README-dashboard.md)
+2. If you need the dashboard built with Vue.js, please refer to [master-vue](https://github.com/apache/incubator-apisix-dashboard/tree/master-vue).
diff --git a/api/Dockerfile b/api/Dockerfile
new file mode 100644
index 0000000..85092a1
--- /dev/null
+++ b/api/Dockerfile
@@ -0,0 +1,49 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+FROM golang:1.13.8 AS build-env
+
+WORKDIR /go/src/github.com/apisix/manager-api
+COPY . .
+RUN mkdir /root/manager-api \
+ && go env -w GOPROXY=https://goproxy.io,direct \
+ && export GOPROXY=https://goproxy.io \
+ && go build -o /root/manager-api/manager-api \
+ && mv /go/src/github.com/apisix/manager-api/build.sh /root/manager-api/ \
+ && mv /go/src/github.com/apisix/manager-api/conf.json /root/manager-api/ \
+ && rm -rf /go/src/github.com/apisix/manager-api \
+ && rm -rf /etc/localtime \
+ && ln -s /usr/share/zoneinfo/Hongkong /etc/localtime \
+ && dpkg-reconfigure -f noninteractive tzdata
+
+FROM alpine:3.11
+
+RUN mkdir /root/manager-api \
+ && apk update \
+ && apk add ca-certificates \
+ && update-ca-certificates \
+ && apk add --no-cache libc6-compat \
+ && echo "hosts: files dns" > /etc/nsswitch.conf \
+ && rm -rf /var/cache/apk/*
+
+
+WORKDIR /root/manager-api
+COPY --from=build-env /root/manager-api/* /root/manager-api/
+COPY --from=build-env /usr/share/zoneinfo/Hongkong /etc/localtime
+EXPOSE 8080
+RUN chmod +x ./build.sh
+CMD ["/root/manager-api/build.sh"]
diff --git a/api/README.md b/api/README.md
new file mode 100644
index 0000000..24befa6
--- /dev/null
+++ b/api/README.md
@@ -0,0 +1,22 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+# manager-api
+
+This is a back-end project that the dashboard depends on, implemented through golang.
diff --git a/api/build.sh b/api/build.sh
new file mode 100644
index 0000000..086fe73
--- /dev/null
+++ b/api/build.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+pwd=`pwd`
+
+sed -i -e "s%#mysqlAddress#%`echo $MYSQL_SERVER_ADDRESS`%g" ${pwd}/conf.json
+sed -i -e "s%#mysqlUser#%`echo $MYSQL_USER`%g" ${pwd}/conf.json
+sed -i -e "s%#mysqlPWD#%`echo $MYSQL_PASSWORD`%g" ${pwd}/conf.json
+sed -i -e "s%#syslogAddress#%`echo $SYSLOG_HOST`%g" ${pwd}/conf.json
+sed -i -e "s%#apisixBaseUrl#%`echo $APISIX_BASE_URL`%g" ${pwd}/conf.json
+sed -i -e "s%#apisixApiKey#%`echo $APISIX_API_KEY`%g" ${pwd}/conf.json
+
+cd /root/manager-api
+exec ./manager-api
+
diff --git a/api/conf.json b/api/conf.json
new file mode 100644
index 0000000..31cbcce
--- /dev/null
+++ b/api/conf.json
@@ -0,0 +1,19 @@
+{
+ "conf": {
+ "mysql":{
+ "address": "#mysqlAddress#",
+ "user": "#mysqlUser#",
+ "password": "#mysqlPWD#",
+ "maxConns": 50,
+ "maxIdleConns": 25,
+ "maxLifeTime": 10
+ },
+ "syslog": {
+ "host": "#syslogAddress#"
+ },
+ "apisix": {
+ "base_url": "#apisixBaseUrl#",
+ "api_key": "#apisixApiKey#"
+ }
+ }
+}
diff --git a/api/conf/conf.go b/api/conf/conf.go
new file mode 100644
index 0000000..5819789
--- /dev/null
+++ b/api/conf/conf.go
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package conf
+
+import (
+ "fmt"
+ "github.com/tidwall/gjson"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "runtime"
+)
+
+const ServerPort = 8080
+const PROD = "prod"
+const BETA = "beta"
+const DEV = "dev"
+const LOCAL = "local"
+const confPath = "/root/manager-api/conf.json"
+const RequestId = "requestId"
+
+var (
+ ENV string
+ basePath string
+ ApiKey = "edd1c9f034335f136f87ad84b625c8f1"
+ BaseUrl = "http://127.0.0.1:9080/apisix/admin"
+)
+
+func init() {
+ setEnvironment()
+ initMysql()
+ initApisix()
+}
+
+func setEnvironment() {
+ if env := os.Getenv("ENV"); env == "" {
+ ENV = LOCAL
+ } else {
+ ENV = env
+ }
+ _, basePath, _, _ = runtime.Caller(1)
+}
+
+func configurationPath() string {
+ if ENV == LOCAL {
+ return filepath.Join(filepath.Dir(basePath), "conf.json")
+ } else {
+ return confPath
+ }
+}
+
+type mysqlConfig struct {
+ Address string
+ User string
+ Password string
+
+ MaxConns int
+ MaxIdleConns int
+ MaxLifeTime int
+}
+
+var MysqlConfig mysqlConfig
+
+func initMysql() {
+ filePath := configurationPath()
+ if configurationContent, err := ioutil.ReadFile(filePath); err != nil {
+ panic(fmt.Sprintf("fail to read configuration: %s", filePath))
+ } else {
+ configuration := gjson.ParseBytes(configurationContent)
+ mysqlConf := configuration.Get("conf.mysql")
+ MysqlConfig.Address = mysqlConf.Get("address").String()
+ MysqlConfig.User = mysqlConf.Get("user").String()
+ MysqlConfig.Password = mysqlConf.Get("password").String()
+ MysqlConfig.MaxConns = int(mysqlConf.Get("maxConns").Int())
+ MysqlConfig.MaxIdleConns = int(mysqlConf.Get("maxIdleConns").Int())
+ MysqlConfig.MaxLifeTime = int(mysqlConf.Get("maxLifeTime").Int())
+ }
+}
+
+func initApisix() {
+ filePath := configurationPath()
+ if configurationContent, err := ioutil.ReadFile(filePath); err != nil {
+ panic(fmt.Sprintf("fail to read configuration: %s", filePath))
+ } else {
+ configuration := gjson.ParseBytes(configurationContent)
+ apisixConf := configuration.Get("conf.apisix")
+ BaseUrl = apisixConf.Get("base_url").String()
+ ApiKey = apisixConf.Get("api_key").String()
+ }
+}
diff --git a/api/conf/conf.json b/api/conf/conf.json
new file mode 100644
index 0000000..95fe86e
--- /dev/null
+++ b/api/conf/conf.json
@@ -0,0 +1,19 @@
+{
+ "conf":{
+ "mysql":{
+ "address": "127.0.0.1:3306",
+ "user": "root",
+ "password": "123456",
+ "maxConns": 50,
+ "maxIdleConns": 25,
+ "maxLifeTime": 10
+ },
+ "syslog":{
+ "host": "localhost"
+ },
+ "apisix":{
+ "base_url": "http://127.0.0.1:9080/apisix/admin",
+ "api_key": "edd1c9f034335f136f87ad84b625c8f1"
+ }
+ }
+}
\ No newline at end of file
diff --git a/api/conf/mysql.go b/api/conf/mysql.go
new file mode 100644
index 0000000..62e0f9e
--- /dev/null
+++ b/api/conf/mysql.go
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package conf
+
+import (
+ "fmt"
+ "github.com/jinzhu/gorm"
+ _ "github.com/jinzhu/gorm/dialects/mysql"
+ "time"
+)
+
+var db *gorm.DB
+
+func DB() *gorm.DB {
+ return db
+}
+
+// InitializeMysql creates mysql's *sqlDB instance
+func InitializeMysql() {
+ dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local", MysqlConfig.User,
+ MysqlConfig.Password, MysqlConfig.Address, "manager")
+ if tmp, err := gorm.Open("mysql", dsn); err != nil {
+ panic(fmt.Sprintf("fail to connect to DB: %s for %s", err.Error(), dsn))
+ } else {
+ db = tmp
+ db.LogMode(true)
+ db.DB().SetMaxOpenConns(MysqlConfig.MaxConns)
+ db.DB().SetMaxIdleConns(MysqlConfig.MaxIdleConns)
+ db.DB().SetConnMaxLifetime(time.Duration(MysqlConfig.MaxLifeTime) * time.Minute)
+ }
+
+}
diff --git a/api/docker-compose.yml b/api/docker-compose.yml
new file mode 100644
index 0000000..79760d7
--- /dev/null
+++ b/api/docker-compose.yml
@@ -0,0 +1,9 @@
+version: "3"
+
+services:
+ manager:
+ image: golang:1.13.8
+ volumes:
+ - .:/go/src/github.com/apisix/manager-api
+ working_dir: /go/src/github.com/apisix/manager-api
+ command: go test -v github.com/apisix/manager-api/service
diff --git a/api/errno/error.go b/api/errno/error.go
new file mode 100644
index 0000000..20b7e24
--- /dev/null
+++ b/api/errno/error.go
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package errno
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+type Message struct {
+ Code string
+ Msg string
+}
+
+var (
+ //AA 01 api-manager-api
+ SystemSuccess = Message{"010000", "success"}
+ SystemError = Message{"010001", "system error"}
+ BadRequestError = Message{Code: "010002", Msg: "Request format error"}
+ NotFoundError = Message{Code: "010003", Msg: "No resources found"}
+
+ //BB 01 config module
+ ConfEnvError = Message{"010101", "Environment variable not found: %s"}
+ ConfFilePathError = Message{"010102", "Error loading configuration file: %s"}
+
+ // BB 02 route module
+ RouteRequestError = Message{"010201", "Route request parameters are abnormal: %s"}
+ ApisixRouteCreateError = Message{"010202", "Failed to create APISIX route: %s"}
+ DBRouteCreateError = Message{"010203", "Route storage failure: %s"}
+ ApisixRouteUpdateError = Message{"010204", "Update APISIX routing failed: %s"}
+ ApisixRouteDeleteError = Message{"010205", "Failed to remove APISIX route: %s"}
+ DBRouteUpdateError = Message{"010206", "Route update failed: %s"}
+ DBRouteDeleteError = Message{"010207", "Route remove failed: %s"}
+
+ // 03 plugin module
+ ApisixPluginListError = Message{"010301", "List APISIX plugins failed: %s"}
+ ApisixPluginSchemaError = Message{"010301", "Find APISIX plugin schema failed: %s"}
+)
+
+type ManagerError struct {
+ TraceId string
+ Code string
+ Msg string
+ Data interface{}
+}
+
+// toString
+func (e *ManagerError) Error() string {
+ return e.Msg
+}
+
+func FromMessage(m Message, args ...interface{}) *ManagerError {
+ return &ManagerError{TraceId: "", Code: m.Code, Msg: fmt.Sprintf(m.Msg, args...)}
+}
+
+func (e *ManagerError) Response() map[string]interface{} {
+ return map[string]interface{}{
+ "code": e.Code,
+ "msg": e.Msg,
+ }
+}
+
+func (e *ManagerError) ItemResponse(data interface{}) map[string]interface{} {
+ return map[string]interface{}{
+ "code": e.Code,
+ "msg": e.Msg,
+ "data": data,
+ }
+}
+
+func (e *ManagerError) ListResponse(count, list interface{}) map[string]interface{} {
+ return map[string]interface{}{
+ "code": e.Code,
+ "msg": e.Msg,
+ "count": count,
+ "list": list,
+ }
+}
+
+func Success() []byte {
+ w := FromMessage(SystemSuccess).Response()
+ result, _ := json.Marshal(w)
+ return result
+}
diff --git a/api/filter/cors.go b/api/filter/cors.go
new file mode 100644
index 0000000..f2c7ac1
--- /dev/null
+++ b/api/filter/cors.go
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package filter
+
+import "github.com/gin-gonic/gin"
+
+func CORS() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
+ c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
+ c.Writer.Header().Set("Access-Control-Allow-Headers", "*")
+ c.Writer.Header().Set("Access-Control-Allow-Methods", "*")
+ if c.Request.Method == "OPTIONS" {
+ c.AbortWithStatus(204)
+ return
+ }
+ c.Next()
+ }
+}
diff --git a/api/filter/logging.go b/api/filter/logging.go
new file mode 100644
index 0000000..22f5528
--- /dev/null
+++ b/api/filter/logging.go
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package filter
+
+import (
+ "bytes"
+ "github.com/apisix/manager-api/errno"
+ "github.com/gin-gonic/gin"
+ "github.com/sirupsen/logrus"
+ "time"
+)
+
+func RequestLogHandler() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ start, host, remoteIP, path, method := time.Now(), c.Request.Host, c.ClientIP(), c.Request.URL.Path, c.Request.Method
+ var val interface{}
+ if method == "GET" {
+ val = c.Request.URL.Query()
+ } else {
+ val, _ = c.GetRawData()
+ }
+ c.Set("requestBody", val)
+ uuid, _ := c.Get("X-Request-Id")
+
+ param, _ := c.Get("requestBody")
+ switch param.(type) {
+ case []byte:
+ param = string(param.([]byte))
+ default:
+ }
+
+ blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
+ c.Writer = blw
+ c.Next()
+ latency := time.Now().Sub(start) / 1000000
+ statusCode := c.Writer.Status()
+ respBody := blw.body.String()
+ if uuid == "" {
+ uuid = c.Writer.Header().Get("X-Request-Id")
+ }
+ var errs []string
+ for _, err := range c.Errors {
+ if e, ok := err.Err.(*errno.ManagerError); ok {
+ errs = append(errs, e.Error())
+ }
+ }
+ logger.WithFields(logrus.Fields{
+ "requestId": uuid,
+ "latency": latency,
+ "remoteIp": remoteIP,
+ "method": method,
+ "path": path,
+ "statusCode": statusCode,
+ "host": host,
+ "params": param,
+ "respBody": respBody,
+ "errMsg": errs,
+ }).Info("")
+ }
+}
+
+type bodyLogWriter struct {
+ gin.ResponseWriter
+ body *bytes.Buffer
+}
+
+func (w bodyLogWriter) Write(b []byte) (int, error) {
+ w.body.Write(b)
+ return w.ResponseWriter.Write(b)
+}
diff --git a/api/filter/recover.go b/api/filter/recover.go
new file mode 100644
index 0000000..433331f
--- /dev/null
+++ b/api/filter/recover.go
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package filter
+
+import (
+ "bytes"
+ "fmt"
+ "github.com/apisix/manager-api/log"
+ "github.com/gin-gonic/gin"
+ "github.com/sirupsen/logrus"
+ "io/ioutil"
+ "net/http"
+ "runtime"
+ "time"
+)
+
+var (
+ logger = log.GetLogger()
+ dunno = []byte("???")
+ centerDot = []byte("·")
+ dot = []byte(".")
+ slash = []byte("/")
+)
+
+func RecoverHandler() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ defer func() {
+ if err := recover(); err != nil {
+ uuid := c.Writer.Header().Get("X-Request-Id")
+ logger.WithFields(logrus.Fields{
+ "uuid": uuid,
+ })
+ stack := stack(3)
+ logger.Errorf("[Recovery] %s panic recovered:\n\n%s\n%s", timeFormat(time.Now()), err, stack)
+ c.AbortWithStatus(http.StatusInternalServerError)
+ }
+ }()
+ c.Next()
+ }
+}
+
+func WrapGo(f func(...interface{}), args ...interface{}) {
+ defer func() {
+ if err := recover(); err != nil {
+ stack := stack(3)
+ logger.Errorf("[Recovery] %s panic recovered:\n\n%s\n%s", timeFormat(time.Now()), err, stack)
+ }
+ }()
+ f(args...)
+}
+
+func stack(skip int) []byte {
+ buf := new(bytes.Buffer) // the returned data
+ // loaded file.
+ var lines [][]byte
+ var lastFile string
+ for i := skip; ; i++ {
+ pc, file, line, ok := runtime.Caller(i)
+ if !ok {
+ break
+ }
+ // Print this much at least. If we can't find the source, it won't show.
+ fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
+ if file != lastFile {
+ data, err := ioutil.ReadFile(file)
+ if err != nil {
+ continue
+ }
+ lines = bytes.Split(data, []byte{'\n'})
+ lastFile = file
+ }
+ fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
+ }
+ return buf.Bytes()
+}
+
+// source returns a space-trimmed slice of the n'th line.
+func source(lines [][]byte, n int) []byte {
+ n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
+ if n < 0 || n >= len(lines) {
+ return dunno
+ }
+ return bytes.TrimSpace(lines[n])
+}
+
+// function returns, if possible, the name of the function containing the PC.
+func function(pc uintptr) []byte {
+ fn := runtime.FuncForPC(pc)
+ if fn == nil {
+ return dunno
+ }
+ name := []byte(fn.Name())
+ if lastslash := bytes.LastIndex(name, slash); lastslash >= 0 {
+ name = name[lastslash+1:]
+ }
+ if period := bytes.Index(name, dot); period >= 0 {
+ name = name[period+1:]
+ }
+ name = bytes.Replace(name, centerDot, dot, -1)
+ return name
+}
+
+func timeFormat(t time.Time) string {
+ var timeString = t.Format("2006/01/02 - 15:04:05")
+ return timeString
+}
diff --git a/api/filter/request_id.go b/api/filter/request_id.go
new file mode 100644
index 0000000..2992869
--- /dev/null
+++ b/api/filter/request_id.go
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package filter
+
+import (
+ "github.com/gin-gonic/gin"
+ "github.com/satori/go.uuid"
+)
+
+func RequestId() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ // Check for incoming header, use it if exists
+ requestId := c.Request.Header.Get("X-Request-Id")
+
+ // Create request id with UUID4
+ if requestId == "" {
+ u4 := uuid.NewV4()
+ requestId = u4.String()
+ }
+
+ // Expose it for use in the application
+ c.Set("X-Request-Id", requestId)
+
+ // Set X-Request-Id header
+ c.Writer.Header().Set("X-Request-Id", requestId)
+ c.Next()
+ }
+}
diff --git a/api/go.mod b/api/go.mod
new file mode 100644
index 0000000..45372ec
--- /dev/null
+++ b/api/go.mod
@@ -0,0 +1,15 @@
+module github.com/apisix/manager-api
+
+go 1.13
+
+require (
+ github.com/gin-contrib/pprof v1.3.0
+ github.com/gin-gonic/gin v1.6.3
+ github.com/go-sql-driver/mysql v1.5.0
+ github.com/jinzhu/gorm v1.9.12
+ github.com/satori/go.uuid v1.2.0
+ github.com/sirupsen/logrus v1.6.0
+ github.com/stretchr/testify v1.4.0
+ github.com/tidwall/gjson v1.6.0
+ gopkg.in/resty.v1 v1.12.0
+)
diff --git a/api/go.sum b/api/go.sum
new file mode 100644
index 0000000..9e3e3e3
--- /dev/null
+++ b/api/go.sum
@@ -0,0 +1,89 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
+github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
+github.com/gin-contrib/pprof v1.3.0 h1:G9eK6HnbkSqDZBYbzG4wrjCsA4e+cvYAHUZw6W+W9K0=
+github.com/gin-contrib/pprof v1.3.0/go.mod h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwfBaM5OXaB0=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
+github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
+github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
+github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
+github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
+github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
+github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q=
+github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
+github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc=
+github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
+github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
+github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
+github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
+github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
+github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
+github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
+github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/api/log/log.go b/api/log/log.go
new file mode 100644
index 0000000..c1645e8
--- /dev/null
+++ b/api/log/log.go
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package log
+
+import (
+ "bufio"
+ "fmt"
+ "github.com/apisix/manager-api/conf"
+ "github.com/sirupsen/logrus"
+ "os"
+ "runtime"
+ "strings"
+)
+
+var logEntry *logrus.Entry
+
+func GetLogger() *logrus.Entry {
+ if logEntry == nil {
+ var log = logrus.New()
+ setNull(log)
+ log.SetLevel(logrus.DebugLevel)
+ if conf.ENV != conf.LOCAL {
+ log.SetLevel(logrus.ErrorLevel)
+ }
+ log.SetFormatter(&logrus.JSONFormatter{})
+ logEntry = log.WithFields(logrus.Fields{
+ "app": "manager-api",
+ })
+ if hook, err := createHook(); err == nil {
+ log.AddHook(hook)
+ }
+ }
+ return logEntry
+}
+
+func setNull(log *logrus.Logger) {
+ src, err := os.OpenFile(os.DevNull, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
+ if err != nil {
+ fmt.Println("err", err)
+ }
+ writer := bufio.NewWriter(src)
+ log.SetOutput(writer)
+}
+
+type Hook struct {
+ Formatter func(file, function string, line int) string
+}
+
+func createHook() (*Hook, error) {
+ return &Hook{
+ func(file, function string, line int) string {
+ return fmt.Sprintf("%s:%d", file, line)
+ },
+ }, nil
+}
+
+func (hook *Hook) Fire(entry *logrus.Entry) error {
+ str := hook.Formatter(findCaller(5))
+ en := entry.WithField("line", str)
+ en.Level = entry.Level
+ en.Message = entry.Message
+ en.Time = entry.Time
+ line, err := en.String()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Unable to read entry, %v", err)
+ return err
+ }
+ switch en.Level {
+ case logrus.PanicLevel:
+ fmt.Print(line)
+ return nil
+ case logrus.FatalLevel:
+ fmt.Print(line)
+ return nil
+ case logrus.ErrorLevel:
+ fmt.Print(line)
+ return nil
+ case logrus.WarnLevel:
+ fmt.Print(line)
+ return nil
+ case logrus.InfoLevel:
+ fmt.Print(line)
+ return nil
+ case logrus.DebugLevel:
+ fmt.Print(line)
+ return nil
+ default:
+ return nil
+ }
+}
+
+func (hook *Hook) Levels() []logrus.Level {
+ return logrus.AllLevels
+}
+
+func findCaller(skip int) (string, string, int) {
+ var (
+ pc uintptr
+ file string
+ function string
+ line int
+ )
+ for i := 0; i < 10; i++ {
+ pc, file, line = getCaller(skip + i)
+ if !strings.HasPrefix(file, "logrus") {
+ break
+ }
+ }
+ if pc != 0 {
+ frames := runtime.CallersFrames([]uintptr{pc})
+ frame, _ := frames.Next()
+ function = frame.Function
+ }
+ return file, function, line
+}
+
+func getCaller(skip int) (uintptr, string, int) {
+ pc, file, line, ok := runtime.Caller(skip)
+ if !ok {
+ return 0, "", 0
+ }
+ n := 0
+ for i := len(file) - 1; i > 0; i-- {
+ if file[i] == '/' {
+ n += 1
+ if n >= 2 {
+ file = file[i+1:]
+ break
+ }
+ }
+ }
+ return pc, file, line
+}
diff --git a/api/main.go b/api/main.go
new file mode 100644
index 0000000..3d756dd
--- /dev/null
+++ b/api/main.go
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package main
+
+import (
+ "fmt"
+ "github.com/apisix/manager-api/conf"
+ "github.com/apisix/manager-api/filter"
+ "github.com/apisix/manager-api/log"
+ "github.com/apisix/manager-api/route"
+ "github.com/gin-contrib/pprof"
+ "github.com/gin-gonic/gin"
+ "net/http"
+ "time"
+)
+
+var logger = log.GetLogger()
+
+func setUpRouter() *gin.Engine {
+ if conf.ENV != conf.LOCAL && conf.ENV != conf.BETA {
+ gin.SetMode(gin.DebugMode)
+ } else {
+ gin.SetMode(gin.ReleaseMode)
+ }
+ r := gin.New()
+
+ r.Use(filter.CORS(), filter.RequestId(), filter.RequestLogHandler(), filter.RecoverHandler())
+ route.AppendHealthCheck(r)
+ route.AppendRoute(r)
+ route.AppendSsl(r)
+ route.AppendPlugin(r)
+
+ pprof.Register(r)
+
+ return r
+}
+
+func main() {
+ // init
+ conf.InitializeMysql()
+ // routes
+ r := setUpRouter()
+ addr := fmt.Sprintf(":%d", conf.ServerPort)
+ s := &http.Server{
+ Addr: addr,
+ Handler: r,
+ ReadTimeout: time.Duration(1000) * time.Millisecond,
+ WriteTimeout: time.Duration(5000) * time.Millisecond,
+ }
+ s.ListenAndServe()
+}
diff --git a/api/route/healthz.go b/api/route/healthz.go
new file mode 100644
index 0000000..97dcd04
--- /dev/null
+++ b/api/route/healthz.go
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package route
+
+import (
+ "github.com/apisix/manager-api/log"
+ "github.com/gin-gonic/gin"
+ "net/http"
+)
+
+var logger = log.GetLogger()
+
+func healthzHandler() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ c.Copy()
+ c.String(http.StatusOK, "pong")
+ }
+}
+
+func AppendHealthCheck(r *gin.Engine) *gin.Engine {
+ r.GET("/ping", healthzHandler())
+ return r
+}
diff --git a/api/route/plugin.go b/api/route/plugin.go
new file mode 100644
index 0000000..0032b13
--- /dev/null
+++ b/api/route/plugin.go
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package route
+
+import (
+ "encoding/json"
+ "github.com/apisix/manager-api/errno"
+ "github.com/apisix/manager-api/service"
+ "github.com/gin-gonic/gin"
+ "net/http"
+)
+
+func AppendPlugin(r *gin.Engine) *gin.Engine {
+ r.GET("/apisix/admin/plugins", listPlugin)
+ r.GET("/apisix/admin/schema/plugins/:name", findSchema)
+ return r
+}
+
+func findSchema(c *gin.Context) {
+ name := c.Param("name")
+ request := &service.ApisixPluginRequest{Name: name}
+ if result, err := request.Schema(); err != nil {
+ e := errno.FromMessage(errno.ApisixPluginSchemaError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ return
+ } else {
+ resp, _ := json.Marshal(result)
+ c.Data(http.StatusOK, service.ContentType, resp)
+ }
+}
+
+func listPlugin(c *gin.Context) {
+ request := &service.ApisixPluginRequest{}
+ if result, err := request.List(); err != nil {
+ e := errno.FromMessage(errno.ApisixPluginListError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ return
+ } else {
+ resp, _ := json.Marshal(result)
+ c.Data(http.StatusOK, service.ContentType, resp)
+ }
+}
diff --git a/api/route/route.go b/api/route/route.go
new file mode 100644
index 0000000..efc95d2
--- /dev/null
+++ b/api/route/route.go
@@ -0,0 +1,242 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package route
+
+import (
+ "encoding/json"
+ "github.com/apisix/manager-api/conf"
+ "github.com/apisix/manager-api/errno"
+ "github.com/apisix/manager-api/service"
+ "github.com/gin-gonic/gin"
+ "github.com/satori/go.uuid"
+ "net/http"
+ "strconv"
+)
+
+func AppendRoute(r *gin.Engine) *gin.Engine {
+ r.POST("/apisix/admin/routes", createRoute)
+ r.GET("/apisix/admin/routes/:rid", findRoute)
+ r.GET("/apisix/admin/routes", listRoute)
+ r.PUT("/apisix/admin/routes/:rid", updateRoute)
+ r.DELETE("/apisix/admin/routes/:rid", deleteRoute)
+ return r
+}
+
+func listRoute(c *gin.Context) {
+ db := conf.DB()
+ size, _ := strconv.Atoi(c.Query("size"))
+ page, _ := strconv.Atoi(c.Query("page"))
+ if size == 0 {
+ size = 10
+ }
+ isSearch := true
+ if name, exist := c.GetQuery("name"); exist {
+ db = db.Where("name like ? ", "%"+name+"%")
+ isSearch = false
+ }
+ if description, exist := c.GetQuery("description"); exist {
+ db = db.Where("description like ? ", "%"+description+"%")
+ isSearch = false
+ }
+ if host, exist := c.GetQuery("host"); exist {
+ db = db.Where("hosts like ? ", "%"+host+"%")
+ isSearch = false
+ }
+ if uri, exist := c.GetQuery("uri"); exist {
+ db = db.Where("uris like ? ", "%"+uri+"%")
+ isSearch = false
+ }
+ if ip, exist := c.GetQuery("ip"); exist {
+ db = db.Where("upstream_nodes like ? ", "%"+ip+"%")
+ isSearch = false
+ }
+ // search
+ if isSearch {
+ if search, exist := c.GetQuery("search"); exist {
+ db = db.Where("name like ? ", "%"+search+"%").
+ Or("description like ? ", "%"+search+"%").
+ Or("hosts like ? ", "%"+search+"%").
+ Or("uris like ? ", "%"+search+"%").
+ Or("upstream_nodes like ? ", "%"+search+"%")
+ }
+ }
+ // todo params check
+ // mysql
+ routeList := []service.Route{}
+ var count int
+ if err := db.Order("priority, update_time desc").Table("routes").Offset((page - 1) * size).Limit(size).Find(&routeList).Count(&count).Error; err != nil {
+ e := errno.FromMessage(errno.RouteRequestError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ return
+ } else {
+ responseList := make([]service.RouteResponse, 0)
+ for _, r := range routeList {
+ response := &service.RouteResponse{}
+ response.Parse(&r)
+ responseList = append(responseList, *response)
+ }
+ result := &service.ListResponse{Count: count, Data: responseList}
+ resp, _ := json.Marshal(result)
+ c.Data(http.StatusOK, service.ContentType, resp)
+ }
+}
+
+func deleteRoute(c *gin.Context) {
+ rid := c.Param("rid")
+ // todo params check
+ // delete from apisix
+ request := &service.ApisixRouteRequest{}
+ if _, err := request.Delete(rid); err != nil {
+ e := errno.FromMessage(errno.ApisixRouteDeleteError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+ return
+ } else {
+ // delete from mysql
+ rd := &service.Route{}
+ rd.ID = uuid.FromStringOrNil(rid)
+ if err := conf.DB().Delete(rd).Error; err != nil {
+ e := errno.FromMessage(errno.DBRouteDeleteError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+ return
+ }
+ }
+ c.Data(http.StatusOK, service.ContentType, errno.Success())
+}
+func updateRoute(c *gin.Context) {
+ rid := c.Param("rid")
+ // todo params check
+ param, exist := c.Get("requestBody")
+ if !exist || len(param.([]byte)) < 1 {
+ e := errno.FromMessage(errno.RouteRequestError, "route create with no post data")
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ return
+ }
+ routeRequest := &service.RouteRequest{}
+ if err := routeRequest.Parse(param); err != nil {
+ e := errno.FromMessage(errno.RouteRequestError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ return
+ }
+ logger.Info(routeRequest.Plugins)
+
+ arr := service.ToApisixRequest(routeRequest)
+ logger.Info(arr)
+ if resp, err := arr.Update(rid); err != nil {
+ e := errno.FromMessage(errno.ApisixRouteUpdateError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+ return
+ } else {
+ // update mysql
+ if rd, err := service.ToRoute(routeRequest, arr, uuid.FromStringOrNil(rid), resp); err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, err.Response())
+ return
+ } else {
+ if err := conf.DB().Model(&service.Route{}).Update(rd).Error; err != nil {
+ e := errno.FromMessage(errno.DBRouteUpdateError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+ return
+ }
+ logger.Info(rd)
+ }
+ }
+ c.Data(http.StatusOK, service.ContentType, errno.Success())
+}
+
+func findRoute(c *gin.Context) {
+ rid := c.Param("rid")
+ // todo params check
+ // find from apisix
+ request := &service.ApisixRouteRequest{}
+ if response, err := request.FindById(rid); err != nil {
+ e := errno.FromMessage(errno.RouteRequestError, err.Error()+" route ID: "+rid)
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ return
+ } else {
+ // transfer response to dashboard struct
+ if result, err := response.Parse(); err != nil {
+ e := errno.FromMessage(errno.RouteRequestError, err.Error()+" route ID: "+rid)
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ return
+ } else {
+ // need to find name from mysql temporary
+ route := &service.Route{}
+ if err := conf.DB().Table("routes").Where("id=?", rid).First(&route).Error; err != nil {
+ e := errno.FromMessage(errno.RouteRequestError, err.Error()+" route ID: "+rid)
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ return
+ }
+ result.Name = route.Name
+ resp, _ := json.Marshal(result)
+ c.Data(http.StatusOK, service.ContentType, resp)
+ }
+ }
+}
+
+func createRoute(c *gin.Context) {
+ u4 := uuid.NewV4()
+ rid := u4.String()
+ // todo params check
+ param, exist := c.Get("requestBody")
+ if !exist || len(param.([]byte)) < 1 {
+ e := errno.FromMessage(errno.RouteRequestError, "route create with no post data")
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ return
+ }
+ routeRequest := &service.RouteRequest{}
+ if err := routeRequest.Parse(param); err != nil {
+ e := errno.FromMessage(errno.RouteRequestError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ return
+ }
+ logger.Info(routeRequest.Plugins)
+
+ arr := service.ToApisixRequest(routeRequest)
+ logger.Info(arr)
+ if resp, err := arr.Create(rid); err != nil {
+ e := errno.FromMessage(errno.ApisixRouteCreateError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+ return
+ } else {
+ // update mysql
+ if rd, err := service.ToRoute(routeRequest, arr, u4, resp); err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, err.Response())
+ return
+ } else {
+ logger.Info(rd)
+ if err := conf.DB().Create(rd).Error; err != nil {
+ e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+ return
+ }
+ }
+ }
+ c.Data(http.StatusOK, service.ContentType, errno.Success())
+}
diff --git a/api/route/ssl.go b/api/route/ssl.go
new file mode 100644
index 0000000..2a9d6f7
--- /dev/null
+++ b/api/route/ssl.go
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package route
+
+import (
+ "net/http"
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+ "github.com/satori/go.uuid"
+
+ "github.com/apisix/manager-api/errno"
+ "github.com/apisix/manager-api/service"
+)
+
+const contentType = "application/json"
+
+func AppendSsl(r *gin.Engine) *gin.Engine {
+ r.POST("/apisix/admin/check_ssl_cert", sslCheck)
+ r.GET("/apisix/admin/ssls", sslList)
+ r.POST("/apisix/admin/ssls", sslCreate)
+ r.GET("/apisix/admin/ssls/:id", sslItem)
+ r.PUT("/apisix/admin/ssls/:id", sslUpdate)
+ r.DELETE("/apisix/admin/ssls/:id", sslDelete)
+ return r
+}
+
+func sslList(c *gin.Context) {
+ size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
+ page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
+ // todo params check
+ resp, err := service.SslList(page, size)
+
+ if err != nil {
+ e := errno.FromMessage(errno.RouteRequestError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ return
+ }
+
+ c.Data(http.StatusOK, service.ContentType, resp)
+}
+
+func sslItem(c *gin.Context) {
+ id := c.Param("id")
+
+ // todo params check
+ resp, err := service.SslItem(id)
+
+ if err != nil {
+ e := errno.FromMessage(errno.RouteRequestError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ return
+ }
+
+ c.Data(http.StatusOK, service.ContentType, resp)
+}
+
+func sslCheck(c *gin.Context) {
+ // todo params check
+ param, exist := c.Get("requestBody")
+
+ if !exist || len(param.([]byte)) < 1 {
+ e := errno.FromMessage(errno.RouteRequestError, "route create with no post data")
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ return
+ }
+
+ resp, err := service.SslCheck(param)
+ if err != nil {
+ e := errno.FromMessage(errno.RouteRequestError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ return
+ }
+
+ c.Data(http.StatusOK, contentType, resp)
+}
+
+func sslCreate(c *gin.Context) {
+ // todo params check
+ param, exist := c.Get("requestBody")
+
+ u4 := uuid.NewV4()
+
+ if !exist || len(param.([]byte)) < 1 {
+ e := errno.FromMessage(errno.RouteRequestError, "route create with no post data")
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ return
+ }
+
+ if err := service.SslCreate(param, u4.String()); err != nil {
+ e := errno.FromMessage(errno.ApisixRouteCreateError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+ return
+ }
+
+ c.Data(http.StatusOK, contentType, errno.Success())
+}
+
+func sslUpdate(c *gin.Context) {
+ // todo params check
+ param, exist := c.Get("requestBody")
+
+ id := c.Param("id")
+
+ if !exist || len(param.([]byte)) < 1 {
+ e := errno.FromMessage(errno.RouteRequestError, "route create with no post data")
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ return
+ }
+
+ if err := service.SslUpdate(param, id); err != nil {
+ e := errno.FromMessage(errno.ApisixRouteCreateError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+ return
+ }
+
+ c.Data(http.StatusOK, contentType, errno.Success())
+}
+
+func sslDelete(c *gin.Context) {
+ id := c.Param("id")
+ // todo params check
+ if err := service.SslDelete(id); err != nil {
+ e := errno.FromMessage(errno.RouteRequestError, err.Error())
+ logger.Error(e.Msg)
+ c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ return
+ }
+
+ c.Data(http.StatusOK, service.ContentType, errno.Success())
+}
diff --git a/api/script/db/schema.sql b/api/script/db/schema.sql
new file mode 100644
index 0000000..69f088c
--- /dev/null
+++ b/api/script/db/schema.sql
@@ -0,0 +1,31 @@
+-- this is a db script for init
+CREATE DATABASE `manager`;
+use `manager`;
+CREATE TABLE `routes` (
+ `id` varchar(64) NOT NULL unique,
+ `name` varchar(200) NOT NULL unique, -- not support yet
+ `description` varchar(200) DEFAULT NULL,
+ `hosts` text,
+ `uris` text,
+ `upstream_nodes` text,
+ `upstream_id` varchar(32) , -- fk
+ `priority` int NOT NULL DEFAULT 0,
+ `state` int NOT NULL DEFAULT 1, -- 1-normal 0-disable
+ `content` text,
+ `content_admin_api` text,
+ `create_time` bigint(20),
+ `update_time` bigint(20),
+
+ PRIMARY KEY (`id`)
+) DEFAULT CHARSET=utf8;
+CREATE TABLE `ssls` (
+ `id` char(36) NOT NULL DEFAULT '',
+ `public_key` text NOT NULL,
+ `snis` text NOT NULL,
+ `validity_start` bigint(20) unsigned NOT NULL,
+ `validity_end` bigint(20) unsigned NOT NULL,
+ `status` tinyint(1) unsigned NOT NULL DEFAULT '1',
+ `create_time` bigint(20) unsigned NOT NULL,
+ `update_time` bigint(20) unsigned NOT NULL,
+ PRIMARY KEY (`id`)
+) DEFAULT CHARSET=utf8;
\ No newline at end of file
diff --git a/api/service/base.go b/api/service/base.go
new file mode 100644
index 0000000..6ee57b5
--- /dev/null
+++ b/api/service/base.go
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package service
+
+import (
+ "github.com/jinzhu/gorm"
+ "github.com/satori/go.uuid"
+ "time"
+)
+
+// Base contains common columns for all tables.
+type Base struct {
+ ID uuid.UUID `json:"id",sql:"type:uuid;primary_key;"`
+ CreateTime int64 `json:"create_time"`
+ UpdateTime int64 `json:"update_time"`
+}
+
+// BeforeCreate will set a UUID rather than numeric ID.
+func (base *Base) BeforeCreate(scope *gorm.Scope) error {
+ timestamp := time.Now().Unix()
+ err := scope.SetColumn("UpdateTime", timestamp)
+ err = scope.SetColumn("CreateTime", timestamp)
+ if len(base.ID) == 0 {
+ uuid := uuid.NewV4()
+ err = scope.SetColumn("ID", uuid)
+ return err
+ }
+ return err
+}
+
+func (base *Base) BeforeSave(scope *gorm.Scope) error {
+ err := scope.SetColumn("UpdateTime", time.Now().Unix())
+ return err
+}
diff --git a/api/service/plugin.go b/api/service/plugin.go
new file mode 100644
index 0000000..92cd6c3
--- /dev/null
+++ b/api/service/plugin.go
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package service
+
+import (
+ "encoding/json"
+ "fmt"
+ "github.com/apisix/manager-api/conf"
+ "github.com/apisix/manager-api/utils"
+)
+
+type ApisixPluginRequest struct {
+ Name string `json:"name"`
+}
+
+func (apr *ApisixPluginRequest) Schema() (map[string]interface{}, error) {
+ url := fmt.Sprintf("%s/schema/plugins/%s", conf.BaseUrl, apr.Name)
+ if resp, err := utils.Get(url); err != nil {
+ logger.Error(err.Error())
+ return nil, err
+ } else {
+ arresp := make(map[string]interface{})
+ if err := json.Unmarshal(resp, &arresp); err != nil {
+ logger.Error(err.Error())
+ return nil, err
+ } else {
+ return arresp, nil
+ }
+ }
+}
+
+func (apr *ApisixPluginRequest) List() ([]string, error) {
+
+ url := fmt.Sprintf("%s/plugins/list", conf.BaseUrl)
+ if resp, err := utils.Get(url); err != nil {
+ logger.Error(err.Error())
+ return nil, err
+ } else {
+ var arresp []string
+ if err := json.Unmarshal(resp, &arresp); err != nil {
+ logger.Error(err.Error())
+ return nil, err
+ } else {
+ return arresp, nil
+ }
+ }
+}
diff --git a/api/service/route.go b/api/service/route.go
new file mode 100644
index 0000000..ea8033d
--- /dev/null
+++ b/api/service/route.go
@@ -0,0 +1,561 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package service
+
+import (
+ "encoding/json"
+ "fmt"
+ "github.com/apisix/manager-api/conf"
+ "github.com/apisix/manager-api/errno"
+ "github.com/apisix/manager-api/log"
+ "github.com/apisix/manager-api/utils"
+ "github.com/satori/go.uuid"
+ "time"
+)
+
+const (
+ ContentType = "application/json"
+ HTTP = "http"
+ HTTPS = "https"
+ SCHEME = "scheme"
+ WEBSOCKET = "websocket"
+ REDIRECT = "redirect"
+ PROXY_REWRIETE = "proxy-rewrite"
+ UPATHTYPE_STATIC = "static"
+ UPATHTYPE_REGX = "regx"
+ UPATHTYPE_KEEP = "keep"
+)
+
+var logger = log.GetLogger()
+
+func (r *RouteRequest) Parse(body interface{}) error {
+ if err := json.Unmarshal(body.([]byte), r); err != nil {
+ r = nil
+ return err
+ } else {
+ if r.Uris == nil || len(r.Uris) < 1 {
+ r.Uris = []string{"/*"}
+ }
+ }
+ return nil
+}
+
+func (arr *ApisixRouteRequest) Parse(r *RouteRequest) {
+ arr.Desc = r.Desc
+ arr.Priority = r.Priority
+ arr.Methods = r.Methods
+ arr.Uris = r.Uris
+ arr.Hosts = r.Hosts
+ arr.Vars = r.Vars
+ arr.Upstream = r.Upstream
+ arr.Plugins = r.Plugins
+}
+
+func (rd *Route) Parse(r *RouteRequest, arr *ApisixRouteRequest) error {
+ //rd.Name = arr.Name
+ rd.Description = arr.Desc
+ // todo transfer
+ rd.Hosts = ""
+ rd.Uris = ""
+ rd.UpstreamNodes = ""
+ rd.UpstreamId = ""
+ if content, err := json.Marshal(r); err != nil {
+ return err
+ } else {
+ rd.Content = string(content)
+ }
+ timestamp := time.Now().Unix()
+ rd.CreateTime = timestamp
+ return nil
+}
+
+func (arr *ApisixRouteRequest) FindById(rid string) (*ApisixRouteResponse, error) {
+ url := fmt.Sprintf("%s/routes/%s", conf.BaseUrl, rid)
+ if resp, err := utils.Get(url); err != nil {
+ logger.Error(err.Error())
+ return nil, err
+ } else {
+ var arresp ApisixRouteResponse
+ if err := json.Unmarshal(resp, &arresp); err != nil {
+ logger.Error(err.Error())
+ return nil, err
+ } else {
+ return &arresp, nil
+ }
+ }
+}
+
+func (arr *ApisixRouteRequest) Update(rid string) (*ApisixRouteResponse, error) {
+ url := fmt.Sprintf("%s/routes/%s", conf.BaseUrl, rid)
+ if b, err := json.Marshal(arr); err != nil {
+ return nil, err
+ } else {
+ fmt.Println(string(b))
+ if resp, err := utils.Patch(url, b); err != nil {
+ logger.Error(err.Error())
+ return nil, err
+ } else {
+ var arresp ApisixRouteResponse
+ if err := json.Unmarshal(resp, &arresp); err != nil {
+ logger.Error(err.Error())
+ return nil, err
+ } else {
+ return &arresp, nil
+ }
+ }
+ }
+}
+
+func (arr *ApisixRouteRequest) Create(rid string) (*ApisixRouteResponse, error) {
+ url := fmt.Sprintf("%s/routes/%s", conf.BaseUrl, rid)
+ if b, err := json.Marshal(arr); err != nil {
+ return nil, err
+ } else {
+ fmt.Println(string(b))
+ if resp, err := utils.Put(url, b); err != nil {
+ logger.Error(err.Error())
+ return nil, err
+ } else {
+ var arresp ApisixRouteResponse
+ if err := json.Unmarshal(resp, &arresp); err != nil {
+ logger.Error(err.Error())
+ return nil, err
+ } else {
+ return &arresp, nil
+ }
+ }
+ }
+}
+
+func (arr *ApisixRouteRequest) Delete(rid string) (*ApisixRouteResponse, error) {
+ url := fmt.Sprintf("%s/routes/%s", conf.BaseUrl, rid)
+ if resp, err := utils.Delete(url); err != nil {
+ logger.Error(err.Error())
+ return nil, err
+ } else {
+ var arresp ApisixRouteResponse
+ if err := json.Unmarshal(resp, &arresp); err != nil {
+ logger.Error(err.Error())
+ return nil, err
+ } else {
+ return &arresp, nil
+ }
+ }
+}
+
+type RouteRequest struct {
+ ID string `json:"id,omitempty"`
+ Name string `json:"name"`
+ Desc string `json:"desc,omitempty"`
+ Priority int64 `json:"priority,omitempty"`
+ Methods []string `json:"methods,omitempty"`
+ Uris []string `json:"uris"`
+ Hosts []string `json:"hosts,omitempty"`
+ Protocols []string `json:"protocols,omitempty"`
+ Redirect *Redirect `json:"redirect,omitempty"`
+ Vars [][]string `json:"vars,omitempty"`
+ Upstream *Upstream `json:"upstream,omitempty"`
+ UpstreamProtocol string `json:"upstream_protocol,omitempty"`
+ UpstreamPath *UpstreamPath `json:"upstream_path,omitempty"`
+ UpstreamHeader map[string]string `json:"upstream_header,omitempty"`
+ Plugins map[string]interface{} `json:"plugins"`
+}
+
+func (r *ApisixRouteResponse) Parse() (*RouteRequest, error) {
+ o := r.Node.Value
+
+ //Protocols from vars and upstream
+ protocols := make([]string, 0)
+ if o.Upstream != nil && o.Upstream.EnableWebsocket {
+ protocols = append(protocols, WEBSOCKET)
+ }
+ flag := true
+ for _, t := range o.Vars {
+ if t[0] == SCHEME {
+ flag = false
+ protocols = append(protocols, t[2])
+ }
+ }
+ if flag {
+ protocols = append(protocols, HTTP)
+ protocols = append(protocols, HTTPS)
+ }
+ //Redirect from plugins
+ redirect := &Redirect{}
+ upstreamProtocol := UPATHTYPE_KEEP
+ upstreamHeader := make(map[string]string)
+ upstreamPath := &UpstreamPath{}
+ for k, v := range o.Plugins {
+ if k == REDIRECT {
+ if bytes, err := json.Marshal(v); err != nil {
+ return nil, err
+ } else {
+ if err := json.Unmarshal(bytes, redirect); err != nil {
+ return nil, err
+ }
+ }
+
+ }
+ if k == PROXY_REWRIETE {
+ pr := &ProxyRewrite{}
+ if bytes, err := json.Marshal(v); err != nil {
+ return nil, err
+ } else {
+ if err := json.Unmarshal(bytes, pr); err != nil {
+ return nil, err
+ } else {
+ if pr.Scheme != "" {
+ upstreamProtocol = pr.Scheme
+ }
+ upstreamHeader = pr.Headers
+ if pr.RegexUri == nil || len(pr.RegexUri) < 2 {
+ upstreamPath.UPathType = UPATHTYPE_STATIC
+ upstreamPath.To = pr.Uri
+ } else {
+ upstreamPath.UPathType = UPATHTYPE_REGX
+ upstreamPath.From = pr.RegexUri[0]
+ upstreamPath.To = pr.RegexUri[1]
+ }
+ }
+ }
+ }
+ }
+ //Vars
+ requestVars := make([][]string, 0)
+ for _, t := range o.Vars {
+ if t[0] != SCHEME {
+ requestVars = append(requestVars, t)
+ }
+ }
+ //Plugins
+ requestPlugins := utils.CopyMap(o.Plugins)
+ delete(requestPlugins, REDIRECT)
+
+ // check if upstream is not exist
+ if o.Upstream == nil {
+ upstreamProtocol = ""
+ upstreamHeader = nil
+ upstreamPath = nil
+ }
+ if upstreamPath != nil && upstreamPath.UPathType == "" {
+ upstreamPath = nil
+ }
+ result := &RouteRequest{
+ ID: o.Id,
+ Desc: o.Desc,
+ Priority: o.Priority,
+ Methods: o.Methods,
+ Uris: o.Uris,
+ Hosts: o.Hosts,
+ Redirect: redirect,
+ Upstream: o.Upstream,
+ UpstreamProtocol: upstreamProtocol,
+ UpstreamPath: upstreamPath,
+ UpstreamHeader: upstreamHeader,
+ Protocols: protocols,
+ Vars: requestVars,
+ Plugins: requestPlugins,
+ }
+ return result, nil
+}
+
+type Redirect struct {
+ HttpToHttps bool `json:"http_to_https,omitempty"`
+ Code int64 `json:"code,omitempty"`
+ Uri string `json:"uri,omitempty"`
+}
+
+type ProxyRewrite struct {
+ Uri string `json:"uri"`
+ RegexUri []string `json:"regex_uri"`
+ Scheme string `json:"scheme"`
+ Host string `json:"host"`
+ Headers map[string]string `json:"headers"`
+}
+
+func (r ProxyRewrite) MarshalJSON() ([]byte, error) {
+ m := make(map[string]interface{})
+ if r.RegexUri != nil {
+ m["regex_uri"] = r.RegexUri
+ }
+ if r.Uri != "" {
+ m["uri"] = r.Uri
+ }
+ if r.Scheme != UPATHTYPE_KEEP && r.Scheme != "" {
+ m["scheme"] = r.Scheme
+ }
+ if r.Host != "" {
+ m["host"] = r.Host
+ }
+ if r.Headers != nil && len(r.Headers) > 0 {
+ m["headers"] = r.Headers
+ }
+ if result, err := json.Marshal(m); err != nil {
+ return nil, err
+ } else {
+ return result, nil
+ }
+}
+
+func (r Redirect) MarshalJSON() ([]byte, error) {
+ m := make(map[string]interface{})
+ if r.HttpToHttps {
+ m["http_to_https"] = true
+ } else {
+ m["code"] = r.Code
+ m["uri"] = r.Uri
+ }
+ if result, err := json.Marshal(m); err != nil {
+ return nil, err
+ } else {
+ return result, nil
+ }
+}
+
+type Upstream struct {
+ UType string `json:"type"`
+ Nodes map[string]int64 `json:"nodes"`
+ Timeout UpstreamTimeout `json:"timeout"`
+ EnableWebsocket bool `json:"enable_websocket"`
+}
+
+type UpstreamTimeout struct {
+ Connect int64 `json:"connect"`
+ Send int64 `json:"send"`
+ Read int64 `json:"read"`
+}
+
+type UpstreamPath struct {
+ UPathType string `json:"type"`
+ From string `json:"from"`
+ To string `json:"to"`
+}
+
+type ApisixRouteRequest struct {
+ Desc string `json:"desc,omitempty"`
+ Priority int64 `json:"priority"`
+ Methods []string `json:"methods,omitempty"`
+ Uris []string `json:"uris,omitempty"`
+ Hosts []string `json:"hosts,omitempty"`
+ Vars [][]string `json:"vars,omitempty"`
+ Upstream *Upstream `json:"upstream,omitempty"`
+ Plugins map[string]interface{} `json:"plugins,omitempty"`
+ //Name string `json:"name"`
+}
+
+// ApisixRouteResponse is response from apisix admin api
+type ApisixRouteResponse struct {
+ Action string `json:"action"`
+ Node *Node `json:"node"`
+}
+
+type Node struct {
+ Value Value `json:"value"`
+ ModifiedIndex uint64 `json:"modifiedIndex"`
+}
+
+type Value struct {
+ Id string `json:"id"`
+ Name string `json:"name"`
+ Desc string `json:"desc,omitempty"`
+ Priority int64 `json:"priority"`
+ Methods []string `json:"methods"`
+ Uris []string `json:"uris"`
+ Hosts []string `json:"hosts"`
+ Vars [][]string `json:"vars"`
+ Upstream *Upstream `json:"upstream,omitempty"`
+ UpstreamId string `json:"upstream_id,omitempty"`
+ Plugins map[string]interface{} `json:"plugins"`
+}
+
+type Route struct {
+ Base
+ Name string `json:"name"`
+ Description string `json:"description,omitempty"`
+ Hosts string `json:"hosts"`
+ Uris string `json:"uris"`
+ UpstreamNodes string `json:"upstream_nodes"`
+ UpstreamId string `json:"upstream_id"`
+ Priority int64 `json:"priority"`
+ Content string `json:"content"`
+ ContentAdminApi string `json:"content_admin_api"`
+}
+
+type RouteResponse struct {
+ Base
+ Name string `json:"name"`
+ Description string `json:"description,omitempty"`
+ Hosts []string `json:"hosts,omitempty"`
+ Uris []string `json:"uris,omitempty"`
+ Upstream *Upstream `json:"upstream,omitempty"`
+ UpstreamId string `json:"upstream_id,omitempty"`
+ Priority int64 `json:"priority"`
+}
+
+type ListResponse struct {
+ Count int `json:"count"`
+ Data interface{} `json:"data"`
+}
+
+func (rr *RouteResponse) Parse(r *Route) {
+ rr.Base = r.Base
+ rr.Name = r.Name
+ rr.Description = r.Description
+ rr.UpstreamId = r.UpstreamId
+ rr.Priority = r.Priority
+ // hosts
+ if len(r.Hosts) > 0 {
+ var hosts []string
+ if err := json.Unmarshal([]byte(r.Hosts), &hosts); err == nil {
+ rr.Hosts = hosts
+ } else {
+ logger.Error(err.Error())
+ }
+ }
+
+ // uris
+ if len(r.Uris) > 0 {
+ var uris []string
+ if err := json.Unmarshal([]byte(r.Uris), &uris); err == nil {
+ rr.Uris = uris
+ }
+ }
+
+ // uris
+ var resp ApisixRouteResponse
+ if err := json.Unmarshal([]byte(r.ContentAdminApi), &resp); err == nil {
+ rr.Upstream = resp.Node.Value.Upstream
+ }
+}
+
+// RouteRequest -> ApisixRouteRequest
+func ToApisixRequest(routeRequest *RouteRequest) *ApisixRouteRequest {
+ // redirect -> plugins
+ plugins := utils.CopyMap(routeRequest.Plugins)
+ redirect := routeRequest.Redirect
+ if redirect != nil {
+ plugins["redirect"] = redirect
+ }
+
+ logger.Info(routeRequest.Plugins)
+
+ // scheme https and not http -> vars ['scheme', '==', 'https']
+ pMap := utils.Set2Map(routeRequest.Protocols)
+
+ arr := &ApisixRouteRequest{}
+ arr.Parse(routeRequest)
+
+ // protocols[websokect] -> upstream
+ if pMap[WEBSOCKET] == 1 && arr.Upstream != nil {
+ arr.Upstream.EnableWebsocket = true
+ }
+ vars := utils.CopyStrings(routeRequest.Vars)
+ if pMap[HTTP] != 1 || pMap[HTTPS] != 1 {
+ if pMap[HTTP] == 1 {
+ vars = append(vars, []string{SCHEME, "==", HTTP})
+ }
+ if pMap[HTTPS] == 1 {
+ vars = append(vars, []string{SCHEME, "==", HTTPS})
+ }
+ }
+ if len(vars) > 0 {
+ arr.Vars = vars
+ } else {
+ arr.Vars = nil
+ }
+
+ // upstream protocol
+ if arr.Upstream != nil {
+ pr := &ProxyRewrite{}
+ pr.Scheme = routeRequest.UpstreamProtocol
+ // upstream path
+ proxyPath := routeRequest.UpstreamPath
+ if proxyPath != nil {
+ if proxyPath.UPathType == UPATHTYPE_STATIC {
+ pr.Uri = proxyPath.To
+ pr.RegexUri = nil
+ } else {
+ pr.RegexUri = []string{proxyPath.From, proxyPath.To}
+ }
+ }
+ // upstream headers
+ pr.Headers = routeRequest.UpstreamHeader
+ if proxyPath != nil {
+ plugins[PROXY_REWRIETE] = pr
+ }
+ }
+
+ if plugins != nil && len(plugins) > 0 {
+ arr.Plugins = plugins
+ } else {
+ arr.Plugins = nil
+ }
+ return arr
+}
+
+func ToRoute(routeRequest *RouteRequest,
+ arr *ApisixRouteRequest,
+ u4 uuid.UUID,
+ resp *ApisixRouteResponse) (*Route, *errno.ManagerError) {
+ rd := &Route{}
+ if err := rd.Parse(routeRequest, arr); err != nil {
+ e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
+ return nil, e
+ }
+ if rd.Name == "" {
+ rd.Name = routeRequest.Name
+ }
+ rd.ID = u4
+ // content_admin_api
+ if respStr, err := json.Marshal(resp); err != nil {
+ e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
+ return nil, e
+ } else {
+ rd.ContentAdminApi = string(respStr)
+ }
+ // hosts
+ hosts := resp.Node.Value.Hosts
+ if hb, err := json.Marshal(hosts); err != nil {
+ e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
+ logger.Warn(e.Msg)
+ } else {
+ rd.Hosts = string(hb)
+ }
+ // uris
+ uris := resp.Node.Value.Uris
+ if ub, err := json.Marshal(uris); err != nil {
+ e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
+ logger.Warn(e.Msg)
+ } else {
+ rd.Uris = string(ub)
+ }
+ // upstreamNodes
+ if resp.Node.Value.Upstream != nil {
+ nodes := resp.Node.Value.Upstream.Nodes
+ ips := make([]string, 0)
+ for k, _ := range nodes {
+ ips = append(ips, k)
+ }
+ if nb, err := json.Marshal(ips); err != nil {
+ e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
+ logger.Warn(e.Msg)
+ } else {
+ rd.UpstreamNodes = string(nb)
+ }
+ }
+ return rd, nil
+}
diff --git a/api/service/route_test.go b/api/service/route_test.go
new file mode 100644
index 0000000..74ab02f
--- /dev/null
+++ b/api/service/route_test.go
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package service
+
+import (
+ "encoding/json"
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func TestRedirectMarshal(t *testing.T) {
+ a := assert.New(t)
+ r := &Redirect{
+ HttpToHttps: true,
+ Code: 302,
+ Uri: "/hello",
+ }
+ if result, err := json.Marshal(r); err != nil {
+ t.Error(err.Error())
+ } else {
+ h := make(map[string]interface{})
+ json.Unmarshal(result, &h)
+ a.Equal(1, len(h))
+ a.Equal(nil, h["code"])
+ a.Equal(true, h["http_to_https"])
+ }
+}
+
+func TestToApisixRequest_RediretPlugins(t *testing.T) {
+ rr := &RouteRequest{
+ ID: "u guess a uuid",
+ Name: "a special name",
+ Desc: "any description",
+ Priority: 0,
+ Methods: []string{"GET"},
+ Uris: []string{},
+ Hosts: []string{"www.baidu.com"},
+ Protocols: []string{"http", "https", "websocket"},
+ Redirect: &Redirect{HttpToHttps: true, Code: 200, Uri: "/hello"},
+ Vars: [][]string{},
+ }
+ ar := ToApisixRequest(rr)
+ a := assert.New(t)
+ a.Equal(1, len(ar.Plugins))
+ a.NotEqual(nil, ar.Plugins["redirect"])
+}
+
+func TestToApisixRequest_Vars(t *testing.T) {
+ rr := &RouteRequest{
+ ID: "u guess a uuid",
+ Name: "a special name",
+ Desc: "any description",
+ Priority: 0,
+ Methods: []string{"GET"},
+ Uris: []string{},
+ Hosts: []string{"www.baidu.com"},
+ Protocols: []string{"http", "https", "websocket"},
+ Redirect: &Redirect{HttpToHttps: true, Code: 200, Uri: "/hello"},
+ Vars: [][]string{},
+ }
+ ar := ToApisixRequest(rr)
+ a := assert.New(t)
+ b, err := json.Marshal(ar)
+ a.Equal(nil, err)
+
+ m := make(map[string]interface{})
+ err = json.Unmarshal(b, &m)
+ a.Equal(nil, err)
+ t.Log(m["vars"])
+ a.Equal(nil, m["vars"])
+}
+
+func TestToApisixRequest_Upstream(t *testing.T) {
+ nodes := make(map[string]int64)
+ nodes["127.0.0.1:8080"] = 100
+ upstream := &Upstream{
+ UType: "roundrobin",
+ Nodes: nodes,
+ Timeout: UpstreamTimeout{15, 15, 15},
+ }
+ rr := &RouteRequest{
+ ID: "u guess a uuid",
+ Name: "a special name",
+ Desc: "any description",
+ Priority: 0,
+ Methods: []string{"GET"},
+ Uris: []string{},
+ Hosts: []string{"www.baidu.com"},
+ Protocols: []string{"http", "https", "websocket"},
+ Redirect: &Redirect{HttpToHttps: true, Code: 200, Uri: "/hello"},
+ Vars: [][]string{},
+ Upstream: upstream,
+ }
+ ar := ToApisixRequest(rr)
+ a := assert.New(t)
+ a.Equal("roundrobin", ar.Upstream.UType)
+ a.Equal(true, ar.Upstream.EnableWebsocket)
+}
+
+func TestToApisixRequest_UpstreamUnable(t *testing.T) {
+ nodes := make(map[string]int64)
+ nodes["127.0.0.1:8080"] = 100
+ upstream := &Upstream{
+ UType: "roundrobin",
+ Nodes: nodes,
+ Timeout: UpstreamTimeout{15, 15, 15},
+ }
+ rr := &RouteRequest{
+ ID: "u guess a uuid",
+ Name: "a special name",
+ Desc: "any description",
+ Priority: 0,
+ Methods: []string{"GET"},
+ Uris: []string{},
+ Hosts: []string{"www.baidu.com"},
+ Protocols: []string{"http", "https"},
+ Redirect: &Redirect{HttpToHttps: true, Code: 200, Uri: "/hello"},
+ Vars: [][]string{},
+ Upstream: upstream,
+ }
+ ar := ToApisixRequest(rr)
+ a := assert.New(t)
+ a.Equal("roundrobin", ar.Upstream.UType)
+ a.Equal(false, ar.Upstream.EnableWebsocket)
+}
+
+// no upstream
+func TestApisixRouteResponse_Parse(t *testing.T) {
+ a := assert.New(t)
+ plugins := make(map[string]interface{})
+ redirect := &Redirect{
+ Code: 302,
+ Uri: "/foo",
+ }
+ plugins["redirect"] = redirect
+ arr := &ApisixRouteResponse{
+ Action: "get",
+ Node: &Node{
+ Value: Value{
+ Id: "",
+ Name: "",
+ Desc: "",
+ Priority: 0,
+ Methods: []string{"GET"},
+ Uris: []string{"/*"},
+ Hosts: []string{"www.baidu.com"},
+ Vars: [][]string{},
+ Upstream: nil,
+ Plugins: plugins,
+ },
+ },
+ }
+ _, err := arr.Parse()
+ a.Equal(nil, err)
+}
diff --git a/api/service/ssl.go b/api/service/ssl.go
new file mode 100644
index 0000000..839e3b8
--- /dev/null
+++ b/api/service/ssl.go
@@ -0,0 +1,300 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package service
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/json"
+ "encoding/pem"
+ "errors"
+ "fmt"
+
+ "github.com/satori/go.uuid"
+
+ "github.com/apisix/manager-api/conf"
+ "github.com/apisix/manager-api/errno"
+ "github.com/apisix/manager-api/utils"
+)
+
+type Ssl struct {
+ Base
+ ValidityStart uint64 `json:"validity_start"`
+ ValidityEnd uint64 `json:"validity_end"`
+ Snis string `json:"snis"`
+ Status uint64 `json:"status"`
+ PublicKey string `json:"public_key,omitempty"`
+}
+
+type SslDto struct {
+ Base
+ ValidityStart uint64 `json:"validity_start"`
+ ValidityEnd uint64 `json:"validity_end"`
+ Snis []string `json:"snis"`
+ Status uint64 `json:"status"`
+ PublicKey string `json:"public_key,omitempty"`
+}
+
+type SslRequest struct {
+ ID string `json:"id,omitempty"`
+ PublicKey string `json:"cert"`
+ PrivateKey string `json:"key"`
+ Snis []string `json:"snis"`
+}
+
+// ApisixSslResponse is response from apisix admin api
+type ApisixSslResponse struct {
+ Action string `json:"action"`
+ Node *SslNode `json:"node"`
+}
+
+type SslNode struct {
+ Value SslRequest `json:"value"`
+ ModifiedIndex uint64 `json:"modifiedIndex"`
+}
+
+func (req *SslRequest) Parse(body interface{}) {
+ if err := json.Unmarshal(body.([]byte), req); err != nil {
+ req = nil
+ logger.Error(errno.FromMessage(errno.RouteRequestError, err.Error()).Msg)
+ }
+}
+
+func (sslDto *SslDto) Parse(ssl *Ssl) error {
+ sslDto.ID = ssl.ID
+ sslDto.ValidityStart = ssl.ValidityStart
+ sslDto.ValidityEnd = ssl.ValidityEnd
+
+ var snis []string
+ _ = json.Unmarshal([]byte(ssl.Snis), &snis)
+ sslDto.Snis = snis
+
+ sslDto.Status = ssl.Status
+ sslDto.PublicKey = ssl.PublicKey
+ sslDto.CreateTime = ssl.CreateTime
+ sslDto.UpdateTime = ssl.UpdateTime
+
+ return nil
+}
+
+func SslList(page, size int) ([]byte, error) {
+ var count int
+ sslList := []Ssl{}
+ if err := conf.DB().Table("ssls").Offset((page - 1) * size).Limit(size).Find(&sslList).Count(&count).Error; err != nil {
+ return nil, err
+ }
+
+ sslDtoList := []SslDto{}
+
+ for _, ssl := range sslList {
+ sslDto := SslDto{}
+ sslDto.Parse(&ssl)
+
+ sslDtoList = append(sslDtoList, sslDto)
+ }
+
+ data := errno.FromMessage(errno.SystemSuccess).ListResponse(count, sslDtoList)
+
+ return json.Marshal(data)
+}
+
+func SslItem(id string) ([]byte, error) {
+ ssl := &Ssl{}
+ if err := conf.DB().Table("ssls").Where("id = ?", id).First(ssl).Error; err != nil {
+ return nil, err
+ }
+
+ sslDto := &SslDto{}
+ sslDto.Parse(ssl)
+
+ data := errno.FromMessage(errno.SystemSuccess).ItemResponse(sslDto)
+
+ return json.Marshal(data)
+}
+
+func SslCheck(param interface{}) ([]byte, error) {
+ sslReq := &SslRequest{}
+ sslReq.Parse(param)
+
+ ssl, err := ParseCert(sslReq.PublicKey, sslReq.PrivateKey)
+
+ if err != nil {
+ return nil, err
+ }
+
+ ssl.PublicKey = ""
+
+ sslDto := &SslDto{}
+ sslDto.Parse(ssl)
+
+ data := errno.FromMessage(errno.SystemSuccess).ItemResponse(sslDto)
+
+ return json.Marshal(data)
+}
+
+func SslCreate(param interface{}, id string) error {
+ sslReq := &SslRequest{}
+ sslReq.Parse(param)
+
+ ssl, err := ParseCert(sslReq.PublicKey, sslReq.PrivateKey)
+
+ if err != nil {
+ return err
+ }
+
+ // first admin api
+ var snis []string
+ _ = json.Unmarshal([]byte(ssl.Snis), &snis)
+ sslReq.Snis = snis
+
+ if _, err := sslReq.PutToApisix(id); err != nil {
+ return err
+ }
+ // then mysql
+ ssl.ID = uuid.FromStringOrNil(id)
+ if err := conf.DB().Create(ssl).Error; err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func SslUpdate(param interface{}, id string) error {
+ sslReq := &SslRequest{}
+ sslReq.Parse(param)
+
+ ssl, err := ParseCert(sslReq.PublicKey, sslReq.PrivateKey)
+
+ if err != nil {
+ return err
+ }
+
+ // first admin api
+ var snis []string
+ _ = json.Unmarshal([]byte(ssl.Snis), &snis)
+ sslReq.Snis = snis
+
+ if _, err := sslReq.PutToApisix(id); err != nil {
+ return err
+ }
+
+ // then mysql
+ ssl.ID = uuid.FromStringOrNil(id)
+ data := Ssl{PublicKey: ssl.PublicKey, Snis: ssl.Snis, ValidityStart: ssl.ValidityStart, ValidityEnd: ssl.ValidityEnd}
+ if err := conf.DB().Model(&ssl).Updates(data).Error; err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func SslDelete(id string) error {
+ // delete from apisix
+ request := &SslRequest{}
+ request.ID = id
+ if _, err := request.DeleteFromApisix(); err != nil {
+ return err
+ }
+ // delete from mysql
+ ssl := &Ssl{}
+ ssl.ID = uuid.FromStringOrNil(id)
+ if err := conf.DB().Delete(ssl).Error; err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (req *SslRequest) PutToApisix(rid string) (*ApisixSslResponse, error) {
+ url := fmt.Sprintf("%s/ssl/%s", conf.BaseUrl, rid)
+ if data, err := json.Marshal(req); err != nil {
+ return nil, err
+ } else {
+ if resp, err := utils.Put(url, data); err != nil {
+ logger.Error(url)
+ logger.Error(string(data))
+ logger.Error(err.Error())
+ return nil, err
+ } else {
+ var arresp ApisixSslResponse
+ if err := json.Unmarshal(resp, &arresp); err != nil {
+ logger.Error(err.Error())
+ return nil, err
+ } else {
+ return &arresp, nil
+ }
+ }
+ }
+}
+
+func (req *SslRequest) DeleteFromApisix() (*ApisixSslResponse, error) {
+ id := req.ID
+ url := fmt.Sprintf("%s/ssl/%s", conf.BaseUrl, id)
+
+ if resp, err := utils.Delete(url); err != nil {
+ logger.Error(err.Error())
+ return nil, err
+ } else {
+ var arresp ApisixSslResponse
+ if err := json.Unmarshal(resp, &arresp); err != nil {
+ logger.Error(err.Error())
+ return nil, err
+ } else {
+ return &arresp, nil
+ }
+ }
+}
+
+func ParseCert(crt, key string) (*Ssl, error) {
+ // print private key
+ certDERBlock, _ := pem.Decode([]byte(crt))
+ if certDERBlock == nil {
+ return nil, errors.New("Certificate resolution failed")
+ }
+ // match
+ _, err := tls.X509KeyPair([]byte(crt), []byte(key))
+ if err != nil {
+ return nil, err
+ }
+
+ x509Cert, err := x509.ParseCertificate(certDERBlock.Bytes)
+
+ if err != nil {
+ return nil, errors.New("Certificate resolution failed")
+ } else {
+ ssl := Ssl{}
+
+ //domain
+ snis := []byte{}
+ if x509Cert.DNSNames == nil || len(x509Cert.DNSNames) < 1 {
+ tmp := []string{}
+ if x509Cert.Subject.CommonName != "" {
+ tmp = []string{x509Cert.Subject.CommonName}
+ }
+ snis, _ = json.Marshal(tmp)
+ } else {
+ snis, _ = json.Marshal(x509Cert.DNSNames)
+ }
+ ssl.Snis = string(snis)
+
+ ssl.ValidityStart = uint64(x509Cert.NotBefore.Unix())
+ ssl.ValidityEnd = uint64(x509Cert.NotAfter.Unix())
+ ssl.PublicKey = crt
+
+ return &ssl, nil
+ }
+}
diff --git a/api/utils/copy.go b/api/utils/copy.go
new file mode 100644
index 0000000..30f228a
--- /dev/null
+++ b/api/utils/copy.go
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package utils
+
+func CopyMap(origin map[string]interface{}) map[string]interface{} {
+ result := make(map[string]interface{})
+ for k, v := range origin {
+ result[k] = v
+ }
+ return result
+}
+
+func CopyStrings(origin [][]string) [][]string {
+ result := make([][]string, 0)
+ for _, s := range origin {
+ result = append(result, s)
+ }
+ return result
+}
+
+func Set2Map(origin []string) map[string]int {
+ result := make(map[string]int)
+ for _, s := range origin {
+ result[s] = 1
+ }
+ return result
+}
diff --git a/api/utils/http.go b/api/utils/http.go
new file mode 100644
index 0000000..119cf0b
--- /dev/null
+++ b/api/utils/http.go
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package utils
+
+import (
+ "fmt"
+ "github.com/apisix/manager-api/conf"
+ "github.com/apisix/manager-api/log"
+ "gopkg.in/resty.v1"
+ "net/http"
+ "time"
+)
+
+const timeout = 3000
+
+var logger = log.GetLogger()
+
+func Get(url string) ([]byte, error) {
+ r := resty.New().
+ SetTimeout(time.Duration(timeout)*time.Millisecond).
+ R().
+ SetHeader("content-type", "application/json").
+ SetHeader("X-API-KEY", conf.ApiKey)
+ resp, err := r.Get(url)
+ if err != nil {
+ return nil, err
+ }
+ if resp.StatusCode() != http.StatusOK {
+ return nil, fmt.Errorf("status: %d, body: %s", resp.StatusCode(), resp.Body())
+ }
+ return resp.Body(), nil
+}
+
+func Post(url string, bytes []byte) ([]byte, error) {
+ r := resty.New().
+ SetTimeout(time.Duration(timeout)*time.Millisecond).
+ R().
+ SetHeader("content-type", "application/json").
+ SetHeader("X-API-KEY", conf.ApiKey)
+ r.SetBody(bytes)
+ resp, err := r.Post(url)
+ if err != nil {
+ return nil, err
+ }
+ if resp.StatusCode() != http.StatusOK && resp.StatusCode() != http.StatusCreated {
+ return nil, fmt.Errorf("status: %d, body: %s", resp.StatusCode(), resp.Body())
+ }
+ return resp.Body(), nil
+}
+
+func Put(url string, bytes []byte) ([]byte, error) {
+ r := resty.New().
+ SetTimeout(time.Duration(timeout)*time.Millisecond).
+ R().
+ SetHeader("content-type", "application/json").
+ SetHeader("X-API-KEY", conf.ApiKey)
+ r.SetBody(bytes)
+ resp, err := r.Put(url)
+ if err != nil {
+ return nil, err
+ }
+ if resp.StatusCode() != http.StatusOK && resp.StatusCode() != http.StatusCreated {
+ return nil, fmt.Errorf("status: %d, body: %s", resp.StatusCode(), resp.Body())
+ }
+ return resp.Body(), nil
+}
+
+func Patch(url string, bytes []byte) ([]byte, error) {
+ r := resty.New().
+ SetTimeout(time.Duration(timeout)*time.Millisecond).
+ R().
+ SetHeader("content-type", "application/json").
+ SetHeader("X-API-KEY", conf.ApiKey)
+ r.SetBody(bytes)
+ resp, err := r.Patch(url)
+ if err != nil {
+ return nil, err
+ }
+ if resp.StatusCode() != http.StatusOK {
+ return nil, fmt.Errorf("status: %d, body: %s", resp.StatusCode(), resp.Body())
+ }
+ return resp.Body(), nil
+}
+
+func Delete(url string) ([]byte, error) {
+ r := resty.New().
+ SetTimeout(time.Duration(timeout)*time.Millisecond).
+ R().
+ SetHeader("content-type", "application/json").
+ SetHeader("X-API-KEY", conf.ApiKey)
+ resp, err := r.Delete(url)
+ if err != nil {
+ return nil, err
+ }
+ if resp.StatusCode() != http.StatusOK {
+ return nil, fmt.Errorf("status: %d, body: %s", resp.StatusCode(), resp.Body())
+ }
+ return resp.Body(), nil
+}
diff --git a/compose/README.md b/compose/README.md
new file mode 100644
index 0000000..7d7c7ce
--- /dev/null
+++ b/compose/README.md
@@ -0,0 +1,56 @@
+## Deploy
+
+```sh
+$ cd incubator-apisix-dashboard/compose
+
+$ chmod +x ./manager_conf/build.sh
+
+$ docker-compose -p dashboard up -d
+```
+
+## Usage
+
+### 1. login dashboard
+
+Visit `http://127.0.0.1/dashboard/` in the browser,
+Enter `http://127.0.0.1:8080/apisix/admin` into the first input box, this is the backend management service address
+
+
+
+now, click `save`.
+
+### 2. If you want to display the grafana metric dashboard, please fill in the grafana shared link as follows
+
+1.get grafana shared link
+
+Visit `http://127.0.0.1:3000/?search=open&orgId=1`
+
+
+
+click `Apache APISIX` dashboard, and you can see the page as follow
+
+
+
+click the button `shard dashboard` on the right of `Apache APISIX`
+
+
+
+copy the link, and then return to dashboard on the step 1
+
+
+
+click metric on the left, and then the config button
+
+Paste shared link
+
+
+
+save, and you can see the metrics
+
+
+
+
+
+
+
+
diff --git a/compose/apisix_conf/config.yaml b/compose/apisix_conf/config.yaml
new file mode 100644
index 0000000..bbdc65b
--- /dev/null
+++ b/compose/apisix_conf/config.yaml
@@ -0,0 +1,137 @@
+apisix:
+ node_listen: 9080 # APISIX listening port
+ enable_heartbeat: true
+ enable_admin: true
+ enable_admin_cors: true # Admin API support CORS response headers.
+ enable_debug: false
+ enable_dev_mode: false # Sets nginx worker_processes to 1 if set to true
+ enable_reuseport: true # Enable nginx SO_REUSEPORT switch if set to true.
+ enable_ipv6: true
+ config_center: etcd # etcd: use etcd to store the config value
+ # yaml: fetch the config value from local yaml file `/your_path/conf/apisix.yaml`
+
+ #proxy_protocol: # Proxy Protocol configuration
+ # listen_http_port: 9181 # The port with proxy protocol for http, it differs from node_listen and port_admin.
+ # This port can only receive http request with proxy protocol, but node_listen & port_admin
+ # can only receive http request. If you enable proxy protocol, you must use this port to
+ # receive http request with proxy protocol
+ # listen_https_port: 9182 # The port with proxy protocol for https
+ # enable_tcp_pp: true # Enable the proxy protocol for tcp proxy, it works for stream_proxy.tcp option
+ # enable_tcp_pp_to_upstream: true # Enables the proxy protocol to the upstream server
+
+ proxy_cache: # Proxy Caching configuration
+ cache_ttl: 10s # The default caching time if the upstream does not specify the cache time
+ zones: # The parameters of a cache
+ - name: disk_cache_one # The name of the cache, administrator can be specify
+ # which cache to use by name in the admin api
+ memory_size: 50m # The size of shared memory, it's used to store the cache index
+ disk_size: 1G # The size of disk, it's used to store the cache data
+ disk_path: "/tmp/disk_cache_one" # The path to store the cache data
+ cache_levels: "1:2" # The hierarchy levels of a cache
+ # - name: disk_cache_two
+ # memory_size: 50m
+ # disk_size: 1G
+ # disk_path: "/tmp/disk_cache_two"
+ # cache_levels: "1:2"
+
+# allow_admin: # http://nginx.org/en/docs/http/ngx_http_access_module.html#allow
+# - 127.0.0.0/24 # If we don't set any IP list, then any IP access is allowed by default.
+# - 172.17.0.0/24
+ # - "::/64"
+ # port_admin: 9180 # use a separate port
+
+ # Default token when use API to call for Admin API.
+ # *NOTE*: Highly recommended to modify this value to protect APISIX's Admin API.
+ # Disabling this configuration item means that the Admin API does not
+ # require any authentication.
+ admin_key:
+ -
+ name: "admin"
+ key: edd1c9f034335f136f87ad84b625c8f1
+ role: admin # admin: manage all configuration data
+ # viewer: only can view configuration data
+ -
+ name: "viewer"
+ key: 4054f7cf07e344346cd3f287985e76a2
+ role: viewer
+ router:
+ http: 'radixtree_uri' # radixtree_uri: match route by uri(base on radixtree)
+ # radixtree_host_uri: match route by host + uri(base on radixtree)
+ ssl: 'radixtree_sni' # radixtree_sni: match route by SNI(base on radixtree)
+ # stream_proxy: # TCP/UDP proxy
+ # tcp: # TCP proxy port list
+ # - 9100
+ # - 9101
+ # udp: # UDP proxy port list
+ # - 9200
+ # - 9211
+ dns_resolver: # default DNS resolver, with disable IPv6 and enable local DNS
+ - 127.0.0.11
+ - 114.114.114.114
+ - 223.5.5.5
+ - 1.1.1.1
+ - 8.8.8.8
+ dns_resolver_valid: 30 # valid time for dns result 30 seconds
+ resolver_timeout: 5 # resolver timeout
+ ssl:
+ enable: true
+ enable_http2: true
+ listen_port: 9443
+ ssl_protocols: "TLSv1 TLSv1.1 TLSv1.2 TLSv1.3"
+ ssl_ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES2 [...]
+
+nginx_config: # config for render the template to genarate nginx.conf
+ error_log: "logs/error.log"
+ error_log_level: "warn" # warn,error
+ worker_rlimit_nofile: 20480 # the number of files a worker process can open, should be larger than worker_connections
+ event:
+ worker_connections: 10620
+ http:
+ access_log: "logs/access.log"
+ keepalive_timeout: 60s # timeout during which a keep-alive client connection will stay open on the server side.
+ client_header_timeout: 60s # timeout for reading client request header, then 408 (Request Time-out) error is returned to the client
+ client_body_timeout: 60s # timeout for reading client request body, then 408 (Request Time-out) error is returned to the client
+ send_timeout: 10s # timeout for transmitting a response to the client.then the connection is closed
+ underscores_in_headers: "on" # default enables the use of underscores in client request header fields
+ real_ip_header: "X-Real-IP" # http://nginx.org/en/docs/http/ngx_http_realip_module.html#real_ip_header
+ real_ip_from: # http://nginx.org/en/docs/http/ngx_http_realip_module.html#set_real_ip_from
+ - 127.0.0.1
+ - 'unix:'
+ #lua_shared_dicts: # add custom shared cache to nginx.conf
+ # ipc_shared_dict: 100m # custom shared cache, format: `cache-key: cache-size`
+
+etcd:
+ host: # it's possible to define multiple etcd hosts addresses of the same etcd cluster.
+ - "http://192.17.5.10:2379" # multiple etcd address
+ prefix: "/apisix" # apisix configurations prefix
+ timeout: 3 # 3 seconds
+
+plugins: # plugin list
+ - example-plugin
+ - limit-req
+ - limit-count
+ - limit-conn
+ - key-auth
+ - basic-auth
+ - prometheus
+ - node-status
+ - jwt-auth
+ - zipkin
+ - ip-restriction
+ - grpc-transcode
+ - serverless-pre-function
+ - serverless-post-function
+ - openid-connect
+ - proxy-rewrite
+ - redirect
+ - response-rewrite
+ - fault-injection
+ - udp-logger
+ - wolf-rbac
+ - proxy-cache
+ - tcp-logger
+ - proxy-mirror
+ - kafka-logger
+ - cors
+stream_plugins:
+ - mqtt-proxy
diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml
new file mode 100644
index 0000000..e4ad481
--- /dev/null
+++ b/compose/docker-compose.yml
@@ -0,0 +1,124 @@
+version: "3"
+
+services:
+ apisix:
+ image: apache/apisix:dev
+ restart: always
+ volumes:
+ - ./apisix_log:/usr/local/apisix/logs
+ - ./apisix_conf/config.yaml:/usr/local/apisix/conf/config.yaml:ro
+ depends_on:
+ - etcd
+ ##network_mode: host
+ ports:
+ - "9080:9080/tcp"
+ - "9443:9443/tcp"
+ networks:
+ apisix-dashboard:
+ ipv4_address: 192.17.5.11
+
+ etcd:
+ image: gcr.io/etcd-development/etcd:v3.3.12
+ command: /usr/local/bin/etcd --advertise-client-urls http://0.0.0.0:2379 --listen-client-urls http://0.0.0.0:2379
+ restart: always
+ volumes:
+ - ./etcd_data:/etcd_data
+ environment:
+ ETCD_DATA_DIR: /etcd_data
+ ports:
+ - "2379:2379/tcp"
+ networks:
+ apisix-dashboard:
+ ipv4_address: 192.17.5.10
+
+ web1:
+ image: ruby:2-alpine
+ command: sh -c "mkdir -p /tmp/www && echo 'web1' > /tmp/www/web1.txt && ruby -run -ehttpd /tmp/www -p8000"
+ restart: always
+ ports:
+ - "9081:8000/tcp"
+ networks:
+ apisix-dashboard:
+ ipv4_address: 192.17.5.12
+
+ web2:
+ image: ruby:2-alpine
+ command: sh -c "mkdir -p /tmp/www && echo 'web2' > /tmp/www/web2.txt && ruby -run -ehttpd /tmp/www -p8000"
+ restart: always
+ ports:
+ - "9082:8000/tcp"
+ networks:
+ apisix-dashboard:
+ ipv4_address: 192.17.5.13
+
+ mysql:
+ image: mysql:latest
+ restart: always
+ ports:
+ - "3309:3306/tcp"
+ environment:
+ - MYSQL_ROOT_PASSWORD=123456
+ volumes:
+ - /tmp/datadir:/var/lib/mysql
+ - /tmp/conf.d:/etc/mysql/conf.d
+ - ../api/script/db:/docker-entrypoint-initdb.d
+ networks:
+ apisix-dashboard:
+ ipv4_address: 192.17.5.14
+
+ manager:
+ build:
+ context: ./../api
+ dockerfile: Dockerfile
+ restart: always
+ ports:
+ - "8080:8080/tcp"
+ environment:
+ - ENV=prod
+ volumes:
+ - ./manager_conf/build.sh:/root/manager-api/build.sh
+ networks:
+ apisix-dashboard:
+ ipv4_address: 192.17.5.15
+
+ prometheus:
+ image: prom/prometheus
+ hostname: prometheus
+ restart: always
+ volumes:
+ - ./prometheus_conf/prometheus.yml:/etc/prometheus/prometheus.yml
+ ports:
+ - "9090:9090"
+ networks:
+ apisix-dashboard:
+ ipv4_address: 192.17.5.16
+
+ grafana:
+ image: grafana/grafana
+ container_name: grafana
+ hostname: grafana
+ restart: always
+ ports:
+ - "3000:3000"
+ volumes:
+ - "./grafana_conf/provisioning:/etc/grafana/provisioning"
+ - "./grafana_conf/dashboards:/var/lib/grafana/dashboards"
+ - "./grafana_conf/config/grafana.ini:/etc/grafana/grafana.ini"
+ networks:
+ apisix-dashboard:
+ ipv4_address: 192.17.5.17
+
+ dashboard:
+ build:
+ context: ./..
+ dockerfile: Dockerfile
+ restart: always
+ ports:
+ - "80:80/tcp"
+
+networks:
+ apisix-dashboard:
+ driver: bridge
+ ipam:
+ config:
+ - subnet: 192.17.0.0/16
diff --git a/compose/grafana_conf/config/grafana.ini b/compose/grafana_conf/config/grafana.ini
new file mode 100644
index 0000000..cb6a737
--- /dev/null
+++ b/compose/grafana_conf/config/grafana.ini
@@ -0,0 +1,756 @@
+##################### Grafana Configuration Example #####################
+#
+# Everything has defaults so you only need to uncomment things you want to
+# change
+
+# possible values : production, development
+;app_mode = production
+
+# instance name, defaults to HOSTNAME environment variable value or hostname if HOSTNAME var is empty
+;instance_name = ${HOSTNAME}
+
+#################################### Paths ####################################
+[paths]
+# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used)
+;data = /var/lib/grafana
+
+# Temporary files in `data` directory older than given duration will be removed
+;temp_data_lifetime = 24h
+
+# Directory where grafana can store logs
+;logs = /var/log/grafana
+
+# Directory where grafana will automatically scan and look for plugins
+;plugins = /var/lib/grafana/plugins
+
+# folder that contains provisioning config files that grafana will apply on startup and while running.
+;provisioning = conf/provisioning
+
+#################################### Server ####################################
+[server]
+# Protocol (http, https, h2, socket)
+;protocol = http
+
+# The ip address to bind to, empty will bind to all interfaces
+;http_addr =
+
+# The http port to use
+;http_port = 3000
+
+# The public facing domain name used to access grafana from a browser
+;domain = localhost
+
+# Redirect to correct domain if host header does not match domain
+# Prevents DNS rebinding attacks
+;enforce_domain = false
+
+# The full public facing url you use in browser, used for redirects and emails
+# If you use reverse proxy and sub path specify full url (with sub path)
+;root_url = %(protocol)s://%(domain)s:%(http_port)s/
+
+# Serve Grafana from subpath specified in `root_url` setting. By default it is set to `false` for compatibility reasons.
+;serve_from_sub_path = false
+
+# Log web requests
+;router_logging = false
+
+# the path relative working path
+;static_root_path = public
+
+# enable gzip
+;enable_gzip = false
+
+# https certs & key file
+;cert_file =
+;cert_key =
+
+# Unix socket path
+;socket =
+
+#################################### Database ####################################
+[database]
+# You can configure the database connection by specifying type, host, name, user and password
+# as separate properties or as on string using the url properties.
+
+# Either "mysql", "postgres" or "sqlite3", it's your choice
+;type = sqlite3
+;host = 127.0.0.1:3306
+;name = grafana
+;user = root
+# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
+;password =
+
+# Use either URL or the previous fields to configure the database
+# Example: mysql://user:secret@host:port/database
+;url =
+
+# For "postgres" only, either "disable", "require" or "verify-full"
+;ssl_mode = disable
+
+;ca_cert_path =
+;client_key_path =
+;client_cert_path =
+;server_cert_name =
+
+# For "sqlite3" only, path relative to data_path setting
+;path = grafana.db
+
+# Max idle conn setting default is 2
+;max_idle_conn = 2
+
+# Max conn setting default is 0 (mean not set)
+;max_open_conn =
+
+# Connection Max Lifetime default is 14400 (means 14400 seconds or 4 hours)
+;conn_max_lifetime = 14400
+
+# Set to true to log the sql calls and execution times.
+;log_queries =
+
+# For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared)
+;cache_mode = private
+
+#################################### Cache server #############################
+[remote_cache]
+# Either "redis", "memcached" or "database" default is "database"
+;type = database
+
+# cache connectionstring options
+# database: will use Grafana primary database.
+# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=0,ssl=false`. Only addr is required. ssl may be 'true', 'false', or 'insecure'.
+# memcache: 127.0.0.1:11211
+;connstr =
+
+#################################### Data proxy ###########################
+[dataproxy]
+
+# This enables data proxy logging, default is false
+;logging = false
+
+# How long the data proxy should wait before timing out default is 30 (seconds)
+;timeout = 30
+
+# If enabled and user is not anonymous, data proxy will add X-Grafana-User header with username into the request, default is false.
+;send_user_header = false
+
+#################################### Analytics ####################################
+[analytics]
+# Server reporting, sends usage counters to stats.grafana.org every 24 hours.
+# No ip addresses are being tracked, only simple counters to track
+# running instances, dashboard and error counts. It is very helpful to us.
+# Change this option to false to disable reporting.
+;reporting_enabled = true
+
+# Set to false to disable all checks to https://grafana.net
+# for new vesions (grafana itself and plugins), check is used
+# in some UI views to notify that grafana or plugin update exists
+# This option does not cause any auto updates, nor send any information
+# only a GET request to http://grafana.com to get latest versions
+;check_for_updates = true
+
+# Google Analytics universal tracking code, only enabled if you specify an id here
+;google_analytics_ua_id =
+
+# Google Tag Manager ID, only enabled if you specify an id here
+;google_tag_manager_id =
+
+#################################### Security ####################################
+[security]
+# disable creation of admin user on first start of grafana
+;disable_initial_admin_creation = false
+
+# default admin user, created on startup
+;admin_user = admin
+
+# default admin password, can be changed before first start of grafana, or in profile settings
+;admin_password = admin
+
+# used for signing
+;secret_key = SW2YcwTIb9zpOOhoPsMm
+
+# disable gravatar profile images
+;disable_gravatar = false
+
+# data source proxy whitelist (ip_or_domain:port separated by spaces)
+;data_source_proxy_whitelist =
+
+# disable protection against brute force login attempts
+;disable_brute_force_login_protection = false
+
+# set to true if you host Grafana behind HTTPS. default is false.
+;cookie_secure = false
+
+# set cookie SameSite attribute. defaults to `lax`. can be set to "lax", "strict", "none" and "disabled"
+;cookie_samesite = none
+
+# set to true if you want to allow browsers to render Grafana in a <frame>, <iframe>, <embed> or <object>. default is false.
+allow_embedding = true
+
+# Set to true if you want to enable http strict transport security (HSTS) response header.
+# This is only sent when HTTPS is enabled in this configuration.
+# HSTS tells browsers that the site should only be accessed using HTTPS.
+# The default version will change to true in the next minor release, 6.3.
+;strict_transport_security = false
+
+# Sets how long a browser should cache HSTS. Only applied if strict_transport_security is enabled.
+;strict_transport_security_max_age_seconds = 86400
+
+# Set to true if to enable HSTS preloading option. Only applied if strict_transport_security is enabled.
+;strict_transport_security_preload = false
+
+# Set to true if to enable the HSTS includeSubDomains option. Only applied if strict_transport_security is enabled.
+;strict_transport_security_subdomains = false
+
+# Set to true to enable the X-Content-Type-Options response header.
+# The X-Content-Type-Options response HTTP header is a marker used by the server to indicate that the MIME types advertised
+# in the Content-Type headers should not be changed and be followed. The default will change to true in the next minor release, 6.3.
+;x_content_type_options = false
+
+# Set to true to enable the X-XSS-Protection header, which tells browsers to stop pages from loading
+# when they detect reflected cross-site scripting (XSS) attacks. The default will change to true in the next minor release, 6.3.
+;x_xss_protection = false
+
+#################################### Snapshots ###########################
+[snapshots]
+# snapshot sharing options
+;external_enabled = true
+;external_snapshot_url = https://snapshots-origin.raintank.io
+;external_snapshot_name = Publish to snapshot.raintank.io
+
+# Set to true to enable this Grafana instance act as an external snapshot server and allow unauthenticated requests for
+# creating and deleting snapshots.
+;public_mode = false
+
+# remove expired snapshot
+;snapshot_remove_expired = true
+
+#################################### Dashboards History ##################
+[dashboards]
+# Number dashboard versions to keep (per dashboard). Default: 20, Minimum: 1
+;versions_to_keep = 20
+
+# Minimum dashboard refresh interval. When set, this will restrict users to set the refresh interval of a dashboard lower than given interval. Per default this is 5 seconds.
+# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
+;min_refresh_interval = 5s
+
+#################################### Users ###############################
+[users]
+# disable user signup / registration
+;allow_sign_up = true
+
+# Allow non admin users to create organizations
+;allow_org_create = true
+
+# Set to true to automatically assign new users to the default organization (id 1)
+;auto_assign_org = true
+
+# Set this value to automatically add new users to the provided organization (if auto_assign_org above is set to true)
+;auto_assign_org_id = 1
+
+# Default role new users will be automatically assigned (if disabled above is set to true)
+;auto_assign_org_role = Viewer
+
+# Require email validation before sign up completes
+;verify_email_enabled = false
+
+# Background text for the user field on the login page
+;login_hint = email or username
+;password_hint = password
+
+# Default UI theme ("dark" or "light")
+;default_theme = dark
+
+# External user management, these options affect the organization users view
+;external_manage_link_url =
+;external_manage_link_name =
+;external_manage_info =
+
+# Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard.
+;viewers_can_edit = false
+
+# Editors can administrate dashboard, folders and teams they create
+;editors_can_admin = false
+
+[auth]
+# Login cookie name
+;login_cookie_name = grafana_session
+
+# The lifetime (days) an authenticated user can be inactive before being required to login at next visit. Default is 7 days,
+;login_maximum_inactive_lifetime_days = 7
+
+# The maximum lifetime (days) an authenticated user can be logged in since login time before being required to login. Default is 30 days.
+;login_maximum_lifetime_days = 30
+
+# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes.
+;token_rotation_interval_minutes = 10
+
+# Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false
+;disable_login_form = false
+
+# Set to true to disable the signout link in the side menu. useful if you use auth.proxy, defaults to false
+;disable_signout_menu = false
+
+# URL to redirect the user to after sign out
+;signout_redirect_url =
+
+# Set to true to attempt login with OAuth automatically, skipping the login screen.
+# This setting is ignored if multiple OAuth providers are configured.
+;oauth_auto_login = false
+
+# OAuth state max age cookie duration. Defaults to 60 seconds.
+;oauth_state_cookie_max_age = 60
+
+# limit of api_key seconds to live before expiration
+;api_key_max_seconds_to_live = -1
+
+#################################### Anonymous Auth ######################
+[auth.anonymous]
+# enable anonymous access
+enabled = true
+
+# specify organization name that should be used for unauthenticated users
+;org_name = Main Org.
+
+# specify role for unauthenticated users
+;org_role = Viewer
+
+#################################### Github Auth ##########################
+[auth.github]
+;enabled = false
+;allow_sign_up = true
+;client_id = some_id
+;client_secret = some_secret
+;scopes = user:email,read:org
+;auth_url = https://github.com/login/oauth/authorize
+;token_url = https://github.com/login/oauth/access_token
+;api_url = https://api.github.com/user
+;allowed_domains =
+;team_ids =
+;allowed_organizations =
+
+#################################### GitLab Auth #########################
+[auth.gitlab]
+;enabled = false
+;allow_sign_up = true
+;client_id = some_id
+;client_secret = some_secret
+;scopes = api
+;auth_url = https://gitlab.com/oauth/authorize
+;token_url = https://gitlab.com/oauth/token
+;api_url = https://gitlab.com/api/v4
+;allowed_domains =
+;allowed_groups =
+
+#################################### Google Auth ##########################
+[auth.google]
+;enabled = false
+;allow_sign_up = true
+;client_id = some_client_id
+;client_secret = some_client_secret
+;scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email
+;auth_url = https://accounts.google.com/o/oauth2/auth
+;token_url = https://accounts.google.com/o/oauth2/token
+;api_url = https://www.googleapis.com/oauth2/v1/userinfo
+;allowed_domains =
+;hosted_domain =
+
+#################################### Grafana.com Auth ####################
+[auth.grafana_com]
+;enabled = false
+;allow_sign_up = true
+;client_id = some_id
+;client_secret = some_secret
+;scopes = user:email
+;allowed_organizations =
+
+#################################### Azure AD OAuth #######################
+[auth.azuread]
+;name = Azure AD
+;enabled = false
+;allow_sign_up = true
+;client_id = some_client_id
+;client_secret = some_client_secret
+;scopes = openid email profile
+;auth_url = https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize
+;token_url = https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
+;allowed_domains =
+;allowed_groups =
+
+#################################### Okta OAuth #######################
+[auth.okta]
+;name = Okta
+;enabled = false
+;allow_sign_up = true
+;client_id = some_id
+;client_secret = some_secret
+;scopes = openid profile email groups
+;auth_url = https://<tenant-id>.okta.com/oauth2/v1/authorize
+;token_url = https://<tenant-id>.okta.com/oauth2/v1/token
+;api_url = https://<tenant-id>.okta.com/oauth2/v1/userinfo
+;allowed_domains =
+;allowed_groups =
+;role_attribute_path =
+
+#################################### Generic OAuth ##########################
+[auth.generic_oauth]
+;enabled = false
+;name = OAuth
+;allow_sign_up = true
+;client_id = some_id
+;client_secret = some_secret
+;scopes = user:email,read:org
+;email_attribute_name = email:primary
+;email_attribute_path =
+;auth_url = https://foo.bar/login/oauth/authorize
+;token_url = https://foo.bar/login/oauth/access_token
+;api_url = https://foo.bar/user
+;allowed_domains =
+;team_ids =
+;allowed_organizations =
+;role_attribute_path =
+;tls_skip_verify_insecure = false
+;tls_client_cert =
+;tls_client_key =
+;tls_client_ca =
+
+#################################### Basic Auth ##########################
+[auth.basic]
+;enabled = true
+
+#################################### Auth Proxy ##########################
+[auth.proxy]
+;enabled = false
+;header_name = X-WEBAUTH-USER
+;header_property = username
+;auto_sign_up = true
+;sync_ttl = 60
+;whitelist = 192.168.1.1, 192.168.2.1
+;headers = Email:X-User-Email, Name:X-User-Name
+# Read the auth proxy docs for details on what the setting below enables
+;enable_login_token = false
+
+#################################### Auth LDAP ##########################
+[auth.ldap]
+;enabled = false
+;config_file = /etc/grafana/ldap.toml
+;allow_sign_up = true
+
+# LDAP backround sync (Enterprise only)
+# At 1 am every day
+;sync_cron = "0 0 1 * * *"
+;active_sync_enabled = true
+
+#################################### SMTP / Emailing ##########################
+[smtp]
+;enabled = false
+;host = localhost:25
+;user =
+# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
+;password =
+;cert_file =
+;key_file =
+;skip_verify = false
+;from_address = admin@grafana.localhost
+;from_name = Grafana
+# EHLO identity in SMTP dialog (defaults to instance_name)
+;ehlo_identity = dashboard.example.com
+
+[emails]
+;welcome_email_on_sign_up = false
+;templates_pattern = emails/*.html
+
+#################################### Logging ##########################
+[log]
+# Either "console", "file", "syslog". Default is console and file
+# Use space to separate multiple modes, e.g. "console file"
+;mode = console file
+
+# Either "debug", "info", "warn", "error", "critical", default is "info"
+;level = info
+
+# optional settings to set different levels for specific loggers. Ex filters = sqlstore:debug
+;filters =
+
+# For "console" mode only
+[log.console]
+;level =
+
+# log line format, valid options are text, console and json
+;format = console
+
+# For "file" mode only
+[log.file]
+;level =
+
+# log line format, valid options are text, console and json
+;format = text
+
+# This enables automated log rotate(switch of following options), default is true
+;log_rotate = true
+
+# Max line number of single file, default is 1000000
+;max_lines = 1000000
+
+# Max size shift of single file, default is 28 means 1 << 28, 256MB
+;max_size_shift = 28
+
+# Segment log daily, default is true
+;daily_rotate = true
+
+# Expired days of log file(delete after max days), default is 7
+;max_days = 7
+
+[log.syslog]
+;level =
+
+# log line format, valid options are text, console and json
+;format = text
+
+# Syslog network type and address. This can be udp, tcp, or unix. If left blank, the default unix endpoints will be used.
+;network =
+;address =
+
+# Syslog facility. user, daemon and local0 through local7 are valid.
+;facility =
+
+# Syslog tag. By default, the process' argv[0] is used.
+;tag =
+
+#################################### Usage Quotas ########################
+[quota]
+; enabled = false
+
+#### set quotas to -1 to make unlimited. ####
+# limit number of users per Org.
+; org_user = 10
+
+# limit number of dashboards per Org.
+; org_dashboard = 100
+
+# limit number of data_sources per Org.
+; org_data_source = 10
+
+# limit number of api_keys per Org.
+; org_api_key = 10
+
+# limit number of orgs a user can create.
+; user_org = 10
+
+# Global limit of users.
+; global_user = -1
+
+# global limit of orgs.
+; global_org = -1
+
+# global limit of dashboards
+; global_dashboard = -1
+
+# global limit of api_keys
+; global_api_key = -1
+
+# global limit on number of logged in users.
+; global_session = -1
+
+#################################### Alerting ############################
+[alerting]
+# Disable alerting engine & UI features
+;enabled = true
+# Makes it possible to turn off alert rule execution but alerting UI is visible
+;execute_alerts = true
+
+# Default setting for new alert rules. Defaults to categorize error and timeouts as alerting. (alerting, keep_state)
+;error_or_timeout = alerting
+
+# Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok)
+;nodata_or_nullvalues = no_data
+
+# Alert notifications can include images, but rendering many images at the same time can overload the server
+# This limit will protect the server from render overloading and make sure notifications are sent out quickly
+;concurrent_render_limit = 5
+
+
+# Default setting for alert calculation timeout. Default value is 30
+;evaluation_timeout_seconds = 30
+
+# Default setting for alert notification timeout. Default value is 30
+;notification_timeout_seconds = 30
+
+# Default setting for max attempts to sending alert notifications. Default value is 3
+;max_attempts = 3
+
+# Makes it possible to enforce a minimal interval between evaluations, to reduce load on the backend
+;min_interval_seconds = 1
+
+#################################### Explore #############################
+[explore]
+# Enable the Explore section
+;enabled = true
+
+#################################### Internal Grafana Metrics ##########################
+# Metrics available at HTTP API Url /metrics
+[metrics]
+# Disable / Enable internal metrics
+;enabled = true
+# Graphite Publish interval
+;interval_seconds = 10
+# Disable total stats (stat_totals_*) metrics to be generated
+;disable_total_stats = false
+
+#If both are set, basic auth will be required for the metrics endpoint.
+; basic_auth_username =
+; basic_auth_password =
+
+# Send internal metrics to Graphite
+[metrics.graphite]
+# Enable by setting the address setting (ex localhost:2003)
+;address =
+;prefix = prod.grafana.%(instance_name)s.
+
+#################################### Grafana.com integration ##########################
+# Url used to import dashboards directly from Grafana.com
+[grafana_com]
+;url = https://grafana.com
+
+#################################### Distributed tracing ############
+[tracing.jaeger]
+# Enable by setting the address sending traces to jaeger (ex localhost:6831)
+;address = localhost:6831
+# Tag that will always be included in when creating new spans. ex (tag1:value1,tag2:value2)
+;always_included_tag = tag1:value1
+# Type specifies the type of the sampler: const, probabilistic, rateLimiting, or remote
+;sampler_type = const
+# jaeger samplerconfig param
+# for "const" sampler, 0 or 1 for always false/true respectively
+# for "probabilistic" sampler, a probability between 0 and 1
+# for "rateLimiting" sampler, the number of spans per second
+# for "remote" sampler, param is the same as for "probabilistic"
+# and indicates the initial sampling rate before the actual one
+# is received from the mothership
+;sampler_param = 1
+# Whether or not to use Zipkin propagation (x-b3- HTTP headers).
+;zipkin_propagation = false
+# Setting this to true disables shared RPC spans.
+# Not disabling is the most common setting when using Zipkin elsewhere in your infrastructure.
+;disable_shared_zipkin_spans = false
+
+#################################### External image storage ##########################
+[external_image_storage]
+# Used for uploading images to public servers so they can be included in slack/email messages.
+# you can choose between (s3, webdav, gcs, azure_blob, local)
+;provider =
+
+[external_image_storage.s3]
+;endpoint =
+;path_style_access =
+;bucket =
+;region =
+;path =
+;access_key =
+;secret_key =
+
+[external_image_storage.webdav]
+;url =
+;public_url =
+;username =
+;password =
+
+[external_image_storage.gcs]
+;key_file =
+;bucket =
+;path =
+
+[external_image_storage.azure_blob]
+;account_name =
+;account_key =
+;container_name =
+
+[external_image_storage.local]
+# does not require any configuration
+
+[rendering]
+# Options to configure a remote HTTP image rendering service, e.g. using https://github.com/grafana/grafana-image-renderer.
+# URL to a remote HTTP image renderer service, e.g. http://localhost:8081/render, will enable Grafana to render panels and dashboards to PNG-images using HTTP requests to an external service.
+;server_url =
+# If the remote HTTP image renderer service runs on a different server than the Grafana server you may have to configure this to a URL where Grafana is reachable, e.g. http://grafana.domain/.
+;callback_url =
+# Concurrent render request limit affects when the /render HTTP endpoint is used. Rendering many images at the same time can overload the server,
+# which this setting can help protect against by only allowing a certain amount of concurrent requests.
+;concurrent_render_request_limit = 30
+
+[panels]
+# If set to true Grafana will allow script tags in text panels. Not recommended as it enable XSS vulnerabilities.
+;disable_sanitize_html = false
+
+[plugins]
+;enable_alpha = false
+;app_tls_skip_verify_insecure = false
+# Enter a comma-separated list of plugin identifiers to identify plugins that are allowed to be loaded even if they lack a valid signature.
+;allow_loading_unsigned_plugins =
+
+#################################### Grafana Image Renderer Plugin ##########################
+[plugin.grafana-image-renderer]
+# Instruct headless browser instance to use a default timezone when not provided by Grafana, e.g. when rendering panel image of alert.
+# See ICU’s metaZones.txt (https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt) for a list of supported
+# timezone IDs. Fallbacks to TZ environment variable if not set.
+;rendering_timezone =
+
+# Instruct headless browser instance to use a default language when not provided by Grafana, e.g. when rendering panel image of alert.
+# Please refer to the HTTP header Accept-Language to understand how to format this value, e.g. 'fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5'.
+;rendering_language =
+
+# Instruct headless browser instance to use a default device scale factor when not provided by Grafana, e.g. when rendering panel image of alert.
+# Default is 1. Using a higher value will produce more detailed images (higher DPI), but will require more disk space to store an image.
+;rendering_viewport_device_scale_factor =
+
+# Instruct headless browser instance whether to ignore HTTPS errors during navigation. Per default HTTPS errors are not ignored. Due to
+# the security risk it's not recommended to ignore HTTPS errors.
+;rendering_ignore_https_errors =
+
+# Instruct headless browser instance whether to capture and log verbose information when rendering an image. Default is false and will
+# only capture and log error messages. When enabled, debug messages are captured and logged as well.
+# For the verbose information to be included in the Grafana server log you have to adjust the rendering log level to debug, configure
+# [log].filter = rendering:debug.
+;rendering_verbose_logging =
+
+# Instruct headless browser instance whether to output its debug and error messages into running process of remote rendering service.
+# Default is false. This can be useful to enable (true) when troubleshooting.
+;rendering_dumpio =
+
+# Additional arguments to pass to the headless browser instance. Default is --no-sandbox. The list of Chromium flags can be found
+# here (https://peter.sh/experiments/chromium-command-line-switches/). Multiple arguments is separated with comma-character.
+;rendering_args =
+
+# You can configure the plugin to use a different browser binary instead of the pre-packaged version of Chromium.
+# Please note that this is not recommended, since you may encounter problems if the installed version of Chrome/Chromium is not
+# compatible with the plugin.
+;rendering_chrome_bin =
+
+# Instruct how headless browser instances are created. Default is 'default' and will create a new browser instance on each request.
+# Mode 'clustered' will make sure that only a maximum of browsers/incognito pages can execute concurrently.
+# Mode 'reusable' will have one browser instance and will create a new incognito page on each request.
+;rendering_mode =
+
+# When rendering_mode = clustered you can instruct how many browsers or incognito pages can execute concurrently. Default is 'browser'
+# and will cluster using browser instances.
+# Mode 'context' will cluster using incognito pages.
+;rendering_clustering_mode =
+# When rendering_mode = clustered you can define maximum number of browser instances/incognito pages that can execute concurrently..
+;rendering_clustering_max_concurrency =
+
+# Limit the maxiumum viewport width, height and device scale factor that can be requested.
+;rendering_viewport_max_width =
+;rendering_viewport_max_height =
+;rendering_viewport_max_device_scale_factor =
+
+# Change the listening host and port of the gRPC server. Default host is 127.0.0.1 and default port is 0 and will automatically assign
+# a port not in use.
+;grpc_host =
+;grpc_port =
+
+[enterprise]
+# Path to a valid Grafana Enterprise license.jwt file
+;license_path =
+
+[feature_toggles]
+# enable features, separated by spaces
+;enable =
diff --git a/compose/grafana_conf/dashboards/apisix_http_prometheus.json b/compose/grafana_conf/dashboards/apisix_http_prometheus.json
new file mode 100644
index 0000000..8dd9f8b
--- /dev/null
+++ b/compose/grafana_conf/dashboards/apisix_http_prometheus.json
@@ -0,0 +1,956 @@
+{
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": "apisix",
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "limit": 100,
+ "name": "Annotations & Alerts",
+ "showIn": 0,
+ "type": "dashboard"
+ }
+ ]
+ },
+ "description": "MicroService API Gateway Apache APISIX",
+ "editable": true,
+ "gnetId": 11719,
+ "graphTooltip": 0,
+ "id": 10,
+ "iteration": 1591947413854,
+ "links": [],
+ "panels": [
+ {
+ "collapsed": false,
+ "datasource": "apisix",
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 0
+ },
+ "id": 10,
+ "panels": [],
+ "title": "Nginx",
+ "type": "row"
+ },
+ {
+ "cacheTimeout": null,
+ "colorBackground": false,
+ "colorValue": false,
+ "colors": [
+ "#299c46",
+ "rgba(237, 129, 40, 0.89)",
+ "#d44a3a"
+ ],
+ "datasource": "apisix",
+ "description": "",
+ "fieldConfig": {
+ "defaults": {
+ "custom": {}
+ },
+ "overrides": []
+ },
+ "format": "none",
+ "gauge": {
+ "maxValue": 100,
+ "minValue": 0,
+ "show": false,
+ "thresholdLabels": false,
+ "thresholdMarkers": true
+ },
+ "gridPos": {
+ "h": 5,
+ "w": 5,
+ "x": 0,
+ "y": 1
+ },
+ "id": 8,
+ "interval": null,
+ "links": [],
+ "mappingType": 1,
+ "mappingTypes": [
+ {
+ "name": "value to text",
+ "value": 1
+ },
+ {
+ "name": "range to text",
+ "value": 2
+ }
+ ],
+ "maxDataPoints": 100,
+ "nullPointMode": "connected",
+ "nullText": null,
+ "postfix": "",
+ "postfixFontSize": "50%",
+ "prefix": "",
+ "prefixFontSize": "50%",
+ "rangeMaps": [
+ {
+ "from": "null",
+ "text": "N/A",
+ "to": "null"
+ }
+ ],
+ "sparkline": {
+ "fillColor": "rgba(31, 118, 189, 0.18)",
+ "full": true,
+ "lineColor": "rgb(31, 120, 193)",
+ "show": true,
+ "ymax": null,
+ "ymin": null
+ },
+ "tableColumn": "Total",
+ "targets": [
+ {
+ "expr": "sum(apisix_nginx_http_current_connections{state=\"total\", instance=~\"$instance\"})",
+ "intervalFactor": 2,
+ "legendFormat": "Total",
+ "refId": "A"
+ }
+ ],
+ "thresholds": "",
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Total Connections",
+ "type": "singlestat",
+ "valueFontSize": "80%",
+ "valueMaps": [
+ {
+ "op": "=",
+ "text": "N/A",
+ "value": "null"
+ }
+ ],
+ "valueName": "current"
+ },
+ {
+ "cacheTimeout": null,
+ "colorBackground": false,
+ "colorValue": false,
+ "colors": [
+ "#299c46",
+ "rgba(237, 129, 40, 0.89)",
+ "#d44a3a"
+ ],
+ "datasource": "apisix",
+ "description": "",
+ "fieldConfig": {
+ "defaults": {
+ "custom": {}
+ },
+ "overrides": []
+ },
+ "format": "none",
+ "gauge": {
+ "maxValue": 100,
+ "minValue": 0,
+ "show": false,
+ "thresholdLabels": false,
+ "thresholdMarkers": true
+ },
+ "gridPos": {
+ "h": 5,
+ "w": 5,
+ "x": 5,
+ "y": 1
+ },
+ "id": 16,
+ "interval": null,
+ "links": [],
+ "mappingType": 1,
+ "mappingTypes": [
+ {
+ "name": "value to text",
+ "value": 1
+ },
+ {
+ "name": "range to text",
+ "value": 2
+ }
+ ],
+ "maxDataPoints": 100,
+ "nullPointMode": "connected",
+ "nullText": null,
+ "postfix": "",
+ "postfixFontSize": "50%",
+ "prefix": "",
+ "prefixFontSize": "50%",
+ "rangeMaps": [
+ {
+ "from": "null",
+ "text": "N/A",
+ "to": "null"
+ }
+ ],
+ "sparkline": {
+ "fillColor": "rgba(31, 118, 189, 0.18)",
+ "full": true,
+ "lineColor": "rgb(31, 120, 193)",
+ "show": true,
+ "ymax": null,
+ "ymin": null
+ },
+ "tableColumn": "Accepted",
+ "targets": [
+ {
+ "expr": "sum(apisix_nginx_http_current_connections{state=\"accepted\", instance=~\"$instance\"})",
+ "intervalFactor": 2,
+ "legendFormat": "Accepted",
+ "refId": "A"
+ }
+ ],
+ "thresholds": "",
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Accepted Connections",
+ "type": "singlestat",
+ "valueFontSize": "80%",
+ "valueMaps": [
+ {
+ "op": "=",
+ "text": "N/A",
+ "value": "null"
+ }
+ ],
+ "valueName": "current"
+ },
+ {
+ "cacheTimeout": null,
+ "colorBackground": false,
+ "colorValue": false,
+ "colors": [
+ "#299c46",
+ "rgba(237, 129, 40, 0.89)",
+ "#d44a3a"
+ ],
+ "datasource": "apisix",
+ "description": "",
+ "fieldConfig": {
+ "defaults": {
+ "custom": {}
+ },
+ "overrides": []
+ },
+ "format": "none",
+ "gauge": {
+ "maxValue": 100,
+ "minValue": 0,
+ "show": false,
+ "thresholdLabels": false,
+ "thresholdMarkers": true
+ },
+ "gridPos": {
+ "h": 5,
+ "w": 5,
+ "x": 10,
+ "y": 1
+ },
+ "id": 11,
+ "interval": null,
+ "links": [],
+ "mappingType": 1,
+ "mappingTypes": [
+ {
+ "name": "value to text",
+ "value": 1
+ },
+ {
+ "name": "range to text",
+ "value": 2
+ }
+ ],
+ "maxDataPoints": 100,
+ "nullPointMode": "connected",
+ "nullText": null,
+ "postfix": "",
+ "postfixFontSize": "50%",
+ "prefix": "",
+ "prefixFontSize": "50%",
+ "rangeMaps": [
+ {
+ "from": "null",
+ "text": "N/A",
+ "to": "null"
+ }
+ ],
+ "sparkline": {
+ "fillColor": "rgba(31, 118, 189, 0.18)",
+ "full": true,
+ "lineColor": "rgb(31, 120, 193)",
+ "show": true,
+ "ymax": null,
+ "ymin": null
+ },
+ "tableColumn": "Total",
+ "targets": [
+ {
+ "expr": "sum(apisix_nginx_http_current_connections{state=\"handled\", instance=~\"$instance\"})",
+ "intervalFactor": 2,
+ "legendFormat": "Total",
+ "refId": "A"
+ }
+ ],
+ "thresholds": "",
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Handled Connections",
+ "type": "singlestat",
+ "valueFontSize": "80%",
+ "valueMaps": [
+ {
+ "op": "=",
+ "text": "N/A",
+ "value": "null"
+ }
+ ],
+ "valueName": "current"
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "apisix",
+ "fieldConfig": {
+ "defaults": {
+ "custom": {}
+ },
+ "overrides": []
+ },
+ "fill": 1,
+ "fillGradient": 0,
+ "gridPos": {
+ "h": 6,
+ "w": 15,
+ "x": 0,
+ "y": 6
+ },
+ "hiddenSeries": false,
+ "id": 17,
+ "legend": {
+ "alignAsTable": false,
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "rightSide": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 1,
+ "nullPointMode": "null",
+ "options": {
+ "dataLinks": []
+ },
+ "percentage": false,
+ "pointradius": 2,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum(apisix_nginx_http_current_connections{state=~\"active|reading|writing|waiting\", instance=~\"$instance\"}) by (state)",
+ "intervalFactor": 1,
+ "legendFormat": "{{state}}",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Nginx connection state",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "collapsed": false,
+ "datasource": "apisix",
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 12
+ },
+ "id": 13,
+ "panels": [],
+ "title": "Bandwidth",
+ "type": "row"
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "apisix",
+ "fieldConfig": {
+ "defaults": {
+ "custom": {}
+ },
+ "overrides": []
+ },
+ "fill": 1,
+ "fillGradient": 0,
+ "gridPos": {
+ "h": 6,
+ "w": 15,
+ "x": 0,
+ "y": 13
+ },
+ "hiddenSeries": false,
+ "id": 6,
+ "legend": {
+ "alignAsTable": true,
+ "avg": true,
+ "current": true,
+ "max": true,
+ "min": true,
+ "rightSide": true,
+ "show": true,
+ "total": false,
+ "values": true
+ },
+ "lines": true,
+ "linewidth": 1,
+ "nullPointMode": "null",
+ "options": {
+ "dataLinks": []
+ },
+ "percentage": false,
+ "pointradius": 2,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum(rate(apisix_bandwidth{instance=~\"$instance\"}[30s])) by (type)",
+ "legendFormat": "{{type}}",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Total Bandwidth",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "apisix",
+ "fieldConfig": {
+ "defaults": {
+ "custom": {}
+ },
+ "overrides": []
+ },
+ "fill": 1,
+ "fillGradient": 0,
+ "gridPos": {
+ "h": 6,
+ "w": 7,
+ "x": 0,
+ "y": 19
+ },
+ "hiddenSeries": false,
+ "id": 19,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 1,
+ "nullPointMode": "null",
+ "options": {
+ "dataLinks": []
+ },
+ "percentage": false,
+ "pointradius": 2,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum(rate(apisix_bandwidth{type=\"egress\", service =~\"$service\",route=~\"$route\",instance=~\"$instance\"}[1m])) by (service)",
+ "legendFormat": "service:{{service}}",
+ "refId": "A"
+ },
+ {
+ "expr": "sum(rate(apisix_bandwidth{type=\"egress\", service =~\"$service\",route=~\"$route\",instance=~\"$instance\"}[1m])) by (route)",
+ "legendFormat": "route:{{route}}",
+ "refId": "B"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Egress per service/route",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "apisix",
+ "fieldConfig": {
+ "defaults": {
+ "custom": {}
+ },
+ "overrides": []
+ },
+ "fill": 1,
+ "fillGradient": 0,
+ "gridPos": {
+ "h": 6,
+ "w": 8,
+ "x": 7,
+ "y": 19
+ },
+ "hiddenSeries": false,
+ "id": 21,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 1,
+ "nullPointMode": "null",
+ "options": {
+ "dataLinks": []
+ },
+ "percentage": false,
+ "pointradius": 2,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum(irate(apisix_bandwidth{type=\"ingress\", service =~\"$service\",route=~\"$route\",instance=~\"$instance\"}[1m])) by (service)",
+ "legendFormat": "service:{{service}}",
+ "refId": "A"
+ },
+ {
+ "expr": "sum(irate(apisix_bandwidth{type=\"ingress\", service =~\"$service\",route=~\"$route\",instance=~\"$instance\"}[1m])) by (route)",
+ "legendFormat": "route:{{route}}",
+ "refId": "B"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Ingress per service/route",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "collapsed": false,
+ "datasource": "apisix",
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 25
+ },
+ "id": 15,
+ "panels": [],
+ "title": "HTTP Status",
+ "type": "row"
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "cacheTimeout": null,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "apisix",
+ "fieldConfig": {
+ "defaults": {
+ "custom": {}
+ },
+ "overrides": []
+ },
+ "fill": 3,
+ "fillGradient": 0,
+ "gridPos": {
+ "h": 6,
+ "w": 15,
+ "x": 0,
+ "y": 26
+ },
+ "hiddenSeries": false,
+ "id": 2,
+ "interval": "",
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 1,
+ "links": [],
+ "nullPointMode": "null",
+ "options": {
+ "dataLinks": []
+ },
+ "percentage": false,
+ "pluginVersion": "6.5.2",
+ "pointradius": 2,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [
+ {
+ "alias": "state",
+ "lines": true
+ }
+ ],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum(rate(apisix_http_status{code=~\"2..\",service=~\"$service\",route=~\"$route\",instance=~\"$instance\"}[30s])) by (status_2xx)",
+ "instant": false,
+ "intervalFactor": 1,
+ "legendFormat": "{{status_2xx}}",
+ "refId": "A"
+ },
+ {
+ "expr": "sum(rate(apisix_http_status{code=~\"3..\",service=~\"$service\",route=~\"$route\",instance=~\"$instance\"}[30s])) by (status_3xx)",
+ "legendFormat": "{{status_3xx}}",
+ "refId": "D"
+ },
+ {
+ "expr": "sum(rate(apisix_http_status{code=~\"4..\",service=~\"$service\",route=~\"$route\",instance=~\"$instance\"}[30s])) by (status_4xx)",
+ "intervalFactor": 1,
+ "legendFormat": "{{status_4xx}}",
+ "refId": "B"
+ },
+ {
+ "expr": "sum(rate(apisix_http_status{code=~\"5..\",service=~\"$service\",route=~\"$route\",instance=~\"$instance\"}[30s])) by (status_5xx)",
+ "legendFormat": "{{status_5xx}}",
+ "refId": "C"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Service HTTP Code",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ }
+ ],
+ "refresh": "5s",
+ "schemaVersion": 25,
+ "style": "dark",
+ "tags": [],
+ "templating": {
+ "list": [
+ {
+ "allValue": ".*",
+ "current": {
+ "selected": false,
+ "text": "All",
+ "value": "$__all"
+ },
+ "datasource": "apisix",
+ "definition": "label_values(apisix_http_status,service)",
+ "hide": 0,
+ "includeAll": true,
+ "label": null,
+ "multi": true,
+ "name": "service",
+ "options": [],
+ "query": "label_values(apisix_http_status,service)",
+ "refresh": 1,
+ "regex": "",
+ "skipUrlSync": false,
+ "sort": 1,
+ "tagValuesQuery": "",
+ "tags": [],
+ "tagsQuery": "",
+ "type": "query",
+ "useTags": false
+ },
+ {
+ "allValue": ".*",
+ "current": {
+ "selected": false,
+ "text": "All",
+ "value": "$__all"
+ },
+ "datasource": "apisix",
+ "definition": "label_values(apisix_http_status,route)",
+ "hide": 0,
+ "includeAll": true,
+ "label": null,
+ "multi": true,
+ "name": "route",
+ "options": [],
+ "query": "label_values(apisix_http_status,route)",
+ "refresh": 1,
+ "regex": "",
+ "skipUrlSync": false,
+ "sort": 1,
+ "tagValuesQuery": "",
+ "tags": [],
+ "tagsQuery": "",
+ "type": "query",
+ "useTags": false
+ },
+ {
+ "allValue": ".*",
+ "current": {
+ "selected": false,
+ "text": "All",
+ "value": "$__all"
+ },
+ "datasource": "apisix",
+ "definition": "label_values(apisix_http_status,instance)",
+ "hide": 0,
+ "includeAll": true,
+ "label": null,
+ "multi": true,
+ "name": "instance",
+ "options": [],
+ "query": "label_values(apisix_http_status,instance)",
+ "refresh": 2,
+ "regex": ".*",
+ "skipUrlSync": false,
+ "sort": 1,
+ "tagValuesQuery": "",
+ "tags": [],
+ "tagsQuery": "",
+ "type": "query",
+ "useTags": false
+ }
+ ]
+ },
+ "time": {
+ "from": "now-30m",
+ "to": "now"
+ },
+ "timepicker": {
+ "refresh_intervals": [
+ "5s",
+ "10s",
+ "30s",
+ "1m",
+ "5m",
+ "15m",
+ "30m",
+ "1h",
+ "2h",
+ "1d"
+ ]
+ },
+ "timezone": "",
+ "title": "Apache APISIX",
+ "uid": "bLlNuRLWz",
+ "version": 1
+}
\ No newline at end of file
diff --git a/compose/grafana_conf/provisioning/dashboards/all.yaml b/compose/grafana_conf/provisioning/dashboards/all.yaml
new file mode 100644
index 0000000..c58cbc6
--- /dev/null
+++ b/compose/grafana_conf/provisioning/dashboards/all.yaml
@@ -0,0 +1,11 @@
+apiVersion: 1
+
+providers:
+- name: 'default'
+ orgId: 1
+ folder: ''
+ type: file
+ disableDeletion: false
+ editable: false
+ options:
+ path: /var/lib/grafana/dashboards
\ No newline at end of file
diff --git a/compose/grafana_conf/provisioning/datasources/all.yaml b/compose/grafana_conf/provisioning/datasources/all.yaml
new file mode 100644
index 0000000..4245eac
--- /dev/null
+++ b/compose/grafana_conf/provisioning/datasources/all.yaml
@@ -0,0 +1,9 @@
+datasources:
+ - access: 'proxy'
+ editable: true
+ is_default: true
+ name: 'apisix'
+ org_id: 1
+ type: 'prometheus'
+ url: 'http://prometheus:9090'
+ version: 1
\ No newline at end of file
diff --git a/compose/manager_conf/build.sh b/compose/manager_conf/build.sh
new file mode 100755
index 0000000..efedb1c
--- /dev/null
+++ b/compose/manager_conf/build.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+
+pwd=`pwd`
+
+export MYSQL_SERVER_ADDRESS="192.17.5.14:3306"
+export MYSQL_USER=root
+export MYSQL_PASSWORD=123456
+export SYSLOG_HOST=127.0.0.1
+export APISIX_BASE_URL="http://192.17.5.11:9080/apisix/admin"
+export APISIX_API_KEY="edd1c9f034335f136f87ad84b625c8f1"
+
+sed -i -e "s%#mysqlAddress#%`echo $MYSQL_SERVER_ADDRESS`%g" ${pwd}/conf.json
+sed -i -e "s%#mysqlUser#%`echo $MYSQL_USER`%g" ${pwd}/conf.json
+sed -i -e "s%#mysqlPWD#%`echo $MYSQL_PASSWORD`%g" ${pwd}/conf.json
+sed -i -e "s%#syslogAddress#%`echo $SYSLOG_HOST`%g" ${pwd}/conf.json
+sed -i -e "s%#apisixBaseUrl#%`echo $APISIX_BASE_URL`%g" ${pwd}/conf.json
+sed -i -e "s%#apisixApiKey#%`echo $APISIX_API_KEY`%g" ${pwd}/conf.json
+
+cd /root/manager-api
+exec ./manager-api
diff --git a/compose/pics/grafana_1.png b/compose/pics/grafana_1.png
new file mode 100644
index 0000000..631276e
Binary files /dev/null and b/compose/pics/grafana_1.png differ
diff --git a/compose/pics/grafana_2.png b/compose/pics/grafana_2.png
new file mode 100644
index 0000000..03711eb
Binary files /dev/null and b/compose/pics/grafana_2.png differ
diff --git a/compose/pics/grafana_3.png b/compose/pics/grafana_3.png
new file mode 100644
index 0000000..5b2e834
Binary files /dev/null and b/compose/pics/grafana_3.png differ
diff --git a/compose/pics/grafana_4.png b/compose/pics/grafana_4.png
new file mode 100644
index 0000000..237e4dc
Binary files /dev/null and b/compose/pics/grafana_4.png differ
diff --git a/compose/pics/grafana_5.png b/compose/pics/grafana_5.png
new file mode 100644
index 0000000..dd9654d
Binary files /dev/null and b/compose/pics/grafana_5.png differ
diff --git a/compose/pics/grafana_6.png b/compose/pics/grafana_6.png
new file mode 100644
index 0000000..8931a0d
Binary files /dev/null and b/compose/pics/grafana_6.png differ
diff --git a/compose/pics/login.png b/compose/pics/login.png
new file mode 100644
index 0000000..c42e375
Binary files /dev/null and b/compose/pics/login.png differ
diff --git a/compose/prometheus_conf/prometheus.yml b/compose/prometheus_conf/prometheus.yml
new file mode 100644
index 0000000..0804c81
--- /dev/null
+++ b/compose/prometheus_conf/prometheus.yml
@@ -0,0 +1,23 @@
+global:
+ scrape_interval: 15s # By default, scrape targets every 15 seconds.
+
+ # Attach these labels to any time series or alerts when communicating with
+ # external systems (federation, remote storage, Alertmanager).
+ external_labels:
+ monitor: "codelab-monitor"
+
+# A scrape configuration containing exactly one endpoint to scrape:
+# Here it's Prometheus itself.
+scrape_configs:
+ # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
+ - job_name: "prometheus"
+
+ # Override the global default and scrape targets from this job every 5 seconds.
+ scrape_interval: 5s
+
+ static_configs:
+ - targets: ["localhost:9090"]
+ - job_name: "apisix"
+ metrics_path: "/apisix/prometheus/metrics"
+ static_configs:
+ - targets: ["192.17.5.11:9080"]