Repository: caicloud/nirvana
Pull request #314 (2020-04-29 16:06:09)

feat(middleware): implemented metrics middleware
<!--  Thanks for sending a pull request! See below for tips! -->

**What this PR does / why we need it**:

Added metric middleware that mimick the functionality of the `metrics` plugin. Unlike a plugin, middleware can be added only to a subset of the descriptors.

**Which issue(s) this PR is related to** *(optional, link to 3rd issue(s))*:

Fixes #

Reference to #
<!-- 填在 Fixes，PR 合并就会关 issue。填在 Reference to 会关联 issue，不会联动关闭。-->

**Special notes for your reviewer**:

/cc @supereagle @iawia002 

<!-- 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: **iawia002**

Assign the PR to them by writing `/assign @iawia002` 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":["iawia002"]} -->
lichuan0620: /retest
lichuan0620: rebuild
lichuan0620: ...how does this work? @iawia002 

Patch:
diff --git a/metrics/metrics.go b/metrics/metrics.go
new file mode 100644
index 0000000..270340b
--- /dev/null
+++ b/metrics/metrics.go
@@ -0,0 +1,100 @@
+/*
+Copyright 2017 Caicloud Authors
+
+Licensed 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 metrics
+
+import (
+	"strconv"
+	"sync"
+	"time"
+
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promauto"
+
+	"github.com/caicloud/nirvana/service"
+)
+
+var (
+	once            sync.Once
+	requestCounter  *prometheus.CounterVec
+	requestDuration *prometheus.HistogramVec
+	responseSize    *prometheus.HistogramVec
+	startTime       prometheus.Gauge
+)
+
+// Install registers the metrics under the given namespace. This take effect only once; subsequent calls
+// has no effect.
+func Install(namespace string) {
+	once.Do(func() {
+		if namespace == "" {
+			namespace = "nirvana"
+		}
+		constLabel := prometheus.Labels{"component": namespace}
+		httpLabels := []string{"verb", "path"}
+
+		startTime = promauto.NewGauge(
+			prometheus.GaugeOpts{
+				Namespace:   namespace,
+				Name:        "start_time_seconds",
+				Help:        "Start time of the service in unix timestamp",
+				ConstLabels: constLabel,
+			},
+		)
+		startTime.Set(float64(time.Now().Unix()))
+
+		requestCounter = promauto.NewCounterVec(
+			prometheus.CounterOpts{
+				Namespace:   namespace,
+				Name:        "request_total",
+				Help:        "Counter of server requests for each verb, API resource, and HTTP response code.",
+				ConstLabels: constLabel,
+			},
+			append(httpLabels, "code"),
+		)
+
+		requestDuration = promauto.NewHistogramVec(
+			prometheus.HistogramOpts{
+				Namespace:   namespace,
+				Name:        "request_duration_seconds",
+				Help:        "Request duration distribution in seconds for each verb and path.",
+				ConstLabels: constLabel,
+				Buckets:     prometheus.DefBuckets,
+			},
+			httpLabels,
+		)
+
+		responseSize = promauto.NewHistogramVec(
+			prometheus.HistogramOpts{
+				Namespace:   namespace,
+				Name:        "response_sizes",
+				Help:        "Response content length distribution in bytes for each verb and path.",
+				ConstLabels: constLabel,
+				Buckets:     []float64{1e04, 1e05, 1e06, 1e07, 1e08, 1e09},
+			},
+			httpLabels,
+		)
+	})
+}
+
+// RecordRequest can be used at the end of each request to record its metric values.
+func RecordRequest(start time.Time, ctx service.HTTPContext) {
+	req := ctx.Request()
+	resp := ctx.ResponseWriter()
+	path := ctx.RoutePath()
+	requestCounter.WithLabelValues(req.Method, path, strconv.Itoa(resp.StatusCode())).Inc()
+	responseSize.WithLabelValues(req.Method, path).Observe(float64(resp.ContentLength()))
+	requestDuration.WithLabelValues(req.Method, path).Observe(time.Since(start).Seconds())
+}
diff --git a/middlewares/metrics/metrics.go b/middlewares/metrics/metrics.go
new file mode 100644
index 0000000..adea2d0
--- /dev/null
+++ b/middlewares/metrics/metrics.go
@@ -0,0 +1,71 @@
+/*
+Copyright 2017 Caicloud Authors
+
+Licensed 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 metrics
+
+import (
+	"context"
+	"time"
+
+	"github.com/prometheus/client_golang/prometheus/promhttp"
+
+	"github.com/caicloud/nirvana/definition"
+	"github.com/caicloud/nirvana/metrics"
+	"github.com/caicloud/nirvana/service"
+)
+
+// Default returns a metric Middleware under the namespace "nirvana".
+//
+// Once called, the namespace is set and can not be changed. Future attempt to build more Middleware
+// will result in ones with the same namespace as the first one.
+//
+// Unlike the metrics plugin which takes care of everything, you must call Descriptor() to build a
+// Descriptor and configure it to a server yourself.
+func Default() definition.Middleware {
+	metrics.Install("")
+	return func(ctx context.Context, next definition.Chain) error {
+		startTime := time.Now()
+		err := next.Continue(ctx)
+		metrics.RecordRequest(startTime, service.HTTPContextFrom(ctx))
+		return err
+	}
+}
+
+// Namespace returns a metric Middleware under the given namespace.
+//
+// Once called, the namespace is set and can not be changed. Future attempt to build more Middleware
+// will result in ones with the same namespace as the first one.
+//
+// Unlike the metrics plugin which takes care of everything, you must call Descriptor() to build a
+// Descriptor and configure it to a server yourself.
+func Namespace(namespace string) definition.Middleware {
+	metrics.Install(namespace)
+	return func(ctx context.Context, next definition.Chain) error {
+		startTime := time.Now()
+		err := next.Continue(ctx)
+		metrics.RecordRequest(startTime, service.HTTPContextFrom(ctx))
+		return err
+	}
+}
+
+// Descriptor returns a descriptor for the API; it must be configured to a server in order to serve the
+// metric API.
+func Descriptor(path string) definition.Descriptor {
+	if path == "" {
+		path = "/metrics"
+	}
+	return definition.SimpleDescriptor(definition.Get, path, service.WrapHTTPHandler(promhttp.Handler()))
+}
diff --git a/middlewares/reqlog/reqlog.go b/middlewares/reqlog/reqlog.go
new file mode 100644
index 0000000..48d0079
--- /dev/null
+++ b/middlewares/reqlog/reqlog.go
@@ -0,0 +1,70 @@
+/*
+Copyright 2017 Caicloud Authors
+
+Licensed 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 reqlog
+
+import (
+	"context"
+	"time"
+
+	"github.com/caicloud/nirvana/definition"
+	"github.com/caicloud/nirvana/log"
+	"github.com/caicloud/nirvana/service"
+)
+
+// Default returns a reqlog middleware that uses the default Nirvana logger.
+func Default() definition.Middleware {
+	return func(ctx context.Context, chain definition.Chain) error {
+		start := time.Now()
+		httpCtx := service.HTTPContextFrom(ctx)
+
+		err := chain.Continue(ctx)
+
+		request := httpCtx.Request()
+		response := httpCtx.ResponseWriter()
+		log.Infoln(
+			request.Method,
+			response.StatusCode(),
+			response.ContentLength(),
+			time.Since(start).String(),
+			request.URL.String(),
+		)
+
+		return err
+	}
+}
+
+// Custom returns a reqlog middleware with a custom logger and designated logging level.
+func Custom(logger log.Logger, level log.Level) definition.Middleware {
+	return func(ctx context.Context, chain definition.Chain) error {
+		start := time.Now()
+		httpCtx := service.HTTPContextFrom(ctx)
+
+		err := chain.Continue(ctx)
+
+		request := httpCtx.Request()
+		response := httpCtx.ResponseWriter()
+		logger.V(level).Infoln(
+			request.Method,
+			response.StatusCode(),
+			response.ContentLength(),
+			time.Since(start).String(),
+			request.URL.String(),
+		)
+
+		return err
+	}
+}
diff --git a/plugins/metrics/metrics.go b/plugins/metrics/metrics.go
index e1565ba..9e1f36d 100644
--- a/plugins/metrics/metrics.go
+++ b/plugins/metrics/metrics.go
@@ -17,15 +17,10 @@ limitations under the License.
 package metrics
 
 import (
-	"context"
-	"strconv"
-	"time"
-
 	"github.com/caicloud/nirvana"
 	"github.com/caicloud/nirvana/definition"
+	"github.com/caicloud/nirvana/middlewares/metrics"
 	"github.com/caicloud/nirvana/service"
-	"github.com/prometheus/client_golang/prometheus"
-	"github.com/prometheus/client_golang/prometheus/promhttp"
 )
 
 func init() {
@@ -43,47 +38,6 @@ type config struct {
 
 type metricsInstaller struct{}
 
-func newMetricsMiddleware(namespace string) definition.Middleware {
-	constLabel := prometheus.Labels{"component": namespace}
-	requestCounter := prometheus.NewCounterVec(
-		prometheus.CounterOpts{
-			Namespace:   namespace,
-			Name:        "request_total",
-			Help:        "Counter of server requests broken out for each verb, API resource, and HTTP response code.",
-			ConstLabels: constLabel,
-		},
-		[]string{"method", "path", "code"},
-	)
-	requestLatencies := prometheus.NewHistogramVec(
-		prometheus.HistogramOpts{
-			Namespace:   namespace,
-			Name:        "request_duration_seconds",
-			Help:        "Response latency distribution in seconds for each verb, resource and client.",
-			ConstLabels: constLabel,
-			Buckets:     prometheus.DefBuckets,
-		},
-		[]string{"method", "path"},
-	)
-
-	prometheus.MustRegister(requestCounter)
-	prometheus.MustRegister(requestLatencies)
-
-	return func(ctx context.Context, next definition.Chain) error {
-		startTime := time.Now()
-		err := next.Continue(ctx)
-
-		httpCtx := service.HTTPContextFrom(ctx)
-		req := httpCtx.Request()
-		resp := httpCtx.ResponseWriter()
-		path := httpCtx.RoutePath()
-
-		requestCounter.WithLabelValues(req.Method, path, strconv.Itoa(resp.StatusCode())).Inc()
-		requestLatencies.WithLabelValues(req.Method, path).Observe(float64((time.Since(startTime)) / time.Second))
-
-		return err
-	}
-}
-
 // Name is the external config name.
 func (i *metricsInstaller) Name() string {
 	return ExternalConfigName
@@ -96,10 +50,9 @@ func (i *metricsInstaller) Install(builder service.Builder, cfg *nirvana.Config)
 
 		monitorMiddleware := definition.Descriptor{
 			Path:        "/",
-			Middlewares: []definition.Middleware{newMetricsMiddleware(c.namespace)},
+			Middlewares: []definition.Middleware{metrics.Namespace(c.namespace)},
 		}
-		metricsEndpoint := definition.SimpleDescriptor(definition.Get, c.path, service.WrapHTTPHandler(promhttp.Handler()))
-		err = builder.AddDescriptor(monitorMiddleware, metricsEndpoint)
+		err = builder.AddDescriptor(monitorMiddleware, metrics.Descriptor(c.path))
 	})
 	return err
 }
diff --git a/service/filter.go b/service/filter.go
index 88b22e6..9f2247a 100644
--- a/service/filter.go
+++ b/service/filter.go
@@ -64,10 +64,10 @@ func FillLeadingSlash() Filter {
 	}
 }
 
-// ParseRequestForm returns a filter to parse request form when content
+// ParseRequestFormWithMaxMemory returns a filter to parse request form when content
 // type is "application/x-www-form-urlencoded" or "multipart/form-data".
 // The filter won't filter anything unless some error occurs in parsing.
-func ParseRequestForm() Filter {
+func ParseRequestFormWithMaxMemory(maxMemory int64) Filter {
 	return func(resp http.ResponseWriter, req *http.Request) bool {
 		ct, err := ContentType(req)
 		if err == nil {
@@ -75,7 +75,7 @@ func ParseRequestForm() Filter {
 			case definition.MIMEURLEncoded:
 				err = req.ParseForm()
 			case definition.MIMEFormData:
-				err = req.ParseMultipartForm(32 << 20)
+				err = req.ParseMultipartForm(maxMemory)
 			default:
 				req.Form = req.URL.Query()
 			}
@@ -88,6 +88,12 @@ func ParseRequestForm() Filter {
 	}
 }
 
+// ParseRequestForm returns a filter to parse request form.
+// Same as ParseRequestFormWithMaxMemory, except that maxMemory is set to 32MB by default.
+func ParseRequestForm() Filter {
+	return ParseRequestFormWithMaxMemory(32 << 20)
+}
+
 // isGTZero returns a boolean result indicating if the content length is greater than 0.
 func isGTZero(length string) bool {
 	if length == "" {
diff --git a/vendor/github.com/prometheus/client_golang/prometheus/promauto/auto.go b/vendor/github.com/prometheus/client_golang/prometheus/promauto/auto.go
new file mode 100644
index 0000000..3c10c85
--- /dev/null
+++ b/vendor/github.com/prometheus/client_golang/prometheus/promauto/auto.go
@@ -0,0 +1,374 @@
+// Copyright 2018 The Prometheus Authors
+// Licensed 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 promauto provides alternative constructors for the fundamental
+// Prometheus metric types and their …Vec and …Func variants. The difference to
+// their counterparts in the prometheus package is that the promauto
+// constructors return Collectors that are already registered with a
+// registry. There are two sets of constructors. The constructors in the first
+// set are top-level functions, while the constructors in the other set are
+// methods of the Factory type. The top-level function return Collectors
+// registered with the global registry (prometheus.DefaultRegisterer), while the
+// methods return Collectors registered with the registry the Factory was
+// constructed with. All constructors panic if the registration fails.
+//
+// The following example is a complete program to create a histogram of normally
+// distributed random numbers from the math/rand package:
+//
+//      package main
+//
+//      import (
+//              "math/rand"
+//              "net/http"
+//
+//              "github.com/prometheus/client_golang/prometheus"
+//              "github.com/prometheus/client_golang/prometheus/promauto"
+//              "github.com/prometheus/client_golang/prometheus/promhttp"
+//      )
+//
+//      var histogram = promauto.NewHistogram(prometheus.HistogramOpts{
+//              Name:    "random_numbers",
+//              Help:    "A histogram of normally distributed random numbers.",
+//              Buckets: prometheus.LinearBuckets(-3, .1, 61),
+//      })
+//
+//      func Random() {
+//              for {
+//                      histogram.Observe(rand.NormFloat64())
+//              }
+//      }
+//
+//      func main() {
+//              go Random()
+//              http.Handle("/metrics", promhttp.Handler())
+//              http.ListenAndServe(":1971", nil)
+//      }
+//
+// Prometheus's version of a minimal hello-world program:
+//
+//      package main
+//
+//      import (
+//      	"fmt"
+//      	"net/http"
+//
+//      	"github.com/prometheus/client_golang/prometheus"
+//      	"github.com/prometheus/client_golang/prometheus/promauto"
+//      	"github.com/prometheus/client_golang/prometheus/promhttp"
+//      )
+//
+//      func main() {
+//      	http.Handle("/", promhttp.InstrumentHandlerCounter(
+//      		promauto.NewCounterVec(
+//      			prometheus.CounterOpts{
+//      				Name: "hello_requests_total",
+//      				Help: "Total number of hello-world requests by HTTP code.",
+//      			},
+//      			[]string{"code"},
+//      		),
+//      		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+//      			fmt.Fprint(w, "Hello, world!")
+//      		}),
+//      	))
+//      	http.Handle("/metrics", promhttp.Handler())
+//      	http.ListenAndServe(":1971", nil)
+//      }
+//
+// A Factory is created with the With(prometheus.Registerer) function, which
+// enables two usage pattern. With(prometheus.Registerer) can be called once per
+// line:
+//
+//        var (
+//        	reg           = prometheus.NewRegistry()
+//        	randomNumbers = promauto.With(reg).NewHistogram(prometheus.HistogramOpts{
+//        		Name:    "random_numbers",
+//        		Help:    "A histogram of normally distributed random numbers.",
+//        		Buckets: prometheus.LinearBuckets(-3, .1, 61),
+//        	})
+//        	requestCount = promauto.With(reg).NewCounterVec(
+//        		prometheus.CounterOpts{
+//        			Name: "http_requests_total",
+//        			Help: "Total number of HTTP requests by status code end method.",
+//        		},
+//        		[]string{"code", "method"},
+//        	)
+//        )
+//
+// Or it can be used to create a Factory once to be used multiple times:
+//
+//        var (
+//        	reg           = prometheus.NewRegistry()
+//        	factory       = promauto.With(reg)
+//        	randomNumbers = factory.NewHistogram(prometheus.HistogramOpts{
+//        		Name:    "random_numbers",
+//        		Help:    "A histogram of normally distributed random numbers.",
+//        		Buckets: prometheus.LinearBuckets(-3, .1, 61),
+//        	})
+//        	requestCount = factory.NewCounterVec(
+//        		prometheus.CounterOpts{
+//        			Name: "http_requests_total",
+//        			Help: "Total number of HTTP requests by status code end method.",
+//        		},
+//        		[]string{"code", "method"},
+//        	)
+//        )
+//
+// This appears very handy. So why are these constructors locked away in a
+// separate package?
+//
+// The main problem is that registration may fail, e.g. if a metric inconsistent
+// with the newly to be registered one is already registered. Therefore, the
+// Register method in the prometheus.Registerer interface returns an error, and
+// the same is the case for the top-level prometheus.Register function that
+// registers with the global registry. The prometheus package also provides
+// MustRegister versions for both. They panic if the registration fails, and
+// they clearly call this out by using the Must… idiom. Panicking is a bit
+// problematic here because it doesn't just happen on input provided by the
+// caller that is invalid on its own. Things are a bit more subtle here: Metric
+// creation and registration tend to be spread widely over the codebase. It can
+// easily happen that an incompatible metric is added to an unrelated part of
+// the code, and suddenly code that used to work perfectly fine starts to panic
+// (provided that the registration of the newly added metric happens before the
+// registration of the previously existing metric). This may come as an even
+// bigger surprise with the global registry, where simply importing another
+// package can trigger a panic (if the newly imported package registers metrics
+// in its init function). At least, in the prometheus package, creation of
+// metrics and other collectors is separate from registration. You first create
+// the metric, and then you decide explicitly if you want to register it with a
+// local or the global registry, and if you want to handle the error or risk a
+// panic. With the constructors in the promauto package, registration is
+// automatic, and if it fails, it will always panic. Furthermore, the
+// constructors will often be called in the var section of a file, which means
+// that panicking will happen as a side effect of merely importing a package.
+//
+// A separate package allows conservative users to entirely ignore it. And
+// whoever wants to use it, will do so explicitly, with an opportunity to read
+// this warning.
+//
+// Enjoy promauto responsibly!
+package promauto
+
+import "github.com/prometheus/client_golang/prometheus"
+
+// NewCounter works like the function of the same name in the prometheus package
+// but it automatically registers the Counter with the
+// prometheus.DefaultRegisterer. If the registration fails, NewCounter panics.
+func NewCounter(opts prometheus.CounterOpts) prometheus.Counter {
+	return With(prometheus.DefaultRegisterer).NewCounter(opts)
+}
+
+// NewCounterVec works like the function of the same name in the prometheus
+// package but it automatically registers the CounterVec with the
+// prometheus.DefaultRegisterer. If the registration fails, NewCounterVec
+// panics.
+func NewCounterVec(opts prometheus.CounterOpts, labelNames []string) *prometheus.CounterVec {
+	return With(prometheus.DefaultRegisterer).NewCounterVec(opts, labelNames)
+}
+
+// NewCounterFunc works like the function of the same name in the prometheus
+// package but it automatically registers the CounterFunc with the
+// prometheus.DefaultRegisterer. If the registration fails, NewCounterFunc
+// panics.
+func NewCounterFunc(opts prometheus.CounterOpts, function func() float64) prometheus.CounterFunc {
+	return With(prometheus.DefaultRegisterer).NewCounterFunc(opts, function)
+}
+
+// NewGauge works like the function of the same name in the prometheus package
+// but it automatically registers the Gauge with the
+// prometheus.DefaultRegisterer. If the registration fails, NewGauge panics.
+func NewGauge(opts prometheus.GaugeOpts) prometheus.Gauge {
+	return With(prometheus.DefaultRegisterer).NewGauge(opts)
+}
+
+// NewGaugeVec works like the function of the same name in the prometheus
+// package but it automatically registers the GaugeVec with the
+// prometheus.DefaultRegisterer. If the registration fails, NewGaugeVec panics.
+func NewGaugeVec(opts prometheus.GaugeOpts, labelNames []string) *prometheus.GaugeVec {
+	return With(prometheus.DefaultRegisterer).NewGaugeVec(opts, labelNames)
+}
+
+// NewGaugeFunc works like the function of the same name in the prometheus
+// package but it automatically registers the GaugeFunc with the
+// prometheus.DefaultRegisterer. If the registration fails, NewGaugeFunc panics.
+func NewGaugeFunc(opts prometheus.GaugeOpts, function func() float64) prometheus.GaugeFunc {
+	return With(prometheus.DefaultRegisterer).NewGaugeFunc(opts, function)
+}
+
+// NewSummary works like the function of the same name in the prometheus package
+// but it automatically registers the Summary with the
+// prometheus.DefaultRegisterer. If the registration fails, NewSummary panics.
+func NewSummary(opts prometheus.SummaryOpts) prometheus.Summary {
+	return With(prometheus.DefaultRegisterer).NewSummary(opts)
+}
+
+// NewSummaryVec works like the function of the same name in the prometheus
+// package but it automatically registers the SummaryVec with the
+// prometheus.DefaultRegisterer. If the registration fails, NewSummaryVec
+// panics.
+func NewSummaryVec(opts prometheus.SummaryOpts, labelNames []string) *prometheus.SummaryVec {
+	return With(prometheus.DefaultRegisterer).NewSummaryVec(opts, labelNames)
+}
+
+// NewHistogram works like the function of the same name in the prometheus
+// package but it automatically registers the Histogram with the
+// prometheus.DefaultRegisterer. If the registration fails, NewHistogram panics.
+func NewHistogram(opts prometheus.HistogramOpts) prometheus.Histogram {
+	return With(prometheus.DefaultRegisterer).NewHistogram(opts)
+}
+
+// NewHistogramVec works like the function of the same name in the prometheus
+// package but it automatically registers the HistogramVec with the
+// prometheus.DefaultRegisterer. If the registration fails, NewHistogramVec
+// panics.
+func NewHistogramVec(opts prometheus.HistogramOpts, labelNames []string) *prometheus.HistogramVec {
+	return With(prometheus.DefaultRegisterer).NewHistogramVec(opts, labelNames)
+}
+
+// NewUntypedFunc works like the function of the same name in the prometheus
+// package but it automatically registers the UntypedFunc with the
+// prometheus.DefaultRegisterer. If the registration fails, NewUntypedFunc
+// panics.
+func NewUntypedFunc(opts prometheus.UntypedOpts, function func() float64) prometheus.UntypedFunc {
+	return With(prometheus.DefaultRegisterer).NewUntypedFunc(opts, function)
+}
+
+// Factory provides factory methods to create Collectors that are automatically
+// registered with a Registerer. Create a Factory with the With function,
+// providing a Registerer to auto-register created Collectors with. The zero
+// value of a Factory creates Collectors that are not registered with any
+// Registerer. All methods of the Factory panic if the registration fails.
+type Factory struct {
+	r prometheus.Registerer
+}
+
+// With creates a Factory using the provided Registerer for registration of the
+// created Collectors.
+func With(r prometheus.Registerer) Factory { return Factory{r} }
+
+// NewCounter works like the function of the same name in the prometheus package
+// but it automatically registers the Counter with the Factory's Registerer.
+func (f Factory) NewCounter(opts prometheus.CounterOpts) prometheus.Counter {
+	c := prometheus.NewCounter(opts)
+	if f.r != nil {
+		f.r.MustRegister(c)
+	}
+	return c
+}
+
+// NewCounterVec works like the function of the same name in the prometheus
+// package but it automatically registers the CounterVec with the Factory's
+// Registerer.
+func (f Factory) NewCounterVec(opts prometheus.CounterOpts, labelNames []string) *prometheus.CounterVec {
+	c := prometheus.NewCounterVec(opts, labelNames)
+	if f.r != nil {
+		f.r.MustRegister(c)
+	}
+	return c
+}
+
+// NewCounterFunc works like the function of the same name in the prometheus
+// package but it automatically registers the CounterFunc with the Factory's
+// Registerer.
+func (f Factory) NewCounterFunc(opts prometheus.CounterOpts, function func() float64) prometheus.CounterFunc {
+	c := prometheus.NewCounterFunc(opts, function)
+	if f.r != nil {
+		f.r.MustRegister(c)
+	}
+	return c
+}
+
+// NewGauge works like the function of the same name in the prometheus package
+// but it automatically registers the Gauge with the Factory's Registerer.
+func (f Factory) NewGauge(opts prometheus.GaugeOpts) prometheus.Gauge {
+	g := prometheus.NewGauge(opts)
+	if f.r != nil {
+		f.r.MustRegister(g)
+	}
+	return g
+}
+
+// NewGaugeVec works like the function of the same name in the prometheus
+// package but it automatically registers the GaugeVec with the Factory's
+// Registerer.
+func (f Factory) NewGaugeVec(opts prometheus.GaugeOpts, labelNames []string) *prometheus.GaugeVec {
+	g := prometheus.NewGaugeVec(opts, labelNames)
+	if f.r != nil {
+		f.r.MustRegister(g)
+	}
+	return g
+}
+
+// NewGaugeFunc works like the function of the same name in the prometheus
+// package but it automatically registers the GaugeFunc with the Factory's
+// Registerer.
+func (f Factory) NewGaugeFunc(opts prometheus.GaugeOpts, function func() float64) prometheus.GaugeFunc {
+	g := prometheus.NewGaugeFunc(opts, function)
+	if f.r != nil {
+		f.r.MustRegister(g)
+	}
+	return g
+}
+
+// NewSummary works like the function of the same name in the prometheus package
+// but it automatically registers the Summary with the Factory's Registerer.
+func (f Factory) NewSummary(opts prometheus.SummaryOpts) prometheus.Summary {
+	s := prometheus.NewSummary(opts)
+	if f.r != nil {
+		f.r.MustRegister(s)
+	}
+	return s
+}
+
+// NewSummaryVec works like the function of the same name in the prometheus
+// package but it automatically registers the SummaryVec with the Factory's
+// Registerer.
+func (f Factory) NewSummaryVec(opts prometheus.SummaryOpts, labelNames []string) *prometheus.SummaryVec {
+	s := prometheus.NewSummaryVec(opts, labelNames)
+	if f.r != nil {
+		f.r.MustRegister(s)
+	}
+	return s
+}
+
+// NewHistogram works like the function of the same name in the prometheus
+// package but it automatically registers the Histogram with the Factory's
+// Registerer.
+func (f Factory) NewHistogram(opts prometheus.HistogramOpts) prometheus.Histogram {
+	h := prometheus.NewHistogram(opts)
+	if f.r != nil {
+		f.r.MustRegister(h)
+	}
+	return h
+}
+
+// NewHistogramVec works like the function of the same name in the prometheus
+// package but it automatically registers the HistogramVec with the Factory's
+// Registerer.
+func (f Factory) NewHistogramVec(opts prometheus.HistogramOpts, labelNames []string) *prometheus.HistogramVec {
+	h := prometheus.NewHistogramVec(opts, labelNames)
+	if f.r != nil {
+		f.r.MustRegister(h)
+	}
+	return h
+}
+
+// NewUntypedFunc works like the function of the same name in the prometheus
+// package but it automatically registers the UntypedFunc with the Factory's
+// Registerer.
+func (f Factory) NewUntypedFunc(opts prometheus.UntypedOpts, function func() float64) prometheus.UntypedFunc {
+	u := prometheus.NewUntypedFunc(opts, function)
+	if f.r != nil {
+		f.r.MustRegister(u)
+	}
+	return u
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 8d6fd48..4c61382 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -61,6 +61,7 @@ github.com/pkg/errors
 # github.com/prometheus/client_golang v1.5.1
 github.com/prometheus/client_golang/prometheus
 github.com/prometheus/client_golang/prometheus/internal
+github.com/prometheus/client_golang/prometheus/promauto
 github.com/prometheus/client_golang/prometheus/promhttp
 # github.com/prometheus/client_model v0.2.0
 github.com/prometheus/client_model/go


Test patch:
diff --git a/middlewares/reqlog/reqlog_test.go b/middlewares/reqlog/reqlog_test.go
new file mode 100644
index 0000000..21d28ab
--- /dev/null
+++ b/middlewares/reqlog/reqlog_test.go
@@ -0,0 +1,231 @@
+/*
+Copyright 2017 Caicloud Authors
+
+Licensed 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 reqlog
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"os"
+	"regexp"
+	"strings"
+	"testing"
+
+	"github.com/caicloud/nirvana/definition"
+	"github.com/caicloud/nirvana/log"
+	"github.com/caicloud/nirvana/service"
+)
+
+const tenantHeader = "X-Tenant"
+
+type responseWriter struct {
+	code   int
+	header http.Header
+	buf    *bytes.Buffer
+}
+
+func newRW() *responseWriter {
+	return &responseWriter{0, http.Header{}, bytes.NewBuffer(nil)}
+}
+
+func (r *responseWriter) Header() http.Header {
+	return r.header
+}
+
+func (r *responseWriter) Write(d []byte) (int, error) {
+	return r.buf.Write(d)
+}
+
+func (r *responseWriter) WriteHeader(code int) {
+	r.code = code
+}
+
+func mustURL(rawURL string) *url.URL {
+	u, _ := url.Parse(rawURL)
+	return u
+}
+
+func buildService(middleware definition.Middleware) (service.Service, error) {
+	builder := service.NewBuilder()
+	builder.SetModifier(service.FirstContextParameter())
+	if err := builder.AddDescriptor(
+		definition.Descriptor{
+			Path:        "/api/v1/foo",
+			Consumes:    []string{"*/*"},
+			Produces:    []string{"application/json"},
+			Middlewares: []definition.Middleware{middleware},
+			Definitions: []definition.Definition{
+				{
+					Method: definition.Create,
+					Parameters: []definition.Parameter{
+						definition.HeaderParameterFor(tenantHeader, ""),
+						definition.QueryParameterFor("query", ""),
+						definition.BodyParameterFor(""),
+					},
+					Function: func(ctx context.Context, tenant, query string, body []byte) (string, error) {
+						request := service.HTTPContextFrom(ctx).Request()
+						return fmt.Sprintf(
+							"%s %s %s %s %s",
+							request.Method, request.URL.String(),
+							tenant, query, string(body),
+						), nil
+					},
+					Results: definition.DataErrorResults(""),
+				},
+				{
+					Method: definition.Get,
+					Parameters: []definition.Parameter{
+						definition.HeaderParameterFor(tenantHeader, ""),
+						definition.QueryParameterFor("query", ""),
+					},
+					Function: func(ctx context.Context, tenant, query string) (string, error) {
+						request := service.HTTPContextFrom(ctx).Request()
+						return fmt.Sprintf(
+							"%s %s %s %s",
+							request.Method, request.URL.String(), tenant, query,
+						), nil
+					},
+					Results: definition.DataErrorResults(""),
+				},
+			},
+		},
+	); err != nil {
+		return nil, err
+	}
+	return builder.Build()
+}
+
+type testCase struct {
+	name      string
+	req       http.Request
+	wantRegex string
+	respStr   string
+	respCode  int
+}
+
+func buildTestCases() []testCase {
+	return []testCase{
+		{
+			name: "POST",
+			req: http.Request{
+				Method: http.MethodPost,
+				URL:    mustURL("/api/v1/foo?query=bar"),
+				Header: http.Header{
+					"Content-Type": []string{"application/json"},
+					"Accept":       []string{"application/json"},
+					"X-Tenant":     []string{"system-tenant"},
+				},
+				Body: ioutil.NopCloser(strings.NewReader("{\"key\": \"value\"}")),
+			},
+			wantRegex: `^INFO\s(.+) | POST 201 61 [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+ /api/v1/foo?query=bar$`,
+			respStr:   `POST /api/v1/foo?query=bar system-tenant bar {"key": "value"}`,
+			respCode:  201,
+		},
+		{
+			name: "GET",
+			req: http.Request{
+				Method: http.MethodGet,
+				URL:    mustURL("/api/v1/foo?query=bar"),
+				Header: http.Header{
+					"X-Tenant": []string{"system-tenant"},
+				},
+			},
+			wantRegex: `^INFO\s(.+) | GET 200 43 [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+ /api/v1/foo?query=bar$`,
+			respStr:   `GET /api/v1/foo?query=bar system-tenant bar`,
+			respCode:  200,
+		},
+	}
+}
+
+func TestDefault(t *testing.T) {
+	for _, tc := range buildTestCases() {
+		t.Run(tc.name, func(t *testing.T) {
+			old := os.Stderr
+			r, w, _ := os.Pipe()
+			os.Stderr = w
+			outC := make(chan string)
+			go func() {
+				var buf bytes.Buffer
+				io.Copy(&buf, r)
+				outC <- buf.String()
+			}()
+
+			log.SetDefaultLogger(log.NewStdLogger(log.LevelDebug))
+			svc, err := buildService(Default())
+			if err != nil {
+				t.Fatal(err)
+			}
+			rw := newRW()
+			svc.ServeHTTP(rw, &tc.req)
+
+			w.Close()
+			out := <-outC
+			os.Stderr = old
+
+			if rw.buf.String() != tc.respStr {
+				t.Fatalf("%s != %s", rw.buf.String(), tc.respStr)
+			}
+			if rw.code != tc.respCode {
+				t.Fatal("unexpected response code, the middleware might have altered the request/response")
+			}
+			if !regexp.MustCompile(tc.wantRegex).MatchString(out) {
+				t.Fatal("unexpected log content")
+			}
+		})
+	}
+}
+
+func TestCustom(t *testing.T) {
+	for _, tc := range buildTestCases() {
+		t.Run(tc.name, func(t *testing.T) {
+			old := os.Stderr
+			r, w, _ := os.Pipe()
+			os.Stderr = w
+			outC := make(chan string)
+			go func() {
+				var buf bytes.Buffer
+				io.Copy(&buf, r)
+				outC <- buf.String()
+			}()
+
+			svc, err := buildService(Custom(log.NewStdLogger(0), 0))
+			if err != nil {
+				t.Fatal(err)
+			}
+			rw := newRW()
+			svc.ServeHTTP(rw, &tc.req)
+
+			w.Close()
+			out := <-outC
+			os.Stderr = old
+
+			if rw.buf.String() != tc.respStr {
+				t.Fatalf("%s != %s", rw.buf.String(), tc.respStr)
+			}
+			if rw.code != tc.respCode {
+				t.Fatal("unexpected response code, the middleware might have altered the request/response")
+			}
+			if !regexp.MustCompile(tc.wantRegex).MatchString(out) {
+				t.Fatal("unexpected log content")
+			}
+		})
+	}
+}