Skip to content
This repository was archived by the owner on Dec 5, 2023. It is now read-only.

Commit ed60362

Browse files
Merge pull request #43 from microservices-demo/refactoring/prometheus
Use standard prometheus naming
2 parents ca1adfe + 2708659 commit ed60362

File tree

6 files changed

+149
-5
lines changed

6 files changed

+149
-5
lines changed

api/transport.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import (
99
"net/http"
1010
"strings"
1111

12+
"github.com/go-kit/kit/circuitbreaker"
1213
"github.com/go-kit/kit/log"
1314
"github.com/go-kit/kit/tracing/opentracing"
1415
httptransport "github.com/go-kit/kit/transport/http"
15-
"github.com/go-kit/kit/circuitbreaker"
1616
"github.com/gorilla/mux"
1717
"github.com/microservices-demo/user/users"
1818
stdopentracing "github.com/opentracing/opentracing-go"
@@ -25,7 +25,7 @@ var (
2525
)
2626

2727
// MakeHTTPHandler mounts the endpoints into a REST-y HTTP handler.
28-
func MakeHTTPHandler(ctx context.Context, e Endpoints, logger log.Logger, tracer stdopentracing.Tracer) http.Handler {
28+
func MakeHTTPHandler(ctx context.Context, e Endpoints, logger log.Logger, tracer stdopentracing.Tracer) *mux.Router {
2929
r := mux.NewRouter().StrictSlash(false)
3030
options := []httptransport.ServerOption{
3131
httptransport.ServerErrorLogger(logger),

glide.lock

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

glide.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ import:
2626
- package: github.com/afex/hystrix-go
2727
subpackages:
2828
- hystrix
29+
- package: github.com/felixge/httpsnoop

main.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/microservices-demo/user/api"
1616
"github.com/microservices-demo/user/db"
1717
"github.com/microservices-demo/user/db/mongodb"
18+
"github.com/microservices-demo/user/middleware"
1819
stdopentracing "github.com/opentracing/opentracing-go"
1920
zipkin "github.com/openzipkin/zipkin-go-opentracing"
2021
stdprometheus "github.com/prometheus/client_golang/prometheus"
@@ -120,10 +121,23 @@ func main() {
120121
// Endpoint domain.
121122
endpoints := api.MakeEndpoints(service, tracer)
122123

124+
// HTTP router
125+
router := api.MakeHTTPHandler(ctx, endpoints, logger, tracer)
126+
127+
httpMiddleware := []middleware.Interface{
128+
middleware.Instrument{
129+
Duration: middleware.HTTPLatency,
130+
RouteMatcher: router,
131+
Service: ServiceName,
132+
},
133+
}
134+
135+
// Handler
136+
handler := middleware.Merge(httpMiddleware...).Wrap(router)
137+
123138
// Create and launch the HTTP server.
124139
go func() {
125140
logger.Log("transport", "HTTP", "port", port)
126-
handler := api.MakeHTTPHandler(ctx, endpoints, logger, tracer)
127141
errc <- http.ListenAndServe(fmt.Sprintf(":%v", port), handler)
128142
}()
129143

middleware/instrument.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package middleware
2+
3+
import (
4+
"net/http"
5+
"regexp"
6+
"strconv"
7+
"strings"
8+
"time"
9+
10+
"github.com/felixge/httpsnoop"
11+
"github.com/gorilla/mux"
12+
"github.com/prometheus/client_golang/prometheus"
13+
)
14+
15+
var (
16+
HTTPLatency = prometheus.NewHistogramVec(prometheus.HistogramOpts{
17+
Name: "request_duration_seconds",
18+
Help: "Time (in seconds) spent serving HTTP requests.",
19+
Buckets: prometheus.DefBuckets,
20+
}, []string{"service", "method", "route", "status_code"})
21+
)
22+
23+
func init() {
24+
prometheus.MustRegister(HTTPLatency)
25+
}
26+
27+
// RouteMatcher matches routes
28+
type RouteMatcher interface {
29+
Match(*http.Request, *mux.RouteMatch) bool
30+
}
31+
32+
// Instrument is a Middleware which records timings for every HTTP request
33+
type Instrument struct {
34+
RouteMatcher RouteMatcher
35+
Duration *prometheus.HistogramVec
36+
Service string
37+
}
38+
39+
// Wrap implements middleware.Interface
40+
func (i Instrument) Wrap(next http.Handler) http.Handler {
41+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
42+
begin := time.Now()
43+
interceptor := httpsnoop.CaptureMetrics(next, w, r)
44+
route := i.getRouteName(r)
45+
var (
46+
status = strconv.Itoa(interceptor.Code)
47+
took = time.Since(begin)
48+
)
49+
i.Duration.WithLabelValues(i.Service, r.Method, route, status).Observe(took.Seconds())
50+
})
51+
}
52+
53+
// Return a name identifier for ths request. There are three options:
54+
// 1. The request matches a gorilla mux route, with a name. Use that.
55+
// 2. The request matches an unamed gorilla mux router. Munge the path
56+
// template such that templates like '/api/{org}/foo' come out as
57+
// 'api_org_foo'.
58+
// 3. The request doesn't match a mux route. Munge the Path in the same
59+
// manner as (2).
60+
// We do all this as we do not wish to emit high cardinality labels to
61+
// prometheus.
62+
func (i Instrument) getRouteName(r *http.Request) string {
63+
var routeMatch mux.RouteMatch
64+
if i.RouteMatcher != nil && i.RouteMatcher.Match(r, &routeMatch) {
65+
if name := routeMatch.Route.GetName(); name != "" {
66+
return name
67+
}
68+
if tmpl, err := routeMatch.Route.GetPathTemplate(); err == nil {
69+
return MakeLabelValue(tmpl)
70+
}
71+
}
72+
return MakeLabelValue(r.URL.Path)
73+
}
74+
75+
var invalidChars = regexp.MustCompile(`[^a-zA-Z0-9]+`)
76+
77+
// MakeLabelValue converts a Gorilla mux path to a string suitable for use in
78+
// a Prometheus label value.
79+
func MakeLabelValue(path string) string {
80+
// Convert non-alnums to underscores.
81+
result := invalidChars.ReplaceAllString(path, "_")
82+
83+
// Trim leading and trailing underscores.
84+
result = strings.Trim(result, "_")
85+
86+
// Make it all lowercase
87+
result = strings.ToLower(result)
88+
89+
// Special case.
90+
if result == "" {
91+
result = "root"
92+
}
93+
return result
94+
}

middleware/middleware.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package middleware
2+
3+
import (
4+
"net/http"
5+
)
6+
7+
// Interface is the shared contract for all middlesware, and allows middlesware
8+
// to wrap handlers.
9+
type Interface interface {
10+
Wrap(http.Handler) http.Handler
11+
}
12+
13+
// Func is to Interface as http.HandlerFunc is to http.Handler
14+
type Func func(http.Handler) http.Handler
15+
16+
// Wrap implements Interface
17+
func (m Func) Wrap(next http.Handler) http.Handler {
18+
return m(next)
19+
}
20+
21+
// Identity is an Interface which doesn't do anything.
22+
var Identity Interface = Func(func(h http.Handler) http.Handler { return h })
23+
24+
// Merge produces a middleware that applies multiple middlesware in turn;
25+
// ie Merge(f,g,h).Wrap(handler) == f.Wrap(g.Wrap(h.Wrap(handler)))
26+
func Merge(middlesware ...Interface) Interface {
27+
return Func(func(next http.Handler) http.Handler {
28+
for i := len(middlesware) - 1; i >= 0; i-- {
29+
next = middlesware[i].Wrap(next)
30+
}
31+
return next
32+
})
33+
}

0 commit comments

Comments
 (0)