Repository: caicloud/nirvana
Pull request #390 (2020-11-22 05:09:37)

feat(*): make multiple plugins support the RPC builder
<!--  Thanks for sending a pull request! See below for tips! -->

**What this PR does / why we need it**:

Other plugins like metrics rely on configuring the middleware for the root path (/) to automatically configure the middleware for all paths which doesn't work for the RPC API style, which requires manual configuration of middleware and descriptor for the RPC style server.

**Which issue(s) this PR is related to** *(optional, link to 3rd issue(s))*:

Fixes #

Reference to #382 
<!-- 填在 Fixes，PR 合并就会关 issue。填在 Reference to 会关联 issue，不会联动关闭。-->

**Special notes for your reviewer**:

/cc @xpofei 

<!-- Please answer the following questions during the code freeze, and delete this line.
**Code freeze questions**

1. What causes this PR to not be merged before code freeze?
2. Why this PR is absolutely necessary for this version? Paste a screenshot of smoke testing docs if you could.
3. What's the effects after merging it?
4. Is there anyway we can skip this to not affect the overall process?
-->

**Release note**:
<!--  Write your release note:
1. Enter your extended release note in the below block. If the PR requires additional action from users switching to the new release, include the string "action required".
2. If no release note is required, just write "NONE".
-->

```release-note
NONE
```

<!--  Thanks for sending a pull request! Here are some tips:

1. https://github.com/caicloud/engineering/blob/master/guidelines/review_conventions.md      <-- what is the review process looks like
2. https://github.com/caicloud/engineering/blob/master/guidelines/git_commit_conventions.md  <-- how to structure your git commit
3. https://github.com/caicloud/engineering/blob/master/guidelines/caicloud_bot.md            <-- how to work with caicloud bot

Other tips:

If this is your first contribution, read our Getting Started guide https://github.com/caicloud/engineering/blob/master/guidelines/README.md
-->


caicloud-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: 
To fully approve this pull request, please assign additional approvers.
We suggest the following additional approver: **kdada**

Assign the PR to them by writing `/assign @kdada` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://github.com/caicloud/engineering/blob/master/guidelines/caicloud_bot.md#bot-commands).

The pull request process is described [here](https://github.com/caicloud/engineering/blob/master/guidelines/caicloud_bot.md#typical-workflow)

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/caicloud/nirvana/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["kdada"]} -->
xpofei: /lgtm
caicloud-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/caicloud/nirvana/pull/390#issuecomment-731912636" title="LGTM">xpofei</a>*
To fully approve this pull request, please assign additional approvers.
We suggest the following additional approver: **kdada**

Assign the PR to them by writing `/assign @kdada` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://github.com/caicloud/engineering/blob/master/guidelines/caicloud_bot.md#bot-commands).

The pull request process is described [here](https://github.com/caicloud/engineering/blob/master/guidelines/caicloud_bot.md#typical-workflow)

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/caicloud/nirvana/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["kdada"]} -->
iawia002: /approve
/cherrypick release-0.3
caicloud-bot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/caicloud/nirvana/pull/390#issuecomment-731949921" title="Approved">iawia002</a>*, *<a href="https://github.com/caicloud/nirvana/pull/390#issuecomment-731912636" title="LGTM">xpofei</a>*

The full list of commands accepted by this bot can be found [here](https://github.com/caicloud/engineering/blob/master/guidelines/caicloud_bot.md#bot-commands).

The pull request process is described [here](https://github.com/caicloud/engineering/blob/master/guidelines/caicloud_bot.md#typical-workflow)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/caicloud/nirvana/blob/master/OWNERS)~~ [iawia002]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
caicloud-bot: @iawia002: once the present PR merges, I will cherry-pick it on top of release-0.3 in a new PR and assign it to you.

<details>

In response to [this](https://github.com/caicloud/nirvana/pull/390#issuecomment-731949921):

>/approve
>/cherrypick release-0.3


Instructions for interacting with me using PR comments are available [here](https://github.com/caicloud/engineering/blob/master/docs/caicloud_bot.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>

Patch:
diff --git a/definition/helper.go b/definition/helper.go
index 55a63db..b9b17b4 100644
--- a/definition/helper.go
+++ b/definition/helper.go
@@ -259,5 +259,4 @@ func SimpleDescriptor(method Method, path string, f interface{}) Descriptor {
 			},
 		},
 	}
-
 }
diff --git a/examples/getting-started/rpc-style/apis/api.app.v1.json b/examples/getting-started/rpc-style/apis/api.app.v1.json
new file mode 100644
index 0000000..cd9f81a
--- /dev/null
+++ b/examples/getting-started/rpc-style/apis/api.app.v1.json
@@ -0,0 +1,116 @@
+{
+  "schemes": [
+    "http"
+  ],
+  "swagger": "2.0",
+  "info": {
+    "description": "Example module management RESTful APIs\u003cbr/\u003eThe app v1 version",
+    "title": "module-template APIs\n",
+    "contact": {
+      "name": "Xinzhao Xu",
+      "email": "xuxinzhao@caicloud.io"
+    },
+    "version": "v1"
+  },
+  "host": "localhost:8080",
+  "paths": {
+    "/apis/app/v1/workloads": {
+      "get": {
+        "description": "Query a specified number of workloads and returns an array",
+        "consumes": [
+          "application/json",
+          ""
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "summary": "List Workloads",
+        "parameters": [
+          {
+            "type": "number",
+            "format": "int",
+            "default": 10,
+            "description": "Number of workloads",
+            "name": "count",
+            "in": "query"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "A list of workloads",
+            "schema": {
+              "type": "array",
+              "items": {
+                "title": "Workload",
+                "$ref": "#/definitions/github.com_caicloud_module-template_pkg_server_apis_app_v1.Workload"
+              }
+            }
+          }
+        }
+      }
+    },
+    "/apis/app/v1/workloads/{workload}": {
+      "get": {
+        "description": "Get a workload by id",
+        "consumes": [
+          "application/json",
+          ""
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "summary": "Get Workload",
+        "parameters": [
+          {
+            "type": "string",
+            "format": "string",
+            "description": "workload name",
+            "name": "workload",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "A workload",
+            "schema": {
+              "$ref": "#/definitions/github.com_caicloud_module-template_pkg_server_apis_app_v1.Workload"
+            }
+          }
+        }
+      }
+    }
+  },
+  "definitions": {
+    "github.com_caicloud_module-template_pkg_server_apis_addon_v1alpha1.Object": {
+      "title": "Object",
+      "properties": {
+        "id": {
+          "type": "number",
+          "format": "int",
+          "title": "int"
+        }
+      }
+    },
+    "github.com_caicloud_module-template_pkg_server_apis_app_v1.Workload": {
+      "title": "Workload",
+      "properties": {
+        "name": {
+          "type": "string",
+          "format": "string",
+          "title": "string"
+        }
+      }
+    },
+    "github.com_caicloud_module-template_pkg_server_apis_storage_v1alpha1.Object": {
+      "title": "Object",
+      "properties": {
+        "id": {
+          "type": "number",
+          "format": "int",
+          "title": "int"
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/examples/getting-started/rpc-style/apis/api.storage.v1alpha1.json b/examples/getting-started/rpc-style/apis/api.storage.v1alpha1.json
new file mode 100644
index 0000000..60f3e58
--- /dev/null
+++ b/examples/getting-started/rpc-style/apis/api.storage.v1alpha1.json
@@ -0,0 +1,116 @@
+{
+  "schemes": [
+    "http"
+  ],
+  "swagger": "2.0",
+  "info": {
+    "description": "Example module management RESTful APIs\u003cbr/\u003eThe storage v1alpha1 version",
+    "title": "module-template APIs\n",
+    "contact": {
+      "name": "Xinzhao Xu",
+      "email": "xuxinzhao@caicloud.io"
+    },
+    "version": "v1alpha1"
+  },
+  "host": "localhost:8080",
+  "paths": {
+    "/apis/storage/v1alpha1/objects": {
+      "get": {
+        "description": "Query a specified number of objects and returns an array",
+        "consumes": [
+          "application/json",
+          ""
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "summary": "List Objects",
+        "parameters": [
+          {
+            "type": "number",
+            "format": "int",
+            "default": 10,
+            "description": "Number of objects",
+            "name": "count",
+            "in": "query"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "A list of objects",
+            "schema": {
+              "type": "array",
+              "items": {
+                "title": "Object",
+                "$ref": "#/definitions/github.com_caicloud_module-template_pkg_server_apis_storage_v1alpha1.Object"
+              }
+            }
+          }
+        }
+      }
+    },
+    "/apis/storage/v1alpha1/objects/{object}": {
+      "get": {
+        "description": "Get a object by id",
+        "consumes": [
+          "application/json",
+          ""
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "summary": "Get Object",
+        "parameters": [
+          {
+            "type": "number",
+            "format": "int",
+            "description": "object id",
+            "name": "object",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "A object",
+            "schema": {
+              "$ref": "#/definitions/github.com_caicloud_module-template_pkg_server_apis_storage_v1alpha1.Object"
+            }
+          }
+        }
+      }
+    }
+  },
+  "definitions": {
+    "github.com_caicloud_module-template_pkg_server_apis_addon_v1alpha1.Object": {
+      "title": "Object",
+      "properties": {
+        "id": {
+          "type": "number",
+          "format": "int",
+          "title": "int"
+        }
+      }
+    },
+    "github.com_caicloud_module-template_pkg_server_apis_app_v1.Workload": {
+      "title": "Workload",
+      "properties": {
+        "name": {
+          "type": "string",
+          "format": "string",
+          "title": "string"
+        }
+      }
+    },
+    "github.com_caicloud_module-template_pkg_server_apis_storage_v1alpha1.Object": {
+      "title": "Object",
+      "properties": {
+        "id": {
+          "type": "number",
+          "format": "int",
+          "title": "int"
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/examples/getting-started/rpc-style/main.go b/examples/getting-started/rpc-style/main.go
index a4585f4..84c7632 100644
--- a/examples/getting-started/rpc-style/main.go
+++ b/examples/getting-started/rpc-style/main.go
@@ -23,8 +23,12 @@ import (
 	"github.com/caicloud/nirvana/config"
 	"github.com/caicloud/nirvana/definition"
 	"github.com/caicloud/nirvana/log"
+	"github.com/caicloud/nirvana/middlewares/metrics"
 	"github.com/caicloud/nirvana/middlewares/reqlog"
-	"github.com/caicloud/nirvana/service/builder"
+	"github.com/caicloud/nirvana/plugins/apidocs"
+	"github.com/caicloud/nirvana/plugins/healthcheck"
+	"github.com/caicloud/nirvana/plugins/profiling"
+	"github.com/caicloud/nirvana/service"
 )
 
 var echo = definition.RPCDescriptor{
@@ -94,8 +98,25 @@ func main() {
 	cmd := config.NewDefaultNirvanaCommand()
 	conf := nirvana.NewDefaultConfig()
 	conf.Configure(
-		nirvana.APIStyle(builder.APIStyleRPC),
-		nirvana.Descriptor(echo, echo2),
+		nirvana.APIStyle(service.APIStyleRPC),
+		nirvana.Descriptor(echo, echo2, metrics.RPCDescriptor("/metrics")),
+
+		profiling.Path("/debug/pprof"),
+		healthcheck.CheckerWithType(func(ctx context.Context, checkType string) error {
+			switch checkType {
+			case healthcheck.LivenessCheck:
+				return nil
+			case healthcheck.ReadinessCheck:
+				// add specific check logic here if needed
+				return nil
+			}
+			return nil
+		}),
+		// enable the API docs plugin
+		func(c *nirvana.Config) error {
+			plugin := &apidocs.Option{FilesPath: "apis", Path: "/docs"}
+			return plugin.Configure(c)
+		},
 	)
 	if err := cmd.ExecuteWithConfig(conf); err != nil {
 		log.Fatal(err)
diff --git a/middlewares/metrics/metrics.go b/middlewares/metrics/metrics.go
index adea2d0..2dd35b2 100644
--- a/middlewares/metrics/metrics.go
+++ b/middlewares/metrics/metrics.go
@@ -69,3 +69,21 @@ func Descriptor(path string) definition.Descriptor {
 	}
 	return definition.SimpleDescriptor(definition.Get, path, service.WrapHTTPHandler(promhttp.Handler()))
 }
+
+// RPCDescriptor returns a rpc descriptor for the API; it must be configured to a server in order to serve the
+// metric API.
+func RPCDescriptor(path string) definition.RPCDescriptor {
+	if path == "" {
+		path = "/metrics"
+	}
+	return definition.RPCDescriptor{
+		Path: path,
+		Actions: []definition.RPCAction{
+			{
+				Function: service.WrapHTTPHandler(promhttp.Handler()),
+				Consumes: []string{definition.MIMEAll},
+				Produces: []string{definition.MIMEAll},
+			},
+		},
+	}
+}
diff --git a/nirvana.go b/nirvana.go
index 9f1d90e..8be8c8b 100644
--- a/nirvana.go
+++ b/nirvana.go
@@ -53,7 +53,7 @@ type Server interface {
 // Config describes configuration of server.
 type Config struct {
 	// apiStyle makes nirvana serve as REST or RPC style, default is REST.
-	apiStyle string
+	apiStyle service.APIStyle
 	// tls cert file
 	certFile string
 	// tls ket file
@@ -177,7 +177,7 @@ func NewDefaultConfig() *Config {
 // modifiers for specific scenario, please use NewDefaultConfig().
 func NewConfig() *Config {
 	return &Config{
-		apiStyle:  builderutil.APIStyleREST,
+		apiStyle:  service.APIStyleREST,
 		port:      8080,
 		logger:    &log.SilentLogger{},
 		configSet: make(map[string]interface{}),
@@ -332,7 +332,7 @@ func RegisterConfigInstaller(ci ConfigInstaller) {
 }
 
 // APIStyle returns a configurer to set api style into config.
-func APIStyle(apiStyle string) Configurer {
+func APIStyle(apiStyle service.APIStyle) Configurer {
 	return func(c *Config) error {
 		c.apiStyle = apiStyle
 		return nil
diff --git a/plugins/apidocs/apidocs.go b/plugins/apidocs/apidocs.go
index a6a6c51..2b35de3 100644
--- a/plugins/apidocs/apidocs.go
+++ b/plugins/apidocs/apidocs.go
@@ -63,16 +63,21 @@ func (i *apiDocsInstaller) Install(builder service.Builder, cfg *nirvana.Config)
 		versions := make([]string, 0, len(files))
 		for v, data := range files {
 			versions = append(versions, v)
-			apiDescriptor = append(apiDescriptor, api.DescriptorForData(api.PathForVersion(c.path, v), data, definition.MIMEJSON))
+			if builder.APIStyle() == service.APIStyleRPC {
+				apiDescriptor = append(apiDescriptor, api.RPCDescriptorForData(api.PathForVersion(c.path, v), data, definition.MIMEJSON))
+			} else {
+				apiDescriptor = append(apiDescriptor, api.DescriptorForData(api.PathForVersion(c.path, v), data, definition.MIMEJSON))
+			}
 		}
 		data, err := api.GenSwaggerPageData(c.path, versions)
 		if err != nil {
 			return err
 		}
-		apiDescriptor = append(
-			apiDescriptor,
-			api.DescriptorForData(c.path, data, definition.MIMEHTML),
-		)
+		if builder.APIStyle() == service.APIStyleRPC {
+			apiDescriptor = append(apiDescriptor, api.RPCDescriptorForData(c.path, data, definition.MIMEHTML))
+		} else {
+			apiDescriptor = append(apiDescriptor, api.DescriptorForData(c.path, data, definition.MIMEHTML))
+		}
 		return builder.AddDescriptor(apiDescriptor...)
 	})
 }
diff --git a/plugins/healthcheck/healthz.go b/plugins/healthcheck/healthz.go
index 18fedfe..83e2568 100644
--- a/plugins/healthcheck/healthz.go
+++ b/plugins/healthcheck/healthz.go
@@ -80,17 +80,30 @@ func (i *healthcheckInstaller) Install(builder service.Builder, cfg *nirvana.Con
 			function = c.checkerWithType
 		}
 
-		err = builder.AddDescriptor(definition.Descriptor{
-			Path:     c.path,
-			Consumes: []string{definition.MIMEAll},
-			Produces: []string{definition.MIMEAll},
-			Definitions: []definition.Definition{{
-				Method:     definition.Get,
-				Results:    []definition.Result{definition.ErrorResult()},
-				Parameters: parameters,
-				Function:   function,
-			}},
-		})
+		if builder.APIStyle() == service.APIStyleRPC {
+			err = builder.AddDescriptor(definition.RPCDescriptor{
+				Path:     c.path,
+				Consumes: []string{definition.MIMEAll},
+				Produces: []string{definition.MIMEAll},
+				Actions: []definition.RPCAction{{
+					Results:    []definition.Result{definition.ErrorResult()},
+					Parameters: parameters,
+					Function:   function,
+				}},
+			})
+		} else {
+			err = builder.AddDescriptor(definition.Descriptor{
+				Path:     c.path,
+				Consumes: []string{definition.MIMEAll},
+				Produces: []string{definition.MIMEAll},
+				Definitions: []definition.Definition{{
+					Method:     definition.Get,
+					Results:    []definition.Result{definition.ErrorResult()},
+					Parameters: parameters,
+					Function:   function,
+				}},
+			})
+		}
 	})
 	return err
 }
diff --git a/plugins/profiling/profiling.go b/plugins/profiling/profiling.go
index d72c8c3..91433b9 100644
--- a/plugins/profiling/profiling.go
+++ b/plugins/profiling/profiling.go
@@ -17,16 +17,14 @@ limitations under the License.
 package profiling
 
 import (
-	"fmt"
 	"html/template"
 	"log"
 	"net/http"
 	"net/http/pprof"
+	"path"
 	"path/filepath"
-	"runtime"
 	rpprof "runtime/pprof"
 	"sort"
-	"strconv"
 
 	"github.com/caicloud/nirvana"
 	"github.com/caicloud/nirvana/definition"
@@ -56,8 +54,14 @@ func (i *profilingInstaller) Name() string {
 func (i *profilingInstaller) Install(builder service.Builder, cfg *nirvana.Config) error {
 	var err error
 	wrapper(cfg, func(c *config) {
-		if err = builder.AddDescriptor(descriptor(c.path)); err != nil {
-			return
+		if builder.APIStyle() == service.APIStyleRPC {
+			if err = builder.AddDescriptor(rpcDescriptors(c.path)...); err != nil {
+				return
+			}
+		} else {
+			if err = builder.AddDescriptor(descriptor(c.path)); err != nil {
+				return
+			}
 		}
 	})
 	return err
@@ -156,26 +160,7 @@ func descriptor(path string) definition.Descriptor {
 				Definitions: []definition.Definition{{
 					Method: definition.Get,
 					Function: service.WrapHTTPHandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-						name := filepath.Base(r.URL.Path)
-						w.Header().Set("X-Content-Type-Options", "nosniff")
-						p := rpprof.Lookup(name)
-						if p == nil {
-							w.WriteHeader(http.StatusNotFound)
-							_, _ = w.Write([]byte("Unknown profile"))
-							return
-						}
-						gc, _ := strconv.Atoi(r.FormValue("gc"))
-						if name == "heap" && gc > 0 {
-							runtime.GC()
-						}
-						debug, _ := strconv.Atoi(r.FormValue("debug"))
-						if debug != 0 {
-							w.Header().Set("Content-Type", "text/plain; charset=utf-8")
-						} else {
-							w.Header().Set("Content-Type", "application/octet-stream")
-							w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name))
-						}
-						_ = p.WriteTo(w, debug)
+						pprof.Handler(filepath.Base(r.URL.Path)).ServeHTTP(w, r)
 					}),
 				}},
 			},
@@ -183,6 +168,99 @@ func descriptor(path string) definition.Descriptor {
 	}
 }
 
+func rpcDescriptors(root string) []interface{} {
+	return []interface{}{
+		definition.RPCDescriptor{
+			Path:     root,
+			Consumes: []string{definition.MIMEAll},
+			Produces: []string{definition.MIMEAll},
+			Actions: []definition.RPCAction{{
+				Function: service.WrapHTTPHandlerFunc(index),
+			}},
+		},
+		definition.RPCDescriptor{
+			Path:     path.Join(root, "cmdline"),
+			Consumes: []string{definition.MIMEAll},
+			Produces: []string{definition.MIMEAll},
+			Actions: []definition.RPCAction{{
+				Function: service.WrapHTTPHandlerFunc(pprof.Cmdline),
+			}},
+		},
+		definition.RPCDescriptor{
+			Path:     path.Join(root, "profile"),
+			Consumes: []string{definition.MIMEAll},
+			Produces: []string{definition.MIMEAll},
+			Actions: []definition.RPCAction{{
+				Function: service.WrapHTTPHandlerFunc(pprof.Profile),
+			}},
+		},
+		definition.RPCDescriptor{
+			Path:     path.Join(root, "symbol"),
+			Consumes: []string{definition.MIMEAll},
+			Produces: []string{definition.MIMEAll},
+			Actions: []definition.RPCAction{{
+				Function: service.WrapHTTPHandlerFunc(pprof.Symbol),
+			}},
+		},
+		definition.RPCDescriptor{
+			Path:     path.Join(root, "trace"),
+			Consumes: []string{definition.MIMEAll},
+			Produces: []string{definition.MIMEAll},
+			Actions: []definition.RPCAction{{
+				Function: service.WrapHTTPHandlerFunc(pprof.Trace),
+			}},
+		},
+		definition.RPCDescriptor{
+			Path:     path.Join(root, "goroutine"),
+			Consumes: []string{definition.MIMEAll},
+			Produces: []string{definition.MIMEAll},
+			Actions: []definition.RPCAction{{
+				Function: service.WrapHTTPHandler(pprof.Handler("goroutine")),
+			}},
+		},
+		definition.RPCDescriptor{
+			Path:     path.Join(root, "threadcreate"),
+			Consumes: []string{definition.MIMEAll},
+			Produces: []string{definition.MIMEAll},
+			Actions: []definition.RPCAction{{
+				Function: service.WrapHTTPHandler(pprof.Handler("threadcreate")),
+			}},
+		},
+		definition.RPCDescriptor{
+			Path:     path.Join(root, "heap"),
+			Consumes: []string{definition.MIMEAll},
+			Produces: []string{definition.MIMEAll},
+			Actions: []definition.RPCAction{{
+				Function: service.WrapHTTPHandler(pprof.Handler("heap")),
+			}},
+		},
+		definition.RPCDescriptor{
+			Path:     path.Join(root, "allocs"),
+			Consumes: []string{definition.MIMEAll},
+			Produces: []string{definition.MIMEAll},
+			Actions: []definition.RPCAction{{
+				Function: service.WrapHTTPHandler(pprof.Handler("allocs")),
+			}},
+		},
+		definition.RPCDescriptor{
+			Path:     path.Join(root, "block"),
+			Consumes: []string{definition.MIMEAll},
+			Produces: []string{definition.MIMEAll},
+			Actions: []definition.RPCAction{{
+				Function: service.WrapHTTPHandler(pprof.Handler("block")),
+			}},
+		},
+		definition.RPCDescriptor{
+			Path:     path.Join(root, "mutex"),
+			Consumes: []string{definition.MIMEAll},
+			Produces: []string{definition.MIMEAll},
+			Actions: []definition.RPCAction{{
+				Function: service.WrapHTTPHandler(pprof.Handler("mutex")),
+			}},
+		},
+	}
+}
+
 var profileDescriptions = map[string]string{
 	"allocs":    "A sampling of all past memory allocations",
 	"block":     "Stack traces that led to blocking on synchronization primitives",
diff --git a/plugins/version/version.go b/plugins/version/version.go
index 83ad822..2712737 100644
--- a/plugins/version/version.go
+++ b/plugins/version/version.go
@@ -64,18 +64,32 @@ func (i *versionInstaller) Install(builder service.Builder, cfg *nirvana.Config)
 			Hash:        c.hash,
 			Description: c.description,
 		}
-		err = builder.AddDescriptor(definition.Descriptor{
-			Path:     c.path,
-			Consumes: []string{definition.MIMEAll},
-			Produces: []string{definition.MIMEAll},
-			Definitions: []definition.Definition{{
-				Method:  definition.Get,
-				Results: definition.DataErrorResults(""),
-				Function: func(ctx context.Context) (*version, error) {
-					return v, nil
-				},
-			}},
-		})
+		if builder.APIStyle() == service.APIStyleRPC {
+			err = builder.AddDescriptor(definition.RPCDescriptor{
+				Path:     c.path,
+				Consumes: []string{definition.MIMEAll},
+				Produces: []string{definition.MIMEAll},
+				Actions: []definition.RPCAction{{
+					Results: definition.DataErrorResults(""),
+					Function: func(ctx context.Context) (*version, error) {
+						return v, nil
+					},
+				}},
+			})
+		} else {
+			err = builder.AddDescriptor(definition.Descriptor{
+				Path:     c.path,
+				Consumes: []string{definition.MIMEAll},
+				Produces: []string{definition.MIMEAll},
+				Definitions: []definition.Definition{{
+					Method:  definition.Get,
+					Results: definition.DataErrorResults(""),
+					Function: func(ctx context.Context) (*version, error) {
+						return v, nil
+					},
+				}},
+			})
+		}
 	})
 	return err
 }
diff --git a/service/builder/builder.go b/service/builder/builder.go
index 5be4b0d..5667b18 100644
--- a/service/builder/builder.go
+++ b/service/builder/builder.go
@@ -22,17 +22,10 @@ import (
 	"github.com/caicloud/nirvana/service/rpc"
 )
 
-const (
-	// APIStyleREST represents the RESTful API style.
-	APIStyleREST = "rest"
-	// APIStyleRPC represents the RPC API style.
-	APIStyleRPC = "rpc"
-)
-
 // New creates a service builder based on the given API style.
-func New(apiStyle string) service.Builder {
+func New(apiStyle service.APIStyle) service.Builder {
 	switch apiStyle {
-	case APIStyleRPC:
+	case service.APIStyleRPC:
 		return rpc.NewBuilder()
 	default:
 		return rest.NewBuilder()
diff --git a/service/rest/builder.go b/service/rest/builder.go
index 783c939..6f3e5f1 100644
--- a/service/rest/builder.go
+++ b/service/rest/builder.go
@@ -199,6 +199,11 @@ func (b *builder) Definitions() map[string][]definition.Definition {
 	return result
 }
 
+// APIStyle returns the API style of this builder.
+func (b *builder) APIStyle() service.APIStyle {
+	return service.APIStyleREST
+}
+
 // Build builds a service to handle request.
 func (b *builder) Build() (service.Service, error) {
 	if len(b.bindings) <= 0 {
diff --git a/service/rpc/builder.go b/service/rpc/builder.go
index 9df2d4a..ddc6263 100644
--- a/service/rpc/builder.go
+++ b/service/rpc/builder.go
@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"net/http"
 	"reflect"
+	"sort"
 
 	"github.com/caicloud/nirvana/definition"
 	"github.com/caicloud/nirvana/log"
@@ -88,6 +89,9 @@ func (b *builder) SetModifier(m service.DefinitionModifier) {
 }
 
 func genRPCPath(path, version, action string) string {
+	if version == "" && action == "" {
+		return path
+	}
 	return fmt.Sprintf("%s?Version=%s&Action=%s", path, version, action)
 }
 
@@ -157,14 +161,26 @@ func (b *builder) Definitions() map[string][]definition.Definition {
 	return result
 }
 
+// APIStyle returns the API style of this builder.
+func (b *builder) APIStyle() service.APIStyle {
+	return service.APIStyleRPC
+}
+
 // Build builds a service to handle request.
 func (b *builder) Build() (service.Service, error) {
 	if len(b.bindings) <= 0 {
 		return nil, fmt.Errorf("no router")
 	}
 
+	paths := make([]string, 0, len(b.bindings))
+	for path := range b.bindings {
+		paths = append(paths, path)
+	}
+	sort.Strings(paths)
+
 	var err error
-	for path, bd := range b.bindings {
+	for _, path := range paths {
+		bd := b.bindings[path]
 		b.logger.V(log.LevelDebug).Infof("Path: %s, Consumes: %v, Produces: %v", path, bd.definition.Consumes, bd.definition.Produces)
 		if b.modifier != nil {
 			b.modifier(&bd.definition)
diff --git a/service/types.go b/service/types.go
index 98b3238..f8aab79 100644
--- a/service/types.go
+++ b/service/types.go
@@ -23,6 +23,16 @@ import (
 	"github.com/caicloud/nirvana/log"
 )
 
+// APIStyle defines the API style type of the builder.
+type APIStyle string
+
+const (
+	// APIStyleREST represents the RESTful API style.
+	APIStyleREST APIStyle = "rest"
+	// APIStyleRPC represents the RPC API style.
+	APIStyleRPC APIStyle = "rpc"
+)
+
 // Builder builds service.
 type Builder interface {
 	// Logger returns logger of builder.
@@ -41,6 +51,8 @@ type Builder interface {
 	AddDescriptor(descriptors ...interface{}) error
 	// Definitions returns all definitions. If a modifier exists, it will be executed.
 	Definitions() map[string][]definition.Definition
+	// APIStyle returns the API style of this builder.
+	APIStyle() APIStyle
 	// Build builds a service to handle request.
 	Build() (Service, error)
 }
diff --git a/utils/api/api.go b/utils/api/api.go
index 394c802..929919c 100644
--- a/utils/api/api.go
+++ b/utils/api/api.go
@@ -135,13 +135,13 @@ func (ac *Container) AddDescriptor(descriptors ...interface{}) {
 
 // Generate generates API definitions.
 func (ac *Container) Generate(apiStyle string) (*Definitions, error) {
-	builder := builderutil.New(apiStyle)
+	builder := builderutil.New(service.APIStyle(apiStyle))
 	builder.SetModifier(ac.modifiers.Combine())
 	if err := builder.AddDescriptor(ac.descriptors...); err != nil {
 		return nil, err
 	}
 	definitions := builder.Definitions()
-	result, err := NewPathDefinitions(ac.typeContainer, definitions, apiStyle)
+	result, err := NewPathDefinitions(ac.typeContainer, definitions, service.APIStyle(apiStyle))
 	if err != nil {
 		return nil, err
 	}
diff --git a/utils/api/definitions.go b/utils/api/definitions.go
index c49dd2a..92021a4 100644
--- a/utils/api/definitions.go
+++ b/utils/api/definitions.go
@@ -22,7 +22,6 @@ import (
 
 	"github.com/caicloud/nirvana/definition"
 	"github.com/caicloud/nirvana/service"
-	"github.com/caicloud/nirvana/service/builder"
 )
 
 // Parameter describes a function parameter.
@@ -98,9 +97,9 @@ type Definition struct {
 }
 
 // NewDefinition creates openapi.Definition from definition.Definition.
-func NewDefinition(tc *TypeContainer, d *definition.Definition, apiStyle string) (*Definition, error) {
+func NewDefinition(tc *TypeContainer, d *definition.Definition, apiStyle service.APIStyle) (*Definition, error) {
 	code := service.HTTPCodeFor(d.Method)
-	if apiStyle == builder.APIStyleRPC {
+	if apiStyle == service.APIStyleRPC {
 		code = http.StatusOK
 	}
 
@@ -170,7 +169,7 @@ func NewDefinition(tc *TypeContainer, d *definition.Definition, apiStyle string)
 }
 
 // NewDefinitions creates a list of definitions.
-func NewDefinitions(tc *TypeContainer, definitions []definition.Definition, apiStyle string) ([]Definition, error) {
+func NewDefinitions(tc *TypeContainer, definitions []definition.Definition, apiStyle service.APIStyle) ([]Definition, error) {
 	result := make([]Definition, len(definitions))
 	for i, d := range definitions {
 		cd, err := NewDefinition(tc, &d, apiStyle)
@@ -183,7 +182,7 @@ func NewDefinitions(tc *TypeContainer, definitions []definition.Definition, apiS
 }
 
 // NewPathDefinitions creates a list of definitions with path.
-func NewPathDefinitions(tc *TypeContainer, definitions map[string][]definition.Definition, apiStyle string) (map[string][]Definition, error) {
+func NewPathDefinitions(tc *TypeContainer, definitions map[string][]definition.Definition, apiStyle service.APIStyle) (map[string][]Definition, error) {
 	result := make(map[string][]Definition)
 	for path, defs := range definitions {
 		cds, err := NewDefinitions(tc, defs, apiStyle)
diff --git a/utils/api/utils.go b/utils/api/utils.go
index 020df3f..94352f1 100644
--- a/utils/api/utils.go
+++ b/utils/api/utils.go
@@ -150,11 +150,27 @@ func DescriptorForData(path string, data []byte, contentType string) definition.
 				Method:   definition.Get,
 				Consumes: []string{definition.MIMENone},
 				Produces: []string{contentType},
-				Function: func(context.Context) ([]byte, error) {
-					return data, nil
+				Function: func(context.Context) []byte {
+					return data
 				},
-				Parameters: []definition.Parameter{},
-				Results:    definition.DataErrorResults(""),
+				Results: []definition.Result{definition.DataResultFor("")},
+			},
+		},
+	}
+}
+
+// RPCDescriptorForData generates a RPCDescriptor for API docs page.
+func RPCDescriptorForData(path string, data []byte, contentType string) definition.RPCDescriptor {
+	return definition.RPCDescriptor{
+		Path: path,
+		Actions: []definition.RPCAction{
+			{
+				Consumes: []string{definition.MIMENone},
+				Produces: []string{contentType},
+				Function: func(context.Context) []byte {
+					return data
+				},
+				Results: []definition.Result{definition.DataResultFor("")},
 			},
 		},
 	}
diff --git a/utils/builder/builder.go b/utils/builder/builder.go
index 04371fd..ff522f3 100644
--- a/utils/builder/builder.go
+++ b/utils/builder/builder.go
@@ -30,7 +30,7 @@ import (
 	"strconv"
 	"text/template"
 
-	builderutil "github.com/caicloud/nirvana/service/builder"
+	"github.com/caicloud/nirvana/service"
 	"github.com/caicloud/nirvana/utils/api"
 )
 
@@ -60,7 +60,7 @@ func (b *APIBuilder) Build() (*api.Definitions, error) {
 	descriptors := make([]function, 0)
 	modifiers := make([]function, 0)
 
-	apiStyle := builderutil.APIStyleREST
+	apiStyle := string(service.APIStyleREST)
 
 	for _, pkg := range analyzer.Paths() {
 		groups := analyzer.PackageComments(pkg)


Test patch:
diff --git a/examples/api-basic/api/v2/app_test.go b/examples/api-basic/api/v2/app_test.go
index 1ac8046..5d6c5d1 100644
--- a/examples/api-basic/api/v2/app_test.go
+++ b/examples/api-basic/api/v2/app_test.go
@@ -22,12 +22,12 @@ import (
 	"testing"
 
 	"github.com/caicloud/nirvana/examples/api-basic/application"
-	"github.com/caicloud/nirvana/service/builder"
+	"github.com/caicloud/nirvana/service"
 	"github.com/caicloud/nirvana/utils/unittest"
 )
 
 func TestCreate(t *testing.T) {
-	s, err := unittest.NewTestService(builder.APIStyleREST, Descriptor())
+	s, err := unittest.NewTestService(service.APIStyleREST, Descriptor())
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/utils/unittest/unittest.go b/utils/unittest/unittest.go
index bba6ffe..25c345b 100644
--- a/utils/unittest/unittest.go
+++ b/utils/unittest/unittest.go
@@ -71,7 +71,7 @@ func (r *responseWriter) Bytes() []byte {
 }
 
 // NewTestService creates a service.Service for testing.
-func NewTestService(apiStyle string, desc ...interface{}) (service.Service, error) {
+func NewTestService(apiStyle service.APIStyle, desc ...interface{}) (service.Service, error) {
 	builder := builderutil.New(apiStyle)
 	builder.SetModifier(service.FirstContextParameter())
 	builder.AddFilter(service.RedirectTrailingSlash(), service.FillLeadingSlash(), service.ParseRequestForm())
@@ -83,7 +83,7 @@ func NewTestService(apiStyle string, desc ...interface{}) (service.Service, erro
 
 // NewTestServiceWithConfig creates a service.Service for testing with user specified modifier and
 // filters. If modifier or filters is nil, default option will be used.
-func NewTestServiceWithConfig(apiStyle string, desc []interface{}, modifier service.DefinitionModifier,
+func NewTestServiceWithConfig(apiStyle service.APIStyle, desc []interface{}, modifier service.DefinitionModifier,
 	filters []service.Filter) (service.Service, error) {
 	builder := builderutil.New(apiStyle)
 	if modifier == nil {