Codebase list golang-github-go-kit-kit / 4f31123
Merge remote-tracking branch 'upstream/master' Olivier Bregeras 8 years ago
57 changed file(s) with 2777 addition(s) and 653 deletion(s). Raw diff Collapse all Expand all
00 examples/addsvc/addsvc
11 examples/addsvc/client/client
2 examples/profilesvc/profilesvc
23 examples/stringsvc1/stringsvc1
34 examples/stringsvc2/stringsvc2
45 examples/stringsvc3/stringsvc3
00 language: go
1
2 script: go test -race -v ./...
13
24 go:
35 - 1.4.2
4 - 1.5
6 - 1.5.3
7 - 1.6
58 - 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)
11
22 **Go kit** is a **distributed programming toolkit** for building microservices
33 in large organizations. We solve common problems in distributed systems, so
44 you can focus on your business logic.
55
66 - 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/))
88
99 ## Documentation
1010
5252 transport, bindings are typically provided by the Go library for the
5353 transport, and there's not much for Go kit to do. In those cases, see the
5454 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.
5858
5959 [transport]: https://github.com/go-kit/kit/tree/master/transport
6060 [addsvc]: https://github.com/go-kit/kit/tree/master/examples/addsvc
165165 recommend use of third-party import proxies.
166166
167167 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
169169 integration providers to find and fix compatibility problems as soon as they
170170 occur.
171171
172172 [gb]: http://getgb.io
173173 [govendor]: https://github.com/kardianos/govendor
174 [Godep]: https://github.com/tools/godep
174 [godep]: https://github.com/tools/godep
175175
176176 ## API stability policy
177177
1414 shouldPass = func(n int) bool { return n <= 5 } // https://github.com/sony/gobreaker/blob/bfa846d/gobreaker.go#L76
1515 circuitOpenError = "circuit breaker is open"
1616 )
17 testFailingEndpoint(t, breaker, primeWith, shouldPass, circuitOpenError)
17 testFailingEndpoint(t, breaker, primeWith, shouldPass, 0, circuitOpenError)
1818 }
1515 shouldPass = func(n int) bool { return (float64(n) / float64(primeWith+n)) <= failureRatio }
1616 openCircuitError = handybreaker.ErrCircuitOpen.Error()
1717 )
18 testFailingEndpoint(t, breaker, primeWith, shouldPass, openCircuitError)
18 testFailingEndpoint(t, breaker, primeWith, shouldPass, 0, openCircuitError)
1919 }
1616 func Hystrix(commandName string) endpoint.Middleware {
1717 return func(next endpoint.Endpoint) endpoint.Endpoint {
1818 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)
2522 return err
26 }, nil)
27
28 select {
29 case resp := <-output:
30 return resp, nil
31 case err := <-errors:
23 }, nil); err != nil {
3224 return nil, err
3325 }
26 return resp, nil
3427 }
3528 }
3629 }
00 package circuitbreaker_test
11
22 import (
3 "io/ioutil"
34 stdlog "log"
4 "os"
55 "testing"
6 "time"
67
78 "github.com/afex/hystrix-go/hystrix"
89
910 "github.com/go-kit/kit/circuitbreaker"
10 kitlog "github.com/go-kit/kit/log"
1111 )
1212
1313 func TestHystrix(t *testing.T) {
14 logger := kitlog.NewLogfmtLogger(os.Stderr)
15 stdlog.SetOutput(kitlog.NewStdlibAdapter(logger))
14 stdlog.SetOutput(ioutil.Discard)
1615
1716 const (
1817 commandName = "my-endpoint"
3029 shouldPass = func(n int) bool { return (float64(n) / float64(primeWith+n)) <= (float64(errorPercent-1) / 100.0) }
3130 openCircuitError = hystrix.ErrCircuitOpen.Error()
3231 )
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)
3439 }
1212 "github.com/go-kit/kit/endpoint"
1313 )
1414
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 ) {
1623 _, file, line, _ := runtime.Caller(1)
1724 caller := fmt.Sprintf("%s:%d", filepath.Base(file), line)
1825
2734 if _, err := e(context.Background(), struct{}{}); err != nil {
2835 t.Fatalf("%s: during priming, got error: %v", caller, err)
2936 }
37 time.Sleep(requestDelay)
3038 }
3139
3240 // Switch the endpoint to start throwing errors.
3846 if _, err := e(context.Background(), struct{}{}); err != m.err {
3947 t.Fatalf("%s: want %v, have %v", caller, m.err, err)
4048 }
49 time.Sleep(requestDelay)
4150 }
4251 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)
4752
4853 // But the rest should be blocked by an open circuit.
4954 for i := 0; i < 10; i++ {
5055 if _, err := e(context.Background(), struct{}{}); err.Error() != openCircuitError {
5156 t.Fatalf("%s: want %q, have %q", caller, openCircuitError, err.Error())
5257 }
58 time.Sleep(requestDelay)
5359 }
5460
5561 // Make sure none of those got through.
672672 It also demonstrates how to create and use client packages.
673673 It's a good example of a fully-featured Go kit service.
674674
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
675680 ### apigateway
676681
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.
4141 }
4242 reply, err := c.AddClient.Sum(c.Context, request)
4343 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...
4545 return 0
4646 }
4747 return int(reply.V)
5454 }
5555 reply, err := c.AddClient.Concat(c.Context, request)
5656 if err != nil {
57 _ = c.Logger.Log("err", err)
57 c.Logger.Log("err", err)
5858 return ""
5959 }
6060 return reply.V
1919 if err != nil {
2020 panic(err)
2121 }
22 sumURL.Path = "/sum"
23
2224 concatURL, err := url.Parse(baseurl.String())
2325 if err != nil {
2426 panic(err)
2527 }
26 sumURL.Path = "/sum"
2728 concatURL.Path = "/concat"
29
2830 return client{
2931 Context: ctx,
3032 Logger: logger,
3335 sumURL,
3436 server.EncodeSumRequest,
3537 server.DecodeSumResponse,
38 httptransport.SetClient(c),
3639 ).Endpoint(),
3740 concat: httptransport.NewClient(
3841 "GET",
3942 concatURL,
4043 server.EncodeConcatRequest,
4144 server.DecodeConcatResponse,
45 httptransport.SetClient(c),
4246 ).Endpoint(),
4347 }
4448 }
5357 func (c client) Sum(a, b int) int {
5458 response, err := c.sum(c.Context, server.SumRequest{A: a, B: b})
5559 if err != nil {
56 _ = c.Logger.Log("err", err)
60 c.Logger.Log("err", err)
5761 return 0
5862 }
5963 return response.(server.SumResponse).V
6266 func (c client) Concat(a, b string) string {
6367 response, err := c.concat(c.Context, server.ConcatRequest{A: a, B: b})
6468 if err != nil {
65 _ = c.Logger.Log("err", err)
69 c.Logger.Log("err", err)
6670 return ""
6771 }
6872 return response.(server.ConcatResponse).V
1919 func (c client) Sum(a, b int) int {
2020 var reply server.SumResponse
2121 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)
2323 return 0
2424 }
2525 return reply.V
2828 func (c client) Concat(a, b string) string {
2929 var reply server.ConcatResponse
3030 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)
3232 return ""
3333 }
3434 return reply.V
1818 func (c client) Sum(a, b int) int {
1919 reply, err := c.AddServiceClient.Sum(int64(a), int64(b))
2020 if err != nil {
21 _ = c.Logger.Log("err", err)
21 c.Logger.Log("err", err)
2222 return 0
2323 }
2424 return int(reply.Value)
2727 func (c client) Concat(a, b string) string {
2828 reply, err := c.AddServiceClient.Concat(a, b)
2929 if err != nil {
30 _ = c.Logger.Log("err", err)
30 c.Logger.Log("err", err)
3131 return ""
3232 }
3333 return reply.Value
6969 var requestDuration metrics.TimeHistogram
7070 {
7171 requestDuration = metrics.NewTimeHistogram(time.Nanosecond, metrics.NewMultiHistogram(
72 "request_duration_ns",
7273 expvar.NewHistogram("request_duration_ns", 0, 5e9, 1, 50, 95, 99),
7374 prometheus.NewSummary(stdprometheus.SummaryOpts{
7475 Namespace: "myorg",
2020
2121 func (m loggingMiddleware) Sum(a, b int) (v int) {
2222 defer func(begin time.Time) {
23 _ = m.Logger.Log(
23 m.Logger.Log(
2424 "method", "sum",
2525 "a", a,
2626 "b", b,
3434
3535 func (m loggingMiddleware) Concat(a, b string) (v string) {
3636 defer func(begin time.Time) {
37 _ = m.Logger.Log(
37 m.Logger.Log(
3838 "method", "concat",
3939 "a", a,
4040 "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 }
1111 )
1212
1313 var consulState = []*consul.ServiceEntry{
14 &consul.ServiceEntry{
14 {
1515 Node: &consul.Node{
1616 Address: "10.0.0.0",
1717 Node: "app00.local",
2626 },
2727 },
2828 },
29 &consul.ServiceEntry{
29 {
3030 Node: &consul.Node{
3131 Address: "10.0.0.1",
3232 Node: "app01.local",
4141 },
4242 },
4343 },
44 &consul.ServiceEntry{
44 {
4545 Node: &consul.Node{
4646 Address: "10.0.0.1",
4747 Node: "app01.local",
1313 // resolved on a fixed schedule. Priorities and weights are ignored.
1414 type Publisher struct {
1515 name string
16 ttl time.Duration
1716 cache *loadbalancer.EndpointCache
1817 logger log.Logger
1918 quit chan struct{}
2423 // constructor will return an error. The factory is used to convert a
2524 // host:port to a usable endpoint. The logger is used to report DNS and
2625 // 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 {
2845 p := &Publisher{
2946 name: name,
30 ttl: ttl,
3147 cache: loadbalancer.NewEndpointCache(factory, logger),
3248 logger: logger,
3349 quit: make(chan struct{}),
3450 }
3551
36 instances, err := p.resolve()
52 instances, err := p.resolve(lookupSRV)
3753 if err == nil {
3854 logger.Log("name", name, "instances", len(instances))
3955 } else {
4157 }
4258 p.cache.Replace(instances)
4359
44 go p.loop()
60 go p.loop(refreshTicker, lookupSRV)
4561 return p
4662 }
4763
5066 close(p.quit)
5167 }
5268
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()
5674 for {
5775 select {
58 case <-t.C:
59 instances, err := p.resolve()
76 case <-refreshTicker.C:
77 instances, err := p.resolve(lookupSRV)
6078 if err != nil {
6179 p.logger.Log(p.name, err)
6280 continue // don't replace potentially-good with bad
7492 return p.cache.Endpoints()
7593 }
7694
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) {
8396 _, addrs, err := lookupSRV("", "", p.name)
8497 if err != nil {
8598 return []string{}, err
+0
-131
loadbalancer/dnssrv/publisher_internal_test.go less more
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 }
55 "time"
66
77 stdzk "github.com/samuel/go-zookeeper/zk"
8
9 "github.com/go-kit/kit/log"
810 )
911
1012 func TestNewClient(t *testing.T) {
1719
1820 c, err := NewClient(
1921 []string{"FailThisInvalidHost!!!"},
20 logger,
22 log.NewNopLogger(),
2123 )
22
23 time.Sleep(1 * time.Millisecond)
2424 if err == nil {
2525 t.Errorf("expected error, got nil")
2626 }
27 calledEventHandler := false
27
28 hasFired := false
29 calledEventHandler := make(chan struct{})
2830 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 }
3036 }
37
3138 c, err = NewClient(
3239 []string{"localhost"},
33 logger,
40 log.NewNopLogger(),
3441 ACL(acl),
3542 ConnectTimeout(connectTimeout),
3643 SessionTimeout(sessionTimeout),
4148 t.Fatal(err)
4249 }
4350 defer c.Stop()
51
4452 clientImpl, ok := c.(*client)
4553 if !ok {
46 t.Errorf("retrieved incorrect Client implementation")
54 t.Fatal("retrieved incorrect Client implementation")
4755 }
4856 if want, have := acl, clientImpl.acl; want[0] != have[0] {
4957 t.Errorf("want %+v, have %+v", want, have)
5765 if want, have := payload, clientImpl.rootNodePayload; bytes.Compare(want[0], have[0]) != 0 || bytes.Compare(want[1], have[1]) != 0 {
5866 t.Errorf("want %s, have %s", want, have)
5967 }
60 // Allow EventHandler to be called
61 time.Sleep(1 * time.Millisecond)
6268
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")
6573 }
6674 }
6775
6876 func TestOptions(t *testing.T) {
69 _, err := NewClient([]string{"localhost"}, logger, Credentials("valid", "credentials"))
77 _, err := NewClient([]string{"localhost"}, log.NewNopLogger(), Credentials("valid", "credentials"))
7078 if err != nil && err != stdzk.ErrNoServer {
7179 t.Errorf("unexpected error: %v", err)
7280 }
7381
74 _, err = NewClient([]string{"localhost"}, logger, Credentials("nopass", ""))
82 _, err = NewClient([]string{"localhost"}, log.NewNopLogger(), Credentials("nopass", ""))
7583 if want, have := err, ErrInvalidCredentials; want != have {
7684 t.Errorf("want %v, have %v", want, have)
7785 }
7886
79 _, err = NewClient([]string{"localhost"}, logger, ConnectTimeout(0))
87 _, err = NewClient([]string{"localhost"}, log.NewNopLogger(), ConnectTimeout(0))
8088 if err == nil {
8189 t.Errorf("expected connect timeout error")
8290 }
8391
84 _, err = NewClient([]string{"localhost"}, logger, SessionTimeout(0))
92 _, err = NewClient([]string{"localhost"}, log.NewNopLogger(), SessionTimeout(0))
8593 if err == nil {
8694 t.Errorf("expected connect timeout error")
8795 }
9098 func TestCreateParentNodes(t *testing.T) {
9199 payload := [][]byte{[]byte("Payload"), []byte("Test")}
92100
93 c, err := NewClient([]string{"localhost:65500"}, logger)
101 c, err := NewClient([]string{"localhost:65500"}, log.NewNopLogger())
94102 if err != nil {
95103 t.Errorf("unexpected error: %v", err)
96104 }
97105 if c == nil {
98 t.Fatalf("expected new Client, got nil")
106 t.Fatal("expected new Client, got nil")
99107 }
100 p, err := NewPublisher(c, "/validpath", newFactory(""), logger)
108
109 p, err := NewPublisher(c, "/validpath", newFactory(""), log.NewNopLogger())
101110 if err != stdzk.ErrNoServer {
102111 t.Errorf("unexpected error: %v", err)
103112 }
104113 if p != nil {
105 t.Errorf("expected failed new Publisher")
114 t.Error("expected failed new Publisher")
106115 }
107 p, err = NewPublisher(c, "invalidpath", newFactory(""), logger)
116
117 p, err = NewPublisher(c, "invalidpath", newFactory(""), log.NewNopLogger())
108118 if err != stdzk.ErrInvalidPath {
109119 t.Errorf("unexpected error: %v", err)
110120 }
112122 if err != stdzk.ErrNoServer {
113123 t.Errorf("unexpected error: %v", err)
114124 }
115 // stopping Client
125
116126 c.Stop()
127
117128 err = c.CreateParentNodes("/validpath")
118129 if err != ErrClientClosed {
119130 t.Errorf("unexpected error: %v", err)
120131 }
121 p, err = NewPublisher(c, "/validpath", newFactory(""), logger)
132
133 p, err = NewPublisher(c, "/validpath", newFactory(""), log.NewNopLogger())
122134 if err != ErrClientClosed {
123135 t.Errorf("unexpected error: %v", err)
124136 }
125137 if p != nil {
126 t.Errorf("expected failed new Publisher")
138 t.Error("expected failed new Publisher")
127139 }
128 c, err = NewClient([]string{"localhost:65500"}, logger, Payload(payload))
140
141 c, err = NewClient([]string{"localhost:65500"}, log.NewNopLogger(), Payload(payload))
129142 if err != nil {
130143 t.Errorf("unexpected error: %v", err)
131144 }
132145 if c == nil {
133 t.Fatalf("expected new Client, got nil")
146 t.Fatal("expected new Client, got nil")
134147 }
135 p, err = NewPublisher(c, "/validpath", newFactory(""), logger)
148
149 p, err = NewPublisher(c, "/validpath", newFactory(""), log.NewNopLogger())
136150 if err != stdzk.ErrNoServer {
137151 t.Errorf("unexpected error: %v", err)
138152 }
139153 if p != nil {
140 t.Errorf("expected failed new Publisher")
154 t.Error("expected failed new Publisher")
141155 }
142156 }
00 package zk
11
22 import (
3 "errors"
4 "io"
53 "testing"
64 "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()
205 )
216
227 func TestPublisher(t *testing.T) {
4227 }
4328 defer p.Stop()
4429
45 endpoints, err := p.Endpoints()
46 if err != nil {
47 t.Fatal(err)
48 }
4930 // instance1 came online
50 client.AddService(path+"/instance1", "zookeeper_node_data")
31 client.AddService(path+"/instance1", "kaboom")
5132
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)
5438 }
5539 }
5640
6751 if err != nil {
6852 t.Fatal(err)
6953 }
54
7055 if want, have := 0, len(endpoints); want != have {
7156 t.Errorf("want %d, have %d", want, have)
7257 }
7459 // instance1 came online
7560 client.AddService(path+"/instance1", "zookeeper_node_data")
7661
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
8662 // instance2 came online
8763 client.AddService(path+"/instance2", "zookeeper_node_data2")
8864
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)
9668 }
9769
9870 // watch triggers an error...
9971 client.SendErrorOnWatch()
10072
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)
10876 }
10977
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
11187 client.RemoveService(path + "/instance1")
88
89 // instance2 goes offline
11290 client.RemoveService(path + "/instance2")
11391
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)
12095 }
12196 }
12297
125100 client.SendErrorOnWatch()
126101 p, err := NewPublisher(client, path, newFactory(""), logger)
127102 if err == nil {
128 t.Errorf("expected error on new publisher")
103 t.Error("expected error on new publisher")
129104 }
130105 if p != nil {
131 t.Errorf("expected publisher not to be created")
106 t.Error("expected publisher not to be created")
132107 }
133108 p, err = NewPublisher(client, "BadPath", newFactory(""), logger)
134109 if err == nil {
135 t.Errorf("expected error on new publisher")
110 t.Error("expected error on new publisher")
136111 }
137112 if p != nil {
138 t.Errorf("expected publisher not to be created")
113 t.Error("expected publisher not to be created")
139114 }
140115 }
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 }
7373
7474 func TestStdlibAdapterSubexps(t *testing.T) {
7575 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: :.;<>_#{[]}\"\\": {
191191 "date": "",
192192 "time": "",
193193 "file": "C:/a/b/c/d.go:23",
201201 }
202202 }
203203 }
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
1212
1313 ## Rationale
1414
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
1620
1721 ## Usage
1822
5256 ```
5357
5458 A gauge for the number of goroutines currently running, exported via statsd.
59
5560 ```go
5661 import (
5762 "net"
6570 func main() {
6671 statsdWriter, err := net.Dial("udp", "127.0.0.1:8126")
6772 if err != nil {
68 os.Exit(1)
73 panic(err)
6974 }
7075
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) {
7479 goroutines.Set(float64(runtime.NumGoroutine()))
7580 }
7681 }
77
7882 ```
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 }
1818 import (
1919 "expvar"
2020 "fmt"
21 "sort"
2122 "strconv"
2223 "sync"
2324 "time"
2829 )
2930
3031 type counter struct {
31 v *expvar.Int
32 name string
33 v *expvar.Int
3234 }
3335
3436 // NewCounter returns a new Counter backed by an expvar with the given name.
3537 // Fields are ignored.
3638 func NewCounter(name string) metrics.Counter {
37 return &counter{expvar.NewInt(name)}
39 return &counter{
40 name: name,
41 v: expvar.NewInt(name),
42 }
3843 }
3944
45 func (c *counter) Name() string { return c.name }
4046 func (c *counter) With(metrics.Field) metrics.Counter { return c }
4147 func (c *counter) Add(delta uint64) { c.v.Add(int64(delta)) }
4248
4349 type gauge struct {
44 v *expvar.Float
50 name string
51 v *expvar.Float
4552 }
4653
4754 // NewGauge returns a new Gauge backed by an expvar with the given name. It
4855 // should be updated manually; for a callback-based approach, see
4956 // PublishCallbackGauge. Fields are ignored.
5057 func NewGauge(name string) metrics.Gauge {
51 return &gauge{expvar.NewFloat(name)}
58 return &gauge{
59 name: name,
60 v: expvar.NewFloat(name),
61 }
5262 }
5363
64 func (g *gauge) Name() string { return g.name }
5465 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()) }
5969
6070 // PublishCallbackGauge publishes a Gauge as an expvar with the given name,
6171 // whose value is determined at collect time by the passed callback function.
100110 return h
101111 }
102112
113 func (h *histogram) Name() string { return h.name }
103114 func (h *histogram) With(metrics.Field) metrics.Histogram { return h }
104115
105116 func (h *histogram) Observe(value int64) {
116127 }
117128 }
118129
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
119151 func (h *histogram) rotateLoop(d time.Duration) {
120152 for range time.Tick(d) {
121153 h.mu.Lock()
123155 h.mu.Unlock()
124156 }
125157 }
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] }
1111
1212 func TestHistogramQuantiles(t *testing.T) {
1313 var (
14 name = "test_histogram"
14 name = "test_histogram_quantiles"
1515 quantiles = []int{50, 90, 95, 99}
1616 h = expvar.NewHistogram(name, 0, 100, 3, quantiles...).With(metrics.Field{Key: "ignored", Value: "field"})
1717 )
88 // between measurements of a counter over intervals of time, an aggregation
99 // layer can derive rates, acceleration, etc.
1010 type Counter interface {
11 Name() string
1112 With(Field) Counter
1213 Add(delta uint64)
1314 }
1516 // Gauge captures instantaneous measurements of something using signed, 64-bit
1617 // floats. The value does not need to be monotonic.
1718 type Gauge interface {
19 Name() string
1820 With(Field) Gauge
1921 Set(value float64)
2022 Add(delta float64)
23 Get() float64
2124 }
2225
2326 // Histogram tracks the distribution of a stream of values (e.g. the number of
2427 // milliseconds it takes to handle requests). Implementations may choose to
2528 // add gauges for values at meaningful quantiles.
2629 type Histogram interface {
30 Name() string
2731 With(Field) Histogram
2832 Observe(value int64)
33 Distribution() ([]Bucket, []Quantile)
2934 }
3035
3136 // Field is a key/value pair associated with an observation for a specific
3439 Key string
3540 Value string
3641 }
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 }
00 package metrics
11
2 type multiCounter []Counter
2 type multiCounter struct {
3 name string
4 a []Counter
5 }
36
47 // 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 }
813 }
914
15 func (c multiCounter) Name() string { return c.name }
16
1017 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)
1424 }
1525 return next
1626 }
1727
1828 func (c multiCounter) Add(delta uint64) {
19 for _, counter := range c {
29 for _, counter := range c.a {
2030 counter.Add(delta)
2131 }
2232 }
2333
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 }
2540
2641 // 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 }
3047 }
3148
3249 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)
3656 }
3757 return next
3858 }
3959
4060 func (g multiGauge) Set(value float64) {
41 for _, gauge := range g {
61 for _, gauge := range g.a {
4262 gauge.Set(value)
4363 }
4464 }
4565
4666 func (g multiGauge) Add(delta float64) {
47 for _, gauge := range g {
67 for _, gauge := range g.a {
4868 gauge.Add(delta)
4969 }
5070 }
5171
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 }
5380
5481 // 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 }
5887 }
5988
89 func (h multiHistogram) Name() string { return h.name }
90
6091 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)
6498 }
6599 return next
66100 }
67101
68102 func (h multiHistogram) Observe(value int64) {
69 for _, histogram := range h {
103 for _, histogram := range h.a {
70104 histogram.Observe(value)
71105 }
72106 }
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 }
44 "fmt"
55 "io/ioutil"
66 "math"
7 "math/rand"
87 "net/http"
98 "net/http/httptest"
109 "regexp"
1716 "github.com/go-kit/kit/metrics"
1817 "github.com/go-kit/kit/metrics/expvar"
1918 "github.com/go-kit/kit/metrics/prometheus"
19 "github.com/go-kit/kit/metrics/teststat"
2020 )
2121
2222 func TestMultiWith(t *testing.T) {
2323 c := metrics.NewMultiCounter(
24 "multifoo",
2425 expvar.NewCounter("foo"),
2526 prometheus.NewCounter(stdprometheus.CounterOpts{
2627 Namespace: "test",
4647
4748 func TestMultiCounter(t *testing.T) {
4849 metrics.NewMultiCounter(
50 "multialpha",
4951 expvar.NewCounter("alpha"),
5052 prometheus.NewCounter(stdprometheus.CounterOpts{
5153 Namespace: "test",
7072
7173 func TestMultiGauge(t *testing.T) {
7274 g := metrics.NewMultiGauge(
75 "multidelta",
7376 expvar.NewGauge("delta"),
7477 prometheus.NewGauge(stdprometheus.GaugeOpts{
7578 Namespace: "test",
110113 func TestMultiHistogram(t *testing.T) {
111114 quantiles := []int{50, 90, 99}
112115 h := metrics.NewMultiHistogram(
116 "multiomicron",
113117 expvar.NewHistogram("omicron", 0, 100, 3, quantiles...),
114118 prometheus.NewSummary(stdprometheus.SummaryOpts{
115119 Namespace: "test",
120124 )
121125
122126 const seed, mean, stdev int64 = 123, 50, 10
123 populateNormalHistogram(t, h, seed, mean, stdev)
127 teststat.PopulateNormalHistogram(t, h, seed, mean, stdev)
124128 assertExpvarNormalHistogram(t, "omicron", mean, stdev, quantiles)
125129 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 }
134130 }
135131
136132 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 }
1515
1616 type prometheusCounter struct {
1717 *prometheus.CounterVec
18 name string
1819 Pairs map[string]string
1920 }
2021
2930 }
3031 return prometheusCounter{
3132 CounterVec: m,
33 name: opts.Name,
3234 Pairs: p,
3335 }
3436 }
37
38 func (c prometheusCounter) Name() string { return c.name }
3539
3640 func (c prometheusCounter) With(f metrics.Field) metrics.Counter {
3741 return prometheusCounter{
3842 CounterVec: c.CounterVec,
43 name: c.name,
3944 Pairs: merge(c.Pairs, f),
4045 }
4146 }
4651
4752 type prometheusGauge struct {
4853 *prometheus.GaugeVec
54 name string
4955 Pairs map[string]string
5056 }
5157
5662 prometheus.MustRegister(m)
5763 return prometheusGauge{
5864 GaugeVec: m,
65 name: opts.Name,
5966 Pairs: pairsFrom(fieldKeys),
6067 }
6168 }
69
70 func (g prometheusGauge) Name() string { return g.name }
6271
6372 func (g prometheusGauge) With(f metrics.Field) metrics.Gauge {
6473 return prometheusGauge{
6574 GaugeVec: g.GaugeVec,
75 name: g.name,
6676 Pairs: merge(g.Pairs, f),
6777 }
6878 }
7383
7484 func (g prometheusGauge) Add(delta float64) {
7585 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
7691 }
7792
7893 // RegisterCallbackGauge registers a Gauge with Prometheus whose value is
85100
86101 type prometheusSummary struct {
87102 *prometheus.SummaryVec
103 name string
88104 Pairs map[string]string
89105 }
90106
98114 prometheus.MustRegister(m)
99115 return prometheusSummary{
100116 SummaryVec: m,
117 name: opts.Name,
101118 Pairs: pairsFrom(fieldKeys),
102119 }
103120 }
121
122 func (s prometheusSummary) Name() string { return s.name }
104123
105124 func (s prometheusSummary) With(f metrics.Field) metrics.Histogram {
106125 return prometheusSummary{
107126 SummaryVec: s.SummaryVec,
127 name: s.name,
108128 Pairs: merge(s.Pairs, f),
109129 }
110130 }
113133 s.SummaryVec.With(prometheus.Labels(s.Pairs)).Observe(float64(value))
114134 }
115135
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
116141 type prometheusHistogram struct {
117142 *prometheus.HistogramVec
143 name string
118144 Pairs map[string]string
119145 }
120146
128154 prometheus.MustRegister(m)
129155 return prometheusHistogram{
130156 HistogramVec: m,
157 name: opts.Name,
131158 Pairs: pairsFrom(fieldKeys),
132159 }
133160 }
161
162 func (h prometheusHistogram) Name() string { return h.name }
134163
135164 func (h prometheusHistogram) With(f metrics.Field) metrics.Histogram {
136165 return prometheusHistogram{
137166 HistogramVec: h.HistogramVec,
167 name: h.name,
138168 Pairs: merge(h.Pairs, f),
139169 }
140170 }
141171
142172 func (h prometheusHistogram) Observe(value int64) {
143173 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{}
144179 }
145180
146181 func pairsFrom(fieldKeys []string) map[string]string {
44
55 "github.com/go-kit/kit/metrics"
66 "github.com/go-kit/kit/metrics/expvar"
7 "github.com/go-kit/kit/metrics/teststat"
78 )
89
910 func TestScaledHistogram(t *testing.T) {
1819 h = metrics.NewScaledHistogram(h, scale)
1920 h = h.With(metrics.Field{Key: "a", Value: "b"})
2021
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
2324 assertExpvarNormalHistogram(t, metricName, mean/scale, stdev/scale, quantiles)
2425 }
1616 "fmt"
1717 "io"
1818 "log"
19 "math"
1920 "time"
21
22 "sync/atomic"
2023
2124 "github.com/go-kit/kit/metrics"
2225 )
2629
2730 const maxBufferSize = 1400 // bytes
2831
29 type statsdCounter chan string
32 type statsdCounter struct {
33 key string
34 c chan string
35 }
3036
3137 // NewCounter returns a Counter that emits observations in the statsd protocol
3238 // to the passed writer. Observations are buffered for the report interval or
3541 //
3642 // TODO: support for sampling.
3743 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 }
4869
4970 // NewGauge returns a Gauge that emits values in the statsd protocol to the
5071 // passed writer. Values are buffered for the report interval or until the
5374 //
5475 // TODO: support for sampling.
5576 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) {
6496 // https://github.com/etsy/statsd/blob/master/docs/metric_types.md#gauges
6597 sign := "+"
6698 if delta < 0 {
6799 sign, delta = "-", -delta
68100 }
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))
74111 }
75112
76113 // NewCallbackGauge emits values in the statsd protocol to the passed writer.
80117 // same. The callback determines the value, and fields are ignored, so
81118 // NewCallbackGauge returns nothing.
82119 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 {
87131 c := make(chan string)
88132 go func() {
89 for range tick(d) {
133 for range emitTicker {
90134 c <- fmt.Sprintf("%f|g", callback())
91135 }
92136 }()
93137 return c
94138 }
95139
96 type statsdHistogram chan string
140 type statsdHistogram struct {
141 key string
142 h chan string
143 }
97144
98145 // NewHistogram returns a Histogram that emits observations in the statsd
99146 // protocol to the passed writer. Observations are buffered for the reporting
113160 //
114161 // TODO: support for sampling.
115162 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) {
130191 buf := &bytes.Buffer{}
131 tick := tick(reportInterval)
132192 for {
133193 select {
134194 case s := <-c:
137197 flush(w, buf)
138198 }
139199
140 case <-tick:
200 case <-reportTicker:
141201 flush(w, buf)
142202 }
143203 }
00 package statsd
1
2 // In package metrics so we can stub tick.
31
42 import (
53 "bytes"
64 "fmt"
7 "runtime"
85 "strings"
6 "sync"
97 "testing"
108 "time"
119 )
1210
1311 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)
2015
2116 c.Add(1)
2217 c.Add(2)
23 ch <- time.Now()
2418
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))
3226 }
3327
3428 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)
4132
4233 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))
5043
5144 buf.Reset()
52
5345 delta = -2.0
5446 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))
6155
6256 buf.Reset()
63
6457 value := 3.0
6558 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))
7267 }
7368
7469 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)
8072 value := 55.55
8173 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)
8375
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()
8878
8979 // 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")
9385
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))
9793 }
9894
9995 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)
10699
107100 h.Observe(123)
108101
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))
115109 }
116110
117111 func by(t *testing.T, d time.Duration, check func() bool, execute func(), msg string) {
123117 execute()
124118 }
125119 }
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 }
1414 // PopulateNormalHistogram populates the Histogram with a normal distribution
1515 // of observations.
1616 func PopulateNormalHistogram(t *testing.T, h metrics.Histogram, seed int64, mean, stdev int64) {
17 rand.Seed(seed)
17 r := rand.New(rand.NewSource(seed))
1818 for i := 0; i < population; i++ {
19 sample := int64(rand.NormFloat64()*float64(stdev) + float64(mean))
19 sample := int64(r.NormFloat64()*float64(stdev) + float64(mean))
2020 if sample < 0 {
2121 sample = 0
2222 }
2020 )
2121
2222 const seed, mean, stdev int64 = 321, 100, 20
23 r := rand.New(rand.NewSource(seed))
2324
2425 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
2627 th.Observe(sample)
2728 }
2829
1515 Bourgon). Once we reach a usable MVP, i.e. semantic version 1.0.0, I hope we'll
1616 transition to a more community-driven governance model.
1717
18 For questions and free-form discussion, please use IRC or the mailing list.
18 For questions and free-form discussion, please use
1919
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
44
55 [Dapper]: http://research.google.com/pubs/pub36356.html
66 [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
88
99 ## Rationale
1010
2525 spanID int64
2626 parentSpanID int64
2727
28 annotations []annotation
29 //binaryAnnotations []BinaryAnnotation // TODO
28 annotations []annotation
29 binaryAnnotations []binaryAnnotation
3030 }
3131
3232 // NewSpan returns a new Span, which can be annotated and collected by a
9393 s.AnnotateDuration(value, 0)
9494 }
9595
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
96116 // AnnotateDuration annotates the span with the given value and duration.
97117 func (s *Span) AnnotateDuration(value string, duration time.Duration) {
98118 s.annotations = append(s.annotations, annotation{
108128 // TODO lots of garbage here. We can improve by preallocating e.g. the
109129 // Thrift stuff into an encoder struct, owned by the ScribeCollector.
110130 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
116135 }
136
117137 if s.parentSpanID != 0 {
118138 zs.ParentId = new(int64)
119139 (*zs.ParentId) = s.parentSpanID
120140 }
141
121142 zs.Annotations = make([]*zipkincore.Annotation, len(s.annotations))
122143 for i, a := range s.annotations {
123144 zs.Annotations[i] = &zipkincore.Annotation{
125146 Value: a.value,
126147 Host: a.host,
127148 }
149
128150 if a.duration > 0 {
129151 zs.Annotations[i].Duration = new(int32)
130152 *(zs.Annotations[i].Duration) = int32(a.duration / time.Microsecond)
131153 }
132154 }
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
133166 return &zs
134167 }
135168
139172 duration time.Duration // optional
140173 host *zipkincore.Endpoint
141174 }
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 }
55 "strconv"
66
77 "golang.org/x/net/context"
8 "google.golang.org/grpc/metadata"
89
910 "github.com/go-kit/kit/endpoint"
1011 "github.com/go-kit/kit/log"
12 "github.com/go-kit/kit/transport/grpc"
1113 )
1214
1315 // In Zipkin, "spans are considered to start and stop with the client." The
2628 traceIDHTTPHeader = "X-B3-TraceId"
2729 spanIDHTTPHeader = "X-B3-SpanId"
2830 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"
2935
3036 // ClientSend is the annotation value used to mark a client sending a
3137 // request to a server.
97103 }
98104 }
99105
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
100116 // ToRequest returns a function that satisfies transport/http.BeforeFunc. It
101117 // takes a Zipkin span from the context, and injects it into the HTTP request.
102118 // It's designed to be wired into a client's HTTP transport Before stack. It's
121137 }
122138 }
123139
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
124167 func fromHTTP(newSpan NewSpanFunc, r *http.Request, logger log.Logger) *Span {
125168 traceIDStr := r.Header.Get(traceIDHTTPHeader)
126169 if traceIDStr == "" {
148191 parentSpanID, err := strconv.ParseInt(parentSpanIDStr, 16, 64)
149192 if err != nil {
150193 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
151237 parentSpanID = 0 // the only way to deal with it
152238 }
153239 return newSpan(traceID, spanID, parentSpanID)
1010 "testing"
1111
1212 "golang.org/x/net/context"
13 "google.golang.org/grpc/metadata"
1314
1415 "github.com/go-kit/kit/endpoint"
1516 "github.com/go-kit/kit/log"
5960 }
6061 }
6162
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
62107 func TestToRequest(t *testing.T) {
63108 const (
64109 hostport = "5.5.5.5:5555"
86131 }
87132 }
88133
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
89161 func TestAnnotateServer(t *testing.T) {
90162 if err := testAnnotate(zipkin.AnnotateServer, zipkin.ServerReceive, zipkin.ServerSend); err != nil {
91163 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 }
2121 bufferedStream bool
2222 }
2323
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 {
2632 c := &Client{
2733 client: http.DefaultClient,
2834 method: method,