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:08 UTC

[incubator-apisix-dashboard] branch apisix-feat-revert created (now 4fef39c)

This is an automated email from the ASF dual-hosted git repository.

juzhiyuan pushed a change to branch apisix-feat-revert
in repository https://gitbox.apache.org/repos/asf/incubator-apisix-dashboard.git.


      at 4fef39c  Revert "feat: remove go (#1)"

This branch includes the following new commits:

     new 4fef39c  Revert "feat: remove go (#1)"

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[incubator-apisix-dashboard] 01/01: Revert "feat: remove go (#1)"

Posted by ju...@apache.org.
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
+
+![login](pics/login.png)
+
+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`
+
+![login](pics/grafana_1.png)
+
+click `Apache APISIX` dashboard, and you can see the page as follow
+
+![login](pics/grafana_2.png)
+
+click the button `shard dashboard` on the right of `Apache APISIX`
+
+![login](pics/grafana_3.png)
+
+copy the link, and then return to dashboard on the step 1
+
+![login](pics/grafana_4.png)
+
+click metric on the left, and then the config button
+
+Paste shared link
+
+![login](pics/grafana_5.png)
+
+save, and you can see the metrics
+ 
+![login](pics/grafana_6.png)
+ 
+
+
+
+
+
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"]