Merge remote-tracking branch 'upstream/master'
Olivier Bregeras
8 years ago
0 | 0 | examples/addsvc/addsvc |
1 | 1 | examples/addsvc/client/client |
2 | examples/profilesvc/profilesvc | |
2 | 3 | examples/stringsvc1/stringsvc1 |
3 | 4 | examples/stringsvc2/stringsvc2 |
4 | 5 | examples/stringsvc3/stringsvc3 |
0 | 0 | language: go |
1 | ||
2 | script: go test -race -v ./... | |
1 | 3 | |
2 | 4 | go: |
3 | 5 | - 1.4.2 |
4 | - 1.5 | |
6 | - 1.5.3 | |
7 | - 1.6 | |
5 | 8 | - tip |
0 | # Go kit [![Circle CI](https://circleci.com/gh/go-kit/kit.svg?style=svg)](https://circleci.com/gh/go-kit/kit) [![Drone.io](https://drone.io/github.com/go-kit/kit/status.png)](https://drone.io/github.com/go-kit/kit/latest) [![Travis CI](https://travis-ci.org/go-kit/kit.svg?branch=master)](https://travis-ci.org/go-kit/kit) [![GoDoc](https://godoc.org/github.com/go-kit/kit?status.svg)](https://godoc.org/github.com/go-kit/kit) [![Coverage Status](https://coveralls.io/repos/go-kit/kit/badge.svg?branch=master&service=github)](https://coveralls.io/github/go-kit/kit?branch=master) | |
0 | # Go kit [![Circle CI](https://circleci.com/gh/go-kit/kit.svg?style=svg)](https://circleci.com/gh/go-kit/kit) [![Drone.io](https://drone.io/github.com/go-kit/kit/status.png)](https://drone.io/github.com/go-kit/kit/latest) [![Travis CI](https://travis-ci.org/go-kit/kit.svg?branch=master)](https://travis-ci.org/go-kit/kit) [![GoDoc](https://godoc.org/github.com/go-kit/kit?status.svg)](https://godoc.org/github.com/go-kit/kit) [![Coverage Status](https://coveralls.io/repos/go-kit/kit/badge.svg?branch=master&service=github)](https://coveralls.io/github/go-kit/kit?branch=master) [![Go Report Card](https://goreportcard.com/badge/go-kit/kit)](https://goreportcard.com/report/go-kit/kit) | |
1 | 1 | |
2 | 2 | **Go kit** is a **distributed programming toolkit** for building microservices |
3 | 3 | in large organizations. We solve common problems in distributed systems, so |
4 | 4 | you can focus on your business logic. |
5 | 5 | |
6 | 6 | - Mailing list: [go-kit](https://groups.google.com/forum/#!forum/go-kit) |
7 | - Slack: [gophers.slack.com](https://gophers.slack.com) **#go-kit** ([invite](http://bit.ly/go-slack-signup)) | |
7 | - Slack: [gophers.slack.com](https://gophers.slack.com) **#go-kit** ([invite](https://gophersinvite.herokuapp.com/)) | |
8 | 8 | |
9 | 9 | ## Documentation |
10 | 10 | |
52 | 52 | transport, bindings are typically provided by the Go library for the |
53 | 53 | transport, and there's not much for Go kit to do. In those cases, see the |
54 | 54 | examples to understand how to write adapters for your endpoints. For now, see |
55 | the [addsvc][addsvc] to understand how transport bindings work. We'll soon | |
56 | have specific examples for Thrift, gRPC, net/rpc, and JSON over HTTP. Avro and | |
57 | JSON/RPC support is planned. | |
55 | the [addsvc][addsvc] to understand how transport bindings work. We have | |
56 | specific examples for Thrift, gRPC, net/rpc, and JSON over HTTP. JSON/RPC and | |
57 | Swagger support is planned. | |
58 | 58 | |
59 | 59 | [transport]: https://github.com/go-kit/kit/tree/master/transport |
60 | 60 | [addsvc]: https://github.com/go-kit/kit/tree/master/examples/addsvc |
165 | 165 | recommend use of third-party import proxies. |
166 | 166 | |
167 | 167 | There are several tools which make vendoring easier, including [gb][], |
168 | [govendor][], and [Godep][]. And Go kit uses a variety of continuous | |
168 | [govendor][], and [godep][]. And Go kit uses a variety of continuous | |
169 | 169 | integration providers to find and fix compatibility problems as soon as they |
170 | 170 | occur. |
171 | 171 | |
172 | 172 | [gb]: http://getgb.io |
173 | 173 | [govendor]: https://github.com/kardianos/govendor |
174 | [Godep]: https://github.com/tools/godep | |
174 | [godep]: https://github.com/tools/godep | |
175 | 175 | |
176 | 176 | ## API stability policy |
177 | 177 |
14 | 14 | shouldPass = func(n int) bool { return n <= 5 } // https://github.com/sony/gobreaker/blob/bfa846d/gobreaker.go#L76 |
15 | 15 | circuitOpenError = "circuit breaker is open" |
16 | 16 | ) |
17 | testFailingEndpoint(t, breaker, primeWith, shouldPass, circuitOpenError) | |
17 | testFailingEndpoint(t, breaker, primeWith, shouldPass, 0, circuitOpenError) | |
18 | 18 | } |
15 | 15 | shouldPass = func(n int) bool { return (float64(n) / float64(primeWith+n)) <= failureRatio } |
16 | 16 | openCircuitError = handybreaker.ErrCircuitOpen.Error() |
17 | 17 | ) |
18 | testFailingEndpoint(t, breaker, primeWith, shouldPass, openCircuitError) | |
18 | testFailingEndpoint(t, breaker, primeWith, shouldPass, 0, openCircuitError) | |
19 | 19 | } |
16 | 16 | func Hystrix(commandName string) endpoint.Middleware { |
17 | 17 | return func(next endpoint.Endpoint) endpoint.Endpoint { |
18 | 18 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { |
19 | output := make(chan interface{}, 1) | |
20 | errors := hystrix.Go(commandName, func() error { | |
21 | resp, err := next(ctx, request) | |
22 | if err == nil { | |
23 | output <- resp | |
24 | } | |
19 | var resp interface{} | |
20 | if err := hystrix.Do(commandName, func() (err error) { | |
21 | resp, err = next(ctx, request) | |
25 | 22 | return err |
26 | }, nil) | |
27 | ||
28 | select { | |
29 | case resp := <-output: | |
30 | return resp, nil | |
31 | case err := <-errors: | |
23 | }, nil); err != nil { | |
32 | 24 | return nil, err |
33 | 25 | } |
26 | return resp, nil | |
34 | 27 | } |
35 | 28 | } |
36 | 29 | } |
0 | 0 | package circuitbreaker_test |
1 | 1 | |
2 | 2 | import ( |
3 | "io/ioutil" | |
3 | 4 | stdlog "log" |
4 | "os" | |
5 | 5 | "testing" |
6 | "time" | |
6 | 7 | |
7 | 8 | "github.com/afex/hystrix-go/hystrix" |
8 | 9 | |
9 | 10 | "github.com/go-kit/kit/circuitbreaker" |
10 | kitlog "github.com/go-kit/kit/log" | |
11 | 11 | ) |
12 | 12 | |
13 | 13 | func TestHystrix(t *testing.T) { |
14 | logger := kitlog.NewLogfmtLogger(os.Stderr) | |
15 | stdlog.SetOutput(kitlog.NewStdlibAdapter(logger)) | |
14 | stdlog.SetOutput(ioutil.Discard) | |
16 | 15 | |
17 | 16 | const ( |
18 | 17 | commandName = "my-endpoint" |
30 | 29 | shouldPass = func(n int) bool { return (float64(n) / float64(primeWith+n)) <= (float64(errorPercent-1) / 100.0) } |
31 | 30 | openCircuitError = hystrix.ErrCircuitOpen.Error() |
32 | 31 | ) |
33 | testFailingEndpoint(t, breaker, primeWith, shouldPass, openCircuitError) | |
32 | ||
33 | // hystrix-go uses buffered channels to receive reports on request success/failure, | |
34 | // and so is basically impossible to test deterministically. We have to make sure | |
35 | // the report buffer is emptied, by injecting a sleep between each invocation. | |
36 | requestDelay := 5 * time.Millisecond | |
37 | ||
38 | testFailingEndpoint(t, breaker, primeWith, shouldPass, requestDelay, openCircuitError) | |
34 | 39 | } |
12 | 12 | "github.com/go-kit/kit/endpoint" |
13 | 13 | ) |
14 | 14 | |
15 | func testFailingEndpoint(t *testing.T, breaker endpoint.Middleware, primeWith int, shouldPass func(int) bool, openCircuitError string) { | |
15 | func testFailingEndpoint( | |
16 | t *testing.T, | |
17 | breaker endpoint.Middleware, | |
18 | primeWith int, | |
19 | shouldPass func(int) bool, | |
20 | requestDelay time.Duration, | |
21 | openCircuitError string, | |
22 | ) { | |
16 | 23 | _, file, line, _ := runtime.Caller(1) |
17 | 24 | caller := fmt.Sprintf("%s:%d", filepath.Base(file), line) |
18 | 25 | |
27 | 34 | if _, err := e(context.Background(), struct{}{}); err != nil { |
28 | 35 | t.Fatalf("%s: during priming, got error: %v", caller, err) |
29 | 36 | } |
37 | time.Sleep(requestDelay) | |
30 | 38 | } |
31 | 39 | |
32 | 40 | // Switch the endpoint to start throwing errors. |
38 | 46 | if _, err := e(context.Background(), struct{}{}); err != m.err { |
39 | 47 | t.Fatalf("%s: want %v, have %v", caller, m.err, err) |
40 | 48 | } |
49 | time.Sleep(requestDelay) | |
41 | 50 | } |
42 | 51 | thru := m.thru |
43 | ||
44 | // Adding the sleep due to https://github.com/afex/hystrix-go/issues/41 | |
45 | // Increasing the sleep due to https://github.com/go-kit/kit/issues/169 | |
46 | time.Sleep(5 * time.Millisecond) | |
47 | 52 | |
48 | 53 | // But the rest should be blocked by an open circuit. |
49 | 54 | for i := 0; i < 10; i++ { |
50 | 55 | if _, err := e(context.Background(), struct{}{}); err.Error() != openCircuitError { |
51 | 56 | t.Fatalf("%s: want %q, have %q", caller, openCircuitError, err.Error()) |
52 | 57 | } |
58 | time.Sleep(requestDelay) | |
53 | 59 | } |
54 | 60 | |
55 | 61 | // Make sure none of those got through. |
672 | 672 | It also demonstrates how to create and use client packages. |
673 | 673 | It's a good example of a fully-featured Go kit service. |
674 | 674 | |
675 | ### profilesvc | |
676 | ||
677 | [profilesvc](https://github.com/go-kit/kit/blob/master/examples/profilesvc) | |
678 | demonstrates how to use Go kit to build a REST-ish microservice. | |
679 | ||
675 | 680 | ### apigateway |
676 | 681 | |
677 | The next example in the works is an API gateway. | |
678 | Track issue #91 for progress. | |
682 | Track [issue #202](https://github.com/go-kit/kit/issues/202) for progress. |
41 | 41 | } |
42 | 42 | reply, err := c.AddClient.Sum(c.Context, request) |
43 | 43 | if err != nil { |
44 | _ = c.Logger.Log("err", err) // Without an error return parameter, we can't do anything else... | |
44 | c.Logger.Log("err", err) // Without an error return parameter, we can't do anything else... | |
45 | 45 | return 0 |
46 | 46 | } |
47 | 47 | return int(reply.V) |
54 | 54 | } |
55 | 55 | reply, err := c.AddClient.Concat(c.Context, request) |
56 | 56 | if err != nil { |
57 | _ = c.Logger.Log("err", err) | |
57 | c.Logger.Log("err", err) | |
58 | 58 | return "" |
59 | 59 | } |
60 | 60 | return reply.V |
19 | 19 | if err != nil { |
20 | 20 | panic(err) |
21 | 21 | } |
22 | sumURL.Path = "/sum" | |
23 | ||
22 | 24 | concatURL, err := url.Parse(baseurl.String()) |
23 | 25 | if err != nil { |
24 | 26 | panic(err) |
25 | 27 | } |
26 | sumURL.Path = "/sum" | |
27 | 28 | concatURL.Path = "/concat" |
29 | ||
28 | 30 | return client{ |
29 | 31 | Context: ctx, |
30 | 32 | Logger: logger, |
33 | 35 | sumURL, |
34 | 36 | server.EncodeSumRequest, |
35 | 37 | server.DecodeSumResponse, |
38 | httptransport.SetClient(c), | |
36 | 39 | ).Endpoint(), |
37 | 40 | concat: httptransport.NewClient( |
38 | 41 | "GET", |
39 | 42 | concatURL, |
40 | 43 | server.EncodeConcatRequest, |
41 | 44 | server.DecodeConcatResponse, |
45 | httptransport.SetClient(c), | |
42 | 46 | ).Endpoint(), |
43 | 47 | } |
44 | 48 | } |
53 | 57 | func (c client) Sum(a, b int) int { |
54 | 58 | response, err := c.sum(c.Context, server.SumRequest{A: a, B: b}) |
55 | 59 | if err != nil { |
56 | _ = c.Logger.Log("err", err) | |
60 | c.Logger.Log("err", err) | |
57 | 61 | return 0 |
58 | 62 | } |
59 | 63 | return response.(server.SumResponse).V |
62 | 66 | func (c client) Concat(a, b string) string { |
63 | 67 | response, err := c.concat(c.Context, server.ConcatRequest{A: a, B: b}) |
64 | 68 | if err != nil { |
65 | _ = c.Logger.Log("err", err) | |
69 | c.Logger.Log("err", err) | |
66 | 70 | return "" |
67 | 71 | } |
68 | 72 | return response.(server.ConcatResponse).V |
19 | 19 | func (c client) Sum(a, b int) int { |
20 | 20 | var reply server.SumResponse |
21 | 21 | if err := c.Client.Call("addsvc.Sum", server.SumRequest{A: a, B: b}, &reply); err != nil { |
22 | _ = c.Logger.Log("err", err) | |
22 | c.Logger.Log("err", err) | |
23 | 23 | return 0 |
24 | 24 | } |
25 | 25 | return reply.V |
28 | 28 | func (c client) Concat(a, b string) string { |
29 | 29 | var reply server.ConcatResponse |
30 | 30 | if err := c.Client.Call("addsvc.Concat", server.ConcatRequest{A: a, B: b}, &reply); err != nil { |
31 | _ = c.Logger.Log("err", err) | |
31 | c.Logger.Log("err", err) | |
32 | 32 | return "" |
33 | 33 | } |
34 | 34 | return reply.V |
18 | 18 | func (c client) Sum(a, b int) int { |
19 | 19 | reply, err := c.AddServiceClient.Sum(int64(a), int64(b)) |
20 | 20 | if err != nil { |
21 | _ = c.Logger.Log("err", err) | |
21 | c.Logger.Log("err", err) | |
22 | 22 | return 0 |
23 | 23 | } |
24 | 24 | return int(reply.Value) |
27 | 27 | func (c client) Concat(a, b string) string { |
28 | 28 | reply, err := c.AddServiceClient.Concat(a, b) |
29 | 29 | if err != nil { |
30 | _ = c.Logger.Log("err", err) | |
30 | c.Logger.Log("err", err) | |
31 | 31 | return "" |
32 | 32 | } |
33 | 33 | return reply.Value |
69 | 69 | var requestDuration metrics.TimeHistogram |
70 | 70 | { |
71 | 71 | requestDuration = metrics.NewTimeHistogram(time.Nanosecond, metrics.NewMultiHistogram( |
72 | "request_duration_ns", | |
72 | 73 | expvar.NewHistogram("request_duration_ns", 0, 5e9, 1, 50, 95, 99), |
73 | 74 | prometheus.NewSummary(stdprometheus.SummaryOpts{ |
74 | 75 | Namespace: "myorg", |
20 | 20 | |
21 | 21 | func (m loggingMiddleware) Sum(a, b int) (v int) { |
22 | 22 | defer func(begin time.Time) { |
23 | _ = m.Logger.Log( | |
23 | m.Logger.Log( | |
24 | 24 | "method", "sum", |
25 | 25 | "a", a, |
26 | 26 | "b", b, |
34 | 34 | |
35 | 35 | func (m loggingMiddleware) Concat(a, b string) (v string) { |
36 | 36 | defer func(begin time.Time) { |
37 | _ = m.Logger.Log( | |
37 | m.Logger.Log( | |
38 | 38 | "method", "concat", |
39 | 39 | "a", a, |
40 | 40 | "b", b, |
0 | # profilesvc | |
1 | ||
2 | This example demonstrates how to use Go kit to implement a REST-y HTTP service. | |
3 | It leverages the excellent [gorilla mux package](https://github.com/gorilla/mux) for routing. |
0 | package main | |
1 | ||
2 | import ( | |
3 | "github.com/go-kit/kit/endpoint" | |
4 | "golang.org/x/net/context" | |
5 | ) | |
6 | ||
7 | type endpoints struct { | |
8 | postProfileEndpoint endpoint.Endpoint | |
9 | getProfileEndpoint endpoint.Endpoint | |
10 | putProfileEndpoint endpoint.Endpoint | |
11 | patchProfileEndpoint endpoint.Endpoint | |
12 | deleteProfileEndpoint endpoint.Endpoint | |
13 | getAddressesEndpoint endpoint.Endpoint | |
14 | getAddressEndpoint endpoint.Endpoint | |
15 | postAddressEndpoint endpoint.Endpoint | |
16 | deleteAddressEndpoint endpoint.Endpoint | |
17 | } | |
18 | ||
19 | func makeEndpoints(s ProfileService) endpoints { | |
20 | return endpoints{ | |
21 | postProfileEndpoint: makePostProfileEndpoint(s), | |
22 | getProfileEndpoint: makeGetProfileEndpoint(s), | |
23 | putProfileEndpoint: makePutProfileEndpoint(s), | |
24 | patchProfileEndpoint: makePatchProfileEndpoint(s), | |
25 | deleteProfileEndpoint: makeDeleteProfileEndpoint(s), | |
26 | getAddressesEndpoint: makeGetAddressesEndpoint(s), | |
27 | getAddressEndpoint: makeGetAddressEndpoint(s), | |
28 | postAddressEndpoint: makePostAddressEndpoint(s), | |
29 | deleteAddressEndpoint: makeDeleteAddressEndpoint(s), | |
30 | } | |
31 | } | |
32 | ||
33 | type postProfileRequest struct { | |
34 | Profile Profile | |
35 | } | |
36 | ||
37 | type postProfileResponse struct { | |
38 | Err error `json:"err,omitempty"` | |
39 | } | |
40 | ||
41 | func (r postProfileResponse) error() error { return r.Err } | |
42 | ||
43 | // Regarding errors returned from service methods, we have two options. We can | |
44 | // return the error via the endpoint itself. That makes certain things a | |
45 | // little bit easier, like providing non-200 HTTP responses to the client. But | |
46 | // Go kit assumes that endpoint errors are (or may be treated as) transport- | |
47 | // level errors. For example, an endpoint error will count against a circuit | |
48 | // breaker error count. Therefore, it's almost certainly better to return | |
49 | // service method (business logic) errors in the response object. This means | |
50 | // we have to do a bit more work in the HTTP response encoder to detect e.g. a | |
51 | // not-found error and provide a proper HTTP status code. | |
52 | ||
53 | func makePostProfileEndpoint(s ProfileService) endpoint.Endpoint { | |
54 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { | |
55 | req := request.(postProfileRequest) | |
56 | e := s.PostProfile(ctx, req.Profile) | |
57 | return postProfileResponse{Err: e}, nil | |
58 | } | |
59 | } | |
60 | ||
61 | type getProfileRequest struct { | |
62 | ID string | |
63 | } | |
64 | ||
65 | type getProfileResponse struct { | |
66 | Profile Profile `json:"profile,omitempty"` | |
67 | Err error `json:"err,omitempty"` | |
68 | } | |
69 | ||
70 | func (r getProfileResponse) error() error { return r.Err } | |
71 | ||
72 | func makeGetProfileEndpoint(s ProfileService) endpoint.Endpoint { | |
73 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { | |
74 | req := request.(getProfileRequest) | |
75 | p, e := s.GetProfile(ctx, req.ID) | |
76 | return getProfileResponse{Profile: p, Err: e}, nil | |
77 | } | |
78 | } | |
79 | ||
80 | type putProfileRequest struct { | |
81 | ID string | |
82 | Profile Profile | |
83 | } | |
84 | ||
85 | type putProfileResponse struct { | |
86 | Err error `json:"err,omitempty"` | |
87 | } | |
88 | ||
89 | func (r putProfileResponse) error() error { return nil } | |
90 | ||
91 | func makePutProfileEndpoint(s ProfileService) endpoint.Endpoint { | |
92 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { | |
93 | req := request.(putProfileRequest) | |
94 | e := s.PutProfile(ctx, req.ID, req.Profile) | |
95 | return putProfileResponse{Err: e}, nil | |
96 | } | |
97 | } | |
98 | ||
99 | type patchProfileRequest struct { | |
100 | ID string | |
101 | Profile Profile | |
102 | } | |
103 | ||
104 | type patchProfileResponse struct { | |
105 | Err error `json:"err,omitempty"` | |
106 | } | |
107 | ||
108 | func (r patchProfileResponse) error() error { return r.Err } | |
109 | ||
110 | func makePatchProfileEndpoint(s ProfileService) endpoint.Endpoint { | |
111 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { | |
112 | req := request.(patchProfileRequest) | |
113 | e := s.PatchProfile(ctx, req.ID, req.Profile) | |
114 | return patchProfileResponse{Err: e}, nil | |
115 | } | |
116 | } | |
117 | ||
118 | type deleteProfileRequest struct { | |
119 | ID string | |
120 | } | |
121 | ||
122 | type deleteProfileResponse struct { | |
123 | Err error `json:"err,omitempty"` | |
124 | } | |
125 | ||
126 | func (r deleteProfileResponse) error() error { return r.Err } | |
127 | ||
128 | func makeDeleteProfileEndpoint(s ProfileService) endpoint.Endpoint { | |
129 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { | |
130 | req := request.(deleteProfileRequest) | |
131 | e := s.DeleteProfile(ctx, req.ID) | |
132 | return deleteProfileResponse{Err: e}, nil | |
133 | } | |
134 | } | |
135 | ||
136 | type getAddressesRequest struct { | |
137 | ProfileID string | |
138 | } | |
139 | ||
140 | type getAddressesResponse struct { | |
141 | Addresses []Address `json:"addresses,omitempty"` | |
142 | Err error `json:"err,omitempty"` | |
143 | } | |
144 | ||
145 | func (r getAddressesResponse) error() error { return r.Err } | |
146 | ||
147 | func makeGetAddressesEndpoint(s ProfileService) endpoint.Endpoint { | |
148 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { | |
149 | req := request.(getAddressesRequest) | |
150 | a, e := s.GetAddresses(ctx, req.ProfileID) | |
151 | return getAddressesResponse{Addresses: a, Err: e}, nil | |
152 | } | |
153 | } | |
154 | ||
155 | type getAddressRequest struct { | |
156 | ProfileID string | |
157 | AddressID string | |
158 | } | |
159 | ||
160 | type getAddressResponse struct { | |
161 | Address Address `json:"address,omitempty"` | |
162 | Err error `json:"err,omitempty"` | |
163 | } | |
164 | ||
165 | func (r getAddressResponse) error() error { return r.Err } | |
166 | ||
167 | func makeGetAddressEndpoint(s ProfileService) endpoint.Endpoint { | |
168 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { | |
169 | req := request.(getAddressRequest) | |
170 | a, e := s.GetAddress(ctx, req.ProfileID, req.AddressID) | |
171 | return getAddressResponse{Address: a, Err: e}, nil | |
172 | } | |
173 | } | |
174 | ||
175 | type postAddressRequest struct { | |
176 | ProfileID string | |
177 | Address Address | |
178 | } | |
179 | ||
180 | type postAddressResponse struct { | |
181 | Err error `json:"err,omitempty"` | |
182 | } | |
183 | ||
184 | func (r postAddressResponse) error() error { return r.Err } | |
185 | ||
186 | func makePostAddressEndpoint(s ProfileService) endpoint.Endpoint { | |
187 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { | |
188 | req := request.(postAddressRequest) | |
189 | e := s.PostAddress(ctx, req.ProfileID, req.Address) | |
190 | return postAddressResponse{Err: e}, nil | |
191 | } | |
192 | } | |
193 | ||
194 | type deleteAddressRequest struct { | |
195 | ProfileID string | |
196 | AddressID string | |
197 | } | |
198 | ||
199 | type deleteAddressResponse struct { | |
200 | Err error `json:"err,omitempty"` | |
201 | } | |
202 | ||
203 | func (r deleteAddressResponse) error() error { return r.Err } | |
204 | ||
205 | func makeDeleteAddressEndpoint(s ProfileService) endpoint.Endpoint { | |
206 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { | |
207 | req := request.(deleteAddressRequest) | |
208 | e := s.DeleteAddress(ctx, req.ProfileID, req.AddressID) | |
209 | return deleteAddressResponse{Err: e}, nil | |
210 | } | |
211 | } |
0 | package main | |
1 | ||
2 | import ( | |
3 | "time" | |
4 | ||
5 | "golang.org/x/net/context" | |
6 | ||
7 | "github.com/go-kit/kit/log" | |
8 | ) | |
9 | ||
10 | type loggingMiddleware struct { | |
11 | next ProfileService | |
12 | logger log.Logger | |
13 | } | |
14 | ||
15 | func (mw loggingMiddleware) PostProfile(ctx context.Context, p Profile) (err error) { | |
16 | defer func(begin time.Time) { | |
17 | mw.logger.Log("method", "PostProfile", "id", p.ID, "took", time.Since(begin), "err", err) | |
18 | }(time.Now()) | |
19 | return mw.next.PostProfile(ctx, p) | |
20 | } | |
21 | ||
22 | func (mw loggingMiddleware) GetProfile(ctx context.Context, id string) (p Profile, err error) { | |
23 | defer func(begin time.Time) { | |
24 | mw.logger.Log("method", "GetProfile", "id", id, "took", time.Since(begin), "err", err) | |
25 | }(time.Now()) | |
26 | return mw.next.GetProfile(ctx, id) | |
27 | } | |
28 | ||
29 | func (mw loggingMiddleware) PutProfile(ctx context.Context, id string, p Profile) (err error) { | |
30 | defer func(begin time.Time) { | |
31 | mw.logger.Log("method", "PutProfile", "id", id, "took", time.Since(begin), "err", err) | |
32 | }(time.Now()) | |
33 | return mw.next.PutProfile(ctx, id, p) | |
34 | } | |
35 | ||
36 | func (mw loggingMiddleware) PatchProfile(ctx context.Context, id string, p Profile) (err error) { | |
37 | defer func(begin time.Time) { | |
38 | mw.logger.Log("method", "PatchProfile", "id", id, "took", time.Since(begin), "err", err) | |
39 | }(time.Now()) | |
40 | return mw.next.PatchProfile(ctx, id, p) | |
41 | } | |
42 | ||
43 | func (mw loggingMiddleware) DeleteProfile(ctx context.Context, id string) (err error) { | |
44 | defer func(begin time.Time) { | |
45 | mw.logger.Log("method", "DeleteProfile", "id", id, "took", time.Since(begin), "err", err) | |
46 | }(time.Now()) | |
47 | return mw.next.DeleteProfile(ctx, id) | |
48 | } | |
49 | ||
50 | func (mw loggingMiddleware) GetAddresses(ctx context.Context, profileID string) (addresses []Address, err error) { | |
51 | defer func(begin time.Time) { | |
52 | mw.logger.Log("method", "GetAddresses", "profileID", profileID, "took", time.Since(begin), "err", err) | |
53 | }(time.Now()) | |
54 | return mw.next.GetAddresses(ctx, profileID) | |
55 | } | |
56 | ||
57 | func (mw loggingMiddleware) GetAddress(ctx context.Context, profileID string, addressID string) (a Address, err error) { | |
58 | defer func(begin time.Time) { | |
59 | mw.logger.Log("method", "GetAddress", "profileID", profileID, "addressID", addressID, "took", time.Since(begin), "err", err) | |
60 | }(time.Now()) | |
61 | return mw.next.GetAddress(ctx, profileID, addressID) | |
62 | } | |
63 | ||
64 | func (mw loggingMiddleware) PostAddress(ctx context.Context, profileID string, a Address) (err error) { | |
65 | defer func(begin time.Time) { | |
66 | mw.logger.Log("method", "PostAddress", "profileID", profileID, "took", time.Since(begin), "err", err) | |
67 | }(time.Now()) | |
68 | return mw.next.PostAddress(ctx, profileID, a) | |
69 | } | |
70 | ||
71 | func (mw loggingMiddleware) DeleteAddress(ctx context.Context, profileID string, addressID string) (err error) { | |
72 | defer func(begin time.Time) { | |
73 | mw.logger.Log("method", "DeleteAddress", "profileID", profileID, "addressID", addressID, "took", time.Since(begin), "err", err) | |
74 | }(time.Now()) | |
75 | return mw.next.DeleteAddress(ctx, profileID, addressID) | |
76 | } |
0 | package main | |
1 | ||
2 | import ( | |
3 | "flag" | |
4 | "fmt" | |
5 | "net/http" | |
6 | "os" | |
7 | "os/signal" | |
8 | "syscall" | |
9 | ||
10 | "golang.org/x/net/context" | |
11 | ||
12 | "github.com/go-kit/kit/log" | |
13 | ) | |
14 | ||
15 | func main() { | |
16 | var ( | |
17 | httpAddr = flag.String("http.addr", ":8080", "HTTP listen address") | |
18 | ) | |
19 | flag.Parse() | |
20 | ||
21 | var logger log.Logger | |
22 | { | |
23 | logger = log.NewLogfmtLogger(os.Stderr) | |
24 | logger = log.NewContext(logger).With("ts", log.DefaultTimestampUTC) | |
25 | logger = log.NewContext(logger).With("caller", log.DefaultCaller) | |
26 | } | |
27 | ||
28 | var ctx context.Context | |
29 | { | |
30 | ctx = context.Background() | |
31 | } | |
32 | ||
33 | var s ProfileService | |
34 | { | |
35 | s = newInmemService() | |
36 | s = loggingMiddleware{s, log.NewContext(logger).With("component", "svc")} | |
37 | } | |
38 | ||
39 | var h http.Handler | |
40 | { | |
41 | h = makeHandler(ctx, s, log.NewContext(logger).With("component", "http")) | |
42 | } | |
43 | ||
44 | errs := make(chan error, 2) | |
45 | go func() { | |
46 | logger.Log("transport", "http", "address", *httpAddr, "msg", "listening") | |
47 | errs <- http.ListenAndServe(*httpAddr, h) | |
48 | }() | |
49 | go func() { | |
50 | c := make(chan os.Signal) | |
51 | signal.Notify(c, syscall.SIGINT) | |
52 | errs <- fmt.Errorf("%s", <-c) | |
53 | }() | |
54 | ||
55 | logger.Log("terminated", <-errs) | |
56 | } |
0 | package main | |
1 | ||
2 | import ( | |
3 | "errors" | |
4 | "sync" | |
5 | ||
6 | "golang.org/x/net/context" | |
7 | ) | |
8 | ||
9 | // ProfileService is a simple CRUD interface for user profiles. | |
10 | type ProfileService interface { | |
11 | PostProfile(ctx context.Context, p Profile) error | |
12 | GetProfile(ctx context.Context, id string) (Profile, error) | |
13 | PutProfile(ctx context.Context, id string, p Profile) error | |
14 | PatchProfile(ctx context.Context, id string, p Profile) error | |
15 | DeleteProfile(ctx context.Context, id string) error | |
16 | GetAddresses(ctx context.Context, profileID string) ([]Address, error) | |
17 | GetAddress(ctx context.Context, profileID string, addressID string) (Address, error) | |
18 | PostAddress(ctx context.Context, profileID string, a Address) error | |
19 | DeleteAddress(ctx context.Context, profileID string, addressID string) error | |
20 | } | |
21 | ||
22 | // Profile represents a single user profile. | |
23 | // ID should be globally unique. | |
24 | type Profile struct { | |
25 | ID string `json:"id"` | |
26 | Name string `json:"name,omitempty"` | |
27 | Addresses []Address `json:"addresses,omitempty"` | |
28 | } | |
29 | ||
30 | // Address is a field of a user profile. | |
31 | // ID should be unique within the profile (at a minimum). | |
32 | type Address struct { | |
33 | ID string `json:"id"` | |
34 | Location string `json:"location,omitempty"` | |
35 | } | |
36 | ||
37 | var ( | |
38 | errInconsistentIDs = errors.New("inconsistent IDs") | |
39 | errAlreadyExists = errors.New("already exists") | |
40 | errNotFound = errors.New("not found") | |
41 | ) | |
42 | ||
43 | type inmemService struct { | |
44 | mtx sync.RWMutex | |
45 | m map[string]Profile | |
46 | } | |
47 | ||
48 | func newInmemService() ProfileService { | |
49 | return &inmemService{ | |
50 | m: map[string]Profile{}, | |
51 | } | |
52 | } | |
53 | ||
54 | func (s *inmemService) PostProfile(ctx context.Context, p Profile) error { | |
55 | s.mtx.Lock() | |
56 | defer s.mtx.Unlock() | |
57 | if _, ok := s.m[p.ID]; ok { | |
58 | return errAlreadyExists // POST = create, don't overwrite | |
59 | } | |
60 | s.m[p.ID] = p | |
61 | return nil | |
62 | } | |
63 | ||
64 | func (s *inmemService) GetProfile(ctx context.Context, id string) (Profile, error) { | |
65 | s.mtx.RLock() | |
66 | defer s.mtx.RUnlock() | |
67 | p, ok := s.m[id] | |
68 | if !ok { | |
69 | return Profile{}, errNotFound | |
70 | } | |
71 | return p, nil | |
72 | } | |
73 | ||
74 | func (s *inmemService) PutProfile(ctx context.Context, id string, p Profile) error { | |
75 | if id != p.ID { | |
76 | return errInconsistentIDs | |
77 | } | |
78 | s.mtx.Lock() | |
79 | defer s.mtx.Unlock() | |
80 | s.m[id] = p // PUT = create or update | |
81 | return nil | |
82 | } | |
83 | ||
84 | func (s *inmemService) PatchProfile(ctx context.Context, id string, p Profile) error { | |
85 | if p.ID != "" && id != p.ID { | |
86 | return errInconsistentIDs | |
87 | } | |
88 | ||
89 | s.mtx.Lock() | |
90 | defer s.mtx.Unlock() | |
91 | ||
92 | existing, ok := s.m[id] | |
93 | if !ok { | |
94 | return errNotFound // PATCH = update existing, don't create | |
95 | } | |
96 | ||
97 | // We assume that it's not possible to PATCH the ID, and that it's not | |
98 | // possible to PATCH any field to its zero value. That is, the zero value | |
99 | // means not specified. The way around this is to use e.g. Name *string in | |
100 | // the Profile definition. But since this is just a demonstrative example, | |
101 | // I'm leaving that out. | |
102 | ||
103 | if p.Name != "" { | |
104 | existing.Name = p.Name | |
105 | } | |
106 | if len(p.Addresses) > 0 { | |
107 | existing.Addresses = p.Addresses | |
108 | } | |
109 | s.m[id] = existing | |
110 | return nil | |
111 | } | |
112 | ||
113 | func (s *inmemService) DeleteProfile(ctx context.Context, id string) error { | |
114 | s.mtx.Lock() | |
115 | defer s.mtx.Unlock() | |
116 | if _, ok := s.m[id]; !ok { | |
117 | return errNotFound | |
118 | } | |
119 | delete(s.m, id) | |
120 | return nil | |
121 | } | |
122 | ||
123 | func (s *inmemService) GetAddresses(ctx context.Context, profileID string) ([]Address, error) { | |
124 | s.mtx.RLock() | |
125 | defer s.mtx.RUnlock() | |
126 | p, ok := s.m[profileID] | |
127 | if !ok { | |
128 | return []Address{}, errNotFound | |
129 | } | |
130 | return p.Addresses, nil | |
131 | } | |
132 | ||
133 | func (s *inmemService) GetAddress(ctx context.Context, profileID string, addressID string) (Address, error) { | |
134 | s.mtx.RLock() | |
135 | defer s.mtx.RUnlock() | |
136 | p, ok := s.m[profileID] | |
137 | if !ok { | |
138 | return Address{}, errNotFound | |
139 | } | |
140 | for _, address := range p.Addresses { | |
141 | if address.ID == addressID { | |
142 | return address, nil | |
143 | } | |
144 | } | |
145 | return Address{}, errNotFound | |
146 | } | |
147 | ||
148 | func (s *inmemService) PostAddress(ctx context.Context, profileID string, a Address) error { | |
149 | s.mtx.Lock() | |
150 | defer s.mtx.Unlock() | |
151 | p, ok := s.m[profileID] | |
152 | if !ok { | |
153 | return errNotFound | |
154 | } | |
155 | for _, address := range p.Addresses { | |
156 | if address.ID == a.ID { | |
157 | return errAlreadyExists | |
158 | } | |
159 | } | |
160 | p.Addresses = append(p.Addresses, a) | |
161 | s.m[profileID] = p | |
162 | return nil | |
163 | } | |
164 | ||
165 | func (s *inmemService) DeleteAddress(ctx context.Context, profileID string, addressID string) error { | |
166 | s.mtx.Lock() | |
167 | defer s.mtx.Unlock() | |
168 | p, ok := s.m[profileID] | |
169 | if !ok { | |
170 | return errNotFound | |
171 | } | |
172 | newAddresses := make([]Address, 0, len(p.Addresses)) | |
173 | for _, address := range p.Addresses { | |
174 | if address.ID == addressID { | |
175 | continue // delete | |
176 | } | |
177 | newAddresses = append(newAddresses, address) | |
178 | } | |
179 | if len(newAddresses) == len(p.Addresses) { | |
180 | return errNotFound | |
181 | } | |
182 | p.Addresses = newAddresses | |
183 | s.m[profileID] = p | |
184 | return nil | |
185 | } |
0 | package main | |
1 | ||
2 | import ( | |
3 | "encoding/json" | |
4 | "errors" | |
5 | stdhttp "net/http" | |
6 | ||
7 | "github.com/gorilla/mux" | |
8 | "golang.org/x/net/context" | |
9 | ||
10 | kitlog "github.com/go-kit/kit/log" | |
11 | kithttp "github.com/go-kit/kit/transport/http" | |
12 | ) | |
13 | ||
14 | var ( | |
15 | errBadRouting = errors.New("inconsistent mapping between route and handler (programmer error)") | |
16 | ) | |
17 | ||
18 | func makeHandler(ctx context.Context, s ProfileService, logger kitlog.Logger) stdhttp.Handler { | |
19 | e := makeEndpoints(s) | |
20 | r := mux.NewRouter() | |
21 | ||
22 | commonOptions := []kithttp.ServerOption{ | |
23 | kithttp.ServerErrorLogger(logger), | |
24 | kithttp.ServerErrorEncoder(encodeError), | |
25 | } | |
26 | ||
27 | // POST /profiles adds another profile | |
28 | // GET /profiles/:id retrieves the given profile by id | |
29 | // PUT /profiles/:id post updated profile information about the profile | |
30 | // PATCH /profiles/:id partial updated profile information | |
31 | // DELETE /profiles/:id remove the given profile | |
32 | // GET /profiles/:id/addresses retrieve addresses associated with the profile | |
33 | // GET /profiles/:id/addresses/:addressID retrieve a particular profile address | |
34 | // POST /profiles/:id/addresses add a new address | |
35 | // DELETE /profiles/:id/addresses/:addressID remove an address | |
36 | ||
37 | r.Methods("POST").Path("/profiles/").Handler(kithttp.NewServer( | |
38 | ctx, | |
39 | e.postProfileEndpoint, | |
40 | decodePostProfileRequest, | |
41 | encodeResponse, | |
42 | commonOptions..., | |
43 | )) | |
44 | r.Methods("GET").Path("/profiles/{id}").Handler(kithttp.NewServer( | |
45 | ctx, | |
46 | e.getProfileEndpoint, | |
47 | decodeGetProfileRequest, | |
48 | encodeResponse, | |
49 | commonOptions..., | |
50 | )) | |
51 | r.Methods("PUT").Path("/profiles/{id}").Handler(kithttp.NewServer( | |
52 | ctx, | |
53 | e.putProfileEndpoint, | |
54 | decodePutProfileRequest, | |
55 | encodeResponse, | |
56 | commonOptions..., | |
57 | )) | |
58 | r.Methods("PATCH").Path("/profiles/{id}").Handler(kithttp.NewServer( | |
59 | ctx, | |
60 | e.patchProfileEndpoint, | |
61 | decodePatchProfileRequest, | |
62 | encodeResponse, | |
63 | commonOptions..., | |
64 | )) | |
65 | r.Methods("DELETE").Path("/profiles/{id}").Handler(kithttp.NewServer( | |
66 | ctx, | |
67 | e.deleteProfileEndpoint, | |
68 | decodeDeleteProfileRequest, | |
69 | encodeResponse, | |
70 | commonOptions..., | |
71 | )) | |
72 | r.Methods("GET").Path("/profiles/{id}/addresses/").Handler(kithttp.NewServer( | |
73 | ctx, | |
74 | e.getAddressesEndpoint, | |
75 | decodeGetAddressesRequest, | |
76 | encodeResponse, | |
77 | commonOptions..., | |
78 | )) | |
79 | r.Methods("GET").Path("/profiles/{id}/addresses/{addressID}").Handler(kithttp.NewServer( | |
80 | ctx, | |
81 | e.getAddressEndpoint, | |
82 | decodeGetAddressRequest, | |
83 | encodeResponse, | |
84 | commonOptions..., | |
85 | )) | |
86 | r.Methods("POST").Path("/profiles/{id}/addresses/").Handler(kithttp.NewServer( | |
87 | ctx, | |
88 | e.postAddressEndpoint, | |
89 | decodePostAddressRequest, | |
90 | encodeResponse, | |
91 | commonOptions..., | |
92 | )) | |
93 | r.Methods("DELETE").Path("/profiles/{id}/addresses/{addressID}").Handler(kithttp.NewServer( | |
94 | ctx, | |
95 | e.deleteAddressEndpoint, | |
96 | decodeDeleteAddressRequest, | |
97 | encodeResponse, | |
98 | commonOptions..., | |
99 | )) | |
100 | return r | |
101 | } | |
102 | ||
103 | func decodePostProfileRequest(r *stdhttp.Request) (request interface{}, err error) { | |
104 | var req postProfileRequest | |
105 | if e := json.NewDecoder(r.Body).Decode(&req.Profile); e != nil { | |
106 | return nil, e | |
107 | } | |
108 | return req, nil | |
109 | } | |
110 | ||
111 | func decodeGetProfileRequest(r *stdhttp.Request) (request interface{}, err error) { | |
112 | vars := mux.Vars(r) | |
113 | id, ok := vars["id"] | |
114 | if !ok { | |
115 | return nil, errBadRouting | |
116 | } | |
117 | return getProfileRequest{ID: id}, nil | |
118 | } | |
119 | ||
120 | func decodePutProfileRequest(r *stdhttp.Request) (request interface{}, err error) { | |
121 | vars := mux.Vars(r) | |
122 | id, ok := vars["id"] | |
123 | if !ok { | |
124 | return nil, errBadRouting | |
125 | } | |
126 | var profile Profile | |
127 | if err := json.NewDecoder(r.Body).Decode(&profile); err != nil { | |
128 | return nil, err | |
129 | } | |
130 | return putProfileRequest{ | |
131 | ID: id, | |
132 | Profile: profile, | |
133 | }, nil | |
134 | } | |
135 | ||
136 | func decodePatchProfileRequest(r *stdhttp.Request) (request interface{}, err error) { | |
137 | vars := mux.Vars(r) | |
138 | id, ok := vars["id"] | |
139 | if !ok { | |
140 | return nil, errBadRouting | |
141 | } | |
142 | var profile Profile | |
143 | if err := json.NewDecoder(r.Body).Decode(&profile); err != nil { | |
144 | return nil, err | |
145 | } | |
146 | return patchProfileRequest{ | |
147 | ID: id, | |
148 | Profile: profile, | |
149 | }, nil | |
150 | } | |
151 | ||
152 | func decodeDeleteProfileRequest(r *stdhttp.Request) (request interface{}, err error) { | |
153 | vars := mux.Vars(r) | |
154 | id, ok := vars["id"] | |
155 | if !ok { | |
156 | return nil, errBadRouting | |
157 | } | |
158 | return deleteProfileRequest{ID: id}, nil | |
159 | } | |
160 | ||
161 | func decodeGetAddressesRequest(r *stdhttp.Request) (request interface{}, err error) { | |
162 | vars := mux.Vars(r) | |
163 | id, ok := vars["id"] | |
164 | if !ok { | |
165 | return nil, errBadRouting | |
166 | } | |
167 | return getAddressesRequest{ProfileID: id}, nil | |
168 | } | |
169 | ||
170 | func decodeGetAddressRequest(r *stdhttp.Request) (request interface{}, err error) { | |
171 | vars := mux.Vars(r) | |
172 | id, ok := vars["id"] | |
173 | if !ok { | |
174 | return nil, errBadRouting | |
175 | } | |
176 | addressID, ok := vars["addressID"] | |
177 | if !ok { | |
178 | return nil, errBadRouting | |
179 | } | |
180 | return getAddressRequest{ | |
181 | ProfileID: id, | |
182 | AddressID: addressID, | |
183 | }, nil | |
184 | } | |
185 | ||
186 | func decodePostAddressRequest(r *stdhttp.Request) (request interface{}, err error) { | |
187 | vars := mux.Vars(r) | |
188 | id, ok := vars["id"] | |
189 | if !ok { | |
190 | return nil, errBadRouting | |
191 | } | |
192 | var address Address | |
193 | if err := json.NewDecoder(r.Body).Decode(&address); err != nil { | |
194 | return nil, err | |
195 | } | |
196 | return postAddressRequest{ | |
197 | ProfileID: id, | |
198 | Address: address, | |
199 | }, nil | |
200 | } | |
201 | ||
202 | func decodeDeleteAddressRequest(r *stdhttp.Request) (request interface{}, err error) { | |
203 | vars := mux.Vars(r) | |
204 | id, ok := vars["id"] | |
205 | if !ok { | |
206 | return nil, errBadRouting | |
207 | } | |
208 | addressID, ok := vars["addressID"] | |
209 | if !ok { | |
210 | return nil, errBadRouting | |
211 | } | |
212 | return deleteAddressRequest{ | |
213 | ProfileID: id, | |
214 | AddressID: addressID, | |
215 | }, nil | |
216 | } | |
217 | ||
218 | // errorer is implemented by all concrete response types. It allows us to | |
219 | // change the HTTP response code without needing to trigger an endpoint | |
220 | // (transport-level) error. For more information, read the big comment in | |
221 | // endpoint.go. | |
222 | type errorer interface { | |
223 | error() error | |
224 | } | |
225 | ||
226 | // encodeResponse is the common method to encode all response types to the | |
227 | // client. I chose to do it this way because I didn't know if something more | |
228 | // specific was necessary. It's certainly possible to specialize on a | |
229 | // per-response (per-method) basis. | |
230 | func encodeResponse(w stdhttp.ResponseWriter, response interface{}) error { | |
231 | if e, ok := response.(errorer); ok && e.error() != nil { | |
232 | // Not a Go kit transport error, but a business-logic error. | |
233 | // Provide those as HTTP errors. | |
234 | encodeError(w, e.error()) | |
235 | return nil | |
236 | } | |
237 | return json.NewEncoder(w).Encode(response) | |
238 | } | |
239 | ||
240 | func encodeError(w stdhttp.ResponseWriter, err error) { | |
241 | w.WriteHeader(codeFrom(err)) | |
242 | json.NewEncoder(w).Encode(map[string]interface{}{ | |
243 | "error": err.Error(), | |
244 | }) | |
245 | } | |
246 | ||
247 | func codeFrom(err error) int { | |
248 | switch err { | |
249 | case nil: | |
250 | return stdhttp.StatusOK | |
251 | case errNotFound: | |
252 | return stdhttp.StatusNotFound | |
253 | case errAlreadyExists, errInconsistentIDs: | |
254 | return stdhttp.StatusBadRequest | |
255 | default: | |
256 | if _, ok := err.(kithttp.BadRequestError); ok { | |
257 | return stdhttp.StatusBadRequest | |
258 | } | |
259 | return stdhttp.StatusInternalServerError | |
260 | } | |
261 | } |
11 | 11 | ) |
12 | 12 | |
13 | 13 | var consulState = []*consul.ServiceEntry{ |
14 | &consul.ServiceEntry{ | |
14 | { | |
15 | 15 | Node: &consul.Node{ |
16 | 16 | Address: "10.0.0.0", |
17 | 17 | Node: "app00.local", |
26 | 26 | }, |
27 | 27 | }, |
28 | 28 | }, |
29 | &consul.ServiceEntry{ | |
29 | { | |
30 | 30 | Node: &consul.Node{ |
31 | 31 | Address: "10.0.0.1", |
32 | 32 | Node: "app01.local", |
41 | 41 | }, |
42 | 42 | }, |
43 | 43 | }, |
44 | &consul.ServiceEntry{ | |
44 | { | |
45 | 45 | Node: &consul.Node{ |
46 | 46 | Address: "10.0.0.1", |
47 | 47 | Node: "app01.local", |
13 | 13 | // resolved on a fixed schedule. Priorities and weights are ignored. |
14 | 14 | type Publisher struct { |
15 | 15 | name string |
16 | ttl time.Duration | |
17 | 16 | cache *loadbalancer.EndpointCache |
18 | 17 | logger log.Logger |
19 | 18 | quit chan struct{} |
24 | 23 | // constructor will return an error. The factory is used to convert a |
25 | 24 | // host:port to a usable endpoint. The logger is used to report DNS and |
26 | 25 | // factory errors. |
27 | func NewPublisher(name string, ttl time.Duration, factory loadbalancer.Factory, logger log.Logger) *Publisher { | |
26 | func NewPublisher( | |
27 | name string, | |
28 | ttl time.Duration, | |
29 | factory loadbalancer.Factory, | |
30 | logger log.Logger, | |
31 | ) *Publisher { | |
32 | return NewPublisherDetailed(name, time.NewTicker(ttl), net.LookupSRV, factory, logger) | |
33 | } | |
34 | ||
35 | // NewPublisherDetailed is the same as NewPublisher, but allows users to provide | |
36 | // an explicit lookup refresh ticker instead of a TTL, and specify the function | |
37 | // used to perform lookups instead of using net.LookupSRV. | |
38 | func NewPublisherDetailed( | |
39 | name string, | |
40 | refreshTicker *time.Ticker, | |
41 | lookupSRV func(service, proto, name string) (cname string, addrs []*net.SRV, err error), | |
42 | factory loadbalancer.Factory, | |
43 | logger log.Logger, | |
44 | ) *Publisher { | |
28 | 45 | p := &Publisher{ |
29 | 46 | name: name, |
30 | ttl: ttl, | |
31 | 47 | cache: loadbalancer.NewEndpointCache(factory, logger), |
32 | 48 | logger: logger, |
33 | 49 | quit: make(chan struct{}), |
34 | 50 | } |
35 | 51 | |
36 | instances, err := p.resolve() | |
52 | instances, err := p.resolve(lookupSRV) | |
37 | 53 | if err == nil { |
38 | 54 | logger.Log("name", name, "instances", len(instances)) |
39 | 55 | } else { |
41 | 57 | } |
42 | 58 | p.cache.Replace(instances) |
43 | 59 | |
44 | go p.loop() | |
60 | go p.loop(refreshTicker, lookupSRV) | |
45 | 61 | return p |
46 | 62 | } |
47 | 63 | |
50 | 66 | close(p.quit) |
51 | 67 | } |
52 | 68 | |
53 | func (p *Publisher) loop() { | |
54 | t := newTicker(p.ttl) | |
55 | defer t.Stop() | |
69 | func (p *Publisher) loop( | |
70 | refreshTicker *time.Ticker, | |
71 | lookupSRV func(service, proto, name string) (cname string, addrs []*net.SRV, err error), | |
72 | ) { | |
73 | defer refreshTicker.Stop() | |
56 | 74 | for { |
57 | 75 | select { |
58 | case <-t.C: | |
59 | instances, err := p.resolve() | |
76 | case <-refreshTicker.C: | |
77 | instances, err := p.resolve(lookupSRV) | |
60 | 78 | if err != nil { |
61 | 79 | p.logger.Log(p.name, err) |
62 | 80 | continue // don't replace potentially-good with bad |
74 | 92 | return p.cache.Endpoints() |
75 | 93 | } |
76 | 94 | |
77 | var ( | |
78 | lookupSRV = net.LookupSRV | |
79 | newTicker = time.NewTicker | |
80 | ) | |
81 | ||
82 | func (p *Publisher) resolve() ([]string, error) { | |
95 | func (p *Publisher) resolve(lookupSRV func(service, proto, name string) (cname string, addrs []*net.SRV, err error)) ([]string, error) { | |
83 | 96 | _, addrs, err := lookupSRV("", "", p.name) |
84 | 97 | if err != nil { |
85 | 98 | return []string{}, err |
0 | package dnssrv | |
1 | ||
2 | import ( | |
3 | "errors" | |
4 | "io" | |
5 | "net" | |
6 | "sync/atomic" | |
7 | "testing" | |
8 | "time" | |
9 | ||
10 | "golang.org/x/net/context" | |
11 | ||
12 | "github.com/go-kit/kit/endpoint" | |
13 | "github.com/go-kit/kit/log" | |
14 | ) | |
15 | ||
16 | func TestPublisher(t *testing.T) { | |
17 | var ( | |
18 | name = "foo" | |
19 | ttl = time.Second | |
20 | e = func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil } | |
21 | factory = func(string) (endpoint.Endpoint, io.Closer, error) { return e, nil, nil } | |
22 | logger = log.NewNopLogger() | |
23 | ) | |
24 | ||
25 | p := NewPublisher(name, ttl, factory, logger) | |
26 | defer p.Stop() | |
27 | ||
28 | if _, err := p.Endpoints(); err != nil { | |
29 | t.Fatal(err) | |
30 | } | |
31 | } | |
32 | ||
33 | func TestBadLookup(t *testing.T) { | |
34 | oldLookup := lookupSRV | |
35 | defer func() { lookupSRV = oldLookup }() | |
36 | lookupSRV = mockLookupSRV([]*net.SRV{}, errors.New("kaboom"), nil) | |
37 | ||
38 | var ( | |
39 | name = "some-name" | |
40 | ttl = time.Second | |
41 | e = func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil } | |
42 | factory = func(string) (endpoint.Endpoint, io.Closer, error) { return e, nil, nil } | |
43 | logger = log.NewNopLogger() | |
44 | ) | |
45 | ||
46 | p := NewPublisher(name, ttl, factory, logger) | |
47 | defer p.Stop() | |
48 | ||
49 | endpoints, err := p.Endpoints() | |
50 | if err != nil { | |
51 | t.Error(err) | |
52 | } | |
53 | if want, have := 0, len(endpoints); want != have { | |
54 | t.Errorf("want %d, have %d", want, have) | |
55 | } | |
56 | } | |
57 | ||
58 | func TestBadFactory(t *testing.T) { | |
59 | var ( | |
60 | addr = &net.SRV{Target: "foo", Port: 1234} | |
61 | addrs = []*net.SRV{addr} | |
62 | name = "some-name" | |
63 | ttl = time.Second | |
64 | factory = func(string) (endpoint.Endpoint, io.Closer, error) { return nil, nil, errors.New("kaboom") } | |
65 | logger = log.NewNopLogger() | |
66 | ) | |
67 | ||
68 | oldLookup := lookupSRV | |
69 | defer func() { lookupSRV = oldLookup }() | |
70 | lookupSRV = mockLookupSRV(addrs, nil, nil) | |
71 | ||
72 | p := NewPublisher(name, ttl, factory, logger) | |
73 | defer p.Stop() | |
74 | ||
75 | endpoints, err := p.Endpoints() | |
76 | if err != nil { | |
77 | t.Error(err) | |
78 | } | |
79 | if want, have := 0, len(endpoints); want != have { | |
80 | t.Errorf("want %q, have %q", want, have) | |
81 | } | |
82 | } | |
83 | ||
84 | func TestRefreshWithChange(t *testing.T) { | |
85 | t.Skip("TODO") | |
86 | } | |
87 | ||
88 | func TestRefreshNoChange(t *testing.T) { | |
89 | var ( | |
90 | tick = make(chan time.Time) | |
91 | target = "my-target" | |
92 | port = uint16(5678) | |
93 | addr = &net.SRV{Target: target, Port: port} | |
94 | addrs = []*net.SRV{addr} | |
95 | name = "my-name" | |
96 | ttl = time.Second | |
97 | factory = func(string) (endpoint.Endpoint, io.Closer, error) { return nil, nil, errors.New("kaboom") } | |
98 | logger = log.NewNopLogger() | |
99 | ) | |
100 | ||
101 | oldTicker := newTicker | |
102 | defer func() { newTicker = oldTicker }() | |
103 | newTicker = func(time.Duration) *time.Ticker { return &time.Ticker{C: tick} } | |
104 | ||
105 | var resolves uint64 | |
106 | oldLookup := lookupSRV | |
107 | defer func() { lookupSRV = oldLookup }() | |
108 | lookupSRV = mockLookupSRV(addrs, nil, &resolves) | |
109 | ||
110 | p := NewPublisher(name, ttl, factory, logger) | |
111 | defer p.Stop() | |
112 | ||
113 | tick <- time.Now() | |
114 | if want, have := uint64(2), resolves; want != have { | |
115 | t.Errorf("want %d, have %d", want, have) | |
116 | } | |
117 | } | |
118 | ||
119 | func TestRefreshResolveError(t *testing.T) { | |
120 | t.Skip("TODO") | |
121 | } | |
122 | ||
123 | func mockLookupSRV(addrs []*net.SRV, err error, count *uint64) func(service, proto, name string) (string, []*net.SRV, error) { | |
124 | return func(service, proto, name string) (string, []*net.SRV, error) { | |
125 | if count != nil { | |
126 | atomic.AddUint64(count, 1) | |
127 | } | |
128 | return "", addrs, err | |
129 | } | |
130 | } |
0 | package dnssrv | |
1 | ||
2 | import ( | |
3 | "errors" | |
4 | "io" | |
5 | "net" | |
6 | "sync/atomic" | |
7 | "testing" | |
8 | "time" | |
9 | ||
10 | "golang.org/x/net/context" | |
11 | ||
12 | "github.com/go-kit/kit/endpoint" | |
13 | "github.com/go-kit/kit/log" | |
14 | ) | |
15 | ||
16 | func TestPublisher(t *testing.T) { | |
17 | var ( | |
18 | name = "foo" | |
19 | ttl = time.Second | |
20 | e = func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil } | |
21 | factory = func(string) (endpoint.Endpoint, io.Closer, error) { return e, nil, nil } | |
22 | logger = log.NewNopLogger() | |
23 | ) | |
24 | ||
25 | p := NewPublisher(name, ttl, factory, logger) | |
26 | defer p.Stop() | |
27 | ||
28 | if _, err := p.Endpoints(); err != nil { | |
29 | t.Fatal(err) | |
30 | } | |
31 | } | |
32 | ||
33 | func TestBadLookup(t *testing.T) { | |
34 | var ( | |
35 | name = "some-name" | |
36 | ticker = time.NewTicker(time.Second) | |
37 | lookups = uint32(0) | |
38 | lookupSRV = func(string, string, string) (string, []*net.SRV, error) { | |
39 | atomic.AddUint32(&lookups, 1) | |
40 | return "", nil, errors.New("kaboom") | |
41 | } | |
42 | e = func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil } | |
43 | factory = func(string) (endpoint.Endpoint, io.Closer, error) { return e, nil, nil } | |
44 | logger = log.NewNopLogger() | |
45 | ) | |
46 | ||
47 | p := NewPublisherDetailed(name, ticker, lookupSRV, factory, logger) | |
48 | defer p.Stop() | |
49 | ||
50 | endpoints, err := p.Endpoints() | |
51 | if err != nil { | |
52 | t.Error(err) | |
53 | } | |
54 | if want, have := 0, len(endpoints); want != have { | |
55 | t.Errorf("want %d, have %d", want, have) | |
56 | } | |
57 | if want, have := uint32(1), atomic.LoadUint32(&lookups); want != have { | |
58 | t.Errorf("want %d, have %d", want, have) | |
59 | } | |
60 | } | |
61 | ||
62 | func TestBadFactory(t *testing.T) { | |
63 | var ( | |
64 | name = "some-name" | |
65 | ticker = time.NewTicker(time.Second) | |
66 | addr = &net.SRV{Target: "foo", Port: 1234} | |
67 | addrs = []*net.SRV{addr} | |
68 | lookupSRV = func(a, b, c string) (string, []*net.SRV, error) { return "", addrs, nil } | |
69 | creates = uint32(0) | |
70 | factory = func(s string) (endpoint.Endpoint, io.Closer, error) { | |
71 | atomic.AddUint32(&creates, 1) | |
72 | return nil, nil, errors.New("kaboom") | |
73 | } | |
74 | logger = log.NewNopLogger() | |
75 | ) | |
76 | ||
77 | p := NewPublisherDetailed(name, ticker, lookupSRV, factory, logger) | |
78 | defer p.Stop() | |
79 | ||
80 | endpoints, err := p.Endpoints() | |
81 | if err != nil { | |
82 | t.Error(err) | |
83 | } | |
84 | if want, have := 0, len(endpoints); want != have { | |
85 | t.Errorf("want %q, have %q", want, have) | |
86 | } | |
87 | if want, have := uint32(1), atomic.LoadUint32(&creates); want != have { | |
88 | t.Errorf("want %d, have %d", want, have) | |
89 | } | |
90 | } | |
91 | ||
92 | func TestRefreshWithChange(t *testing.T) { | |
93 | t.Skip("TODO") | |
94 | } | |
95 | ||
96 | func TestRefreshNoChange(t *testing.T) { | |
97 | var ( | |
98 | addr = &net.SRV{Target: "my-target", Port: 5678} | |
99 | addrs = []*net.SRV{addr} | |
100 | name = "my-name" | |
101 | ticker = time.NewTicker(time.Second) | |
102 | lookups = uint32(0) | |
103 | lookupSRV = func(string, string, string) (string, []*net.SRV, error) { | |
104 | atomic.AddUint32(&lookups, 1) | |
105 | return "", addrs, nil | |
106 | } | |
107 | e = func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil } | |
108 | factory = func(string) (endpoint.Endpoint, io.Closer, error) { return e, nil, nil } | |
109 | logger = log.NewNopLogger() | |
110 | ) | |
111 | ||
112 | ticker.Stop() | |
113 | tickc := make(chan time.Time) | |
114 | ticker.C = tickc | |
115 | ||
116 | p := NewPublisherDetailed(name, ticker, lookupSRV, factory, logger) | |
117 | defer p.Stop() | |
118 | ||
119 | if want, have := uint32(1), atomic.LoadUint32(&lookups); want != have { | |
120 | t.Errorf("want %d, have %d", want, have) | |
121 | } | |
122 | ||
123 | tickc <- time.Now() | |
124 | ||
125 | if want, have := uint32(2), atomic.LoadUint32(&lookups); want != have { | |
126 | t.Errorf("want %d, have %d", want, have) | |
127 | } | |
128 | } | |
129 | ||
130 | func TestRefreshResolveError(t *testing.T) { | |
131 | t.Skip("TODO") | |
132 | } |
5 | 5 | "time" |
6 | 6 | |
7 | 7 | stdzk "github.com/samuel/go-zookeeper/zk" |
8 | ||
9 | "github.com/go-kit/kit/log" | |
8 | 10 | ) |
9 | 11 | |
10 | 12 | func TestNewClient(t *testing.T) { |
17 | 19 | |
18 | 20 | c, err := NewClient( |
19 | 21 | []string{"FailThisInvalidHost!!!"}, |
20 | logger, | |
22 | log.NewNopLogger(), | |
21 | 23 | ) |
22 | ||
23 | time.Sleep(1 * time.Millisecond) | |
24 | 24 | if err == nil { |
25 | 25 | t.Errorf("expected error, got nil") |
26 | 26 | } |
27 | calledEventHandler := false | |
27 | ||
28 | hasFired := false | |
29 | calledEventHandler := make(chan struct{}) | |
28 | 30 | eventHandler := func(event stdzk.Event) { |
29 | calledEventHandler = true | |
31 | if !hasFired { | |
32 | // test is successful if this function has fired at least once | |
33 | hasFired = true | |
34 | close(calledEventHandler) | |
35 | } | |
30 | 36 | } |
37 | ||
31 | 38 | c, err = NewClient( |
32 | 39 | []string{"localhost"}, |
33 | logger, | |
40 | log.NewNopLogger(), | |
34 | 41 | ACL(acl), |
35 | 42 | ConnectTimeout(connectTimeout), |
36 | 43 | SessionTimeout(sessionTimeout), |
41 | 48 | t.Fatal(err) |
42 | 49 | } |
43 | 50 | defer c.Stop() |
51 | ||
44 | 52 | clientImpl, ok := c.(*client) |
45 | 53 | if !ok { |
46 | t.Errorf("retrieved incorrect Client implementation") | |
54 | t.Fatal("retrieved incorrect Client implementation") | |
47 | 55 | } |
48 | 56 | if want, have := acl, clientImpl.acl; want[0] != have[0] { |
49 | 57 | t.Errorf("want %+v, have %+v", want, have) |
57 | 65 | if want, have := payload, clientImpl.rootNodePayload; bytes.Compare(want[0], have[0]) != 0 || bytes.Compare(want[1], have[1]) != 0 { |
58 | 66 | t.Errorf("want %s, have %s", want, have) |
59 | 67 | } |
60 | // Allow EventHandler to be called | |
61 | time.Sleep(1 * time.Millisecond) | |
62 | 68 | |
63 | if want, have := true, calledEventHandler; want != have { | |
64 | t.Errorf("want %t, have %t", want, have) | |
69 | select { | |
70 | case <-calledEventHandler: | |
71 | case <-time.After(100 * time.Millisecond): | |
72 | t.Errorf("event handler never called") | |
65 | 73 | } |
66 | 74 | } |
67 | 75 | |
68 | 76 | func TestOptions(t *testing.T) { |
69 | _, err := NewClient([]string{"localhost"}, logger, Credentials("valid", "credentials")) | |
77 | _, err := NewClient([]string{"localhost"}, log.NewNopLogger(), Credentials("valid", "credentials")) | |
70 | 78 | if err != nil && err != stdzk.ErrNoServer { |
71 | 79 | t.Errorf("unexpected error: %v", err) |
72 | 80 | } |
73 | 81 | |
74 | _, err = NewClient([]string{"localhost"}, logger, Credentials("nopass", "")) | |
82 | _, err = NewClient([]string{"localhost"}, log.NewNopLogger(), Credentials("nopass", "")) | |
75 | 83 | if want, have := err, ErrInvalidCredentials; want != have { |
76 | 84 | t.Errorf("want %v, have %v", want, have) |
77 | 85 | } |
78 | 86 | |
79 | _, err = NewClient([]string{"localhost"}, logger, ConnectTimeout(0)) | |
87 | _, err = NewClient([]string{"localhost"}, log.NewNopLogger(), ConnectTimeout(0)) | |
80 | 88 | if err == nil { |
81 | 89 | t.Errorf("expected connect timeout error") |
82 | 90 | } |
83 | 91 | |
84 | _, err = NewClient([]string{"localhost"}, logger, SessionTimeout(0)) | |
92 | _, err = NewClient([]string{"localhost"}, log.NewNopLogger(), SessionTimeout(0)) | |
85 | 93 | if err == nil { |
86 | 94 | t.Errorf("expected connect timeout error") |
87 | 95 | } |
90 | 98 | func TestCreateParentNodes(t *testing.T) { |
91 | 99 | payload := [][]byte{[]byte("Payload"), []byte("Test")} |
92 | 100 | |
93 | c, err := NewClient([]string{"localhost:65500"}, logger) | |
101 | c, err := NewClient([]string{"localhost:65500"}, log.NewNopLogger()) | |
94 | 102 | if err != nil { |
95 | 103 | t.Errorf("unexpected error: %v", err) |
96 | 104 | } |
97 | 105 | if c == nil { |
98 | t.Fatalf("expected new Client, got nil") | |
106 | t.Fatal("expected new Client, got nil") | |
99 | 107 | } |
100 | p, err := NewPublisher(c, "/validpath", newFactory(""), logger) | |
108 | ||
109 | p, err := NewPublisher(c, "/validpath", newFactory(""), log.NewNopLogger()) | |
101 | 110 | if err != stdzk.ErrNoServer { |
102 | 111 | t.Errorf("unexpected error: %v", err) |
103 | 112 | } |
104 | 113 | if p != nil { |
105 | t.Errorf("expected failed new Publisher") | |
114 | t.Error("expected failed new Publisher") | |
106 | 115 | } |
107 | p, err = NewPublisher(c, "invalidpath", newFactory(""), logger) | |
116 | ||
117 | p, err = NewPublisher(c, "invalidpath", newFactory(""), log.NewNopLogger()) | |
108 | 118 | if err != stdzk.ErrInvalidPath { |
109 | 119 | t.Errorf("unexpected error: %v", err) |
110 | 120 | } |
112 | 122 | if err != stdzk.ErrNoServer { |
113 | 123 | t.Errorf("unexpected error: %v", err) |
114 | 124 | } |
115 | // stopping Client | |
125 | ||
116 | 126 | c.Stop() |
127 | ||
117 | 128 | err = c.CreateParentNodes("/validpath") |
118 | 129 | if err != ErrClientClosed { |
119 | 130 | t.Errorf("unexpected error: %v", err) |
120 | 131 | } |
121 | p, err = NewPublisher(c, "/validpath", newFactory(""), logger) | |
132 | ||
133 | p, err = NewPublisher(c, "/validpath", newFactory(""), log.NewNopLogger()) | |
122 | 134 | if err != ErrClientClosed { |
123 | 135 | t.Errorf("unexpected error: %v", err) |
124 | 136 | } |
125 | 137 | if p != nil { |
126 | t.Errorf("expected failed new Publisher") | |
138 | t.Error("expected failed new Publisher") | |
127 | 139 | } |
128 | c, err = NewClient([]string{"localhost:65500"}, logger, Payload(payload)) | |
140 | ||
141 | c, err = NewClient([]string{"localhost:65500"}, log.NewNopLogger(), Payload(payload)) | |
129 | 142 | if err != nil { |
130 | 143 | t.Errorf("unexpected error: %v", err) |
131 | 144 | } |
132 | 145 | if c == nil { |
133 | t.Fatalf("expected new Client, got nil") | |
146 | t.Fatal("expected new Client, got nil") | |
134 | 147 | } |
135 | p, err = NewPublisher(c, "/validpath", newFactory(""), logger) | |
148 | ||
149 | p, err = NewPublisher(c, "/validpath", newFactory(""), log.NewNopLogger()) | |
136 | 150 | if err != stdzk.ErrNoServer { |
137 | 151 | t.Errorf("unexpected error: %v", err) |
138 | 152 | } |
139 | 153 | if p != nil { |
140 | t.Errorf("expected failed new Publisher") | |
154 | t.Error("expected failed new Publisher") | |
141 | 155 | } |
142 | 156 | } |
0 | 0 | package zk |
1 | 1 | |
2 | 2 | import ( |
3 | "errors" | |
4 | "io" | |
5 | 3 | "testing" |
6 | 4 | "time" |
7 | ||
8 | "golang.org/x/net/context" | |
9 | ||
10 | "github.com/go-kit/kit/endpoint" | |
11 | "github.com/go-kit/kit/loadbalancer" | |
12 | "github.com/go-kit/kit/log" | |
13 | "github.com/samuel/go-zookeeper/zk" | |
14 | ) | |
15 | ||
16 | var ( | |
17 | path = "/gokit.test/service.name" | |
18 | e = func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil } | |
19 | logger = log.NewNopLogger() | |
20 | 5 | ) |
21 | 6 | |
22 | 7 | func TestPublisher(t *testing.T) { |
42 | 27 | } |
43 | 28 | defer p.Stop() |
44 | 29 | |
45 | endpoints, err := p.Endpoints() | |
46 | if err != nil { | |
47 | t.Fatal(err) | |
48 | } | |
49 | 30 | // instance1 came online |
50 | client.AddService(path+"/instance1", "zookeeper_node_data") | |
31 | client.AddService(path+"/instance1", "kaboom") | |
51 | 32 | |
52 | if want, have := 0, len(endpoints); want != have { | |
53 | t.Errorf("want %d, have %d", want, have) | |
33 | // instance2 came online | |
34 | client.AddService(path+"/instance2", "zookeeper_node_data") | |
35 | ||
36 | if err = asyncTest(100*time.Millisecond, 1, p); err != nil { | |
37 | t.Error(err) | |
54 | 38 | } |
55 | 39 | } |
56 | 40 | |
67 | 51 | if err != nil { |
68 | 52 | t.Fatal(err) |
69 | 53 | } |
54 | ||
70 | 55 | if want, have := 0, len(endpoints); want != have { |
71 | 56 | t.Errorf("want %d, have %d", want, have) |
72 | 57 | } |
74 | 59 | // instance1 came online |
75 | 60 | client.AddService(path+"/instance1", "zookeeper_node_data") |
76 | 61 | |
77 | // test if we received the instance | |
78 | endpoints, err = p.Endpoints() | |
79 | if err != nil { | |
80 | t.Fatal(err) | |
81 | } | |
82 | if want, have := 1, len(endpoints); want != have { | |
83 | t.Errorf("want %d, have %d", want, have) | |
84 | } | |
85 | ||
86 | 62 | // instance2 came online |
87 | 63 | client.AddService(path+"/instance2", "zookeeper_node_data2") |
88 | 64 | |
89 | // test if we received the instance | |
90 | endpoints, err = p.Endpoints() | |
91 | if err != nil { | |
92 | t.Fatal(err) | |
93 | } | |
94 | if want, have := 2, len(endpoints); want != have { | |
95 | t.Errorf("want %d, have %d", want, have) | |
65 | // we should have 2 instances | |
66 | if err = asyncTest(100*time.Millisecond, 2, p); err != nil { | |
67 | t.Error(err) | |
96 | 68 | } |
97 | 69 | |
98 | 70 | // watch triggers an error... |
99 | 71 | client.SendErrorOnWatch() |
100 | 72 | |
101 | // test if we ignored the empty instance response due to the error | |
102 | endpoints, err = p.Endpoints() | |
103 | if err != nil { | |
104 | t.Fatal(err) | |
105 | } | |
106 | if want, have := 2, len(endpoints); want != have { | |
107 | t.Errorf("want %d, have %d", want, have) | |
73 | // test if error was consumed | |
74 | if err = client.ErrorIsConsumed(100 * time.Millisecond); err != nil { | |
75 | t.Error(err) | |
108 | 76 | } |
109 | 77 | |
110 | // instances go offline | |
78 | // instance3 came online | |
79 | client.AddService(path+"/instance3", "zookeeper_node_data3") | |
80 | ||
81 | // we should have 3 instances | |
82 | if err = asyncTest(100*time.Millisecond, 3, p); err != nil { | |
83 | t.Error(err) | |
84 | } | |
85 | ||
86 | // instance1 goes offline | |
111 | 87 | client.RemoveService(path + "/instance1") |
88 | ||
89 | // instance2 goes offline | |
112 | 90 | client.RemoveService(path + "/instance2") |
113 | 91 | |
114 | endpoints, err = p.Endpoints() | |
115 | if err != nil { | |
116 | t.Fatal(err) | |
117 | } | |
118 | if want, have := 0, len(endpoints); want != have { | |
119 | t.Errorf("want %d, have %d", want, have) | |
92 | // we should have 1 instance | |
93 | if err = asyncTest(100*time.Millisecond, 1, p); err != nil { | |
94 | t.Error(err) | |
120 | 95 | } |
121 | 96 | } |
122 | 97 | |
125 | 100 | client.SendErrorOnWatch() |
126 | 101 | p, err := NewPublisher(client, path, newFactory(""), logger) |
127 | 102 | if err == nil { |
128 | t.Errorf("expected error on new publisher") | |
103 | t.Error("expected error on new publisher") | |
129 | 104 | } |
130 | 105 | if p != nil { |
131 | t.Errorf("expected publisher not to be created") | |
106 | t.Error("expected publisher not to be created") | |
132 | 107 | } |
133 | 108 | p, err = NewPublisher(client, "BadPath", newFactory(""), logger) |
134 | 109 | if err == nil { |
135 | t.Errorf("expected error on new publisher") | |
110 | t.Error("expected error on new publisher") | |
136 | 111 | } |
137 | 112 | if p != nil { |
138 | t.Errorf("expected publisher not to be created") | |
113 | t.Error("expected publisher not to be created") | |
139 | 114 | } |
140 | 115 | } |
141 | ||
142 | type fakeClient struct { | |
143 | ch chan zk.Event | |
144 | responses map[string]string | |
145 | result bool | |
146 | } | |
147 | ||
148 | func newFakeClient() *fakeClient { | |
149 | return &fakeClient{ | |
150 | make(chan zk.Event, 1), | |
151 | make(map[string]string), | |
152 | true, | |
153 | } | |
154 | } | |
155 | ||
156 | func (c *fakeClient) CreateParentNodes(path string) error { | |
157 | if path == "BadPath" { | |
158 | return errors.New("Dummy Error") | |
159 | } | |
160 | return nil | |
161 | } | |
162 | ||
163 | func (c *fakeClient) GetEntries(path string) ([]string, <-chan zk.Event, error) { | |
164 | responses := []string{} | |
165 | if c.result == false { | |
166 | c.result = true | |
167 | return responses, c.ch, errors.New("Dummy Error") | |
168 | } | |
169 | for _, data := range c.responses { | |
170 | responses = append(responses, data) | |
171 | } | |
172 | return responses, c.ch, nil | |
173 | } | |
174 | ||
175 | func (c *fakeClient) AddService(node, data string) { | |
176 | c.responses[node] = data | |
177 | c.triggerWatch() | |
178 | } | |
179 | ||
180 | func (c *fakeClient) RemoveService(node string) { | |
181 | delete(c.responses, node) | |
182 | c.triggerWatch() | |
183 | } | |
184 | ||
185 | func (c *fakeClient) SendErrorOnWatch() { | |
186 | c.result = false | |
187 | c.triggerWatch() | |
188 | } | |
189 | ||
190 | func (c *fakeClient) Stop() {} | |
191 | ||
192 | func newFactory(fakeError string) loadbalancer.Factory { | |
193 | return func(string) (endpoint.Endpoint, io.Closer, error) { | |
194 | if fakeError == "" { | |
195 | return e, nil, nil | |
196 | } | |
197 | return nil, nil, errors.New(fakeError) | |
198 | } | |
199 | } | |
200 | ||
201 | func (c *fakeClient) triggerWatch() { | |
202 | c.ch <- zk.Event{} | |
203 | // watches on ZooKeeper Nodes trigger once, most ZooKeeper libraries also | |
204 | // implement "fire once" channels for these watches | |
205 | close(c.ch) | |
206 | c.ch = make(chan zk.Event, 1) | |
207 | ||
208 | // make sure we allow the Publisher to handle this update | |
209 | time.Sleep(1 * time.Millisecond) | |
210 | } |
0 | package zk | |
1 | ||
2 | import ( | |
3 | "errors" | |
4 | "fmt" | |
5 | "io" | |
6 | "sync" | |
7 | "time" | |
8 | ||
9 | "github.com/samuel/go-zookeeper/zk" | |
10 | "golang.org/x/net/context" | |
11 | ||
12 | "github.com/go-kit/kit/endpoint" | |
13 | "github.com/go-kit/kit/loadbalancer" | |
14 | "github.com/go-kit/kit/log" | |
15 | ) | |
16 | ||
17 | var ( | |
18 | path = "/gokit.test/service.name" | |
19 | e = func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil } | |
20 | logger = log.NewNopLogger() | |
21 | ) | |
22 | ||
23 | type fakeClient struct { | |
24 | mtx sync.Mutex | |
25 | ch chan zk.Event | |
26 | responses map[string]string | |
27 | result bool | |
28 | } | |
29 | ||
30 | func newFakeClient() *fakeClient { | |
31 | return &fakeClient{ | |
32 | ch: make(chan zk.Event, 5), | |
33 | responses: make(map[string]string), | |
34 | result: true, | |
35 | } | |
36 | } | |
37 | ||
38 | func (c *fakeClient) CreateParentNodes(path string) error { | |
39 | if path == "BadPath" { | |
40 | return errors.New("Dummy Error") | |
41 | } | |
42 | return nil | |
43 | } | |
44 | ||
45 | func (c *fakeClient) GetEntries(path string) ([]string, <-chan zk.Event, error) { | |
46 | c.mtx.Lock() | |
47 | defer c.mtx.Unlock() | |
48 | if c.result == false { | |
49 | c.result = true | |
50 | return []string{}, c.ch, errors.New("Dummy Error") | |
51 | } | |
52 | responses := []string{} | |
53 | for _, data := range c.responses { | |
54 | responses = append(responses, data) | |
55 | } | |
56 | return responses, c.ch, nil | |
57 | } | |
58 | ||
59 | func (c *fakeClient) AddService(node, data string) { | |
60 | c.mtx.Lock() | |
61 | defer c.mtx.Unlock() | |
62 | c.responses[node] = data | |
63 | c.ch <- zk.Event{} | |
64 | } | |
65 | ||
66 | func (c *fakeClient) RemoveService(node string) { | |
67 | c.mtx.Lock() | |
68 | defer c.mtx.Unlock() | |
69 | delete(c.responses, node) | |
70 | c.ch <- zk.Event{} | |
71 | } | |
72 | ||
73 | func (c *fakeClient) SendErrorOnWatch() { | |
74 | c.mtx.Lock() | |
75 | defer c.mtx.Unlock() | |
76 | c.result = false | |
77 | c.ch <- zk.Event{} | |
78 | } | |
79 | ||
80 | func (c *fakeClient) ErrorIsConsumed(t time.Duration) error { | |
81 | timeout := time.After(t) | |
82 | for { | |
83 | select { | |
84 | case <-timeout: | |
85 | return fmt.Errorf("expected error not consumed after timeout %s", t.String()) | |
86 | default: | |
87 | c.mtx.Lock() | |
88 | if c.result == false { | |
89 | c.mtx.Unlock() | |
90 | return nil | |
91 | } | |
92 | c.mtx.Unlock() | |
93 | } | |
94 | } | |
95 | } | |
96 | ||
97 | func (c *fakeClient) Stop() {} | |
98 | ||
99 | func newFactory(fakeError string) loadbalancer.Factory { | |
100 | return func(instance string) (endpoint.Endpoint, io.Closer, error) { | |
101 | if fakeError == instance { | |
102 | return nil, nil, errors.New(fakeError) | |
103 | } | |
104 | return e, nil, nil | |
105 | } | |
106 | } | |
107 | ||
108 | func asyncTest(timeout time.Duration, want int, p *Publisher) (err error) { | |
109 | var endpoints []endpoint.Endpoint | |
110 | // want can never be -1 | |
111 | have := -1 | |
112 | t := time.After(timeout) | |
113 | for { | |
114 | select { | |
115 | case <-t: | |
116 | return fmt.Errorf("want %d, have %d after timeout %s", want, have, timeout.String()) | |
117 | default: | |
118 | endpoints, err = p.Endpoints() | |
119 | have = len(endpoints) | |
120 | if err != nil || want == have { | |
121 | return | |
122 | } | |
123 | time.Sleep(time.Millisecond) | |
124 | } | |
125 | } | |
126 | } |
73 | 73 | |
74 | 74 | func TestStdlibAdapterSubexps(t *testing.T) { |
75 | 75 | for input, wantMap := range map[string]map[string]string{ |
76 | "hello world": map[string]string{ | |
77 | "date": "", | |
78 | "time": "", | |
79 | "file": "", | |
80 | "msg": "hello world", | |
81 | }, | |
82 | "2009/01/23: hello world": map[string]string{ | |
83 | "date": "2009/01/23", | |
84 | "time": "", | |
85 | "file": "", | |
86 | "msg": "hello world", | |
87 | }, | |
88 | "2009/01/23 01:23:23: hello world": map[string]string{ | |
89 | "date": "2009/01/23", | |
90 | "time": "01:23:23", | |
91 | "file": "", | |
92 | "msg": "hello world", | |
93 | }, | |
94 | "01:23:23: hello world": map[string]string{ | |
95 | "date": "", | |
96 | "time": "01:23:23", | |
97 | "file": "", | |
98 | "msg": "hello world", | |
99 | }, | |
100 | "2009/01/23 01:23:23.123123: hello world": map[string]string{ | |
101 | "date": "2009/01/23", | |
102 | "time": "01:23:23.123123", | |
103 | "file": "", | |
104 | "msg": "hello world", | |
105 | }, | |
106 | "2009/01/23 01:23:23.123123 /a/b/c/d.go:23: hello world": map[string]string{ | |
107 | "date": "2009/01/23", | |
108 | "time": "01:23:23.123123", | |
109 | "file": "/a/b/c/d.go:23", | |
110 | "msg": "hello world", | |
111 | }, | |
112 | "01:23:23.123123 /a/b/c/d.go:23: hello world": map[string]string{ | |
113 | "date": "", | |
114 | "time": "01:23:23.123123", | |
115 | "file": "/a/b/c/d.go:23", | |
116 | "msg": "hello world", | |
117 | }, | |
118 | "2009/01/23 01:23:23 /a/b/c/d.go:23: hello world": map[string]string{ | |
119 | "date": "2009/01/23", | |
120 | "time": "01:23:23", | |
121 | "file": "/a/b/c/d.go:23", | |
122 | "msg": "hello world", | |
123 | }, | |
124 | "2009/01/23 /a/b/c/d.go:23: hello world": map[string]string{ | |
125 | "date": "2009/01/23", | |
126 | "time": "", | |
127 | "file": "/a/b/c/d.go:23", | |
128 | "msg": "hello world", | |
129 | }, | |
130 | "/a/b/c/d.go:23: hello world": map[string]string{ | |
131 | "date": "", | |
132 | "time": "", | |
133 | "file": "/a/b/c/d.go:23", | |
134 | "msg": "hello world", | |
135 | }, | |
136 | "2009/01/23 01:23:23.123123 C:/a/b/c/d.go:23: hello world": map[string]string{ | |
137 | "date": "2009/01/23", | |
138 | "time": "01:23:23.123123", | |
139 | "file": "C:/a/b/c/d.go:23", | |
140 | "msg": "hello world", | |
141 | }, | |
142 | "01:23:23.123123 C:/a/b/c/d.go:23: hello world": map[string]string{ | |
143 | "date": "", | |
144 | "time": "01:23:23.123123", | |
145 | "file": "C:/a/b/c/d.go:23", | |
146 | "msg": "hello world", | |
147 | }, | |
148 | "2009/01/23 01:23:23 C:/a/b/c/d.go:23: hello world": map[string]string{ | |
149 | "date": "2009/01/23", | |
150 | "time": "01:23:23", | |
151 | "file": "C:/a/b/c/d.go:23", | |
152 | "msg": "hello world", | |
153 | }, | |
154 | "2009/01/23 C:/a/b/c/d.go:23: hello world": map[string]string{ | |
155 | "date": "2009/01/23", | |
156 | "time": "", | |
157 | "file": "C:/a/b/c/d.go:23", | |
158 | "msg": "hello world", | |
159 | }, | |
160 | "C:/a/b/c/d.go:23: hello world": map[string]string{ | |
161 | "date": "", | |
162 | "time": "", | |
163 | "file": "C:/a/b/c/d.go:23", | |
164 | "msg": "hello world", | |
165 | }, | |
166 | "2009/01/23 01:23:23.123123 C:/a/b/c/d.go:23: :.;<>_#{[]}\"\\": map[string]string{ | |
167 | "date": "2009/01/23", | |
168 | "time": "01:23:23.123123", | |
169 | "file": "C:/a/b/c/d.go:23", | |
170 | "msg": ":.;<>_#{[]}\"\\", | |
171 | }, | |
172 | "01:23:23.123123 C:/a/b/c/d.go:23: :.;<>_#{[]}\"\\": map[string]string{ | |
173 | "date": "", | |
174 | "time": "01:23:23.123123", | |
175 | "file": "C:/a/b/c/d.go:23", | |
176 | "msg": ":.;<>_#{[]}\"\\", | |
177 | }, | |
178 | "2009/01/23 01:23:23 C:/a/b/c/d.go:23: :.;<>_#{[]}\"\\": map[string]string{ | |
179 | "date": "2009/01/23", | |
180 | "time": "01:23:23", | |
181 | "file": "C:/a/b/c/d.go:23", | |
182 | "msg": ":.;<>_#{[]}\"\\", | |
183 | }, | |
184 | "2009/01/23 C:/a/b/c/d.go:23: :.;<>_#{[]}\"\\": map[string]string{ | |
185 | "date": "2009/01/23", | |
186 | "time": "", | |
187 | "file": "C:/a/b/c/d.go:23", | |
188 | "msg": ":.;<>_#{[]}\"\\", | |
189 | }, | |
190 | "C:/a/b/c/d.go:23: :.;<>_#{[]}\"\\": map[string]string{ | |
76 | "hello world": { | |
77 | "date": "", | |
78 | "time": "", | |
79 | "file": "", | |
80 | "msg": "hello world", | |
81 | }, | |
82 | "2009/01/23: hello world": { | |
83 | "date": "2009/01/23", | |
84 | "time": "", | |
85 | "file": "", | |
86 | "msg": "hello world", | |
87 | }, | |
88 | "2009/01/23 01:23:23: hello world": { | |
89 | "date": "2009/01/23", | |
90 | "time": "01:23:23", | |
91 | "file": "", | |
92 | "msg": "hello world", | |
93 | }, | |
94 | "01:23:23: hello world": { | |
95 | "date": "", | |
96 | "time": "01:23:23", | |
97 | "file": "", | |
98 | "msg": "hello world", | |
99 | }, | |
100 | "2009/01/23 01:23:23.123123: hello world": { | |
101 | "date": "2009/01/23", | |
102 | "time": "01:23:23.123123", | |
103 | "file": "", | |
104 | "msg": "hello world", | |
105 | }, | |
106 | "2009/01/23 01:23:23.123123 /a/b/c/d.go:23: hello world": { | |
107 | "date": "2009/01/23", | |
108 | "time": "01:23:23.123123", | |
109 | "file": "/a/b/c/d.go:23", | |
110 | "msg": "hello world", | |
111 | }, | |
112 | "01:23:23.123123 /a/b/c/d.go:23: hello world": { | |
113 | "date": "", | |
114 | "time": "01:23:23.123123", | |
115 | "file": "/a/b/c/d.go:23", | |
116 | "msg": "hello world", | |
117 | }, | |
118 | "2009/01/23 01:23:23 /a/b/c/d.go:23: hello world": { | |
119 | "date": "2009/01/23", | |
120 | "time": "01:23:23", | |
121 | "file": "/a/b/c/d.go:23", | |
122 | "msg": "hello world", | |
123 | }, | |
124 | "2009/01/23 /a/b/c/d.go:23: hello world": { | |
125 | "date": "2009/01/23", | |
126 | "time": "", | |
127 | "file": "/a/b/c/d.go:23", | |
128 | "msg": "hello world", | |
129 | }, | |
130 | "/a/b/c/d.go:23: hello world": { | |
131 | "date": "", | |
132 | "time": "", | |
133 | "file": "/a/b/c/d.go:23", | |
134 | "msg": "hello world", | |
135 | }, | |
136 | "2009/01/23 01:23:23.123123 C:/a/b/c/d.go:23: hello world": { | |
137 | "date": "2009/01/23", | |
138 | "time": "01:23:23.123123", | |
139 | "file": "C:/a/b/c/d.go:23", | |
140 | "msg": "hello world", | |
141 | }, | |
142 | "01:23:23.123123 C:/a/b/c/d.go:23: hello world": { | |
143 | "date": "", | |
144 | "time": "01:23:23.123123", | |
145 | "file": "C:/a/b/c/d.go:23", | |
146 | "msg": "hello world", | |
147 | }, | |
148 | "2009/01/23 01:23:23 C:/a/b/c/d.go:23: hello world": { | |
149 | "date": "2009/01/23", | |
150 | "time": "01:23:23", | |
151 | "file": "C:/a/b/c/d.go:23", | |
152 | "msg": "hello world", | |
153 | }, | |
154 | "2009/01/23 C:/a/b/c/d.go:23: hello world": { | |
155 | "date": "2009/01/23", | |
156 | "time": "", | |
157 | "file": "C:/a/b/c/d.go:23", | |
158 | "msg": "hello world", | |
159 | }, | |
160 | "C:/a/b/c/d.go:23: hello world": { | |
161 | "date": "", | |
162 | "time": "", | |
163 | "file": "C:/a/b/c/d.go:23", | |
164 | "msg": "hello world", | |
165 | }, | |
166 | "2009/01/23 01:23:23.123123 C:/a/b/c/d.go:23: :.;<>_#{[]}\"\\": { | |
167 | "date": "2009/01/23", | |
168 | "time": "01:23:23.123123", | |
169 | "file": "C:/a/b/c/d.go:23", | |
170 | "msg": ":.;<>_#{[]}\"\\", | |
171 | }, | |
172 | "01:23:23.123123 C:/a/b/c/d.go:23: :.;<>_#{[]}\"\\": { | |
173 | "date": "", | |
174 | "time": "01:23:23.123123", | |
175 | "file": "C:/a/b/c/d.go:23", | |
176 | "msg": ":.;<>_#{[]}\"\\", | |
177 | }, | |
178 | "2009/01/23 01:23:23 C:/a/b/c/d.go:23: :.;<>_#{[]}\"\\": { | |
179 | "date": "2009/01/23", | |
180 | "time": "01:23:23", | |
181 | "file": "C:/a/b/c/d.go:23", | |
182 | "msg": ":.;<>_#{[]}\"\\", | |
183 | }, | |
184 | "2009/01/23 C:/a/b/c/d.go:23: :.;<>_#{[]}\"\\": { | |
185 | "date": "2009/01/23", | |
186 | "time": "", | |
187 | "file": "C:/a/b/c/d.go:23", | |
188 | "msg": ":.;<>_#{[]}\"\\", | |
189 | }, | |
190 | "C:/a/b/c/d.go:23: :.;<>_#{[]}\"\\": { | |
191 | 191 | "date": "", |
192 | 192 | "time": "", |
193 | 193 | "file": "C:/a/b/c/d.go:23", |
201 | 201 | } |
202 | 202 | } |
203 | 203 | } |
204 | }⏎ | |
204 | } |
0 | package term | |
1 | ||
2 | import "syscall" | |
3 | ||
4 | const ioctlReadTermios = syscall.TIOCGETA | |
0 | package term | |
1 | ||
2 | import "syscall" | |
3 | ||
4 | const ioctlReadTermios = syscall.TIOCGETA |
12 | 12 | |
13 | 13 | ## Rationale |
14 | 14 | |
15 | TODO | |
15 | Code instrumentation is absolutely essential to achieve [observability][] into a distributed system. | |
16 | Metrics and instrumentation tools have coalesced around a few well-defined idioms. | |
17 | `package metrics` provides a common, minimal interface those idioms for service authors. | |
18 | ||
19 | [observability]: https://speakerdeck.com/mattheath/observability-in-micro-service-architectures | |
16 | 20 | |
17 | 21 | ## Usage |
18 | 22 | |
52 | 56 | ``` |
53 | 57 | |
54 | 58 | A gauge for the number of goroutines currently running, exported via statsd. |
59 | ||
55 | 60 | ```go |
56 | 61 | import ( |
57 | 62 | "net" |
65 | 70 | func main() { |
66 | 71 | statsdWriter, err := net.Dial("udp", "127.0.0.1:8126") |
67 | 72 | if err != nil { |
68 | os.Exit(1) | |
73 | panic(err) | |
69 | 74 | } |
70 | 75 | |
71 | reportingDuration := 5 * time.Second | |
72 | goroutines := statsd.NewGauge(statsdWriter, "total_goroutines", reportingDuration) | |
73 | for range time.Tick(reportingDuration) { | |
76 | reportInterval := 5 * time.Second | |
77 | goroutines := statsd.NewGauge(statsdWriter, "total_goroutines", reportInterval) | |
78 | for range time.Tick(reportInterval) { | |
74 | 79 | goroutines.Set(float64(runtime.NumGoroutine())) |
75 | 80 | } |
76 | 81 | } |
77 | ||
78 | 82 | ``` |
0 | // Package dogstatsd implements a DogStatsD backend for package metrics. | |
1 | // | |
2 | // This implementation supports Datadog tags that provide additional metric | |
3 | // filtering capabilities. See the DogStatsD documentation for protocol | |
4 | // specifics: | |
5 | // http://docs.datadoghq.com/guides/dogstatsd/ | |
6 | // | |
7 | package dogstatsd | |
8 | ||
9 | import ( | |
10 | "bytes" | |
11 | "fmt" | |
12 | "io" | |
13 | "log" | |
14 | "math" | |
15 | "time" | |
16 | ||
17 | "sync/atomic" | |
18 | ||
19 | "github.com/go-kit/kit/metrics" | |
20 | ) | |
21 | ||
22 | // dogstatsd metrics were based on the statsd package in go-kit | |
23 | ||
24 | const maxBufferSize = 1400 // bytes | |
25 | ||
26 | type dogstatsdCounter struct { | |
27 | key string | |
28 | c chan string | |
29 | tags []metrics.Field | |
30 | } | |
31 | ||
32 | // NewCounter returns a Counter that emits observations in the DogStatsD protocol | |
33 | // to the passed writer. Observations are buffered for the report interval or | |
34 | // until the buffer exceeds a max packet size, whichever comes first. | |
35 | // | |
36 | // TODO: support for sampling. | |
37 | func NewCounter(w io.Writer, key string, reportInterval time.Duration, globalTags []metrics.Field) metrics.Counter { | |
38 | return NewCounterTick(w, key, time.Tick(reportInterval), globalTags) | |
39 | } | |
40 | ||
41 | // NewCounterTick is the same as NewCounter, but allows the user to pass in a | |
42 | // ticker channel instead of invoking time.Tick. | |
43 | func NewCounterTick(w io.Writer, key string, reportTicker <-chan time.Time, tags []metrics.Field) metrics.Counter { | |
44 | c := &dogstatsdCounter{ | |
45 | key: key, | |
46 | c: make(chan string), | |
47 | tags: tags, | |
48 | } | |
49 | go fwd(w, key, reportTicker, c.c) | |
50 | return c | |
51 | } | |
52 | ||
53 | func (c *dogstatsdCounter) Name() string { return c.key } | |
54 | ||
55 | func (c *dogstatsdCounter) With(f metrics.Field) metrics.Counter { | |
56 | return &dogstatsdCounter{ | |
57 | key: c.key, | |
58 | c: c.c, | |
59 | tags: append(c.tags, f), | |
60 | } | |
61 | } | |
62 | ||
63 | func (c *dogstatsdCounter) Add(delta uint64) { c.c <- applyTags(fmt.Sprintf("%d|c", delta), c.tags) } | |
64 | ||
65 | type dogstatsdGauge struct { | |
66 | key string | |
67 | lastValue uint64 // math.Float64frombits | |
68 | g chan string | |
69 | tags []metrics.Field | |
70 | } | |
71 | ||
72 | // NewGauge returns a Gauge that emits values in the DogStatsD protocol to the | |
73 | // passed writer. Values are buffered for the report interval or until the | |
74 | // buffer exceeds a max packet size, whichever comes first. | |
75 | // | |
76 | // TODO: support for sampling. | |
77 | func NewGauge(w io.Writer, key string, reportInterval time.Duration, tags []metrics.Field) metrics.Gauge { | |
78 | return NewGaugeTick(w, key, time.Tick(reportInterval), tags) | |
79 | } | |
80 | ||
81 | // NewGaugeTick is the same as NewGauge, but allows the user to pass in a ticker | |
82 | // channel instead of invoking time.Tick. | |
83 | func NewGaugeTick(w io.Writer, key string, reportTicker <-chan time.Time, tags []metrics.Field) metrics.Gauge { | |
84 | g := &dogstatsdGauge{ | |
85 | key: key, | |
86 | g: make(chan string), | |
87 | tags: tags, | |
88 | } | |
89 | go fwd(w, key, reportTicker, g.g) | |
90 | return g | |
91 | } | |
92 | ||
93 | func (g *dogstatsdGauge) Name() string { return g.key } | |
94 | ||
95 | func (g *dogstatsdGauge) With(f metrics.Field) metrics.Gauge { | |
96 | return &dogstatsdGauge{ | |
97 | key: g.key, | |
98 | lastValue: g.lastValue, | |
99 | g: g.g, | |
100 | tags: append(g.tags, f), | |
101 | } | |
102 | } | |
103 | ||
104 | func (g *dogstatsdGauge) Add(delta float64) { | |
105 | // https://github.com/etsy/statsd/blob/master/docs/metric_types.md#gauges | |
106 | sign := "+" | |
107 | if delta < 0 { | |
108 | sign, delta = "-", -delta | |
109 | } | |
110 | g.g <- applyTags(fmt.Sprintf("%s%f|g", sign, delta), g.tags) | |
111 | } | |
112 | ||
113 | func (g *dogstatsdGauge) Set(value float64) { | |
114 | atomic.StoreUint64(&g.lastValue, math.Float64bits(value)) | |
115 | g.g <- applyTags(fmt.Sprintf("%f|g", value), g.tags) | |
116 | } | |
117 | ||
118 | func (g *dogstatsdGauge) Get() float64 { | |
119 | return math.Float64frombits(atomic.LoadUint64(&g.lastValue)) | |
120 | } | |
121 | ||
122 | // NewCallbackGauge emits values in the DogStatsD protocol to the passed writer. | |
123 | // It collects values every scrape interval from the callback. Values are | |
124 | // buffered for the report interval or until the buffer exceeds a max packet | |
125 | // size, whichever comes first. The report and scrape intervals may be the | |
126 | // same. The callback determines the value, and fields are ignored, so | |
127 | // NewCallbackGauge returns nothing. | |
128 | func NewCallbackGauge(w io.Writer, key string, reportInterval, scrapeInterval time.Duration, callback func() float64) { | |
129 | NewCallbackGaugeTick(w, key, time.Tick(reportInterval), time.Tick(scrapeInterval), callback) | |
130 | } | |
131 | ||
132 | // NewCallbackGaugeTick is the same as NewCallbackGauge, but allows the user to | |
133 | // pass in ticker channels instead of durations to control report and scrape | |
134 | // intervals. | |
135 | func NewCallbackGaugeTick(w io.Writer, key string, reportTicker, scrapeTicker <-chan time.Time, callback func() float64) { | |
136 | go fwd(w, key, reportTicker, emitEvery(scrapeTicker, callback)) | |
137 | } | |
138 | ||
139 | func emitEvery(emitTicker <-chan time.Time, callback func() float64) <-chan string { | |
140 | c := make(chan string) | |
141 | go func() { | |
142 | for range emitTicker { | |
143 | c <- fmt.Sprintf("%f|g", callback()) | |
144 | } | |
145 | }() | |
146 | return c | |
147 | } | |
148 | ||
149 | type dogstatsdHistogram struct { | |
150 | key string | |
151 | h chan string | |
152 | tags []metrics.Field | |
153 | } | |
154 | ||
155 | // NewHistogram returns a Histogram that emits observations in the DogStatsD | |
156 | // protocol to the passed writer. Observations are buffered for the reporting | |
157 | // interval or until the buffer exceeds a max packet size, whichever comes | |
158 | // first. | |
159 | // | |
160 | // NewHistogram is mapped to a statsd Timing, so observations should represent | |
161 | // milliseconds. If you observe in units of nanoseconds, you can make the | |
162 | // translation with a ScaledHistogram: | |
163 | // | |
164 | // NewScaledHistogram(dogstatsdHistogram, time.Millisecond) | |
165 | // | |
166 | // You can also enforce the constraint in a typesafe way with a millisecond | |
167 | // TimeHistogram: | |
168 | // | |
169 | // NewTimeHistogram(dogstatsdHistogram, time.Millisecond) | |
170 | // | |
171 | // TODO: support for sampling. | |
172 | func NewHistogram(w io.Writer, key string, reportInterval time.Duration, tags []metrics.Field) metrics.Histogram { | |
173 | return NewHistogramTick(w, key, time.Tick(reportInterval), tags) | |
174 | } | |
175 | ||
176 | // NewHistogramTick is the same as NewHistogram, but allows the user to pass a | |
177 | // ticker channel instead of invoking time.Tick. | |
178 | func NewHistogramTick(w io.Writer, key string, reportTicker <-chan time.Time, tags []metrics.Field) metrics.Histogram { | |
179 | h := &dogstatsdHistogram{ | |
180 | key: key, | |
181 | h: make(chan string), | |
182 | tags: tags, | |
183 | } | |
184 | go fwd(w, key, reportTicker, h.h) | |
185 | return h | |
186 | } | |
187 | ||
188 | func (h *dogstatsdHistogram) Name() string { return h.key } | |
189 | ||
190 | func (h *dogstatsdHistogram) With(f metrics.Field) metrics.Histogram { | |
191 | return &dogstatsdHistogram{ | |
192 | key: h.key, | |
193 | h: h.h, | |
194 | tags: append(h.tags, f), | |
195 | } | |
196 | } | |
197 | ||
198 | func (h *dogstatsdHistogram) Observe(value int64) { | |
199 | h.h <- applyTags(fmt.Sprintf("%d|ms", value), h.tags) | |
200 | } | |
201 | ||
202 | func (h *dogstatsdHistogram) Distribution() ([]metrics.Bucket, []metrics.Quantile) { | |
203 | // TODO(pb): no way to do this without introducing e.g. codahale/hdrhistogram | |
204 | return []metrics.Bucket{}, []metrics.Quantile{} | |
205 | } | |
206 | ||
207 | func fwd(w io.Writer, key string, reportTicker <-chan time.Time, c <-chan string) { | |
208 | buf := &bytes.Buffer{} | |
209 | for { | |
210 | select { | |
211 | case s := <-c: | |
212 | fmt.Fprintf(buf, "%s:%s\n", key, s) | |
213 | if buf.Len() > maxBufferSize { | |
214 | flush(w, buf) | |
215 | } | |
216 | ||
217 | case <-reportTicker: | |
218 | flush(w, buf) | |
219 | } | |
220 | } | |
221 | } | |
222 | ||
223 | func flush(w io.Writer, buf *bytes.Buffer) { | |
224 | if buf.Len() <= 0 { | |
225 | return | |
226 | } | |
227 | if _, err := w.Write(buf.Bytes()); err != nil { | |
228 | log.Printf("error: could not write to dogstatsd: %v", err) | |
229 | } | |
230 | buf.Reset() | |
231 | } | |
232 | ||
233 | func applyTags(value string, tags []metrics.Field) string { | |
234 | if len(tags) > 0 { | |
235 | var tagsString string | |
236 | for _, t := range tags { | |
237 | switch tagsString { | |
238 | case "": | |
239 | tagsString = t.Key + ":" + t.Value | |
240 | default: | |
241 | tagsString = tagsString + "," + t.Key + ":" + t.Value | |
242 | } | |
243 | } | |
244 | value = value + "|#" + tagsString | |
245 | } | |
246 | return value | |
247 | } |
0 | package dogstatsd | |
1 | ||
2 | import ( | |
3 | "bytes" | |
4 | "fmt" | |
5 | "github.com/go-kit/kit/metrics" | |
6 | "strings" | |
7 | "sync" | |
8 | "testing" | |
9 | "time" | |
10 | ) | |
11 | ||
12 | func TestCounter(t *testing.T) { | |
13 | buf := &syncbuf{buf: &bytes.Buffer{}} | |
14 | reportc := make(chan time.Time) | |
15 | tags := []metrics.Field{} | |
16 | c := NewCounterTick(buf, "test_statsd_counter", reportc, tags) | |
17 | ||
18 | c.Add(1) | |
19 | c.With(metrics.Field{"foo", "bar"}).Add(2) | |
20 | c.With(metrics.Field{"foo", "bar"}).With(metrics.Field{"abc", "123"}).Add(2) | |
21 | c.Add(3) | |
22 | ||
23 | want, have := "test_statsd_counter:1|c\ntest_statsd_counter:2|c|#foo:bar\ntest_statsd_counter:2|c|#foo:bar,abc:123\ntest_statsd_counter:3|c\n", "" | |
24 | by(t, 100*time.Millisecond, func() bool { | |
25 | have = buf.String() | |
26 | return want == have | |
27 | }, func() { | |
28 | reportc <- time.Now() | |
29 | }, fmt.Sprintf("want %q, have %q", want, have)) | |
30 | } | |
31 | ||
32 | func TestGauge(t *testing.T) { | |
33 | buf := &syncbuf{buf: &bytes.Buffer{}} | |
34 | reportc := make(chan time.Time) | |
35 | tags := []metrics.Field{} | |
36 | g := NewGaugeTick(buf, "test_statsd_gauge", reportc, tags) | |
37 | ||
38 | delta := 1.0 | |
39 | g.Add(delta) | |
40 | ||
41 | want, have := fmt.Sprintf("test_statsd_gauge:+%f|g\n", delta), "" | |
42 | by(t, 100*time.Millisecond, func() bool { | |
43 | have = buf.String() | |
44 | return want == have | |
45 | }, func() { | |
46 | reportc <- time.Now() | |
47 | }, fmt.Sprintf("want %q, have %q", want, have)) | |
48 | ||
49 | buf.Reset() | |
50 | delta = -2.0 | |
51 | g.With(metrics.Field{"foo", "bar"}).Add(delta) | |
52 | ||
53 | want, have = fmt.Sprintf("test_statsd_gauge:%f|g|#foo:bar\n", delta), "" | |
54 | by(t, 100*time.Millisecond, func() bool { | |
55 | have = buf.String() | |
56 | return want == have | |
57 | }, func() { | |
58 | reportc <- time.Now() | |
59 | }, fmt.Sprintf("want %q, have %q", want, have)) | |
60 | ||
61 | buf.Reset() | |
62 | value := 3.0 | |
63 | g.With(metrics.Field{"foo", "bar"}).With(metrics.Field{"abc", "123"}).Set(value) | |
64 | ||
65 | want, have = fmt.Sprintf("test_statsd_gauge:%f|g|#foo:bar,abc:123\n", value), "" | |
66 | by(t, 100*time.Millisecond, func() bool { | |
67 | have = buf.String() | |
68 | return want == have | |
69 | }, func() { | |
70 | reportc <- time.Now() | |
71 | }, fmt.Sprintf("want %q, have %q", want, have)) | |
72 | } | |
73 | ||
74 | func TestCallbackGauge(t *testing.T) { | |
75 | buf := &syncbuf{buf: &bytes.Buffer{}} | |
76 | reportc, scrapec := make(chan time.Time), make(chan time.Time) | |
77 | value := 55.55 | |
78 | cb := func() float64 { return value } | |
79 | NewCallbackGaugeTick(buf, "test_statsd_callback_gauge", reportc, scrapec, cb) | |
80 | ||
81 | scrapec <- time.Now() | |
82 | reportc <- time.Now() | |
83 | ||
84 | // Travis is annoying | |
85 | by(t, time.Second, func() bool { | |
86 | return buf.String() != "" | |
87 | }, func() { | |
88 | reportc <- time.Now() | |
89 | }, "buffer never got write+flush") | |
90 | ||
91 | want, have := fmt.Sprintf("test_statsd_callback_gauge:%f|g\n", value), "" | |
92 | by(t, 100*time.Millisecond, func() bool { | |
93 | have = buf.String() | |
94 | return strings.HasPrefix(have, want) // HasPrefix because we might get multiple writes | |
95 | }, func() { | |
96 | reportc <- time.Now() | |
97 | }, fmt.Sprintf("want %q, have %q", want, have)) | |
98 | } | |
99 | ||
100 | func TestHistogram(t *testing.T) { | |
101 | buf := &syncbuf{buf: &bytes.Buffer{}} | |
102 | reportc := make(chan time.Time) | |
103 | tags := []metrics.Field{} | |
104 | h := NewHistogramTick(buf, "test_statsd_histogram", reportc, tags) | |
105 | ||
106 | h.Observe(123) | |
107 | h.With(metrics.Field{"foo", "bar"}).Observe(456) | |
108 | ||
109 | want, have := "test_statsd_histogram:123|ms\ntest_statsd_histogram:456|ms|#foo:bar\n", "" | |
110 | by(t, 100*time.Millisecond, func() bool { | |
111 | have = buf.String() | |
112 | return want == have | |
113 | }, func() { | |
114 | reportc <- time.Now() | |
115 | }, fmt.Sprintf("want %q, have %q", want, have)) | |
116 | } | |
117 | ||
118 | func by(t *testing.T, d time.Duration, check func() bool, execute func(), msg string) { | |
119 | deadline := time.Now().Add(d) | |
120 | for !check() { | |
121 | if time.Now().After(deadline) { | |
122 | t.Fatal(msg) | |
123 | } | |
124 | execute() | |
125 | } | |
126 | } | |
127 | ||
128 | type syncbuf struct { | |
129 | mtx sync.Mutex | |
130 | buf *bytes.Buffer | |
131 | } | |
132 | ||
133 | func (s *syncbuf) Write(p []byte) (int, error) { | |
134 | s.mtx.Lock() | |
135 | defer s.mtx.Unlock() | |
136 | return s.buf.Write(p) | |
137 | } | |
138 | ||
139 | func (s *syncbuf) String() string { | |
140 | s.mtx.Lock() | |
141 | defer s.mtx.Unlock() | |
142 | return s.buf.String() | |
143 | } | |
144 | ||
145 | func (s *syncbuf) Reset() { | |
146 | s.mtx.Lock() | |
147 | defer s.mtx.Unlock() | |
148 | s.buf.Reset() | |
149 | } |
18 | 18 | import ( |
19 | 19 | "expvar" |
20 | 20 | "fmt" |
21 | "sort" | |
21 | 22 | "strconv" |
22 | 23 | "sync" |
23 | 24 | "time" |
28 | 29 | ) |
29 | 30 | |
30 | 31 | type counter struct { |
31 | v *expvar.Int | |
32 | name string | |
33 | v *expvar.Int | |
32 | 34 | } |
33 | 35 | |
34 | 36 | // NewCounter returns a new Counter backed by an expvar with the given name. |
35 | 37 | // Fields are ignored. |
36 | 38 | func NewCounter(name string) metrics.Counter { |
37 | return &counter{expvar.NewInt(name)} | |
39 | return &counter{ | |
40 | name: name, | |
41 | v: expvar.NewInt(name), | |
42 | } | |
38 | 43 | } |
39 | 44 | |
45 | func (c *counter) Name() string { return c.name } | |
40 | 46 | func (c *counter) With(metrics.Field) metrics.Counter { return c } |
41 | 47 | func (c *counter) Add(delta uint64) { c.v.Add(int64(delta)) } |
42 | 48 | |
43 | 49 | type gauge struct { |
44 | v *expvar.Float | |
50 | name string | |
51 | v *expvar.Float | |
45 | 52 | } |
46 | 53 | |
47 | 54 | // NewGauge returns a new Gauge backed by an expvar with the given name. It |
48 | 55 | // should be updated manually; for a callback-based approach, see |
49 | 56 | // PublishCallbackGauge. Fields are ignored. |
50 | 57 | func NewGauge(name string) metrics.Gauge { |
51 | return &gauge{expvar.NewFloat(name)} | |
58 | return &gauge{ | |
59 | name: name, | |
60 | v: expvar.NewFloat(name), | |
61 | } | |
52 | 62 | } |
53 | 63 | |
64 | func (g *gauge) Name() string { return g.name } | |
54 | 65 | func (g *gauge) With(metrics.Field) metrics.Gauge { return g } |
55 | ||
56 | func (g *gauge) Add(delta float64) { g.v.Add(delta) } | |
57 | ||
58 | func (g *gauge) Set(value float64) { g.v.Set(value) } | |
66 | func (g *gauge) Add(delta float64) { g.v.Add(delta) } | |
67 | func (g *gauge) Set(value float64) { g.v.Set(value) } | |
68 | func (g *gauge) Get() float64 { return mustParseFloat64(g.v.String()) } | |
59 | 69 | |
60 | 70 | // PublishCallbackGauge publishes a Gauge as an expvar with the given name, |
61 | 71 | // whose value is determined at collect time by the passed callback function. |
100 | 110 | return h |
101 | 111 | } |
102 | 112 | |
113 | func (h *histogram) Name() string { return h.name } | |
103 | 114 | func (h *histogram) With(metrics.Field) metrics.Histogram { return h } |
104 | 115 | |
105 | 116 | func (h *histogram) Observe(value int64) { |
116 | 127 | } |
117 | 128 | } |
118 | 129 | |
130 | func (h *histogram) Distribution() ([]metrics.Bucket, []metrics.Quantile) { | |
131 | bars := h.hist.Merge().Distribution() | |
132 | buckets := make([]metrics.Bucket, len(bars)) | |
133 | for i, bar := range bars { | |
134 | buckets[i] = metrics.Bucket{ | |
135 | From: bar.From, | |
136 | To: bar.To, | |
137 | Count: bar.Count, | |
138 | } | |
139 | } | |
140 | quantiles := make([]metrics.Quantile, 0, len(h.gauges)) | |
141 | for quantile, gauge := range h.gauges { | |
142 | quantiles = append(quantiles, metrics.Quantile{ | |
143 | Quantile: quantile, | |
144 | Value: int64(gauge.Get()), | |
145 | }) | |
146 | } | |
147 | sort.Sort(quantileSlice(quantiles)) | |
148 | return buckets, quantiles | |
149 | } | |
150 | ||
119 | 151 | func (h *histogram) rotateLoop(d time.Duration) { |
120 | 152 | for range time.Tick(d) { |
121 | 153 | h.mu.Lock() |
123 | 155 | h.mu.Unlock() |
124 | 156 | } |
125 | 157 | } |
158 | ||
159 | func mustParseFloat64(s string) float64 { | |
160 | f, err := strconv.ParseFloat(s, 64) | |
161 | if err != nil { | |
162 | panic(err) | |
163 | } | |
164 | return f | |
165 | } | |
166 | ||
167 | type quantileSlice []metrics.Quantile | |
168 | ||
169 | func (a quantileSlice) Len() int { return len(a) } | |
170 | func (a quantileSlice) Less(i, j int) bool { return a[i].Quantile < a[j].Quantile } | |
171 | func (a quantileSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] } |
11 | 11 | |
12 | 12 | func TestHistogramQuantiles(t *testing.T) { |
13 | 13 | var ( |
14 | name = "test_histogram" | |
14 | name = "test_histogram_quantiles" | |
15 | 15 | quantiles = []int{50, 90, 95, 99} |
16 | 16 | h = expvar.NewHistogram(name, 0, 100, 3, quantiles...).With(metrics.Field{Key: "ignored", Value: "field"}) |
17 | 17 | ) |
8 | 8 | // between measurements of a counter over intervals of time, an aggregation |
9 | 9 | // layer can derive rates, acceleration, etc. |
10 | 10 | type Counter interface { |
11 | Name() string | |
11 | 12 | With(Field) Counter |
12 | 13 | Add(delta uint64) |
13 | 14 | } |
15 | 16 | // Gauge captures instantaneous measurements of something using signed, 64-bit |
16 | 17 | // floats. The value does not need to be monotonic. |
17 | 18 | type Gauge interface { |
19 | Name() string | |
18 | 20 | With(Field) Gauge |
19 | 21 | Set(value float64) |
20 | 22 | Add(delta float64) |
23 | Get() float64 | |
21 | 24 | } |
22 | 25 | |
23 | 26 | // Histogram tracks the distribution of a stream of values (e.g. the number of |
24 | 27 | // milliseconds it takes to handle requests). Implementations may choose to |
25 | 28 | // add gauges for values at meaningful quantiles. |
26 | 29 | type Histogram interface { |
30 | Name() string | |
27 | 31 | With(Field) Histogram |
28 | 32 | Observe(value int64) |
33 | Distribution() ([]Bucket, []Quantile) | |
29 | 34 | } |
30 | 35 | |
31 | 36 | // Field is a key/value pair associated with an observation for a specific |
34 | 39 | Key string |
35 | 40 | Value string |
36 | 41 | } |
42 | ||
43 | // Bucket is a range in a histogram which aggregates observations. | |
44 | type Bucket struct { | |
45 | From int64 | |
46 | To int64 | |
47 | Count int64 | |
48 | } | |
49 | ||
50 | // Quantile is a pair of quantile (0..100) and its observed maximum value. | |
51 | type Quantile struct { | |
52 | Quantile int // 0..100 | |
53 | Value int64 | |
54 | } |
0 | 0 | package metrics |
1 | 1 | |
2 | type multiCounter []Counter | |
2 | type multiCounter struct { | |
3 | name string | |
4 | a []Counter | |
5 | } | |
3 | 6 | |
4 | 7 | // NewMultiCounter returns a wrapper around multiple Counters. |
5 | func NewMultiCounter(counters ...Counter) Counter { | |
6 | c := make(multiCounter, 0, len(counters)) | |
7 | return append(c, counters...) | |
8 | func NewMultiCounter(name string, counters ...Counter) Counter { | |
9 | return &multiCounter{ | |
10 | name: name, | |
11 | a: counters, | |
12 | } | |
8 | 13 | } |
9 | 14 | |
15 | func (c multiCounter) Name() string { return c.name } | |
16 | ||
10 | 17 | func (c multiCounter) With(f Field) Counter { |
11 | next := make(multiCounter, len(c)) | |
12 | for i, counter := range c { | |
13 | next[i] = counter.With(f) | |
18 | next := &multiCounter{ | |
19 | name: c.name, | |
20 | a: make([]Counter, len(c.a)), | |
21 | } | |
22 | for i, counter := range c.a { | |
23 | next.a[i] = counter.With(f) | |
14 | 24 | } |
15 | 25 | return next |
16 | 26 | } |
17 | 27 | |
18 | 28 | func (c multiCounter) Add(delta uint64) { |
19 | for _, counter := range c { | |
29 | for _, counter := range c.a { | |
20 | 30 | counter.Add(delta) |
21 | 31 | } |
22 | 32 | } |
23 | 33 | |
24 | type multiGauge []Gauge | |
34 | type multiGauge struct { | |
35 | name string | |
36 | a []Gauge | |
37 | } | |
38 | ||
39 | func (g multiGauge) Name() string { return g.name } | |
25 | 40 | |
26 | 41 | // NewMultiGauge returns a wrapper around multiple Gauges. |
27 | func NewMultiGauge(gauges ...Gauge) Gauge { | |
28 | g := make(multiGauge, 0, len(gauges)) | |
29 | return append(g, gauges...) | |
42 | func NewMultiGauge(name string, gauges ...Gauge) Gauge { | |
43 | return &multiGauge{ | |
44 | name: name, | |
45 | a: gauges, | |
46 | } | |
30 | 47 | } |
31 | 48 | |
32 | 49 | func (g multiGauge) With(f Field) Gauge { |
33 | next := make(multiGauge, len(g)) | |
34 | for i, gauge := range g { | |
35 | next[i] = gauge.With(f) | |
50 | next := &multiGauge{ | |
51 | name: g.name, | |
52 | a: make([]Gauge, len(g.a)), | |
53 | } | |
54 | for i, gauge := range g.a { | |
55 | next.a[i] = gauge.With(f) | |
36 | 56 | } |
37 | 57 | return next |
38 | 58 | } |
39 | 59 | |
40 | 60 | func (g multiGauge) Set(value float64) { |
41 | for _, gauge := range g { | |
61 | for _, gauge := range g.a { | |
42 | 62 | gauge.Set(value) |
43 | 63 | } |
44 | 64 | } |
45 | 65 | |
46 | 66 | func (g multiGauge) Add(delta float64) { |
47 | for _, gauge := range g { | |
67 | for _, gauge := range g.a { | |
48 | 68 | gauge.Add(delta) |
49 | 69 | } |
50 | 70 | } |
51 | 71 | |
52 | type multiHistogram []Histogram | |
72 | func (g multiGauge) Get() float64 { | |
73 | panic("cannot call Get on a MultiGauge") | |
74 | } | |
75 | ||
76 | type multiHistogram struct { | |
77 | name string | |
78 | a []Histogram | |
79 | } | |
53 | 80 | |
54 | 81 | // NewMultiHistogram returns a wrapper around multiple Histograms. |
55 | func NewMultiHistogram(histograms ...Histogram) Histogram { | |
56 | h := make(multiHistogram, 0, len(histograms)) | |
57 | return append(h, histograms...) | |
82 | func NewMultiHistogram(name string, histograms ...Histogram) Histogram { | |
83 | return &multiHistogram{ | |
84 | name: name, | |
85 | a: histograms, | |
86 | } | |
58 | 87 | } |
59 | 88 | |
89 | func (h multiHistogram) Name() string { return h.name } | |
90 | ||
60 | 91 | func (h multiHistogram) With(f Field) Histogram { |
61 | next := make(multiHistogram, len(h)) | |
62 | for i, histogram := range h { | |
63 | next[i] = histogram.With(f) | |
92 | next := &multiHistogram{ | |
93 | name: h.name, | |
94 | a: make([]Histogram, len(h.a)), | |
95 | } | |
96 | for i, histogram := range h.a { | |
97 | next.a[i] = histogram.With(f) | |
64 | 98 | } |
65 | 99 | return next |
66 | 100 | } |
67 | 101 | |
68 | 102 | func (h multiHistogram) Observe(value int64) { |
69 | for _, histogram := range h { | |
103 | for _, histogram := range h.a { | |
70 | 104 | histogram.Observe(value) |
71 | 105 | } |
72 | 106 | } |
107 | ||
108 | func (h multiHistogram) Distribution() ([]Bucket, []Quantile) { | |
109 | // TODO(pb): there may be a way to do this | |
110 | panic("cannot call Distribution on a MultiHistogram") | |
111 | } |
4 | 4 | "fmt" |
5 | 5 | "io/ioutil" |
6 | 6 | "math" |
7 | "math/rand" | |
8 | 7 | "net/http" |
9 | 8 | "net/http/httptest" |
10 | 9 | "regexp" |
17 | 16 | "github.com/go-kit/kit/metrics" |
18 | 17 | "github.com/go-kit/kit/metrics/expvar" |
19 | 18 | "github.com/go-kit/kit/metrics/prometheus" |
19 | "github.com/go-kit/kit/metrics/teststat" | |
20 | 20 | ) |
21 | 21 | |
22 | 22 | func TestMultiWith(t *testing.T) { |
23 | 23 | c := metrics.NewMultiCounter( |
24 | "multifoo", | |
24 | 25 | expvar.NewCounter("foo"), |
25 | 26 | prometheus.NewCounter(stdprometheus.CounterOpts{ |
26 | 27 | Namespace: "test", |
46 | 47 | |
47 | 48 | func TestMultiCounter(t *testing.T) { |
48 | 49 | metrics.NewMultiCounter( |
50 | "multialpha", | |
49 | 51 | expvar.NewCounter("alpha"), |
50 | 52 | prometheus.NewCounter(stdprometheus.CounterOpts{ |
51 | 53 | Namespace: "test", |
70 | 72 | |
71 | 73 | func TestMultiGauge(t *testing.T) { |
72 | 74 | g := metrics.NewMultiGauge( |
75 | "multidelta", | |
73 | 76 | expvar.NewGauge("delta"), |
74 | 77 | prometheus.NewGauge(stdprometheus.GaugeOpts{ |
75 | 78 | Namespace: "test", |
110 | 113 | func TestMultiHistogram(t *testing.T) { |
111 | 114 | quantiles := []int{50, 90, 99} |
112 | 115 | h := metrics.NewMultiHistogram( |
116 | "multiomicron", | |
113 | 117 | expvar.NewHistogram("omicron", 0, 100, 3, quantiles...), |
114 | 118 | prometheus.NewSummary(stdprometheus.SummaryOpts{ |
115 | 119 | Namespace: "test", |
120 | 124 | ) |
121 | 125 | |
122 | 126 | const seed, mean, stdev int64 = 123, 50, 10 |
123 | populateNormalHistogram(t, h, seed, mean, stdev) | |
127 | teststat.PopulateNormalHistogram(t, h, seed, mean, stdev) | |
124 | 128 | assertExpvarNormalHistogram(t, "omicron", mean, stdev, quantiles) |
125 | 129 | assertPrometheusNormalHistogram(t, `test_multi_histogram_nu`, mean, stdev) |
126 | } | |
127 | ||
128 | func populateNormalHistogram(t *testing.T, h metrics.Histogram, seed int64, mean, stdev int64) { | |
129 | rand.Seed(seed) | |
130 | for i := 0; i < 1234; i++ { | |
131 | sample := int64(rand.NormFloat64()*float64(stdev) + float64(mean)) | |
132 | h.Observe(sample) | |
133 | } | |
134 | 130 | } |
135 | 131 | |
136 | 132 | func assertExpvarNormalHistogram(t *testing.T, metricName string, mean, stdev int64, quantiles []int) { |
0 | package metrics | |
1 | ||
2 | import ( | |
3 | "fmt" | |
4 | "io" | |
5 | "text/tabwriter" | |
6 | ) | |
7 | ||
8 | const ( | |
9 | bs = "####################################################################################################" | |
10 | bsz = float64(len(bs)) | |
11 | ) | |
12 | ||
13 | // PrintDistribution writes a human-readable graph of the distribution to the | |
14 | // passed writer. | |
15 | func PrintDistribution(w io.Writer, h Histogram) { | |
16 | buckets, quantiles := h.Distribution() | |
17 | ||
18 | fmt.Fprintf(w, "name: %v\n", h.Name()) | |
19 | fmt.Fprintf(w, "quantiles: %v\n", quantiles) | |
20 | ||
21 | var total float64 | |
22 | for _, bucket := range buckets { | |
23 | total += float64(bucket.Count) | |
24 | } | |
25 | ||
26 | tw := tabwriter.NewWriter(w, 0, 2, 2, ' ', 0) | |
27 | fmt.Fprintf(tw, "From\tTo\tCount\tProb\tBar\n") | |
28 | ||
29 | axis := "|" | |
30 | for _, bucket := range buckets { | |
31 | if bucket.Count > 0 { | |
32 | p := float64(bucket.Count) / total | |
33 | fmt.Fprintf(tw, "%d\t%d\t%d\t%.4f\t%s%s\n", bucket.From, bucket.To, bucket.Count, p, axis, bs[:int(p*bsz)]) | |
34 | axis = "|" | |
35 | } else { | |
36 | axis = ":" // show that some bars were skipped | |
37 | } | |
38 | } | |
39 | ||
40 | tw.Flush() | |
41 | } |
0 | package metrics_test | |
1 | ||
2 | import ( | |
3 | "bytes" | |
4 | "testing" | |
5 | ||
6 | "math" | |
7 | ||
8 | "github.com/go-kit/kit/metrics" | |
9 | "github.com/go-kit/kit/metrics/expvar" | |
10 | "github.com/go-kit/kit/metrics/teststat" | |
11 | ) | |
12 | ||
13 | func TestPrintDistribution(t *testing.T) { | |
14 | var ( | |
15 | quantiles = []int{50, 90, 95, 99} | |
16 | h = expvar.NewHistogram("test_print_distribution", 0, 100, 3, quantiles...) | |
17 | seed = int64(555) | |
18 | mean = int64(5) | |
19 | stdev = int64(1) | |
20 | ) | |
21 | teststat.PopulateNormalHistogram(t, h, seed, mean, stdev) | |
22 | ||
23 | var buf bytes.Buffer | |
24 | metrics.PrintDistribution(&buf, h) | |
25 | t.Logf("\n%s\n", buf.String()) | |
26 | ||
27 | // Count the number of bar chart characters. | |
28 | // We should have ca. 100 in any distribution with a small-enough stdev. | |
29 | ||
30 | var n int | |
31 | for _, r := range buf.String() { | |
32 | if r == '#' { | |
33 | n++ | |
34 | } | |
35 | } | |
36 | if want, have, tol := 100, n, 5; int(math.Abs(float64(want-have))) > tol { | |
37 | t.Errorf("want %d, have %d (tolerance %d)", want, have, tol) | |
38 | } | |
39 | } |
15 | 15 | |
16 | 16 | type prometheusCounter struct { |
17 | 17 | *prometheus.CounterVec |
18 | name string | |
18 | 19 | Pairs map[string]string |
19 | 20 | } |
20 | 21 | |
29 | 30 | } |
30 | 31 | return prometheusCounter{ |
31 | 32 | CounterVec: m, |
33 | name: opts.Name, | |
32 | 34 | Pairs: p, |
33 | 35 | } |
34 | 36 | } |
37 | ||
38 | func (c prometheusCounter) Name() string { return c.name } | |
35 | 39 | |
36 | 40 | func (c prometheusCounter) With(f metrics.Field) metrics.Counter { |
37 | 41 | return prometheusCounter{ |
38 | 42 | CounterVec: c.CounterVec, |
43 | name: c.name, | |
39 | 44 | Pairs: merge(c.Pairs, f), |
40 | 45 | } |
41 | 46 | } |
46 | 51 | |
47 | 52 | type prometheusGauge struct { |
48 | 53 | *prometheus.GaugeVec |
54 | name string | |
49 | 55 | Pairs map[string]string |
50 | 56 | } |
51 | 57 | |
56 | 62 | prometheus.MustRegister(m) |
57 | 63 | return prometheusGauge{ |
58 | 64 | GaugeVec: m, |
65 | name: opts.Name, | |
59 | 66 | Pairs: pairsFrom(fieldKeys), |
60 | 67 | } |
61 | 68 | } |
69 | ||
70 | func (g prometheusGauge) Name() string { return g.name } | |
62 | 71 | |
63 | 72 | func (g prometheusGauge) With(f metrics.Field) metrics.Gauge { |
64 | 73 | return prometheusGauge{ |
65 | 74 | GaugeVec: g.GaugeVec, |
75 | name: g.name, | |
66 | 76 | Pairs: merge(g.Pairs, f), |
67 | 77 | } |
68 | 78 | } |
73 | 83 | |
74 | 84 | func (g prometheusGauge) Add(delta float64) { |
75 | 85 | g.GaugeVec.With(prometheus.Labels(g.Pairs)).Add(delta) |
86 | } | |
87 | ||
88 | func (g prometheusGauge) Get() float64 { | |
89 | // TODO(pb): see https://github.com/prometheus/client_golang/issues/58 | |
90 | return 0.0 | |
76 | 91 | } |
77 | 92 | |
78 | 93 | // RegisterCallbackGauge registers a Gauge with Prometheus whose value is |
85 | 100 | |
86 | 101 | type prometheusSummary struct { |
87 | 102 | *prometheus.SummaryVec |
103 | name string | |
88 | 104 | Pairs map[string]string |
89 | 105 | } |
90 | 106 | |
98 | 114 | prometheus.MustRegister(m) |
99 | 115 | return prometheusSummary{ |
100 | 116 | SummaryVec: m, |
117 | name: opts.Name, | |
101 | 118 | Pairs: pairsFrom(fieldKeys), |
102 | 119 | } |
103 | 120 | } |
121 | ||
122 | func (s prometheusSummary) Name() string { return s.name } | |
104 | 123 | |
105 | 124 | func (s prometheusSummary) With(f metrics.Field) metrics.Histogram { |
106 | 125 | return prometheusSummary{ |
107 | 126 | SummaryVec: s.SummaryVec, |
127 | name: s.name, | |
108 | 128 | Pairs: merge(s.Pairs, f), |
109 | 129 | } |
110 | 130 | } |
113 | 133 | s.SummaryVec.With(prometheus.Labels(s.Pairs)).Observe(float64(value)) |
114 | 134 | } |
115 | 135 | |
136 | func (s prometheusSummary) Distribution() ([]metrics.Bucket, []metrics.Quantile) { | |
137 | // TODO(pb): see https://github.com/prometheus/client_golang/issues/58 | |
138 | return []metrics.Bucket{}, []metrics.Quantile{} | |
139 | } | |
140 | ||
116 | 141 | type prometheusHistogram struct { |
117 | 142 | *prometheus.HistogramVec |
143 | name string | |
118 | 144 | Pairs map[string]string |
119 | 145 | } |
120 | 146 | |
128 | 154 | prometheus.MustRegister(m) |
129 | 155 | return prometheusHistogram{ |
130 | 156 | HistogramVec: m, |
157 | name: opts.Name, | |
131 | 158 | Pairs: pairsFrom(fieldKeys), |
132 | 159 | } |
133 | 160 | } |
161 | ||
162 | func (h prometheusHistogram) Name() string { return h.name } | |
134 | 163 | |
135 | 164 | func (h prometheusHistogram) With(f metrics.Field) metrics.Histogram { |
136 | 165 | return prometheusHistogram{ |
137 | 166 | HistogramVec: h.HistogramVec, |
167 | name: h.name, | |
138 | 168 | Pairs: merge(h.Pairs, f), |
139 | 169 | } |
140 | 170 | } |
141 | 171 | |
142 | 172 | func (h prometheusHistogram) Observe(value int64) { |
143 | 173 | h.HistogramVec.With(prometheus.Labels(h.Pairs)).Observe(float64(value)) |
174 | } | |
175 | ||
176 | func (h prometheusHistogram) Distribution() ([]metrics.Bucket, []metrics.Quantile) { | |
177 | // TODO(pb): see https://github.com/prometheus/client_golang/issues/58 | |
178 | return []metrics.Bucket{}, []metrics.Quantile{} | |
144 | 179 | } |
145 | 180 | |
146 | 181 | func pairsFrom(fieldKeys []string) map[string]string { |
4 | 4 | |
5 | 5 | "github.com/go-kit/kit/metrics" |
6 | 6 | "github.com/go-kit/kit/metrics/expvar" |
7 | "github.com/go-kit/kit/metrics/teststat" | |
7 | 8 | ) |
8 | 9 | |
9 | 10 | func TestScaledHistogram(t *testing.T) { |
18 | 19 | h = metrics.NewScaledHistogram(h, scale) |
19 | 20 | h = h.With(metrics.Field{Key: "a", Value: "b"}) |
20 | 21 | |
21 | const seed, mean, stdev = 333, 500, 100 // input values | |
22 | populateNormalHistogram(t, h, seed, mean, stdev) // will be scaled down | |
22 | const seed, mean, stdev = 333, 500, 100 // input values | |
23 | teststat.PopulateNormalHistogram(t, h, seed, mean, stdev) // will be scaled down | |
23 | 24 | assertExpvarNormalHistogram(t, metricName, mean/scale, stdev/scale, quantiles) |
24 | 25 | } |
16 | 16 | "fmt" |
17 | 17 | "io" |
18 | 18 | "log" |
19 | "math" | |
19 | 20 | "time" |
21 | ||
22 | "sync/atomic" | |
20 | 23 | |
21 | 24 | "github.com/go-kit/kit/metrics" |
22 | 25 | ) |
26 | 29 | |
27 | 30 | const maxBufferSize = 1400 // bytes |
28 | 31 | |
29 | type statsdCounter chan string | |
32 | type statsdCounter struct { | |
33 | key string | |
34 | c chan string | |
35 | } | |
30 | 36 | |
31 | 37 | // NewCounter returns a Counter that emits observations in the statsd protocol |
32 | 38 | // to the passed writer. Observations are buffered for the report interval or |
35 | 41 | // |
36 | 42 | // TODO: support for sampling. |
37 | 43 | func NewCounter(w io.Writer, key string, reportInterval time.Duration) metrics.Counter { |
38 | c := make(chan string) | |
39 | go fwd(w, key, reportInterval, c) | |
40 | return statsdCounter(c) | |
41 | } | |
42 | ||
43 | func (c statsdCounter) With(metrics.Field) metrics.Counter { return c } | |
44 | ||
45 | func (c statsdCounter) Add(delta uint64) { c <- fmt.Sprintf("%d|c", delta) } | |
46 | ||
47 | type statsdGauge chan string | |
44 | return NewCounterTick(w, key, time.Tick(reportInterval)) | |
45 | } | |
46 | ||
47 | // NewCounterTick is the same as NewCounter, but allows the user to pass in a | |
48 | // ticker channel instead of invoking time.Tick. | |
49 | func NewCounterTick(w io.Writer, key string, reportTicker <-chan time.Time) metrics.Counter { | |
50 | c := &statsdCounter{ | |
51 | key: key, | |
52 | c: make(chan string), | |
53 | } | |
54 | go fwd(w, key, reportTicker, c.c) | |
55 | return c | |
56 | } | |
57 | ||
58 | func (c *statsdCounter) Name() string { return c.key } | |
59 | ||
60 | func (c *statsdCounter) With(metrics.Field) metrics.Counter { return c } | |
61 | ||
62 | func (c *statsdCounter) Add(delta uint64) { c.c <- fmt.Sprintf("%d|c", delta) } | |
63 | ||
64 | type statsdGauge struct { | |
65 | key string | |
66 | lastValue uint64 // math.Float64frombits | |
67 | g chan string | |
68 | } | |
48 | 69 | |
49 | 70 | // NewGauge returns a Gauge that emits values in the statsd protocol to the |
50 | 71 | // passed writer. Values are buffered for the report interval or until the |
53 | 74 | // |
54 | 75 | // TODO: support for sampling. |
55 | 76 | func NewGauge(w io.Writer, key string, reportInterval time.Duration) metrics.Gauge { |
56 | g := make(chan string) | |
57 | go fwd(w, key, reportInterval, g) | |
58 | return statsdGauge(g) | |
59 | } | |
60 | ||
61 | func (g statsdGauge) With(metrics.Field) metrics.Gauge { return g } | |
62 | ||
63 | func (g statsdGauge) Add(delta float64) { | |
77 | return NewGaugeTick(w, key, time.Tick(reportInterval)) | |
78 | } | |
79 | ||
80 | // NewGaugeTick is the same as NewGauge, but allows the user to pass in a ticker | |
81 | // channel instead of invoking time.Tick. | |
82 | func NewGaugeTick(w io.Writer, key string, reportTicker <-chan time.Time) metrics.Gauge { | |
83 | g := &statsdGauge{ | |
84 | key: key, | |
85 | g: make(chan string), | |
86 | } | |
87 | go fwd(w, key, reportTicker, g.g) | |
88 | return g | |
89 | } | |
90 | ||
91 | func (g *statsdGauge) Name() string { return g.key } | |
92 | ||
93 | func (g *statsdGauge) With(metrics.Field) metrics.Gauge { return g } | |
94 | ||
95 | func (g *statsdGauge) Add(delta float64) { | |
64 | 96 | // https://github.com/etsy/statsd/blob/master/docs/metric_types.md#gauges |
65 | 97 | sign := "+" |
66 | 98 | if delta < 0 { |
67 | 99 | sign, delta = "-", -delta |
68 | 100 | } |
69 | g <- fmt.Sprintf("%s%f|g", sign, delta) | |
70 | } | |
71 | ||
72 | func (g statsdGauge) Set(value float64) { | |
73 | g <- fmt.Sprintf("%f|g", value) | |
101 | g.g <- fmt.Sprintf("%s%f|g", sign, delta) | |
102 | } | |
103 | ||
104 | func (g *statsdGauge) Set(value float64) { | |
105 | atomic.StoreUint64(&g.lastValue, math.Float64bits(value)) | |
106 | g.g <- fmt.Sprintf("%f|g", value) | |
107 | } | |
108 | ||
109 | func (g *statsdGauge) Get() float64 { | |
110 | return math.Float64frombits(atomic.LoadUint64(&g.lastValue)) | |
74 | 111 | } |
75 | 112 | |
76 | 113 | // NewCallbackGauge emits values in the statsd protocol to the passed writer. |
80 | 117 | // same. The callback determines the value, and fields are ignored, so |
81 | 118 | // NewCallbackGauge returns nothing. |
82 | 119 | func NewCallbackGauge(w io.Writer, key string, reportInterval, scrapeInterval time.Duration, callback func() float64) { |
83 | go fwd(w, key, reportInterval, emitEvery(scrapeInterval, callback)) | |
84 | } | |
85 | ||
86 | func emitEvery(d time.Duration, callback func() float64) <-chan string { | |
120 | NewCallbackGaugeTick(w, key, time.Tick(reportInterval), time.Tick(scrapeInterval), callback) | |
121 | } | |
122 | ||
123 | // NewCallbackGaugeTick is the same as NewCallbackGauge, but allows the user to | |
124 | // pass in ticker channels instead of durations to control report and scrape | |
125 | // intervals. | |
126 | func NewCallbackGaugeTick(w io.Writer, key string, reportTicker, scrapeTicker <-chan time.Time, callback func() float64) { | |
127 | go fwd(w, key, reportTicker, emitEvery(scrapeTicker, callback)) | |
128 | } | |
129 | ||
130 | func emitEvery(emitTicker <-chan time.Time, callback func() float64) <-chan string { | |
87 | 131 | c := make(chan string) |
88 | 132 | go func() { |
89 | for range tick(d) { | |
133 | for range emitTicker { | |
90 | 134 | c <- fmt.Sprintf("%f|g", callback()) |
91 | 135 | } |
92 | 136 | }() |
93 | 137 | return c |
94 | 138 | } |
95 | 139 | |
96 | type statsdHistogram chan string | |
140 | type statsdHistogram struct { | |
141 | key string | |
142 | h chan string | |
143 | } | |
97 | 144 | |
98 | 145 | // NewHistogram returns a Histogram that emits observations in the statsd |
99 | 146 | // protocol to the passed writer. Observations are buffered for the reporting |
113 | 160 | // |
114 | 161 | // TODO: support for sampling. |
115 | 162 | func NewHistogram(w io.Writer, key string, reportInterval time.Duration) metrics.Histogram { |
116 | h := make(chan string) | |
117 | go fwd(w, key, reportInterval, h) | |
118 | return statsdHistogram(h) | |
119 | } | |
120 | ||
121 | func (h statsdHistogram) With(metrics.Field) metrics.Histogram { return h } | |
122 | ||
123 | func (h statsdHistogram) Observe(value int64) { | |
124 | h <- fmt.Sprintf("%d|ms", value) | |
125 | } | |
126 | ||
127 | var tick = time.Tick | |
128 | ||
129 | func fwd(w io.Writer, key string, reportInterval time.Duration, c <-chan string) { | |
163 | return NewHistogramTick(w, key, time.Tick(reportInterval)) | |
164 | } | |
165 | ||
166 | // NewHistogramTick is the same as NewHistogram, but allows the user to pass a | |
167 | // ticker channel instead of invoking time.Tick. | |
168 | func NewHistogramTick(w io.Writer, key string, reportTicker <-chan time.Time) metrics.Histogram { | |
169 | h := &statsdHistogram{ | |
170 | key: key, | |
171 | h: make(chan string), | |
172 | } | |
173 | go fwd(w, key, reportTicker, h.h) | |
174 | return h | |
175 | } | |
176 | ||
177 | func (h *statsdHistogram) Name() string { return h.key } | |
178 | ||
179 | func (h *statsdHistogram) With(metrics.Field) metrics.Histogram { return h } | |
180 | ||
181 | func (h *statsdHistogram) Observe(value int64) { | |
182 | h.h <- fmt.Sprintf("%d|ms", value) | |
183 | } | |
184 | ||
185 | func (h *statsdHistogram) Distribution() ([]metrics.Bucket, []metrics.Quantile) { | |
186 | // TODO(pb): no way to do this without introducing e.g. codahale/hdrhistogram | |
187 | return []metrics.Bucket{}, []metrics.Quantile{} | |
188 | } | |
189 | ||
190 | func fwd(w io.Writer, key string, reportTicker <-chan time.Time, c <-chan string) { | |
130 | 191 | buf := &bytes.Buffer{} |
131 | tick := tick(reportInterval) | |
132 | 192 | for { |
133 | 193 | select { |
134 | 194 | case s := <-c: |
137 | 197 | flush(w, buf) |
138 | 198 | } |
139 | 199 | |
140 | case <-tick: | |
200 | case <-reportTicker: | |
141 | 201 | flush(w, buf) |
142 | 202 | } |
143 | 203 | } |
0 | 0 | package statsd |
1 | ||
2 | // In package metrics so we can stub tick. | |
3 | 1 | |
4 | 2 | import ( |
5 | 3 | "bytes" |
6 | 4 | "fmt" |
7 | "runtime" | |
8 | 5 | "strings" |
6 | "sync" | |
9 | 7 | "testing" |
10 | 8 | "time" |
11 | 9 | ) |
12 | 10 | |
13 | 11 | func TestCounter(t *testing.T) { |
14 | ch := make(chan time.Time) | |
15 | tick = func(time.Duration) <-chan time.Time { return ch } | |
16 | defer func() { tick = time.Tick }() | |
17 | ||
18 | buf := &bytes.Buffer{} | |
19 | c := NewCounter(buf, "test_statsd_counter", time.Second) | |
12 | buf := &syncbuf{buf: &bytes.Buffer{}} | |
13 | reportc := make(chan time.Time) | |
14 | c := NewCounterTick(buf, "test_statsd_counter", reportc) | |
20 | 15 | |
21 | 16 | c.Add(1) |
22 | 17 | c.Add(2) |
23 | ch <- time.Now() | |
24 | 18 | |
25 | for i := 0; i < 10 && buf.Len() == 0; i++ { | |
26 | time.Sleep(time.Millisecond) | |
27 | } | |
28 | ||
29 | if want, have := "test_statsd_counter:1|c\ntest_statsd_counter:2|c\n", buf.String(); want != have { | |
30 | t.Errorf("want %q, have %q", want, have) | |
31 | } | |
19 | want, have := "test_statsd_counter:1|c\ntest_statsd_counter:2|c\n", "" | |
20 | by(t, 100*time.Millisecond, func() bool { | |
21 | have = buf.String() | |
22 | return want == have | |
23 | }, func() { | |
24 | reportc <- time.Now() | |
25 | }, fmt.Sprintf("want %q, have %q", want, have)) | |
32 | 26 | } |
33 | 27 | |
34 | 28 | func TestGauge(t *testing.T) { |
35 | ch := make(chan time.Time) | |
36 | tick = func(time.Duration) <-chan time.Time { return ch } | |
37 | defer func() { tick = time.Tick }() | |
38 | ||
39 | buf := &bytes.Buffer{} | |
40 | g := NewGauge(buf, "test_statsd_gauge", time.Second) | |
29 | buf := &syncbuf{buf: &bytes.Buffer{}} | |
30 | reportc := make(chan time.Time) | |
31 | g := NewGaugeTick(buf, "test_statsd_gauge", reportc) | |
41 | 32 | |
42 | 33 | delta := 1.0 |
43 | g.Add(delta) // send command | |
44 | runtime.Gosched() // yield to buffer write | |
45 | ch <- time.Now() // signal flush | |
46 | runtime.Gosched() // yield to flush | |
47 | if want, have := fmt.Sprintf("test_statsd_gauge:+%f|g\n", delta), buf.String(); want != have { | |
48 | t.Errorf("want %q, have %q", want, have) | |
49 | } | |
34 | g.Add(delta) | |
35 | ||
36 | want, have := fmt.Sprintf("test_statsd_gauge:+%f|g\n", delta), "" | |
37 | by(t, 100*time.Millisecond, func() bool { | |
38 | have = buf.String() | |
39 | return want == have | |
40 | }, func() { | |
41 | reportc <- time.Now() | |
42 | }, fmt.Sprintf("want %q, have %q", want, have)) | |
50 | 43 | |
51 | 44 | buf.Reset() |
52 | ||
53 | 45 | delta = -2.0 |
54 | 46 | g.Add(delta) |
55 | runtime.Gosched() | |
56 | ch <- time.Now() | |
57 | runtime.Gosched() | |
58 | if want, have := fmt.Sprintf("test_statsd_gauge:%f|g\n", delta), buf.String(); want != have { | |
59 | t.Errorf("want %q, have %q", want, have) | |
60 | } | |
47 | ||
48 | want, have = fmt.Sprintf("test_statsd_gauge:%f|g\n", delta), "" | |
49 | by(t, 100*time.Millisecond, func() bool { | |
50 | have = buf.String() | |
51 | return want == have | |
52 | }, func() { | |
53 | reportc <- time.Now() | |
54 | }, fmt.Sprintf("want %q, have %q", want, have)) | |
61 | 55 | |
62 | 56 | buf.Reset() |
63 | ||
64 | 57 | value := 3.0 |
65 | 58 | g.Set(value) |
66 | runtime.Gosched() | |
67 | ch <- time.Now() | |
68 | runtime.Gosched() | |
69 | if want, have := fmt.Sprintf("test_statsd_gauge:%f|g\n", value), buf.String(); want != have { | |
70 | t.Errorf("want %q, have %q", want, have) | |
71 | } | |
59 | ||
60 | want, have = fmt.Sprintf("test_statsd_gauge:%f|g\n", value), "" | |
61 | by(t, 100*time.Millisecond, func() bool { | |
62 | have = buf.String() | |
63 | return want == have | |
64 | }, func() { | |
65 | reportc <- time.Now() | |
66 | }, fmt.Sprintf("want %q, have %q", want, have)) | |
72 | 67 | } |
73 | 68 | |
74 | 69 | func TestCallbackGauge(t *testing.T) { |
75 | ch := make(chan time.Time) | |
76 | tick = func(time.Duration) <-chan time.Time { return ch } | |
77 | defer func() { tick = time.Tick }() | |
78 | ||
79 | buf := &bytes.Buffer{} | |
70 | buf := &syncbuf{buf: &bytes.Buffer{}} | |
71 | reportc, scrapec := make(chan time.Time), make(chan time.Time) | |
80 | 72 | value := 55.55 |
81 | 73 | cb := func() float64 { return value } |
82 | NewCallbackGauge(buf, "test_statsd_callback_gauge", time.Second, time.Nanosecond, cb) | |
74 | NewCallbackGaugeTick(buf, "test_statsd_callback_gauge", reportc, scrapec, cb) | |
83 | 75 | |
84 | ch <- time.Now() // signal emitter | |
85 | runtime.Gosched() // yield to emitter | |
86 | ch <- time.Now() // signal flush | |
87 | runtime.Gosched() // yield to flush | |
76 | scrapec <- time.Now() | |
77 | reportc <- time.Now() | |
88 | 78 | |
89 | 79 | // Travis is annoying |
90 | check := func() bool { return buf.String() != "" } | |
91 | execute := func() { ch <- time.Now(); runtime.Gosched(); time.Sleep(5 * time.Millisecond) } | |
92 | by(t, time.Second, check, execute, "buffer never got write+flush") | |
80 | by(t, time.Second, func() bool { | |
81 | return buf.String() != "" | |
82 | }, func() { | |
83 | reportc <- time.Now() | |
84 | }, "buffer never got write+flush") | |
93 | 85 | |
94 | if want, have := fmt.Sprintf("test_statsd_callback_gauge:%f|g\n", value), buf.String(); !strings.HasPrefix(have, want) { | |
95 | t.Errorf("want %q, have %q", want, have) | |
96 | } | |
86 | want, have := fmt.Sprintf("test_statsd_callback_gauge:%f|g\n", value), "" | |
87 | by(t, 100*time.Millisecond, func() bool { | |
88 | have = buf.String() | |
89 | return strings.HasPrefix(have, want) // HasPrefix because we might get multiple writes | |
90 | }, func() { | |
91 | reportc <- time.Now() | |
92 | }, fmt.Sprintf("want %q, have %q", want, have)) | |
97 | 93 | } |
98 | 94 | |
99 | 95 | func TestHistogram(t *testing.T) { |
100 | ch := make(chan time.Time) | |
101 | tick = func(time.Duration) <-chan time.Time { return ch } | |
102 | defer func() { tick = time.Tick }() | |
103 | ||
104 | buf := &bytes.Buffer{} | |
105 | h := NewHistogram(buf, "test_statsd_histogram", time.Second) | |
96 | buf := &syncbuf{buf: &bytes.Buffer{}} | |
97 | reportc := make(chan time.Time) | |
98 | h := NewHistogramTick(buf, "test_statsd_histogram", reportc) | |
106 | 99 | |
107 | 100 | h.Observe(123) |
108 | 101 | |
109 | runtime.Gosched() | |
110 | ch <- time.Now() | |
111 | runtime.Gosched() | |
112 | if want, have := "test_statsd_histogram:123|ms\n", buf.String(); want != have { | |
113 | t.Errorf("want %q, have %q", want, have) | |
114 | } | |
102 | want, have := "test_statsd_histogram:123|ms\n", "" | |
103 | by(t, 100*time.Millisecond, func() bool { | |
104 | have = buf.String() | |
105 | return want == have | |
106 | }, func() { | |
107 | reportc <- time.Now() | |
108 | }, fmt.Sprintf("want %q, have %q", want, have)) | |
115 | 109 | } |
116 | 110 | |
117 | 111 | func by(t *testing.T, d time.Duration, check func() bool, execute func(), msg string) { |
123 | 117 | execute() |
124 | 118 | } |
125 | 119 | } |
120 | ||
121 | type syncbuf struct { | |
122 | mtx sync.Mutex | |
123 | buf *bytes.Buffer | |
124 | } | |
125 | ||
126 | func (s *syncbuf) Write(p []byte) (int, error) { | |
127 | s.mtx.Lock() | |
128 | defer s.mtx.Unlock() | |
129 | return s.buf.Write(p) | |
130 | } | |
131 | ||
132 | func (s *syncbuf) String() string { | |
133 | s.mtx.Lock() | |
134 | defer s.mtx.Unlock() | |
135 | return s.buf.String() | |
136 | } | |
137 | ||
138 | func (s *syncbuf) Reset() { | |
139 | s.mtx.Lock() | |
140 | defer s.mtx.Unlock() | |
141 | s.buf.Reset() | |
142 | } |
14 | 14 | // PopulateNormalHistogram populates the Histogram with a normal distribution |
15 | 15 | // of observations. |
16 | 16 | func PopulateNormalHistogram(t *testing.T, h metrics.Histogram, seed int64, mean, stdev int64) { |
17 | rand.Seed(seed) | |
17 | r := rand.New(rand.NewSource(seed)) | |
18 | 18 | for i := 0; i < population; i++ { |
19 | sample := int64(rand.NormFloat64()*float64(stdev) + float64(mean)) | |
19 | sample := int64(r.NormFloat64()*float64(stdev) + float64(mean)) | |
20 | 20 | if sample < 0 { |
21 | 21 | sample = 0 |
22 | 22 | } |
20 | 20 | ) |
21 | 21 | |
22 | 22 | const seed, mean, stdev int64 = 321, 100, 20 |
23 | r := rand.New(rand.NewSource(seed)) | |
23 | 24 | |
24 | 25 | for i := 0; i < 4321; i++ { |
25 | sample := time.Duration(rand.NormFloat64()*float64(stdev)+float64(mean)) * time.Millisecond | |
26 | sample := time.Duration(r.NormFloat64()*float64(stdev)+float64(mean)) * time.Millisecond | |
26 | 27 | th.Observe(sample) |
27 | 28 | } |
28 | 29 |
15 | 15 | Bourgon). Once we reach a usable MVP, i.e. semantic version 1.0.0, I hope we'll |
16 | 16 | transition to a more community-driven governance model. |
17 | 17 | |
18 | For questions and free-form discussion, please use IRC or the mailing list. | |
18 | For questions and free-form discussion, please use | |
19 | 19 | |
20 | - Mailing list: [go-kit](https://groups.google.com/forum/#!forum/go-kit) | |
21 | - Slack: [gophers.slack.com](https://gophers.slack.com) **#go-kit** ([invite](https://gophersinvite.herokuapp.com/)) | |
22 |
4 | 4 | |
5 | 5 | [Dapper]: http://research.google.com/pubs/pub36356.html |
6 | 6 | [Zipkin]: https://blog.twitter.com/2012/distributed-systems-tracing-with-zipkin |
7 | [Appdash]: https://sourcegraph.com/blog/117580140734 | |
7 | [Appdash]: https://github.com/sourcegraph/appdash | |
8 | 8 | |
9 | 9 | ## Rationale |
10 | 10 |
25 | 25 | spanID int64 |
26 | 26 | parentSpanID int64 |
27 | 27 | |
28 | annotations []annotation | |
29 | //binaryAnnotations []BinaryAnnotation // TODO | |
28 | annotations []annotation | |
29 | binaryAnnotations []binaryAnnotation | |
30 | 30 | } |
31 | 31 | |
32 | 32 | // NewSpan returns a new Span, which can be annotated and collected by a |
93 | 93 | s.AnnotateDuration(value, 0) |
94 | 94 | } |
95 | 95 | |
96 | // AnnotateBinary annotates the span with a key and a byte value. | |
97 | func (s *Span) AnnotateBinary(key string, value []byte) { | |
98 | s.binaryAnnotations = append(s.binaryAnnotations, binaryAnnotation{ | |
99 | key: key, | |
100 | value: value, | |
101 | annotationType: zipkincore.AnnotationType_BYTES, | |
102 | host: s.host, | |
103 | }) | |
104 | } | |
105 | ||
106 | // AnnotateString annotates the span with a key and a string value. | |
107 | func (s *Span) AnnotateString(key, value string) { | |
108 | s.binaryAnnotations = append(s.binaryAnnotations, binaryAnnotation{ | |
109 | key: key, | |
110 | value: []byte(value), | |
111 | annotationType: zipkincore.AnnotationType_STRING, | |
112 | host: s.host, | |
113 | }) | |
114 | } | |
115 | ||
96 | 116 | // AnnotateDuration annotates the span with the given value and duration. |
97 | 117 | func (s *Span) AnnotateDuration(value string, duration time.Duration) { |
98 | 118 | s.annotations = append(s.annotations, annotation{ |
108 | 128 | // TODO lots of garbage here. We can improve by preallocating e.g. the |
109 | 129 | // Thrift stuff into an encoder struct, owned by the ScribeCollector. |
110 | 130 | zs := zipkincore.Span{ |
111 | TraceId: s.traceID, | |
112 | Name: s.methodName, | |
113 | Id: s.spanID, | |
114 | BinaryAnnotations: []*zipkincore.BinaryAnnotation{}, // TODO | |
115 | Debug: true, // TODO | |
131 | TraceId: s.traceID, | |
132 | Name: s.methodName, | |
133 | Id: s.spanID, | |
134 | Debug: true, // TODO | |
116 | 135 | } |
136 | ||
117 | 137 | if s.parentSpanID != 0 { |
118 | 138 | zs.ParentId = new(int64) |
119 | 139 | (*zs.ParentId) = s.parentSpanID |
120 | 140 | } |
141 | ||
121 | 142 | zs.Annotations = make([]*zipkincore.Annotation, len(s.annotations)) |
122 | 143 | for i, a := range s.annotations { |
123 | 144 | zs.Annotations[i] = &zipkincore.Annotation{ |
125 | 146 | Value: a.value, |
126 | 147 | Host: a.host, |
127 | 148 | } |
149 | ||
128 | 150 | if a.duration > 0 { |
129 | 151 | zs.Annotations[i].Duration = new(int32) |
130 | 152 | *(zs.Annotations[i].Duration) = int32(a.duration / time.Microsecond) |
131 | 153 | } |
132 | 154 | } |
155 | ||
156 | zs.BinaryAnnotations = make([]*zipkincore.BinaryAnnotation, len(s.binaryAnnotations)) | |
157 | for i, a := range s.binaryAnnotations { | |
158 | zs.BinaryAnnotations[i] = &zipkincore.BinaryAnnotation{ | |
159 | Key: a.key, | |
160 | Value: a.value, | |
161 | AnnotationType: a.annotationType, | |
162 | Host: a.host, | |
163 | } | |
164 | } | |
165 | ||
133 | 166 | return &zs |
134 | 167 | } |
135 | 168 | |
139 | 172 | duration time.Duration // optional |
140 | 173 | host *zipkincore.Endpoint |
141 | 174 | } |
175 | ||
176 | type binaryAnnotation struct { | |
177 | key string | |
178 | value []byte | |
179 | annotationType zipkincore.AnnotationType | |
180 | host *zipkincore.Endpoint | |
181 | } |
0 | package zipkin_test | |
1 | ||
2 | import ( | |
3 | "bytes" | |
4 | "testing" | |
5 | ||
6 | "github.com/go-kit/kit/tracing/zipkin" | |
7 | ) | |
8 | ||
9 | func TestAnnotateBinaryEncodesKeyValueAsBytes(t *testing.T) { | |
10 | key := "awesome-bytes-test" | |
11 | value := []byte("this is neat") | |
12 | ||
13 | span := &zipkin.Span{} | |
14 | span.AnnotateBinary(key, value) | |
15 | ||
16 | encodedSpan := span.Encode() | |
17 | annotations := encodedSpan.GetBinaryAnnotations() | |
18 | ||
19 | if len(annotations) == 0 { | |
20 | t.Error("want non-zero length slice, have empty slice") | |
21 | } | |
22 | ||
23 | if want, have := key, annotations[0].Key; want != have { | |
24 | t.Errorf("want %q, got %q", want, have) | |
25 | } | |
26 | ||
27 | if want, have := value, annotations[0].Value; bytes.Compare(want, have) != 0 { | |
28 | t.Errorf("want %s, got %s", want, have) | |
29 | } | |
30 | } | |
31 | ||
32 | func TestAnnotateStringEncodesKeyValueAsBytes(t *testing.T) { | |
33 | key := "awesome-string-test" | |
34 | value := "this is neat" | |
35 | ||
36 | span := &zipkin.Span{} | |
37 | span.AnnotateString(key, value) | |
38 | ||
39 | encodedSpan := span.Encode() | |
40 | annotations := encodedSpan.GetBinaryAnnotations() | |
41 | ||
42 | if len(annotations) == 0 { | |
43 | t.Error("want non-zero length slice, have empty slice") | |
44 | } | |
45 | ||
46 | if want, have := key, annotations[0].Key; want != have { | |
47 | t.Errorf("want %q, got %q", want, have) | |
48 | } | |
49 | ||
50 | if want, have := value, annotations[0].Value; bytes.Compare([]byte(want), have) != 0 { | |
51 | t.Errorf("want %s, got %s", want, have) | |
52 | } | |
53 | } |
5 | 5 | "strconv" |
6 | 6 | |
7 | 7 | "golang.org/x/net/context" |
8 | "google.golang.org/grpc/metadata" | |
8 | 9 | |
9 | 10 | "github.com/go-kit/kit/endpoint" |
10 | 11 | "github.com/go-kit/kit/log" |
12 | "github.com/go-kit/kit/transport/grpc" | |
11 | 13 | ) |
12 | 14 | |
13 | 15 | // In Zipkin, "spans are considered to start and stop with the client." The |
26 | 28 | traceIDHTTPHeader = "X-B3-TraceId" |
27 | 29 | spanIDHTTPHeader = "X-B3-SpanId" |
28 | 30 | parentSpanIDHTTPHeader = "X-B3-ParentSpanId" |
31 | // gRPC keys are always lowercase | |
32 | traceIDGRPCKey = "x-b3-traceid" | |
33 | spanIDGRPCKey = "x-b3-spanid" | |
34 | parentSpanIDGRPCKey = "x-b3-parentspanid" | |
29 | 35 | |
30 | 36 | // ClientSend is the annotation value used to mark a client sending a |
31 | 37 | // request to a server. |
97 | 103 | } |
98 | 104 | } |
99 | 105 | |
106 | // ToGRPCContext returns a function that satisfies transport/grpc.BeforeFunc. It | |
107 | // takes a Zipkin span from the incoming GRPC request, and saves it in the | |
108 | // request context. It's designed to be wired into a server's GRPC transport | |
109 | // Before stack. The logger is used to report errors. | |
110 | func ToGRPCContext(newSpan NewSpanFunc, logger log.Logger) func(ctx context.Context, md *metadata.MD) context.Context { | |
111 | return func(ctx context.Context, md *metadata.MD) context.Context { | |
112 | return context.WithValue(ctx, SpanContextKey, fromGRPC(newSpan, *md, logger)) | |
113 | } | |
114 | } | |
115 | ||
100 | 116 | // ToRequest returns a function that satisfies transport/http.BeforeFunc. It |
101 | 117 | // takes a Zipkin span from the context, and injects it into the HTTP request. |
102 | 118 | // It's designed to be wired into a client's HTTP transport Before stack. It's |
121 | 137 | } |
122 | 138 | } |
123 | 139 | |
140 | // ToGRPCRequest returns a function that satisfies transport/grpc.BeforeFunc. It | |
141 | // takes a Zipkin span from the context, and injects it into the GRPC context. | |
142 | // It's designed to be wired into a client's GRPC transport Before stack. It's | |
143 | // expected that AnnotateClient has already ensured the span in the context is | |
144 | // a child/client span. | |
145 | func ToGRPCRequest(newSpan NewSpanFunc) func(ctx context.Context, md *metadata.MD) context.Context { | |
146 | return func(ctx context.Context, md *metadata.MD) context.Context { | |
147 | span, ok := fromContext(ctx) | |
148 | if !ok { | |
149 | span = newSpan(newID(), newID(), 0) | |
150 | } | |
151 | if id := span.TraceID(); id > 0 { | |
152 | key, value := grpc.EncodeKeyValue(traceIDGRPCKey, strconv.FormatInt(id, 16)) | |
153 | (*md)[key] = append((*md)[key], value) | |
154 | } | |
155 | if id := span.SpanID(); id > 0 { | |
156 | key, value := grpc.EncodeKeyValue(spanIDGRPCKey, strconv.FormatInt(id, 16)) | |
157 | (*md)[key] = append((*md)[key], value) | |
158 | } | |
159 | if id := span.ParentSpanID(); id > 0 { | |
160 | key, value := grpc.EncodeKeyValue(parentSpanIDGRPCKey, strconv.FormatInt(id, 16)) | |
161 | (*md)[key] = append((*md)[key], value) | |
162 | } | |
163 | return ctx | |
164 | } | |
165 | } | |
166 | ||
124 | 167 | func fromHTTP(newSpan NewSpanFunc, r *http.Request, logger log.Logger) *Span { |
125 | 168 | traceIDStr := r.Header.Get(traceIDHTTPHeader) |
126 | 169 | if traceIDStr == "" { |
148 | 191 | parentSpanID, err := strconv.ParseInt(parentSpanIDStr, 16, 64) |
149 | 192 | if err != nil { |
150 | 193 | logger.Log(parentSpanIDHTTPHeader, parentSpanIDStr, "err", err) // abnormal |
194 | parentSpanID = 0 // the only way to deal with it | |
195 | } | |
196 | return newSpan(traceID, spanID, parentSpanID) | |
197 | } | |
198 | ||
199 | func fromGRPC(newSpan NewSpanFunc, md metadata.MD, logger log.Logger) *Span { | |
200 | traceIDSlc := md[traceIDGRPCKey] | |
201 | pos := len(traceIDSlc) - 1 | |
202 | if pos < 0 { | |
203 | return newSpan(newID(), newID(), 0) // normal; just make a new one | |
204 | } | |
205 | traceID, err := strconv.ParseInt(traceIDSlc[pos], 16, 64) | |
206 | if err != nil { | |
207 | logger.Log(traceIDHTTPHeader, traceIDSlc, "err", err) | |
208 | return newSpan(newID(), newID(), 0) | |
209 | } | |
210 | spanIDSlc := md[spanIDGRPCKey] | |
211 | pos = len(spanIDSlc) - 1 | |
212 | if pos < 0 { | |
213 | spanIDSlc = make([]string, 1) | |
214 | pos = 0 | |
215 | } | |
216 | if spanIDSlc[pos] == "" { | |
217 | logger.Log("msg", "trace ID without span ID") // abnormal | |
218 | spanIDSlc[pos] = strconv.FormatInt(newID(), 64) // deal with it | |
219 | } | |
220 | spanID, err := strconv.ParseInt(spanIDSlc[pos], 16, 64) | |
221 | if err != nil { | |
222 | logger.Log(spanIDHTTPHeader, spanIDSlc, "err", err) // abnormal | |
223 | spanID = newID() // deal with it | |
224 | } | |
225 | parentSpanIDSlc := md[parentSpanIDGRPCKey] | |
226 | pos = len(parentSpanIDSlc) - 1 | |
227 | if pos < 0 { | |
228 | parentSpanIDSlc = make([]string, 1) | |
229 | pos = 0 | |
230 | } | |
231 | if parentSpanIDSlc[pos] == "" { | |
232 | parentSpanIDSlc[pos] = "0" // normal | |
233 | } | |
234 | parentSpanID, err := strconv.ParseInt(parentSpanIDSlc[pos], 16, 64) | |
235 | if err != nil { | |
236 | logger.Log(parentSpanIDHTTPHeader, parentSpanIDSlc, "err", err) // abnormal | |
151 | 237 | parentSpanID = 0 // the only way to deal with it |
152 | 238 | } |
153 | 239 | return newSpan(traceID, spanID, parentSpanID) |
10 | 10 | "testing" |
11 | 11 | |
12 | 12 | "golang.org/x/net/context" |
13 | "google.golang.org/grpc/metadata" | |
13 | 14 | |
14 | 15 | "github.com/go-kit/kit/endpoint" |
15 | 16 | "github.com/go-kit/kit/log" |
59 | 60 | } |
60 | 61 | } |
61 | 62 | |
63 | func TestToGRPCContext(t *testing.T) { | |
64 | const ( | |
65 | hostport = "5.5.5.5:5555" | |
66 | serviceName = "foo-service" | |
67 | methodName = "foo-method" | |
68 | traceID int64 = 12 | |
69 | spanID int64 = 34 | |
70 | parentSpanID int64 = 56 | |
71 | ) | |
72 | ||
73 | md := metadata.MD{ | |
74 | "x-b3-traceid": []string{strconv.FormatInt(traceID, 16)}, | |
75 | "x-b3-spanid": []string{strconv.FormatInt(spanID, 16)}, | |
76 | "x-b3-parentspanid": []string{strconv.FormatInt(parentSpanID, 16)}, | |
77 | } | |
78 | ||
79 | newSpan := zipkin.MakeNewSpanFunc(hostport, serviceName, methodName) | |
80 | toContext := zipkin.ToGRPCContext(newSpan, log.NewLogfmtLogger(ioutil.Discard)) | |
81 | ||
82 | ctx := toContext(context.Background(), &md) | |
83 | val := ctx.Value(zipkin.SpanContextKey) | |
84 | if val == nil { | |
85 | t.Fatalf("%s returned no value", zipkin.SpanContextKey) | |
86 | } | |
87 | span, ok := val.(*zipkin.Span) | |
88 | if !ok { | |
89 | t.Fatalf("%s was not a Span object", zipkin.SpanContextKey) | |
90 | } | |
91 | ||
92 | for want, haveFunc := range map[int64]func() int64{ | |
93 | traceID: span.TraceID, | |
94 | spanID: span.SpanID, | |
95 | parentSpanID: span.ParentSpanID, | |
96 | } { | |
97 | if have := haveFunc(); want != have { | |
98 | name := runtime.FuncForPC(reflect.ValueOf(haveFunc).Pointer()).Name() | |
99 | name = strings.Split(name, "·")[0] | |
100 | toks := strings.Split(name, ".") | |
101 | name = toks[len(toks)-1] | |
102 | t.Errorf("%s: want %d, have %d", name, want, have) | |
103 | } | |
104 | } | |
105 | } | |
106 | ||
62 | 107 | func TestToRequest(t *testing.T) { |
63 | 108 | const ( |
64 | 109 | hostport = "5.5.5.5:5555" |
86 | 131 | } |
87 | 132 | } |
88 | 133 | |
134 | func TestToGRPCRequest(t *testing.T) { | |
135 | const ( | |
136 | hostport = "5.5.5.5:5555" | |
137 | serviceName = "foo-service" | |
138 | methodName = "foo-method" | |
139 | traceID int64 = 20 | |
140 | spanID int64 = 40 | |
141 | parentSpanID int64 = 90 | |
142 | ) | |
143 | ||
144 | newSpan := zipkin.MakeNewSpanFunc(hostport, serviceName, methodName) | |
145 | span := newSpan(traceID, spanID, parentSpanID) | |
146 | ctx := context.WithValue(context.Background(), zipkin.SpanContextKey, span) | |
147 | md := &metadata.MD{} | |
148 | ctx = zipkin.ToGRPCRequest(newSpan)(ctx, md) | |
149 | ||
150 | for header, wantInt := range map[string]int64{ | |
151 | "x-b3-traceid": traceID, | |
152 | "x-b3-spanid": spanID, | |
153 | "x-b3-parentspanid": parentSpanID, | |
154 | } { | |
155 | if want, have := strconv.FormatInt(wantInt, 16), (*md)[header][0]; want != have { | |
156 | t.Errorf("%s: want %q, have %q", header, want, have) | |
157 | } | |
158 | } | |
159 | } | |
160 | ||
89 | 161 | func TestAnnotateServer(t *testing.T) { |
90 | 162 | if err := testAnnotate(zipkin.AnnotateServer, zipkin.ServerReceive, zipkin.ServerSend); err != nil { |
91 | 163 | t.Fatal(err) |
0 | package grpc | |
1 | ||
2 | import ( | |
3 | "fmt" | |
4 | ||
5 | "golang.org/x/net/context" | |
6 | "google.golang.org/grpc" | |
7 | "google.golang.org/grpc/metadata" | |
8 | ||
9 | "github.com/go-kit/kit/endpoint" | |
10 | ) | |
11 | ||
12 | // Client wraps a gRPC connection and provides a method that implements | |
13 | // endpoint.Endpoint. | |
14 | type Client struct { | |
15 | client *grpc.ClientConn | |
16 | serviceName string | |
17 | method string | |
18 | enc EncodeRequestFunc | |
19 | dec DecodeResponseFunc | |
20 | grpcReply interface{} | |
21 | before []RequestFunc | |
22 | } | |
23 | ||
24 | // NewClient constructs a usable Client for a single remote endpoint. | |
25 | func NewClient( | |
26 | cc *grpc.ClientConn, | |
27 | serviceName string, | |
28 | method string, | |
29 | enc EncodeRequestFunc, | |
30 | dec DecodeResponseFunc, | |
31 | grpcReply interface{}, | |
32 | options ...ClientOption, | |
33 | ) *Client { | |
34 | c := &Client{ | |
35 | client: cc, | |
36 | method: fmt.Sprintf("/pb.%s/%s", serviceName, method), | |
37 | enc: enc, | |
38 | dec: dec, | |
39 | grpcReply: grpcReply, | |
40 | before: []RequestFunc{}, | |
41 | } | |
42 | for _, option := range options { | |
43 | option(c) | |
44 | } | |
45 | return c | |
46 | } | |
47 | ||
48 | // ClientOption sets an optional parameter for clients. | |
49 | type ClientOption func(*Client) | |
50 | ||
51 | // SetClientBefore sets the RequestFuncs that are applied to the outgoing gRPC | |
52 | // request before it's invoked. | |
53 | func SetClientBefore(before ...RequestFunc) ClientOption { | |
54 | return func(c *Client) { c.before = before } | |
55 | } | |
56 | ||
57 | // Endpoint returns a usable endpoint that will invoke the gRPC specified by the | |
58 | // client. | |
59 | func (c Client) Endpoint() endpoint.Endpoint { | |
60 | return func(ctx context.Context, request interface{}) (interface{}, error) { | |
61 | ctx, cancel := context.WithCancel(ctx) | |
62 | defer cancel() | |
63 | ||
64 | req, err := c.enc(ctx, request) | |
65 | if err != nil { | |
66 | return nil, fmt.Errorf("Encode: %v", err) | |
67 | } | |
68 | ||
69 | md := &metadata.MD{} | |
70 | for _, f := range c.before { | |
71 | ctx = f(ctx, md) | |
72 | } | |
73 | ctx = metadata.NewContext(ctx, *md) | |
74 | ||
75 | if err = grpc.Invoke(ctx, c.method, req, c.grpcReply, c.client); err != nil { | |
76 | return nil, fmt.Errorf("Invoke: %v", err) | |
77 | } | |
78 | ||
79 | response, err := c.dec(ctx, c.grpcReply) | |
80 | if err != nil { | |
81 | return nil, fmt.Errorf("Decode: %v", err) | |
82 | } | |
83 | return response, nil | |
84 | } | |
85 | } |
0 | package grpc | |
1 | ||
2 | import "golang.org/x/net/context" | |
3 | ||
4 | // DecodeRequestFunc extracts a user-domain request object from a gRPC request. | |
5 | // It's designed to be used in gRPC servers, for server-side endpoints. One | |
6 | // straightforward DecodeRequestFunc could be something that | |
7 | // decodes from the gRPC request message to the concrete request type. | |
8 | type DecodeRequestFunc func(context.Context, interface{}) (request interface{}, err error) | |
9 | ||
10 | // EncodeRequestFunc encodes the passed request object into the gRPC request | |
11 | // object. It's designed to be used in gRPC clients, for client-side | |
12 | // endpoints. One straightforward EncodeRequestFunc could something that | |
13 | // encodes the object directly to the gRPC request message. | |
14 | type EncodeRequestFunc func(context.Context, interface{}) (request interface{}, err error) | |
15 | ||
16 | // EncodeResponseFunc encodes the passed response object to the gRPC response | |
17 | // message. It's designed to be used in gRPC servers, for server-side | |
18 | // endpoints. One straightforward EncodeResponseFunc could be something that | |
19 | // encodes the object directly to the gRPC response message. | |
20 | type EncodeResponseFunc func(context.Context, interface{}) (response interface{}, err error) | |
21 | ||
22 | // DecodeResponseFunc extracts a user-domain response object from a gRPC | |
23 | // response object. It's designed to be used in gRPC clients, for client-side | |
24 | // endpoints. One straightforward DecodeResponseFunc could be something that | |
25 | // decodes from the gRPC response message to the concrete response type. | |
26 | type DecodeResponseFunc func(context.Context, interface{}) (response interface{}, err error) |
0 | package grpc | |
1 | ||
2 | import ( | |
3 | "encoding/base64" | |
4 | "strings" | |
5 | ||
6 | "golang.org/x/net/context" | |
7 | "google.golang.org/grpc/metadata" | |
8 | ) | |
9 | ||
10 | const ( | |
11 | binHdrSuffix = "-bin" | |
12 | ) | |
13 | ||
14 | // RequestFunc may take information from an gRPC request and put it into a | |
15 | // request context. In Servers, BeforeFuncs are executed prior to invoking the | |
16 | // endpoint. In Clients, BeforeFuncs are executed after creating the request | |
17 | // but prior to invoking the gRPC client. | |
18 | type RequestFunc func(context.Context, *metadata.MD) context.Context | |
19 | ||
20 | // ResponseFunc may take information from a request context and use it to | |
21 | // manipulate the gRPC metadata header. ResponseFuncs are only executed in | |
22 | // servers, after invoking the endpoint but prior to writing a response. | |
23 | type ResponseFunc func(context.Context, *metadata.MD) | |
24 | ||
25 | // SetResponseHeader returns a ResponseFunc that sets the specified metadata | |
26 | // key-value pair. | |
27 | func SetResponseHeader(key, val string) ResponseFunc { | |
28 | return func(_ context.Context, md *metadata.MD) { | |
29 | key, val := EncodeKeyValue(key, val) | |
30 | (*md)[key] = append((*md)[key], val) | |
31 | } | |
32 | } | |
33 | ||
34 | // SetRequestHeader returns a RequestFunc that sets the specified metadata | |
35 | // key-value pair. | |
36 | func SetRequestHeader(key, val string) RequestFunc { | |
37 | return func(ctx context.Context, md *metadata.MD) context.Context { | |
38 | key, val := EncodeKeyValue(key, val) | |
39 | (*md)[key] = append((*md)[key], val) | |
40 | return ctx | |
41 | } | |
42 | } | |
43 | ||
44 | // EncodeKeyValue sanitizes a key-value pair for use in gRPC metadata headers. | |
45 | func EncodeKeyValue(key, val string) (string, string) { | |
46 | key = strings.ToLower(key) | |
47 | if strings.HasSuffix(key, binHdrSuffix) { | |
48 | v := base64.StdEncoding.EncodeToString([]byte(val)) | |
49 | val = string(v) | |
50 | } | |
51 | return key, val | |
52 | } |
0 | package grpc | |
1 | ||
2 | import ( | |
3 | "golang.org/x/net/context" | |
4 | "google.golang.org/grpc/metadata" | |
5 | ||
6 | "github.com/go-kit/kit/endpoint" | |
7 | "github.com/go-kit/kit/log" | |
8 | ) | |
9 | ||
10 | // Handler which should be called from the grpc binding of the service | |
11 | // implementation. | |
12 | type Handler interface { | |
13 | ServeGRPC(context.Context, interface{}) (context.Context, interface{}, error) | |
14 | } | |
15 | ||
16 | // Server wraps an endpoint and implements grpc.Handler. | |
17 | type Server struct { | |
18 | ctx context.Context | |
19 | e endpoint.Endpoint | |
20 | dec DecodeRequestFunc | |
21 | enc EncodeResponseFunc | |
22 | before []RequestFunc | |
23 | after []ResponseFunc | |
24 | logger log.Logger | |
25 | } | |
26 | ||
27 | // NewServer constructs a new server, which implements grpc.Server and wraps | |
28 | // the provided endpoint. | |
29 | func NewServer( | |
30 | ctx context.Context, | |
31 | e endpoint.Endpoint, | |
32 | dec DecodeRequestFunc, | |
33 | enc EncodeResponseFunc, | |
34 | options ...ServerOption, | |
35 | ) *Server { | |
36 | s := &Server{ | |
37 | ctx: ctx, | |
38 | e: e, | |
39 | dec: dec, | |
40 | enc: enc, | |
41 | logger: log.NewNopLogger(), | |
42 | } | |
43 | for _, option := range options { | |
44 | option(s) | |
45 | } | |
46 | return s | |
47 | } | |
48 | ||
49 | // ServerOption sets an optional parameter for servers. | |
50 | type ServerOption func(*Server) | |
51 | ||
52 | // ServerBefore functions are executed on the HTTP request object before the | |
53 | // request is decoded. | |
54 | func ServerBefore(before ...RequestFunc) ServerOption { | |
55 | return func(s *Server) { s.before = before } | |
56 | } | |
57 | ||
58 | // ServerAfter functions are executed on the HTTP response writer after the | |
59 | // endpoint is invoked, but before anything is written to the client. | |
60 | func ServerAfter(after ...ResponseFunc) ServerOption { | |
61 | return func(s *Server) { s.after = after } | |
62 | } | |
63 | ||
64 | // ServerErrorLogger is used to log non-terminal errors. By default, no errors | |
65 | // are logged. | |
66 | func ServerErrorLogger(logger log.Logger) ServerOption { | |
67 | return func(s *Server) { s.logger = logger } | |
68 | } | |
69 | ||
70 | // ServeGRPC implements grpc.Handler | |
71 | func (s Server) ServeGRPC(grpcCtx context.Context, r interface{}) (context.Context, interface{}, error) { | |
72 | ctx, cancel := context.WithCancel(s.ctx) | |
73 | defer cancel() | |
74 | ||
75 | // retrieve gRPC metadata | |
76 | md, ok := metadata.FromContext(grpcCtx) | |
77 | if !ok { | |
78 | md = metadata.MD{} | |
79 | } | |
80 | ||
81 | for _, f := range s.before { | |
82 | ctx = f(ctx, &md) | |
83 | } | |
84 | ||
85 | // store potentially updated metadata in the gRPC context | |
86 | grpcCtx = metadata.NewContext(grpcCtx, md) | |
87 | ||
88 | request, err := s.dec(grpcCtx, r) | |
89 | if err != nil { | |
90 | s.logger.Log("err", err) | |
91 | return grpcCtx, nil, BadRequestError{err} | |
92 | } | |
93 | ||
94 | response, err := s.e(ctx, request) | |
95 | if err != nil { | |
96 | s.logger.Log("err", err) | |
97 | return grpcCtx, nil, err | |
98 | } | |
99 | ||
100 | for _, f := range s.after { | |
101 | f(ctx, &md) | |
102 | } | |
103 | ||
104 | // store potentially updated metadata in the gRPC context | |
105 | grpcCtx = metadata.NewContext(grpcCtx, md) | |
106 | ||
107 | grpcResp, err := s.enc(grpcCtx, response) | |
108 | if err != nil { | |
109 | s.logger.Log("err", err) | |
110 | return grpcCtx, nil, err | |
111 | } | |
112 | return grpcCtx, grpcResp, nil | |
113 | } | |
114 | ||
115 | // BadRequestError is an error in decoding the request. | |
116 | type BadRequestError struct { | |
117 | Err error | |
118 | } | |
119 | ||
120 | // Error implements the error interface. | |
121 | func (err BadRequestError) Error() string { | |
122 | return err.Err.Error() | |
123 | } |
21 | 21 | bufferedStream bool |
22 | 22 | } |
23 | 23 | |
24 | // NewClient returns a | |
25 | func NewClient(method string, tgt *url.URL, enc EncodeRequestFunc, dec DecodeResponseFunc, options ...ClientOption) *Client { | |
24 | // NewClient constructs a usable Client for a single remote endpoint. | |
25 | func NewClient( | |
26 | method string, | |
27 | tgt *url.URL, | |
28 | enc EncodeRequestFunc, | |
29 | dec DecodeResponseFunc, | |
30 | options ...ClientOption, | |
31 | ) *Client { | |
26 | 32 | c := &Client{ |
27 | 33 | client: http.DefaultClient, |
28 | 34 | method: method, |