You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by ju...@apache.org on 2020/06/17 09:36:09 UTC

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

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

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

commit 4fef39ca53164d0f16e2a8e44a4c3a152aaa1f15
Author: juzhiyuan <jj...@gmail.com>
AuthorDate: Wed Jun 17 17:34:54 2020 +0800

    Revert "feat: remove go (#1)"
    
    This reverts commit ce18fc38ba220674fd56793e4f4255fc37eff39a.
---
 .github/workflows/api_ut.yml                       |  18 +
 README.md => README-dashboard.md                   |   0
 README.md                                          |  47 +-
 api/Dockerfile                                     |  49 ++
 api/README.md                                      |  22 +
 api/build.sh                                       |  30 +
 api/conf.json                                      |  19 +
 api/conf/conf.go                                   | 104 +++
 api/conf/conf.json                                 |  19 +
 api/conf/mysql.go                                  |  46 +
 api/docker-compose.yml                             |   9 +
 api/errno/error.go                                 |  98 +++
 api/filter/cors.go                                 |  33 +
 api/filter/logging.go                              |  84 ++
 api/filter/recover.go                              | 120 +++
 api/filter/request_id.go                           |  42 +
 api/go.mod                                         |  15 +
 api/go.sum                                         |  89 ++
 api/log/log.go                                     | 147 ++++
 api/main.go                                        |  65 ++
 api/route/healthz.go                               |  37 +
 api/route/plugin.go                                |  58 ++
 api/route/route.go                                 | 242 ++++++
 api/route/ssl.go                                   | 153 ++++
 api/script/db/schema.sql                           |  31 +
 api/service/base.go                                |  48 ++
 api/service/plugin.go                              |  61 ++
 api/service/route.go                               | 561 ++++++++++++
 api/service/route_test.go                          | 169 ++++
 api/service/ssl.go                                 | 300 +++++++
 api/utils/copy.go                                  |  41 +
 api/utils/http.go                                  | 113 +++
 compose/README.md                                  |  56 ++
 compose/apisix_conf/config.yaml                    | 137 +++
 compose/docker-compose.yml                         | 124 +++
 compose/grafana_conf/config/grafana.ini            | 756 ++++++++++++++++
 .../dashboards/apisix_http_prometheus.json         | 956 +++++++++++++++++++++
 .../grafana_conf/provisioning/dashboards/all.yaml  |  11 +
 .../grafana_conf/provisioning/datasources/all.yaml |   9 +
 compose/manager_conf/build.sh                      |  20 +
 compose/pics/grafana_1.png                         | Bin 0 -> 57816 bytes
 compose/pics/grafana_2.png                         | Bin 0 -> 127932 bytes
 compose/pics/grafana_3.png                         | Bin 0 -> 103384 bytes
 compose/pics/grafana_4.png                         | Bin 0 -> 82558 bytes
 compose/pics/grafana_5.png                         | Bin 0 -> 105366 bytes
 compose/pics/grafana_6.png                         | Bin 0 -> 179132 bytes
 compose/pics/login.png                             | Bin 0 -> 84958 bytes
 compose/prometheus_conf/prometheus.yml             |  23 +
 48 files changed, 4921 insertions(+), 41 deletions(-)

diff --git a/.github/workflows/api_ut.yml b/.github/workflows/api_ut.yml
new file mode 100644
index 0000000..2b3c4bb
--- /dev/null
+++ b/.github/workflows/api_ut.yml
@@ -0,0 +1,18 @@
+name: API unit test
+
+on:
+  push:
+    branches: 
+      - master
+      - manager
+  pull_request:
+    branches:
+      - master
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: use docker-compose in api
+      run: cd ./api && docker-compose up
\ No newline at end of file
diff --git a/README.md b/README-dashboard.md
similarity index 100%
copy from README.md
copy to README-dashboard.md
diff --git a/README.md b/README.md
index 74f8748..8b13b91 100644
--- a/README.md
+++ b/README.md
@@ -1,47 +1,12 @@
-# READMD for Dashboard
+# Apache APISIX Dashboard
 
-This project is initialized with [Ant Design Pro](https://pro.ant.design). Follow is the quick guide for how to use.
+Dashboard for [Apache APISIX](https://github.com/apache/incubator-apisix-dashboard)
 
-## Environment Prepare
+## Deploy with Docker
 
-1. Make sure you have `Node.js` installed on your machine.
-2. Install [yarn](https://yarnpkg.com/).
-3. Install `node_modules`:
-
-```bash
-$ yarn
-```
-
-### Start project
-
-```bash
-yarn start:no-mock
-```
-
-### Build project
-
-```bash
-yarn build
-```
-
-### Check code style
-
-```bash
-yarn lint
-```
-
-You can also use script to auto fix some lint error:
-
-```bash
-yarn lint:fix
-```
-
-### Test code
-
-```bash
-yarn test
-```
+Please refer to [Deploy with Docker README](./compose/README.md)
 
 ## More
 
-You can view full document on our [official website](https://pro.ant.design). And welcome any feedback in our [github](https://github.com/ant-design/ant-design-pro).
+1. More infomation about the frontend Dashboard, please refer to [README for Dashboard](./README-dashboard.md)
+2. If you need the dashboard built with Vue.js, please refer to [master-vue](https://github.com/apache/incubator-apisix-dashboard/tree/master-vue).
diff --git a/api/Dockerfile b/api/Dockerfile
new file mode 100644
index 0000000..85092a1
--- /dev/null
+++ b/api/Dockerfile
@@ -0,0 +1,49 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+FROM golang:1.13.8 AS build-env
+
+WORKDIR /go/src/github.com/apisix/manager-api
+COPY . .
+RUN mkdir /root/manager-api \
+    && go env -w GOPROXY=https://goproxy.io,direct \
+    && export GOPROXY=https://goproxy.io \
+    && go build -o /root/manager-api/manager-api \
+    && mv /go/src/github.com/apisix/manager-api/build.sh /root/manager-api/ \
+    && mv /go/src/github.com/apisix/manager-api/conf.json /root/manager-api/ \
+    && rm -rf /go/src/github.com/apisix/manager-api \
+    && rm -rf /etc/localtime \
+    && ln -s  /usr/share/zoneinfo/Hongkong /etc/localtime \
+    && dpkg-reconfigure -f noninteractive tzdata
+
+FROM alpine:3.11
+
+RUN mkdir /root/manager-api \
+   && apk update  \
+   && apk add ca-certificates \
+   && update-ca-certificates \
+   && apk add --no-cache libc6-compat \
+   && echo "hosts: files dns" > /etc/nsswitch.conf \
+   && rm -rf /var/cache/apk/*
+
+
+WORKDIR /root/manager-api
+COPY --from=build-env /root/manager-api/* /root/manager-api/
+COPY --from=build-env /usr/share/zoneinfo/Hongkong /etc/localtime
+EXPOSE 8080
+RUN chmod +x ./build.sh
+CMD ["/root/manager-api/build.sh"]
diff --git a/api/README.md b/api/README.md
new file mode 100644
index 0000000..24befa6
--- /dev/null
+++ b/api/README.md
@@ -0,0 +1,22 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+# manager-api
+
+This is a back-end project that the dashboard depends on, implemented through golang.
diff --git a/api/build.sh b/api/build.sh
new file mode 100644
index 0000000..086fe73
--- /dev/null
+++ b/api/build.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+#	
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with	
+# this work for additional information regarding copyright ownership.	
+# The ASF licenses this file to You under the Apache License, Version 2.0	
+# (the "License"); you may not use this file except in compliance with	
+# the License.  You may obtain a copy of the License at	
+#	
+#     http://www.apache.org/licenses/LICENSE-2.0	
+#	
+# Unless required by applicable law or agreed to in writing, software	
+# distributed under the License is distributed on an "AS IS" BASIS,	
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.	
+# See the License for the specific language governing permissions and	
+# limitations under the License.	
+#
+
+pwd=`pwd`
+
+sed -i -e "s%#mysqlAddress#%`echo $MYSQL_SERVER_ADDRESS`%g" ${pwd}/conf.json
+sed -i -e "s%#mysqlUser#%`echo $MYSQL_USER`%g" ${pwd}/conf.json
+sed -i -e "s%#mysqlPWD#%`echo $MYSQL_PASSWORD`%g" ${pwd}/conf.json
+sed -i -e "s%#syslogAddress#%`echo $SYSLOG_HOST`%g" ${pwd}/conf.json
+sed -i -e "s%#apisixBaseUrl#%`echo $APISIX_BASE_URL`%g" ${pwd}/conf.json
+sed -i -e "s%#apisixApiKey#%`echo $APISIX_API_KEY`%g" ${pwd}/conf.json
+
+cd /root/manager-api
+exec ./manager-api
+
diff --git a/api/conf.json b/api/conf.json
new file mode 100644
index 0000000..31cbcce
--- /dev/null
+++ b/api/conf.json
@@ -0,0 +1,19 @@
+{
+  "conf": {
+    "mysql":{
+      "address": "#mysqlAddress#",
+      "user": "#mysqlUser#",
+      "password": "#mysqlPWD#",
+      "maxConns": 50,
+      "maxIdleConns": 25,
+      "maxLifeTime": 10
+    },
+    "syslog": {
+      "host": "#syslogAddress#"
+    },
+    "apisix": {
+      "base_url": "#apisixBaseUrl#",
+      "api_key": "#apisixApiKey#"
+    }
+  }
+}
diff --git a/api/conf/conf.go b/api/conf/conf.go
new file mode 100644
index 0000000..5819789
--- /dev/null
+++ b/api/conf/conf.go
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package conf
+
+import (
+	"fmt"
+	"github.com/tidwall/gjson"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"runtime"
+)
+
+const ServerPort = 8080
+const PROD = "prod"
+const BETA = "beta"
+const DEV = "dev"
+const LOCAL = "local"
+const confPath = "/root/manager-api/conf.json"
+const RequestId = "requestId"
+
+var (
+	ENV      string
+	basePath string
+	ApiKey   = "edd1c9f034335f136f87ad84b625c8f1"
+	BaseUrl  = "http://127.0.0.1:9080/apisix/admin"
+)
+
+func init() {
+	setEnvironment()
+	initMysql()
+	initApisix()
+}
+
+func setEnvironment() {
+	if env := os.Getenv("ENV"); env == "" {
+		ENV = LOCAL
+	} else {
+		ENV = env
+	}
+	_, basePath, _, _ = runtime.Caller(1)
+}
+
+func configurationPath() string {
+	if ENV == LOCAL {
+		return filepath.Join(filepath.Dir(basePath), "conf.json")
+	} else {
+		return confPath
+	}
+}
+
+type mysqlConfig struct {
+	Address  string
+	User     string
+	Password string
+
+	MaxConns     int
+	MaxIdleConns int
+	MaxLifeTime  int
+}
+
+var MysqlConfig mysqlConfig
+
+func initMysql() {
+	filePath := configurationPath()
+	if configurationContent, err := ioutil.ReadFile(filePath); err != nil {
+		panic(fmt.Sprintf("fail to read configuration: %s", filePath))
+	} else {
+		configuration := gjson.ParseBytes(configurationContent)
+		mysqlConf := configuration.Get("conf.mysql")
+		MysqlConfig.Address = mysqlConf.Get("address").String()
+		MysqlConfig.User = mysqlConf.Get("user").String()
+		MysqlConfig.Password = mysqlConf.Get("password").String()
+		MysqlConfig.MaxConns = int(mysqlConf.Get("maxConns").Int())
+		MysqlConfig.MaxIdleConns = int(mysqlConf.Get("maxIdleConns").Int())
+		MysqlConfig.MaxLifeTime = int(mysqlConf.Get("maxLifeTime").Int())
+	}
+}
+
+func initApisix() {
+	filePath := configurationPath()
+	if configurationContent, err := ioutil.ReadFile(filePath); err != nil {
+		panic(fmt.Sprintf("fail to read configuration: %s", filePath))
+	} else {
+		configuration := gjson.ParseBytes(configurationContent)
+		apisixConf := configuration.Get("conf.apisix")
+		BaseUrl = apisixConf.Get("base_url").String()
+		ApiKey = apisixConf.Get("api_key").String()
+	}
+}
diff --git a/api/conf/conf.json b/api/conf/conf.json
new file mode 100644
index 0000000..95fe86e
--- /dev/null
+++ b/api/conf/conf.json
@@ -0,0 +1,19 @@
+{
+  "conf":{
+    "mysql":{
+      "address": "127.0.0.1:3306",
+      "user": "root",
+      "password": "123456",
+      "maxConns": 50,
+      "maxIdleConns": 25,
+      "maxLifeTime": 10
+    },
+    "syslog":{
+      "host": "localhost"
+    },
+    "apisix":{
+      "base_url": "http://127.0.0.1:9080/apisix/admin",
+      "api_key": "edd1c9f034335f136f87ad84b625c8f1"
+    }
+  }
+}
\ No newline at end of file
diff --git a/api/conf/mysql.go b/api/conf/mysql.go
new file mode 100644
index 0000000..62e0f9e
--- /dev/null
+++ b/api/conf/mysql.go
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package conf
+
+import (
+	"fmt"
+	"github.com/jinzhu/gorm"
+	_ "github.com/jinzhu/gorm/dialects/mysql"
+	"time"
+)
+
+var db *gorm.DB
+
+func DB() *gorm.DB {
+	return db
+}
+
+// InitializeMysql creates mysql's *sqlDB instance
+func InitializeMysql() {
+	dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local", MysqlConfig.User,
+		MysqlConfig.Password, MysqlConfig.Address, "manager")
+	if tmp, err := gorm.Open("mysql", dsn); err != nil {
+		panic(fmt.Sprintf("fail to connect to DB: %s for %s", err.Error(), dsn))
+	} else {
+		db = tmp
+		db.LogMode(true)
+		db.DB().SetMaxOpenConns(MysqlConfig.MaxConns)
+		db.DB().SetMaxIdleConns(MysqlConfig.MaxIdleConns)
+		db.DB().SetConnMaxLifetime(time.Duration(MysqlConfig.MaxLifeTime) * time.Minute)
+	}
+
+}
diff --git a/api/docker-compose.yml b/api/docker-compose.yml
new file mode 100644
index 0000000..79760d7
--- /dev/null
+++ b/api/docker-compose.yml
@@ -0,0 +1,9 @@
+version: "3"
+
+services:
+  manager:
+    image: golang:1.13.8
+    volumes:
+      - .:/go/src/github.com/apisix/manager-api
+    working_dir: /go/src/github.com/apisix/manager-api
+    command: go test -v github.com/apisix/manager-api/service
diff --git a/api/errno/error.go b/api/errno/error.go
new file mode 100644
index 0000000..20b7e24
--- /dev/null
+++ b/api/errno/error.go
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package errno
+
+import (
+	"encoding/json"
+	"fmt"
+)
+
+type Message struct {
+	Code string
+	Msg  string
+}
+
+var (
+	//AA 01 api-manager-api
+	SystemSuccess   = Message{"010000", "success"}
+	SystemError     = Message{"010001", "system error"}
+	BadRequestError = Message{Code: "010002", Msg: "Request format error"}
+	NotFoundError   = Message{Code: "010003", Msg: "No resources found"}
+
+	//BB 01 config module
+	ConfEnvError      = Message{"010101", "Environment variable not found: %s"}
+	ConfFilePathError = Message{"010102", "Error loading configuration file: %s"}
+
+	// BB 02 route module
+	RouteRequestError      = Message{"010201", "Route request parameters are abnormal: %s"}
+	ApisixRouteCreateError = Message{"010202", "Failed to create APISIX route: %s"}
+	DBRouteCreateError     = Message{"010203", "Route storage failure: %s"}
+	ApisixRouteUpdateError = Message{"010204", "Update APISIX routing failed: %s"}
+	ApisixRouteDeleteError = Message{"010205", "Failed to remove APISIX route: %s"}
+	DBRouteUpdateError     = Message{"010206", "Route update failed: %s"}
+	DBRouteDeleteError     = Message{"010207", "Route remove failed: %s"}
+
+	// 03 plugin module
+	ApisixPluginListError   = Message{"010301", "List APISIX plugins  failed: %s"}
+	ApisixPluginSchemaError = Message{"010301", "Find APISIX plugin schema failed: %s"}
+)
+
+type ManagerError struct {
+	TraceId string
+	Code    string
+	Msg     string
+	Data    interface{}
+}
+
+// toString
+func (e *ManagerError) Error() string {
+	return e.Msg
+}
+
+func FromMessage(m Message, args ...interface{}) *ManagerError {
+	return &ManagerError{TraceId: "", Code: m.Code, Msg: fmt.Sprintf(m.Msg, args...)}
+}
+
+func (e *ManagerError) Response() map[string]interface{} {
+	return map[string]interface{}{
+		"code": e.Code,
+		"msg":  e.Msg,
+	}
+}
+
+func (e *ManagerError) ItemResponse(data interface{}) map[string]interface{} {
+	return map[string]interface{}{
+		"code": e.Code,
+		"msg":  e.Msg,
+		"data": data,
+	}
+}
+
+func (e *ManagerError) ListResponse(count, list interface{}) map[string]interface{} {
+	return map[string]interface{}{
+		"code":  e.Code,
+		"msg":   e.Msg,
+		"count": count,
+		"list":  list,
+	}
+}
+
+func Success() []byte {
+	w := FromMessage(SystemSuccess).Response()
+	result, _ := json.Marshal(w)
+	return result
+}
diff --git a/api/filter/cors.go b/api/filter/cors.go
new file mode 100644
index 0000000..f2c7ac1
--- /dev/null
+++ b/api/filter/cors.go
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package filter
+
+import "github.com/gin-gonic/gin"
+
+func CORS() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
+		c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
+		c.Writer.Header().Set("Access-Control-Allow-Headers", "*")
+		c.Writer.Header().Set("Access-Control-Allow-Methods", "*")
+		if c.Request.Method == "OPTIONS" {
+			c.AbortWithStatus(204)
+			return
+		}
+		c.Next()
+	}
+}
diff --git a/api/filter/logging.go b/api/filter/logging.go
new file mode 100644
index 0000000..22f5528
--- /dev/null
+++ b/api/filter/logging.go
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package filter
+
+import (
+	"bytes"
+	"github.com/apisix/manager-api/errno"
+	"github.com/gin-gonic/gin"
+	"github.com/sirupsen/logrus"
+	"time"
+)
+
+func RequestLogHandler() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		start, host, remoteIP, path, method := time.Now(), c.Request.Host, c.ClientIP(), c.Request.URL.Path, c.Request.Method
+		var val interface{}
+		if method == "GET" {
+			val = c.Request.URL.Query()
+		} else {
+			val, _ = c.GetRawData()
+		}
+		c.Set("requestBody", val)
+		uuid, _ := c.Get("X-Request-Id")
+
+		param, _ := c.Get("requestBody")
+		switch param.(type) {
+		case []byte:
+			param = string(param.([]byte))
+		default:
+		}
+
+		blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
+		c.Writer = blw
+		c.Next()
+		latency := time.Now().Sub(start) / 1000000
+		statusCode := c.Writer.Status()
+		respBody := blw.body.String()
+		if uuid == "" {
+			uuid = c.Writer.Header().Get("X-Request-Id")
+		}
+		var errs []string
+		for _, err := range c.Errors {
+			if e, ok := err.Err.(*errno.ManagerError); ok {
+				errs = append(errs, e.Error())
+			}
+		}
+		logger.WithFields(logrus.Fields{
+			"requestId":  uuid,
+			"latency":    latency,
+			"remoteIp":   remoteIP,
+			"method":     method,
+			"path":       path,
+			"statusCode": statusCode,
+			"host":       host,
+			"params":     param,
+			"respBody":   respBody,
+			"errMsg":     errs,
+		}).Info("")
+	}
+}
+
+type bodyLogWriter struct {
+	gin.ResponseWriter
+	body *bytes.Buffer
+}
+
+func (w bodyLogWriter) Write(b []byte) (int, error) {
+	w.body.Write(b)
+	return w.ResponseWriter.Write(b)
+}
diff --git a/api/filter/recover.go b/api/filter/recover.go
new file mode 100644
index 0000000..433331f
--- /dev/null
+++ b/api/filter/recover.go
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package filter
+
+import (
+	"bytes"
+	"fmt"
+	"github.com/apisix/manager-api/log"
+	"github.com/gin-gonic/gin"
+	"github.com/sirupsen/logrus"
+	"io/ioutil"
+	"net/http"
+	"runtime"
+	"time"
+)
+
+var (
+	logger    = log.GetLogger()
+	dunno     = []byte("???")
+	centerDot = []byte("·")
+	dot       = []byte(".")
+	slash     = []byte("/")
+)
+
+func RecoverHandler() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		defer func() {
+			if err := recover(); err != nil {
+				uuid := c.Writer.Header().Get("X-Request-Id")
+				logger.WithFields(logrus.Fields{
+					"uuid": uuid,
+				})
+				stack := stack(3)
+				logger.Errorf("[Recovery] %s panic recovered:\n\n%s\n%s", timeFormat(time.Now()), err, stack)
+				c.AbortWithStatus(http.StatusInternalServerError)
+			}
+		}()
+		c.Next()
+	}
+}
+
+func WrapGo(f func(...interface{}), args ...interface{}) {
+	defer func() {
+		if err := recover(); err != nil {
+			stack := stack(3)
+			logger.Errorf("[Recovery] %s panic recovered:\n\n%s\n%s", timeFormat(time.Now()), err, stack)
+		}
+	}()
+	f(args...)
+}
+
+func stack(skip int) []byte {
+	buf := new(bytes.Buffer) // the returned data
+	// loaded file.
+	var lines [][]byte
+	var lastFile string
+	for i := skip; ; i++ {
+		pc, file, line, ok := runtime.Caller(i)
+		if !ok {
+			break
+		}
+		// Print this much at least.  If we can't find the source, it won't show.
+		fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
+		if file != lastFile {
+			data, err := ioutil.ReadFile(file)
+			if err != nil {
+				continue
+			}
+			lines = bytes.Split(data, []byte{'\n'})
+			lastFile = file
+		}
+		fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
+	}
+	return buf.Bytes()
+}
+
+// source returns a space-trimmed slice of the n'th line.
+func source(lines [][]byte, n int) []byte {
+	n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
+	if n < 0 || n >= len(lines) {
+		return dunno
+	}
+	return bytes.TrimSpace(lines[n])
+}
+
+// function returns, if possible, the name of the function containing the PC.
+func function(pc uintptr) []byte {
+	fn := runtime.FuncForPC(pc)
+	if fn == nil {
+		return dunno
+	}
+	name := []byte(fn.Name())
+	if lastslash := bytes.LastIndex(name, slash); lastslash >= 0 {
+		name = name[lastslash+1:]
+	}
+	if period := bytes.Index(name, dot); period >= 0 {
+		name = name[period+1:]
+	}
+	name = bytes.Replace(name, centerDot, dot, -1)
+	return name
+}
+
+func timeFormat(t time.Time) string {
+	var timeString = t.Format("2006/01/02 - 15:04:05")
+	return timeString
+}
diff --git a/api/filter/request_id.go b/api/filter/request_id.go
new file mode 100644
index 0000000..2992869
--- /dev/null
+++ b/api/filter/request_id.go
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package filter
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/satori/go.uuid"
+)
+
+func RequestId() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		// Check for incoming header, use it if exists
+		requestId := c.Request.Header.Get("X-Request-Id")
+
+		// Create request id with UUID4
+		if requestId == "" {
+			u4 := uuid.NewV4()
+			requestId = u4.String()
+		}
+
+		// Expose it for use in the application
+		c.Set("X-Request-Id", requestId)
+
+		// Set X-Request-Id header
+		c.Writer.Header().Set("X-Request-Id", requestId)
+		c.Next()
+	}
+}
diff --git a/api/go.mod b/api/go.mod
new file mode 100644
index 0000000..45372ec
--- /dev/null
+++ b/api/go.mod
@@ -0,0 +1,15 @@
+module github.com/apisix/manager-api
+
+go 1.13
+
+require (
+	github.com/gin-contrib/pprof v1.3.0
+	github.com/gin-gonic/gin v1.6.3
+	github.com/go-sql-driver/mysql v1.5.0
+	github.com/jinzhu/gorm v1.9.12
+	github.com/satori/go.uuid v1.2.0
+	github.com/sirupsen/logrus v1.6.0
+	github.com/stretchr/testify v1.4.0
+	github.com/tidwall/gjson v1.6.0
+	gopkg.in/resty.v1 v1.12.0
+)
diff --git a/api/go.sum b/api/go.sum
new file mode 100644
index 0000000..9e3e3e3
--- /dev/null
+++ b/api/go.sum
@@ -0,0 +1,89 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
+github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
+github.com/gin-contrib/pprof v1.3.0 h1:G9eK6HnbkSqDZBYbzG4wrjCsA4e+cvYAHUZw6W+W9K0=
+github.com/gin-contrib/pprof v1.3.0/go.mod h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwfBaM5OXaB0=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
+github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
+github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
+github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
+github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
+github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
+github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q=
+github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
+github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc=
+github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
+github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
+github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
+github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
+github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
+github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
+github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
+github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/api/log/log.go b/api/log/log.go
new file mode 100644
index 0000000..c1645e8
--- /dev/null
+++ b/api/log/log.go
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package log
+
+import (
+	"bufio"
+	"fmt"
+	"github.com/apisix/manager-api/conf"
+	"github.com/sirupsen/logrus"
+	"os"
+	"runtime"
+	"strings"
+)
+
+var logEntry *logrus.Entry
+
+func GetLogger() *logrus.Entry {
+	if logEntry == nil {
+		var log = logrus.New()
+		setNull(log)
+		log.SetLevel(logrus.DebugLevel)
+		if conf.ENV != conf.LOCAL {
+			log.SetLevel(logrus.ErrorLevel)
+		}
+		log.SetFormatter(&logrus.JSONFormatter{})
+		logEntry = log.WithFields(logrus.Fields{
+			"app": "manager-api",
+		})
+		if hook, err := createHook(); err == nil {
+			log.AddHook(hook)
+		}
+	}
+	return logEntry
+}
+
+func setNull(log *logrus.Logger) {
+	src, err := os.OpenFile(os.DevNull, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
+	if err != nil {
+		fmt.Println("err", err)
+	}
+	writer := bufio.NewWriter(src)
+	log.SetOutput(writer)
+}
+
+type Hook struct {
+	Formatter func(file, function string, line int) string
+}
+
+func createHook() (*Hook, error) {
+	return &Hook{
+		func(file, function string, line int) string {
+			return fmt.Sprintf("%s:%d", file, line)
+		},
+	}, nil
+}
+
+func (hook *Hook) Fire(entry *logrus.Entry) error {
+	str := hook.Formatter(findCaller(5))
+	en := entry.WithField("line", str)
+	en.Level = entry.Level
+	en.Message = entry.Message
+	en.Time = entry.Time
+	line, err := en.String()
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Unable to read entry, %v", err)
+		return err
+	}
+	switch en.Level {
+	case logrus.PanicLevel:
+		fmt.Print(line)
+		return nil
+	case logrus.FatalLevel:
+		fmt.Print(line)
+		return nil
+	case logrus.ErrorLevel:
+		fmt.Print(line)
+		return nil
+	case logrus.WarnLevel:
+		fmt.Print(line)
+		return nil
+	case logrus.InfoLevel:
+		fmt.Print(line)
+		return nil
+	case logrus.DebugLevel:
+		fmt.Print(line)
+		return nil
+	default:
+		return nil
+	}
+}
+
+func (hook *Hook) Levels() []logrus.Level {
+	return logrus.AllLevels
+}
+
+func findCaller(skip int) (string, string, int) {
+	var (
+		pc       uintptr
+		file     string
+		function string
+		line     int
+	)
+	for i := 0; i < 10; i++ {
+		pc, file, line = getCaller(skip + i)
+		if !strings.HasPrefix(file, "logrus") {
+			break
+		}
+	}
+	if pc != 0 {
+		frames := runtime.CallersFrames([]uintptr{pc})
+		frame, _ := frames.Next()
+		function = frame.Function
+	}
+	return file, function, line
+}
+
+func getCaller(skip int) (uintptr, string, int) {
+	pc, file, line, ok := runtime.Caller(skip)
+	if !ok {
+		return 0, "", 0
+	}
+	n := 0
+	for i := len(file) - 1; i > 0; i-- {
+		if file[i] == '/' {
+			n += 1
+			if n >= 2 {
+				file = file[i+1:]
+				break
+			}
+		}
+	}
+	return pc, file, line
+}
diff --git a/api/main.go b/api/main.go
new file mode 100644
index 0000000..3d756dd
--- /dev/null
+++ b/api/main.go
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package main
+
+import (
+	"fmt"
+	"github.com/apisix/manager-api/conf"
+	"github.com/apisix/manager-api/filter"
+	"github.com/apisix/manager-api/log"
+	"github.com/apisix/manager-api/route"
+	"github.com/gin-contrib/pprof"
+	"github.com/gin-gonic/gin"
+	"net/http"
+	"time"
+)
+
+var logger = log.GetLogger()
+
+func setUpRouter() *gin.Engine {
+	if conf.ENV != conf.LOCAL && conf.ENV != conf.BETA {
+		gin.SetMode(gin.DebugMode)
+	} else {
+		gin.SetMode(gin.ReleaseMode)
+	}
+	r := gin.New()
+
+	r.Use(filter.CORS(), filter.RequestId(), filter.RequestLogHandler(), filter.RecoverHandler())
+	route.AppendHealthCheck(r)
+	route.AppendRoute(r)
+	route.AppendSsl(r)
+	route.AppendPlugin(r)
+
+	pprof.Register(r)
+
+	return r
+}
+
+func main() {
+	// init
+	conf.InitializeMysql()
+	// routes
+	r := setUpRouter()
+	addr := fmt.Sprintf(":%d", conf.ServerPort)
+	s := &http.Server{
+		Addr:         addr,
+		Handler:      r,
+		ReadTimeout:  time.Duration(1000) * time.Millisecond,
+		WriteTimeout: time.Duration(5000) * time.Millisecond,
+	}
+	s.ListenAndServe()
+}
diff --git a/api/route/healthz.go b/api/route/healthz.go
new file mode 100644
index 0000000..97dcd04
--- /dev/null
+++ b/api/route/healthz.go
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package route
+
+import (
+	"github.com/apisix/manager-api/log"
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+var logger = log.GetLogger()
+
+func healthzHandler() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		c.Copy()
+		c.String(http.StatusOK, "pong")
+	}
+}
+
+func AppendHealthCheck(r *gin.Engine) *gin.Engine {
+	r.GET("/ping", healthzHandler())
+	return r
+}
diff --git a/api/route/plugin.go b/api/route/plugin.go
new file mode 100644
index 0000000..0032b13
--- /dev/null
+++ b/api/route/plugin.go
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package route
+
+import (
+	"encoding/json"
+	"github.com/apisix/manager-api/errno"
+	"github.com/apisix/manager-api/service"
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func AppendPlugin(r *gin.Engine) *gin.Engine {
+	r.GET("/apisix/admin/plugins", listPlugin)
+	r.GET("/apisix/admin/schema/plugins/:name", findSchema)
+	return r
+}
+
+func findSchema(c *gin.Context) {
+	name := c.Param("name")
+	request := &service.ApisixPluginRequest{Name: name}
+	if result, err := request.Schema(); err != nil {
+		e := errno.FromMessage(errno.ApisixPluginSchemaError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	} else {
+		resp, _ := json.Marshal(result)
+		c.Data(http.StatusOK, service.ContentType, resp)
+	}
+}
+
+func listPlugin(c *gin.Context) {
+	request := &service.ApisixPluginRequest{}
+	if result, err := request.List(); err != nil {
+		e := errno.FromMessage(errno.ApisixPluginListError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	} else {
+		resp, _ := json.Marshal(result)
+		c.Data(http.StatusOK, service.ContentType, resp)
+	}
+}
diff --git a/api/route/route.go b/api/route/route.go
new file mode 100644
index 0000000..efc95d2
--- /dev/null
+++ b/api/route/route.go
@@ -0,0 +1,242 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package route
+
+import (
+	"encoding/json"
+	"github.com/apisix/manager-api/conf"
+	"github.com/apisix/manager-api/errno"
+	"github.com/apisix/manager-api/service"
+	"github.com/gin-gonic/gin"
+	"github.com/satori/go.uuid"
+	"net/http"
+	"strconv"
+)
+
+func AppendRoute(r *gin.Engine) *gin.Engine {
+	r.POST("/apisix/admin/routes", createRoute)
+	r.GET("/apisix/admin/routes/:rid", findRoute)
+	r.GET("/apisix/admin/routes", listRoute)
+	r.PUT("/apisix/admin/routes/:rid", updateRoute)
+	r.DELETE("/apisix/admin/routes/:rid", deleteRoute)
+	return r
+}
+
+func listRoute(c *gin.Context) {
+	db := conf.DB()
+	size, _ := strconv.Atoi(c.Query("size"))
+	page, _ := strconv.Atoi(c.Query("page"))
+	if size == 0 {
+		size = 10
+	}
+	isSearch := true
+	if name, exist := c.GetQuery("name"); exist {
+		db = db.Where("name like ? ", "%"+name+"%")
+		isSearch = false
+	}
+	if description, exist := c.GetQuery("description"); exist {
+		db = db.Where("description like ? ", "%"+description+"%")
+		isSearch = false
+	}
+	if host, exist := c.GetQuery("host"); exist {
+		db = db.Where("hosts like ? ", "%"+host+"%")
+		isSearch = false
+	}
+	if uri, exist := c.GetQuery("uri"); exist {
+		db = db.Where("uris like ? ", "%"+uri+"%")
+		isSearch = false
+	}
+	if ip, exist := c.GetQuery("ip"); exist {
+		db = db.Where("upstream_nodes like ? ", "%"+ip+"%")
+		isSearch = false
+	}
+	// search
+	if isSearch {
+		if search, exist := c.GetQuery("search"); exist {
+			db = db.Where("name like ? ", "%"+search+"%").
+				Or("description like ? ", "%"+search+"%").
+				Or("hosts like ? ", "%"+search+"%").
+				Or("uris like ? ", "%"+search+"%").
+				Or("upstream_nodes like ? ", "%"+search+"%")
+		}
+	}
+	// todo params check
+	// mysql
+	routeList := []service.Route{}
+	var count int
+	if err := db.Order("priority, update_time desc").Table("routes").Offset((page - 1) * size).Limit(size).Find(&routeList).Count(&count).Error; err != nil {
+		e := errno.FromMessage(errno.RouteRequestError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	} else {
+		responseList := make([]service.RouteResponse, 0)
+		for _, r := range routeList {
+			response := &service.RouteResponse{}
+			response.Parse(&r)
+			responseList = append(responseList, *response)
+		}
+		result := &service.ListResponse{Count: count, Data: responseList}
+		resp, _ := json.Marshal(result)
+		c.Data(http.StatusOK, service.ContentType, resp)
+	}
+}
+
+func deleteRoute(c *gin.Context) {
+	rid := c.Param("rid")
+	// todo  params check
+	// delete from apisix
+	request := &service.ApisixRouteRequest{}
+	if _, err := request.Delete(rid); err != nil {
+		e := errno.FromMessage(errno.ApisixRouteDeleteError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+		return
+	} else {
+		// delete from mysql
+		rd := &service.Route{}
+		rd.ID = uuid.FromStringOrNil(rid)
+		if err := conf.DB().Delete(rd).Error; err != nil {
+			e := errno.FromMessage(errno.DBRouteDeleteError, err.Error())
+			logger.Error(e.Msg)
+			c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+			return
+		}
+	}
+	c.Data(http.StatusOK, service.ContentType, errno.Success())
+}
+func updateRoute(c *gin.Context) {
+	rid := c.Param("rid")
+	// todo  params check
+	param, exist := c.Get("requestBody")
+	if !exist || len(param.([]byte)) < 1 {
+		e := errno.FromMessage(errno.RouteRequestError, "route create with no post data")
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+	routeRequest := &service.RouteRequest{}
+	if err := routeRequest.Parse(param); err != nil {
+		e := errno.FromMessage(errno.RouteRequestError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+	logger.Info(routeRequest.Plugins)
+
+	arr := service.ToApisixRequest(routeRequest)
+	logger.Info(arr)
+	if resp, err := arr.Update(rid); err != nil {
+		e := errno.FromMessage(errno.ApisixRouteUpdateError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+		return
+	} else {
+		// update mysql
+		if rd, err := service.ToRoute(routeRequest, arr, uuid.FromStringOrNil(rid), resp); err != nil {
+			c.AbortWithStatusJSON(http.StatusInternalServerError, err.Response())
+			return
+		} else {
+			if err := conf.DB().Model(&service.Route{}).Update(rd).Error; err != nil {
+				e := errno.FromMessage(errno.DBRouteUpdateError, err.Error())
+				logger.Error(e.Msg)
+				c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+				return
+			}
+			logger.Info(rd)
+		}
+	}
+	c.Data(http.StatusOK, service.ContentType, errno.Success())
+}
+
+func findRoute(c *gin.Context) {
+	rid := c.Param("rid")
+	// todo  params check
+	// find from apisix
+	request := &service.ApisixRouteRequest{}
+	if response, err := request.FindById(rid); err != nil {
+		e := errno.FromMessage(errno.RouteRequestError, err.Error()+" route ID: "+rid)
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	} else {
+		// transfer response to dashboard struct
+		if result, err := response.Parse(); err != nil {
+			e := errno.FromMessage(errno.RouteRequestError, err.Error()+" route ID: "+rid)
+			logger.Error(e.Msg)
+			c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+			return
+		} else {
+			// need to find name from mysql temporary
+			route := &service.Route{}
+			if err := conf.DB().Table("routes").Where("id=?", rid).First(&route).Error; err != nil {
+				e := errno.FromMessage(errno.RouteRequestError, err.Error()+" route ID: "+rid)
+				logger.Error(e.Msg)
+				c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+				return
+			}
+			result.Name = route.Name
+			resp, _ := json.Marshal(result)
+			c.Data(http.StatusOK, service.ContentType, resp)
+		}
+	}
+}
+
+func createRoute(c *gin.Context) {
+	u4 := uuid.NewV4()
+	rid := u4.String()
+	// todo params check
+	param, exist := c.Get("requestBody")
+	if !exist || len(param.([]byte)) < 1 {
+		e := errno.FromMessage(errno.RouteRequestError, "route create with no post data")
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+	routeRequest := &service.RouteRequest{}
+	if err := routeRequest.Parse(param); err != nil {
+		e := errno.FromMessage(errno.RouteRequestError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+	logger.Info(routeRequest.Plugins)
+
+	arr := service.ToApisixRequest(routeRequest)
+	logger.Info(arr)
+	if resp, err := arr.Create(rid); err != nil {
+		e := errno.FromMessage(errno.ApisixRouteCreateError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+		return
+	} else {
+		// update mysql
+		if rd, err := service.ToRoute(routeRequest, arr, u4, resp); err != nil {
+			c.AbortWithStatusJSON(http.StatusInternalServerError, err.Response())
+			return
+		} else {
+			logger.Info(rd)
+			if err := conf.DB().Create(rd).Error; err != nil {
+				e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
+				logger.Error(e.Msg)
+				c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+				return
+			}
+		}
+	}
+	c.Data(http.StatusOK, service.ContentType, errno.Success())
+}
diff --git a/api/route/ssl.go b/api/route/ssl.go
new file mode 100644
index 0000000..2a9d6f7
--- /dev/null
+++ b/api/route/ssl.go
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package route
+
+import (
+	"net/http"
+	"strconv"
+
+	"github.com/gin-gonic/gin"
+	"github.com/satori/go.uuid"
+
+	"github.com/apisix/manager-api/errno"
+	"github.com/apisix/manager-api/service"
+)
+
+const contentType = "application/json"
+
+func AppendSsl(r *gin.Engine) *gin.Engine {
+	r.POST("/apisix/admin/check_ssl_cert", sslCheck)
+	r.GET("/apisix/admin/ssls", sslList)
+	r.POST("/apisix/admin/ssls", sslCreate)
+	r.GET("/apisix/admin/ssls/:id", sslItem)
+	r.PUT("/apisix/admin/ssls/:id", sslUpdate)
+	r.DELETE("/apisix/admin/ssls/:id", sslDelete)
+	return r
+}
+
+func sslList(c *gin.Context) {
+	size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
+	page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
+	// todo params check
+	resp, err := service.SslList(page, size)
+
+	if err != nil {
+		e := errno.FromMessage(errno.RouteRequestError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+
+	c.Data(http.StatusOK, service.ContentType, resp)
+}
+
+func sslItem(c *gin.Context) {
+	id := c.Param("id")
+
+	// todo params check
+	resp, err := service.SslItem(id)
+
+	if err != nil {
+		e := errno.FromMessage(errno.RouteRequestError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+
+	c.Data(http.StatusOK, service.ContentType, resp)
+}
+
+func sslCheck(c *gin.Context) {
+	// todo params check
+	param, exist := c.Get("requestBody")
+
+	if !exist || len(param.([]byte)) < 1 {
+		e := errno.FromMessage(errno.RouteRequestError, "route create with no post data")
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+
+	resp, err := service.SslCheck(param)
+	if err != nil {
+		e := errno.FromMessage(errno.RouteRequestError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+
+	c.Data(http.StatusOK, contentType, resp)
+}
+
+func sslCreate(c *gin.Context) {
+	// todo params check
+	param, exist := c.Get("requestBody")
+
+	u4 := uuid.NewV4()
+
+	if !exist || len(param.([]byte)) < 1 {
+		e := errno.FromMessage(errno.RouteRequestError, "route create with no post data")
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+
+	if err := service.SslCreate(param, u4.String()); err != nil {
+		e := errno.FromMessage(errno.ApisixRouteCreateError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+		return
+	}
+
+	c.Data(http.StatusOK, contentType, errno.Success())
+}
+
+func sslUpdate(c *gin.Context) {
+	// todo params check
+	param, exist := c.Get("requestBody")
+
+	id := c.Param("id")
+
+	if !exist || len(param.([]byte)) < 1 {
+		e := errno.FromMessage(errno.RouteRequestError, "route create with no post data")
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+
+	if err := service.SslUpdate(param, id); err != nil {
+		e := errno.FromMessage(errno.ApisixRouteCreateError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+		return
+	}
+
+	c.Data(http.StatusOK, contentType, errno.Success())
+}
+
+func sslDelete(c *gin.Context) {
+	id := c.Param("id")
+	// todo params check
+	if err := service.SslDelete(id); err != nil {
+		e := errno.FromMessage(errno.RouteRequestError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+
+	c.Data(http.StatusOK, service.ContentType, errno.Success())
+}
diff --git a/api/script/db/schema.sql b/api/script/db/schema.sql
new file mode 100644
index 0000000..69f088c
--- /dev/null
+++ b/api/script/db/schema.sql
@@ -0,0 +1,31 @@
+-- this is a db script for init
+CREATE DATABASE `manager`;
+use `manager`;
+CREATE TABLE `routes` (
+  `id` varchar(64) NOT NULL unique,
+  `name` varchar(200) NOT NULL unique, -- not support yet
+  `description` varchar(200) DEFAULT NULL,
+  `hosts` text,
+  `uris` text,
+  `upstream_nodes` text,
+  `upstream_id` varchar(32) , -- fk
+  `priority` int NOT NULL DEFAULT 0,
+  `state` int NOT NULL DEFAULT 1, -- 1-normal 0-disable
+  `content` text,
+  `content_admin_api` text,
+  `create_time` bigint(20),
+  `update_time` bigint(20),
+
+  PRIMARY KEY (`id`)
+) DEFAULT CHARSET=utf8;
+CREATE TABLE `ssls` (
+  `id` char(36) NOT NULL DEFAULT '',
+  `public_key` text NOT NULL,
+  `snis` text NOT NULL,
+  `validity_start` bigint(20) unsigned NOT NULL,
+  `validity_end` bigint(20) unsigned NOT NULL,
+  `status` tinyint(1) unsigned NOT NULL DEFAULT '1',
+  `create_time` bigint(20) unsigned NOT NULL,
+  `update_time` bigint(20) unsigned NOT NULL,
+  PRIMARY KEY (`id`)
+) DEFAULT CHARSET=utf8;
\ No newline at end of file
diff --git a/api/service/base.go b/api/service/base.go
new file mode 100644
index 0000000..6ee57b5
--- /dev/null
+++ b/api/service/base.go
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package service
+
+import (
+	"github.com/jinzhu/gorm"
+	"github.com/satori/go.uuid"
+	"time"
+)
+
+// Base contains common columns for all tables.
+type Base struct {
+	ID         uuid.UUID `json:"id",sql:"type:uuid;primary_key;"`
+	CreateTime int64     `json:"create_time"`
+	UpdateTime int64     `json:"update_time"`
+}
+
+// BeforeCreate will set a UUID rather than numeric ID.
+func (base *Base) BeforeCreate(scope *gorm.Scope) error {
+	timestamp := time.Now().Unix()
+	err := scope.SetColumn("UpdateTime", timestamp)
+	err = scope.SetColumn("CreateTime", timestamp)
+	if len(base.ID) == 0 {
+		uuid := uuid.NewV4()
+		err = scope.SetColumn("ID", uuid)
+		return err
+	}
+	return err
+}
+
+func (base *Base) BeforeSave(scope *gorm.Scope) error {
+	err := scope.SetColumn("UpdateTime", time.Now().Unix())
+	return err
+}
diff --git a/api/service/plugin.go b/api/service/plugin.go
new file mode 100644
index 0000000..92cd6c3
--- /dev/null
+++ b/api/service/plugin.go
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package service
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/apisix/manager-api/conf"
+	"github.com/apisix/manager-api/utils"
+)
+
+type ApisixPluginRequest struct {
+	Name string `json:"name"`
+}
+
+func (apr *ApisixPluginRequest) Schema() (map[string]interface{}, error) {
+	url := fmt.Sprintf("%s/schema/plugins/%s", conf.BaseUrl, apr.Name)
+	if resp, err := utils.Get(url); err != nil {
+		logger.Error(err.Error())
+		return nil, err
+	} else {
+		arresp := make(map[string]interface{})
+		if err := json.Unmarshal(resp, &arresp); err != nil {
+			logger.Error(err.Error())
+			return nil, err
+		} else {
+			return arresp, nil
+		}
+	}
+}
+
+func (apr *ApisixPluginRequest) List() ([]string, error) {
+
+	url := fmt.Sprintf("%s/plugins/list", conf.BaseUrl)
+	if resp, err := utils.Get(url); err != nil {
+		logger.Error(err.Error())
+		return nil, err
+	} else {
+		var arresp []string
+		if err := json.Unmarshal(resp, &arresp); err != nil {
+			logger.Error(err.Error())
+			return nil, err
+		} else {
+			return arresp, nil
+		}
+	}
+}
diff --git a/api/service/route.go b/api/service/route.go
new file mode 100644
index 0000000..ea8033d
--- /dev/null
+++ b/api/service/route.go
@@ -0,0 +1,561 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package service
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/apisix/manager-api/conf"
+	"github.com/apisix/manager-api/errno"
+	"github.com/apisix/manager-api/log"
+	"github.com/apisix/manager-api/utils"
+	"github.com/satori/go.uuid"
+	"time"
+)
+
+const (
+	ContentType      = "application/json"
+	HTTP             = "http"
+	HTTPS            = "https"
+	SCHEME           = "scheme"
+	WEBSOCKET        = "websocket"
+	REDIRECT         = "redirect"
+	PROXY_REWRIETE   = "proxy-rewrite"
+	UPATHTYPE_STATIC = "static"
+	UPATHTYPE_REGX   = "regx"
+	UPATHTYPE_KEEP   = "keep"
+)
+
+var logger = log.GetLogger()
+
+func (r *RouteRequest) Parse(body interface{}) error {
+	if err := json.Unmarshal(body.([]byte), r); err != nil {
+		r = nil
+		return err
+	} else {
+		if r.Uris == nil || len(r.Uris) < 1 {
+			r.Uris = []string{"/*"}
+		}
+	}
+	return nil
+}
+
+func (arr *ApisixRouteRequest) Parse(r *RouteRequest) {
+	arr.Desc = r.Desc
+	arr.Priority = r.Priority
+	arr.Methods = r.Methods
+	arr.Uris = r.Uris
+	arr.Hosts = r.Hosts
+	arr.Vars = r.Vars
+	arr.Upstream = r.Upstream
+	arr.Plugins = r.Plugins
+}
+
+func (rd *Route) Parse(r *RouteRequest, arr *ApisixRouteRequest) error {
+	//rd.Name = arr.Name
+	rd.Description = arr.Desc
+	// todo transfer
+	rd.Hosts = ""
+	rd.Uris = ""
+	rd.UpstreamNodes = ""
+	rd.UpstreamId = ""
+	if content, err := json.Marshal(r); err != nil {
+		return err
+	} else {
+		rd.Content = string(content)
+	}
+	timestamp := time.Now().Unix()
+	rd.CreateTime = timestamp
+	return nil
+}
+
+func (arr *ApisixRouteRequest) FindById(rid string) (*ApisixRouteResponse, error) {
+	url := fmt.Sprintf("%s/routes/%s", conf.BaseUrl, rid)
+	if resp, err := utils.Get(url); err != nil {
+		logger.Error(err.Error())
+		return nil, err
+	} else {
+		var arresp ApisixRouteResponse
+		if err := json.Unmarshal(resp, &arresp); err != nil {
+			logger.Error(err.Error())
+			return nil, err
+		} else {
+			return &arresp, nil
+		}
+	}
+}
+
+func (arr *ApisixRouteRequest) Update(rid string) (*ApisixRouteResponse, error) {
+	url := fmt.Sprintf("%s/routes/%s", conf.BaseUrl, rid)
+	if b, err := json.Marshal(arr); err != nil {
+		return nil, err
+	} else {
+    fmt.Println(string(b))
+		if resp, err := utils.Patch(url, b); err != nil {
+			logger.Error(err.Error())
+			return nil, err
+		} else {
+			var arresp ApisixRouteResponse
+			if err := json.Unmarshal(resp, &arresp); err != nil {
+				logger.Error(err.Error())
+				return nil, err
+			} else {
+				return &arresp, nil
+			}
+		}
+	}
+}
+
+func (arr *ApisixRouteRequest) Create(rid string) (*ApisixRouteResponse, error) {
+	url := fmt.Sprintf("%s/routes/%s", conf.BaseUrl, rid)
+	if b, err := json.Marshal(arr); err != nil {
+		return nil, err
+	} else {
+		fmt.Println(string(b))
+		if resp, err := utils.Put(url, b); err != nil {
+			logger.Error(err.Error())
+			return nil, err
+		} else {
+			var arresp ApisixRouteResponse
+			if err := json.Unmarshal(resp, &arresp); err != nil {
+				logger.Error(err.Error())
+				return nil, err
+			} else {
+				return &arresp, nil
+			}
+		}
+	}
+}
+
+func (arr *ApisixRouteRequest) Delete(rid string) (*ApisixRouteResponse, error) {
+	url := fmt.Sprintf("%s/routes/%s", conf.BaseUrl, rid)
+	if resp, err := utils.Delete(url); err != nil {
+		logger.Error(err.Error())
+		return nil, err
+	} else {
+		var arresp ApisixRouteResponse
+		if err := json.Unmarshal(resp, &arresp); err != nil {
+			logger.Error(err.Error())
+			return nil, err
+		} else {
+			return &arresp, nil
+		}
+	}
+}
+
+type RouteRequest struct {
+	ID               string                 `json:"id,omitempty"`
+	Name             string                 `json:"name"`
+	Desc             string                 `json:"desc,omitempty"`
+	Priority         int64                  `json:"priority,omitempty"`
+	Methods          []string               `json:"methods,omitempty"`
+	Uris             []string               `json:"uris"`
+	Hosts            []string               `json:"hosts,omitempty"`
+	Protocols        []string               `json:"protocols,omitempty"`
+	Redirect         *Redirect              `json:"redirect,omitempty"`
+	Vars             [][]string             `json:"vars,omitempty"`
+	Upstream         *Upstream              `json:"upstream,omitempty"`
+	UpstreamProtocol string                 `json:"upstream_protocol,omitempty"`
+	UpstreamPath     *UpstreamPath          `json:"upstream_path,omitempty"`
+	UpstreamHeader   map[string]string      `json:"upstream_header,omitempty"`
+	Plugins          map[string]interface{} `json:"plugins"`
+}
+
+func (r *ApisixRouteResponse) Parse() (*RouteRequest, error) {
+	o := r.Node.Value
+
+	//Protocols from vars and upstream
+	protocols := make([]string, 0)
+	if o.Upstream != nil && o.Upstream.EnableWebsocket {
+		protocols = append(protocols, WEBSOCKET)
+	}
+	flag := true
+	for _, t := range o.Vars {
+		if t[0] == SCHEME {
+			flag = false
+			protocols = append(protocols, t[2])
+		}
+	}
+	if flag {
+		protocols = append(protocols, HTTP)
+		protocols = append(protocols, HTTPS)
+	}
+	//Redirect from plugins
+	redirect := &Redirect{}
+	upstreamProtocol := UPATHTYPE_KEEP
+	upstreamHeader := make(map[string]string)
+	upstreamPath := &UpstreamPath{}
+	for k, v := range o.Plugins {
+		if k == REDIRECT {
+			if bytes, err := json.Marshal(v); err != nil {
+				return nil, err
+			} else {
+				if err := json.Unmarshal(bytes, redirect); err != nil {
+					return nil, err
+				}
+			}
+
+		}
+		if k == PROXY_REWRIETE {
+			pr := &ProxyRewrite{}
+			if bytes, err := json.Marshal(v); err != nil {
+				return nil, err
+			} else {
+				if err := json.Unmarshal(bytes, pr); err != nil {
+					return nil, err
+				} else {
+					if pr.Scheme != "" {
+						upstreamProtocol = pr.Scheme
+					}
+					upstreamHeader = pr.Headers
+					if pr.RegexUri == nil || len(pr.RegexUri) < 2 {
+						upstreamPath.UPathType = UPATHTYPE_STATIC
+						upstreamPath.To = pr.Uri
+					} else {
+						upstreamPath.UPathType = UPATHTYPE_REGX
+						upstreamPath.From = pr.RegexUri[0]
+						upstreamPath.To = pr.RegexUri[1]
+					}
+				}
+			}
+		}
+	}
+	//Vars
+	requestVars := make([][]string, 0)
+	for _, t := range o.Vars {
+		if t[0] != SCHEME {
+			requestVars = append(requestVars, t)
+		}
+	}
+	//Plugins
+	requestPlugins := utils.CopyMap(o.Plugins)
+	delete(requestPlugins, REDIRECT)
+
+	// check if upstream is not exist
+	if o.Upstream == nil {
+		upstreamProtocol = ""
+		upstreamHeader = nil
+		upstreamPath = nil
+	}
+  if upstreamPath != nil && upstreamPath.UPathType == "" {
+		upstreamPath = nil
+	}
+	result := &RouteRequest{
+		ID:               o.Id,
+		Desc:             o.Desc,
+		Priority:         o.Priority,
+		Methods:          o.Methods,
+		Uris:             o.Uris,
+		Hosts:            o.Hosts,
+		Redirect:         redirect,
+		Upstream:         o.Upstream,
+		UpstreamProtocol: upstreamProtocol,
+		UpstreamPath:     upstreamPath,
+		UpstreamHeader:   upstreamHeader,
+		Protocols:        protocols,
+		Vars:             requestVars,
+		Plugins:          requestPlugins,
+	}
+	return result, nil
+}
+
+type Redirect struct {
+	HttpToHttps bool   `json:"http_to_https,omitempty"`
+	Code        int64  `json:"code,omitempty"`
+	Uri         string `json:"uri,omitempty"`
+}
+
+type ProxyRewrite struct {
+	Uri      string            `json:"uri"`
+	RegexUri []string          `json:"regex_uri"`
+	Scheme   string            `json:"scheme"`
+	Host     string            `json:"host"`
+	Headers  map[string]string `json:"headers"`
+}
+
+func (r ProxyRewrite) MarshalJSON() ([]byte, error) {
+	m := make(map[string]interface{})
+	if r.RegexUri != nil {
+		m["regex_uri"] = r.RegexUri
+	}
+	if r.Uri != "" {
+		m["uri"] = r.Uri
+	}
+	if r.Scheme != UPATHTYPE_KEEP && r.Scheme != "" {
+		m["scheme"] = r.Scheme
+	}
+	if r.Host != "" {
+		m["host"] = r.Host
+	}
+	if r.Headers != nil && len(r.Headers) > 0 {
+		m["headers"] = r.Headers
+	}
+	if result, err := json.Marshal(m); err != nil {
+		return nil, err
+	} else {
+		return result, nil
+	}
+}
+
+func (r Redirect) MarshalJSON() ([]byte, error) {
+	m := make(map[string]interface{})
+	if r.HttpToHttps {
+		m["http_to_https"] = true
+	} else {
+		m["code"] = r.Code
+		m["uri"] = r.Uri
+	}
+	if result, err := json.Marshal(m); err != nil {
+		return nil, err
+	} else {
+		return result, nil
+	}
+}
+
+type Upstream struct {
+	UType           string           `json:"type"`
+	Nodes           map[string]int64 `json:"nodes"`
+	Timeout         UpstreamTimeout  `json:"timeout"`
+	EnableWebsocket bool             `json:"enable_websocket"`
+}
+
+type UpstreamTimeout struct {
+	Connect int64 `json:"connect"`
+	Send    int64 `json:"send"`
+	Read    int64 `json:"read"`
+}
+
+type UpstreamPath struct {
+	UPathType string `json:"type"`
+	From      string `json:"from"`
+	To        string `json:"to"`
+}
+
+type ApisixRouteRequest struct {
+	Desc     string                 `json:"desc,omitempty"`
+	Priority int64                  `json:"priority"`
+	Methods  []string               `json:"methods,omitempty"`
+	Uris     []string               `json:"uris,omitempty"`
+	Hosts    []string               `json:"hosts,omitempty"`
+	Vars     [][]string             `json:"vars,omitempty"`
+	Upstream *Upstream              `json:"upstream,omitempty"`
+	Plugins  map[string]interface{} `json:"plugins,omitempty"`
+	//Name     string                 `json:"name"`
+}
+
+// ApisixRouteResponse is response from apisix admin api
+type ApisixRouteResponse struct {
+	Action string `json:"action"`
+	Node   *Node  `json:"node"`
+}
+
+type Node struct {
+	Value         Value  `json:"value"`
+	ModifiedIndex uint64 `json:"modifiedIndex"`
+}
+
+type Value struct {
+	Id         string                 `json:"id"`
+	Name       string                 `json:"name"`
+	Desc       string                 `json:"desc,omitempty"`
+	Priority   int64                  `json:"priority"`
+	Methods    []string               `json:"methods"`
+	Uris       []string               `json:"uris"`
+	Hosts      []string               `json:"hosts"`
+	Vars       [][]string             `json:"vars"`
+	Upstream   *Upstream              `json:"upstream,omitempty"`
+	UpstreamId string                 `json:"upstream_id,omitempty"`
+	Plugins    map[string]interface{} `json:"plugins"`
+}
+
+type Route struct {
+	Base
+	Name            string `json:"name"`
+	Description     string `json:"description,omitempty"`
+	Hosts           string `json:"hosts"`
+	Uris            string `json:"uris"`
+	UpstreamNodes   string `json:"upstream_nodes"`
+	UpstreamId      string `json:"upstream_id"`
+	Priority        int64  `json:"priority"`
+	Content         string `json:"content"`
+	ContentAdminApi string `json:"content_admin_api"`
+}
+
+type RouteResponse struct {
+	Base
+	Name        string    `json:"name"`
+	Description string    `json:"description,omitempty"`
+	Hosts       []string  `json:"hosts,omitempty"`
+	Uris        []string  `json:"uris,omitempty"`
+	Upstream    *Upstream `json:"upstream,omitempty"`
+	UpstreamId  string    `json:"upstream_id,omitempty"`
+	Priority    int64     `json:"priority"`
+}
+
+type ListResponse struct {
+	Count int         `json:"count"`
+	Data  interface{} `json:"data"`
+}
+
+func (rr *RouteResponse) Parse(r *Route) {
+	rr.Base = r.Base
+	rr.Name = r.Name
+	rr.Description = r.Description
+	rr.UpstreamId = r.UpstreamId
+	rr.Priority = r.Priority
+	// hosts
+	if len(r.Hosts) > 0 {
+		var hosts []string
+		if err := json.Unmarshal([]byte(r.Hosts), &hosts); err == nil {
+			rr.Hosts = hosts
+		} else {
+			logger.Error(err.Error())
+		}
+	}
+
+	// uris
+	if len(r.Uris) > 0 {
+		var uris []string
+		if err := json.Unmarshal([]byte(r.Uris), &uris); err == nil {
+			rr.Uris = uris
+		}
+	}
+
+	// uris
+	var resp ApisixRouteResponse
+	if err := json.Unmarshal([]byte(r.ContentAdminApi), &resp); err == nil {
+		rr.Upstream = resp.Node.Value.Upstream
+	}
+}
+
+// RouteRequest -> ApisixRouteRequest
+func ToApisixRequest(routeRequest *RouteRequest) *ApisixRouteRequest {
+	// redirect -> plugins
+	plugins := utils.CopyMap(routeRequest.Plugins)
+	redirect := routeRequest.Redirect
+	if redirect != nil {
+		plugins["redirect"] = redirect
+	}
+
+	logger.Info(routeRequest.Plugins)
+
+	// scheme https and not http -> vars ['scheme', '==', 'https']
+	pMap := utils.Set2Map(routeRequest.Protocols)
+
+	arr := &ApisixRouteRequest{}
+	arr.Parse(routeRequest)
+
+	// protocols[websokect] -> upstream
+	if pMap[WEBSOCKET] == 1 && arr.Upstream != nil {
+		arr.Upstream.EnableWebsocket = true
+	}
+	vars := utils.CopyStrings(routeRequest.Vars)
+	if pMap[HTTP] != 1 || pMap[HTTPS] != 1 {
+		if pMap[HTTP] == 1 {
+			vars = append(vars, []string{SCHEME, "==", HTTP})
+		}
+		if pMap[HTTPS] == 1 {
+			vars = append(vars, []string{SCHEME, "==", HTTPS})
+		}
+	}
+	if len(vars) > 0 {
+		arr.Vars = vars
+	} else {
+		arr.Vars = nil
+	}
+
+	// upstream protocol
+	if arr.Upstream != nil {
+		pr := &ProxyRewrite{}
+		pr.Scheme = routeRequest.UpstreamProtocol
+		// upstream path
+		proxyPath := routeRequest.UpstreamPath
+		if proxyPath != nil {
+			if proxyPath.UPathType == UPATHTYPE_STATIC {
+				pr.Uri = proxyPath.To
+				pr.RegexUri = nil
+			} else {
+				pr.RegexUri = []string{proxyPath.From, proxyPath.To}
+			}
+		}
+		// upstream headers
+		pr.Headers = routeRequest.UpstreamHeader
+		if proxyPath != nil {
+			plugins[PROXY_REWRIETE] = pr
+		}
+	}
+
+	if plugins != nil && len(plugins) > 0 {
+		arr.Plugins = plugins
+	} else {
+		arr.Plugins = nil
+	}
+	return arr
+}
+
+func ToRoute(routeRequest *RouteRequest,
+	arr *ApisixRouteRequest,
+	u4 uuid.UUID,
+	resp *ApisixRouteResponse) (*Route, *errno.ManagerError) {
+	rd := &Route{}
+	if err := rd.Parse(routeRequest, arr); err != nil {
+		e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
+		return nil, e
+	}
+	if rd.Name == "" {
+		rd.Name = routeRequest.Name
+	}
+	rd.ID = u4
+	// content_admin_api
+	if respStr, err := json.Marshal(resp); err != nil {
+		e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
+		return nil, e
+	} else {
+		rd.ContentAdminApi = string(respStr)
+	}
+	// hosts
+	hosts := resp.Node.Value.Hosts
+	if hb, err := json.Marshal(hosts); err != nil {
+		e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
+		logger.Warn(e.Msg)
+	} else {
+		rd.Hosts = string(hb)
+	}
+	// uris
+	uris := resp.Node.Value.Uris
+	if ub, err := json.Marshal(uris); err != nil {
+		e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
+		logger.Warn(e.Msg)
+	} else {
+		rd.Uris = string(ub)
+	}
+	// upstreamNodes
+	if resp.Node.Value.Upstream != nil {
+		nodes := resp.Node.Value.Upstream.Nodes
+		ips := make([]string, 0)
+		for k, _ := range nodes {
+			ips = append(ips, k)
+		}
+		if nb, err := json.Marshal(ips); err != nil {
+			e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
+			logger.Warn(e.Msg)
+		} else {
+			rd.UpstreamNodes = string(nb)
+		}
+	}
+	return rd, nil
+}
diff --git a/api/service/route_test.go b/api/service/route_test.go
new file mode 100644
index 0000000..74ab02f
--- /dev/null
+++ b/api/service/route_test.go
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package service
+
+import (
+	"encoding/json"
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestRedirectMarshal(t *testing.T) {
+	a := assert.New(t)
+	r := &Redirect{
+		HttpToHttps: true,
+		Code:        302,
+		Uri:         "/hello",
+	}
+	if result, err := json.Marshal(r); err != nil {
+		t.Error(err.Error())
+	} else {
+		h := make(map[string]interface{})
+		json.Unmarshal(result, &h)
+		a.Equal(1, len(h))
+		a.Equal(nil, h["code"])
+		a.Equal(true, h["http_to_https"])
+	}
+}
+
+func TestToApisixRequest_RediretPlugins(t *testing.T) {
+	rr := &RouteRequest{
+		ID:        "u guess a uuid",
+		Name:      "a special name",
+		Desc:      "any description",
+		Priority:  0,
+		Methods:   []string{"GET"},
+		Uris:      []string{},
+		Hosts:     []string{"www.baidu.com"},
+		Protocols: []string{"http", "https", "websocket"},
+		Redirect:  &Redirect{HttpToHttps: true, Code: 200, Uri: "/hello"},
+		Vars:      [][]string{},
+	}
+	ar := ToApisixRequest(rr)
+	a := assert.New(t)
+	a.Equal(1, len(ar.Plugins))
+	a.NotEqual(nil, ar.Plugins["redirect"])
+}
+
+func TestToApisixRequest_Vars(t *testing.T) {
+	rr := &RouteRequest{
+		ID:        "u guess a uuid",
+		Name:      "a special name",
+		Desc:      "any description",
+		Priority:  0,
+		Methods:   []string{"GET"},
+		Uris:      []string{},
+		Hosts:     []string{"www.baidu.com"},
+		Protocols: []string{"http", "https", "websocket"},
+		Redirect:  &Redirect{HttpToHttps: true, Code: 200, Uri: "/hello"},
+		Vars:      [][]string{},
+	}
+	ar := ToApisixRequest(rr)
+	a := assert.New(t)
+	b, err := json.Marshal(ar)
+	a.Equal(nil, err)
+
+	m := make(map[string]interface{})
+	err = json.Unmarshal(b, &m)
+	a.Equal(nil, err)
+	t.Log(m["vars"])
+	a.Equal(nil, m["vars"])
+}
+
+func TestToApisixRequest_Upstream(t *testing.T) {
+	nodes := make(map[string]int64)
+	nodes["127.0.0.1:8080"] = 100
+	upstream := &Upstream{
+		UType:   "roundrobin",
+		Nodes:   nodes,
+		Timeout: UpstreamTimeout{15, 15, 15},
+	}
+	rr := &RouteRequest{
+		ID:        "u guess a uuid",
+		Name:      "a special name",
+		Desc:      "any description",
+		Priority:  0,
+		Methods:   []string{"GET"},
+		Uris:      []string{},
+		Hosts:     []string{"www.baidu.com"},
+		Protocols: []string{"http", "https", "websocket"},
+		Redirect:  &Redirect{HttpToHttps: true, Code: 200, Uri: "/hello"},
+		Vars:      [][]string{},
+		Upstream:  upstream,
+	}
+	ar := ToApisixRequest(rr)
+	a := assert.New(t)
+	a.Equal("roundrobin", ar.Upstream.UType)
+	a.Equal(true, ar.Upstream.EnableWebsocket)
+}
+
+func TestToApisixRequest_UpstreamUnable(t *testing.T) {
+	nodes := make(map[string]int64)
+	nodes["127.0.0.1:8080"] = 100
+	upstream := &Upstream{
+		UType:   "roundrobin",
+		Nodes:   nodes,
+		Timeout: UpstreamTimeout{15, 15, 15},
+	}
+	rr := &RouteRequest{
+		ID:        "u guess a uuid",
+		Name:      "a special name",
+		Desc:      "any description",
+		Priority:  0,
+		Methods:   []string{"GET"},
+		Uris:      []string{},
+		Hosts:     []string{"www.baidu.com"},
+		Protocols: []string{"http", "https"},
+		Redirect:  &Redirect{HttpToHttps: true, Code: 200, Uri: "/hello"},
+		Vars:      [][]string{},
+		Upstream:  upstream,
+	}
+	ar := ToApisixRequest(rr)
+	a := assert.New(t)
+	a.Equal("roundrobin", ar.Upstream.UType)
+	a.Equal(false, ar.Upstream.EnableWebsocket)
+}
+
+// no upstream
+func TestApisixRouteResponse_Parse(t *testing.T) {
+	a := assert.New(t)
+	plugins := make(map[string]interface{})
+	redirect := &Redirect{
+		Code: 302,
+		Uri:  "/foo",
+	}
+	plugins["redirect"] = redirect
+	arr := &ApisixRouteResponse{
+		Action: "get",
+		Node: &Node{
+			Value: Value{
+				Id:       "",
+				Name:     "",
+				Desc:     "",
+				Priority: 0,
+				Methods:  []string{"GET"},
+				Uris:     []string{"/*"},
+				Hosts:    []string{"www.baidu.com"},
+				Vars:     [][]string{},
+				Upstream: nil,
+				Plugins:  plugins,
+			},
+		},
+	}
+	_, err := arr.Parse()
+	a.Equal(nil, err)
+}
diff --git a/api/service/ssl.go b/api/service/ssl.go
new file mode 100644
index 0000000..839e3b8
--- /dev/null
+++ b/api/service/ssl.go
@@ -0,0 +1,300 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package service
+
+import (
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/json"
+	"encoding/pem"
+	"errors"
+	"fmt"
+
+	"github.com/satori/go.uuid"
+
+	"github.com/apisix/manager-api/conf"
+	"github.com/apisix/manager-api/errno"
+	"github.com/apisix/manager-api/utils"
+)
+
+type Ssl struct {
+	Base
+	ValidityStart uint64 `json:"validity_start"`
+	ValidityEnd   uint64 `json:"validity_end"`
+	Snis          string `json:"snis"`
+	Status        uint64 `json:"status"`
+	PublicKey     string `json:"public_key,omitempty"`
+}
+
+type SslDto struct {
+	Base
+	ValidityStart uint64   `json:"validity_start"`
+	ValidityEnd   uint64   `json:"validity_end"`
+	Snis          []string `json:"snis"`
+	Status        uint64   `json:"status"`
+	PublicKey     string   `json:"public_key,omitempty"`
+}
+
+type SslRequest struct {
+	ID         string   `json:"id,omitempty"`
+	PublicKey  string   `json:"cert"`
+	PrivateKey string   `json:"key"`
+	Snis       []string `json:"snis"`
+}
+
+// ApisixSslResponse is response from apisix admin api
+type ApisixSslResponse struct {
+	Action string   `json:"action"`
+	Node   *SslNode `json:"node"`
+}
+
+type SslNode struct {
+	Value         SslRequest `json:"value"`
+	ModifiedIndex uint64     `json:"modifiedIndex"`
+}
+
+func (req *SslRequest) Parse(body interface{}) {
+	if err := json.Unmarshal(body.([]byte), req); err != nil {
+		req = nil
+		logger.Error(errno.FromMessage(errno.RouteRequestError, err.Error()).Msg)
+	}
+}
+
+func (sslDto *SslDto) Parse(ssl *Ssl) error {
+	sslDto.ID = ssl.ID
+	sslDto.ValidityStart = ssl.ValidityStart
+	sslDto.ValidityEnd = ssl.ValidityEnd
+
+	var snis []string
+	_ = json.Unmarshal([]byte(ssl.Snis), &snis)
+	sslDto.Snis = snis
+
+	sslDto.Status = ssl.Status
+	sslDto.PublicKey = ssl.PublicKey
+	sslDto.CreateTime = ssl.CreateTime
+	sslDto.UpdateTime = ssl.UpdateTime
+
+	return nil
+}
+
+func SslList(page, size int) ([]byte, error) {
+	var count int
+	sslList := []Ssl{}
+	if err := conf.DB().Table("ssls").Offset((page - 1) * size).Limit(size).Find(&sslList).Count(&count).Error; err != nil {
+		return nil, err
+	}
+
+	sslDtoList := []SslDto{}
+
+	for _, ssl := range sslList {
+		sslDto := SslDto{}
+		sslDto.Parse(&ssl)
+
+		sslDtoList = append(sslDtoList, sslDto)
+	}
+
+	data := errno.FromMessage(errno.SystemSuccess).ListResponse(count, sslDtoList)
+
+	return json.Marshal(data)
+}
+
+func SslItem(id string) ([]byte, error) {
+	ssl := &Ssl{}
+	if err := conf.DB().Table("ssls").Where("id = ?", id).First(ssl).Error; err != nil {
+		return nil, err
+	}
+
+	sslDto := &SslDto{}
+	sslDto.Parse(ssl)
+
+	data := errno.FromMessage(errno.SystemSuccess).ItemResponse(sslDto)
+
+	return json.Marshal(data)
+}
+
+func SslCheck(param interface{}) ([]byte, error) {
+	sslReq := &SslRequest{}
+	sslReq.Parse(param)
+
+	ssl, err := ParseCert(sslReq.PublicKey, sslReq.PrivateKey)
+
+	if err != nil {
+		return nil, err
+	}
+
+	ssl.PublicKey = ""
+
+	sslDto := &SslDto{}
+	sslDto.Parse(ssl)
+
+	data := errno.FromMessage(errno.SystemSuccess).ItemResponse(sslDto)
+
+	return json.Marshal(data)
+}
+
+func SslCreate(param interface{}, id string) error {
+	sslReq := &SslRequest{}
+	sslReq.Parse(param)
+
+	ssl, err := ParseCert(sslReq.PublicKey, sslReq.PrivateKey)
+
+	if err != nil {
+		return err
+	}
+
+	// first admin api
+	var snis []string
+	_ = json.Unmarshal([]byte(ssl.Snis), &snis)
+	sslReq.Snis = snis
+
+	if _, err := sslReq.PutToApisix(id); err != nil {
+		return err
+	}
+	// then mysql
+	ssl.ID = uuid.FromStringOrNil(id)
+	if err := conf.DB().Create(ssl).Error; err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func SslUpdate(param interface{}, id string) error {
+	sslReq := &SslRequest{}
+	sslReq.Parse(param)
+
+	ssl, err := ParseCert(sslReq.PublicKey, sslReq.PrivateKey)
+
+	if err != nil {
+		return err
+	}
+
+	// first admin api
+	var snis []string
+	_ = json.Unmarshal([]byte(ssl.Snis), &snis)
+	sslReq.Snis = snis
+
+	if _, err := sslReq.PutToApisix(id); err != nil {
+		return err
+	}
+
+	// then mysql
+	ssl.ID = uuid.FromStringOrNil(id)
+	data := Ssl{PublicKey: ssl.PublicKey, Snis: ssl.Snis, ValidityStart: ssl.ValidityStart, ValidityEnd: ssl.ValidityEnd}
+	if err := conf.DB().Model(&ssl).Updates(data).Error; err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func SslDelete(id string) error {
+	// delete from apisix
+	request := &SslRequest{}
+	request.ID = id
+	if _, err := request.DeleteFromApisix(); err != nil {
+		return err
+	}
+	// delete from mysql
+	ssl := &Ssl{}
+	ssl.ID = uuid.FromStringOrNil(id)
+	if err := conf.DB().Delete(ssl).Error; err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (req *SslRequest) PutToApisix(rid string) (*ApisixSslResponse, error) {
+	url := fmt.Sprintf("%s/ssl/%s", conf.BaseUrl, rid)
+	if data, err := json.Marshal(req); err != nil {
+		return nil, err
+	} else {
+		if resp, err := utils.Put(url, data); err != nil {
+			logger.Error(url)
+			logger.Error(string(data))
+			logger.Error(err.Error())
+			return nil, err
+		} else {
+			var arresp ApisixSslResponse
+			if err := json.Unmarshal(resp, &arresp); err != nil {
+				logger.Error(err.Error())
+				return nil, err
+			} else {
+				return &arresp, nil
+			}
+		}
+	}
+}
+
+func (req *SslRequest) DeleteFromApisix() (*ApisixSslResponse, error) {
+	id := req.ID
+	url := fmt.Sprintf("%s/ssl/%s", conf.BaseUrl, id)
+
+	if resp, err := utils.Delete(url); err != nil {
+		logger.Error(err.Error())
+		return nil, err
+	} else {
+		var arresp ApisixSslResponse
+		if err := json.Unmarshal(resp, &arresp); err != nil {
+			logger.Error(err.Error())
+			return nil, err
+		} else {
+			return &arresp, nil
+		}
+	}
+}
+
+func ParseCert(crt, key string) (*Ssl, error) {
+	// print private key
+	certDERBlock, _ := pem.Decode([]byte(crt))
+	if certDERBlock == nil {
+		return nil, errors.New("Certificate resolution failed")
+	}
+	// match
+	_, err := tls.X509KeyPair([]byte(crt), []byte(key))
+	if err != nil {
+		return nil, err
+	}
+
+	x509Cert, err := x509.ParseCertificate(certDERBlock.Bytes)
+
+	if err != nil {
+		return nil, errors.New("Certificate resolution failed")
+	} else {
+		ssl := Ssl{}
+
+		//domain
+		snis := []byte{}
+		if x509Cert.DNSNames == nil || len(x509Cert.DNSNames) < 1 {
+			tmp := []string{}
+			if x509Cert.Subject.CommonName != "" {
+				tmp = []string{x509Cert.Subject.CommonName}
+			}
+			snis, _ = json.Marshal(tmp)
+		} else {
+			snis, _ = json.Marshal(x509Cert.DNSNames)
+		}
+		ssl.Snis = string(snis)
+
+		ssl.ValidityStart = uint64(x509Cert.NotBefore.Unix())
+		ssl.ValidityEnd = uint64(x509Cert.NotAfter.Unix())
+		ssl.PublicKey = crt
+
+		return &ssl, nil
+	}
+}
diff --git a/api/utils/copy.go b/api/utils/copy.go
new file mode 100644
index 0000000..30f228a
--- /dev/null
+++ b/api/utils/copy.go
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package utils
+
+func CopyMap(origin map[string]interface{}) map[string]interface{} {
+	result := make(map[string]interface{})
+	for k, v := range origin {
+		result[k] = v
+	}
+	return result
+}
+
+func CopyStrings(origin [][]string) [][]string {
+	result := make([][]string, 0)
+	for _, s := range origin {
+		result = append(result, s)
+	}
+	return result
+}
+
+func Set2Map(origin []string) map[string]int {
+	result := make(map[string]int)
+	for _, s := range origin {
+		result[s] = 1
+	}
+	return result
+}
diff --git a/api/utils/http.go b/api/utils/http.go
new file mode 100644
index 0000000..119cf0b
--- /dev/null
+++ b/api/utils/http.go
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package utils
+
+import (
+	"fmt"
+	"github.com/apisix/manager-api/conf"
+	"github.com/apisix/manager-api/log"
+	"gopkg.in/resty.v1"
+	"net/http"
+	"time"
+)
+
+const timeout = 3000
+
+var logger = log.GetLogger()
+
+func Get(url string) ([]byte, error) {
+	r := resty.New().
+		SetTimeout(time.Duration(timeout)*time.Millisecond).
+		R().
+		SetHeader("content-type", "application/json").
+		SetHeader("X-API-KEY", conf.ApiKey)
+	resp, err := r.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode() != http.StatusOK {
+		return nil, fmt.Errorf("status: %d, body: %s", resp.StatusCode(), resp.Body())
+	}
+	return resp.Body(), nil
+}
+
+func Post(url string, bytes []byte) ([]byte, error) {
+	r := resty.New().
+		SetTimeout(time.Duration(timeout)*time.Millisecond).
+		R().
+		SetHeader("content-type", "application/json").
+		SetHeader("X-API-KEY", conf.ApiKey)
+	r.SetBody(bytes)
+	resp, err := r.Post(url)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode() != http.StatusOK && resp.StatusCode() != http.StatusCreated {
+		return nil, fmt.Errorf("status: %d, body: %s", resp.StatusCode(), resp.Body())
+	}
+	return resp.Body(), nil
+}
+
+func Put(url string, bytes []byte) ([]byte, error) {
+	r := resty.New().
+		SetTimeout(time.Duration(timeout)*time.Millisecond).
+		R().
+		SetHeader("content-type", "application/json").
+		SetHeader("X-API-KEY", conf.ApiKey)
+	r.SetBody(bytes)
+	resp, err := r.Put(url)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode() != http.StatusOK && resp.StatusCode() != http.StatusCreated {
+		return nil, fmt.Errorf("status: %d, body: %s", resp.StatusCode(), resp.Body())
+	}
+	return resp.Body(), nil
+}
+
+func Patch(url string, bytes []byte) ([]byte, error) {
+	r := resty.New().
+		SetTimeout(time.Duration(timeout)*time.Millisecond).
+		R().
+		SetHeader("content-type", "application/json").
+		SetHeader("X-API-KEY", conf.ApiKey)
+	r.SetBody(bytes)
+	resp, err := r.Patch(url)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode() != http.StatusOK {
+		return nil, fmt.Errorf("status: %d, body: %s", resp.StatusCode(), resp.Body())
+	}
+	return resp.Body(), nil
+}
+
+func Delete(url string) ([]byte, error) {
+	r := resty.New().
+		SetTimeout(time.Duration(timeout)*time.Millisecond).
+		R().
+		SetHeader("content-type", "application/json").
+		SetHeader("X-API-KEY", conf.ApiKey)
+	resp, err := r.Delete(url)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode() != http.StatusOK {
+		return nil, fmt.Errorf("status: %d, body: %s", resp.StatusCode(), resp.Body())
+	}
+	return resp.Body(), nil
+}
diff --git a/compose/README.md b/compose/README.md
new file mode 100644
index 0000000..7d7c7ce
--- /dev/null
+++ b/compose/README.md
@@ -0,0 +1,56 @@
+## Deploy
+
+```sh
+$ cd incubator-apisix-dashboard/compose
+
+$ chmod +x ./manager_conf/build.sh
+
+$ docker-compose -p dashboard up -d
+```
+
+## Usage
+
+### 1. login dashboard
+
+Visit `http://127.0.0.1/dashboard/` in the browser, 
+Enter `http://127.0.0.1:8080/apisix/admin` into the first input box, this is the backend management service address
+
+![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"]