├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yaml │ ├── config.yml │ └── feature_request.yaml └── workflows │ ├── .editorconfig │ └── ci.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── auth ├── basic │ ├── README.md │ ├── middleware.go │ └── middleware_test.go ├── casbin │ ├── middleware.go │ ├── middleware_test.go │ └── testdata │ │ ├── basic_model.conf │ │ ├── basic_policy.csv │ │ └── keymatch_policy.csv └── jwt │ ├── README.md │ ├── middleware.go │ ├── middleware_test.go │ ├── transport.go │ └── transport_test.go ├── circuitbreaker ├── doc.go ├── gobreaker.go ├── gobreaker_test.go ├── handy_breaker.go ├── handy_breaker_test.go ├── hystrix.go ├── hystrix_test.go └── util_test.go ├── codecov.yml ├── docker-compose-integration.yml ├── endpoint ├── doc.go ├── endpoint.go └── endpoint_example_test.go ├── examples └── README.md ├── go.mod ├── go.sum ├── lint ├── log ├── README.md ├── deprecated_levels │ ├── levels.go │ └── levels_test.go ├── doc.go ├── example_test.go ├── json_logger.go ├── level │ ├── doc.go │ ├── example_test.go │ └── level.go ├── log.go ├── logfmt_logger.go ├── logrus │ ├── logrus_logger.go │ └── logrus_logger_test.go ├── nop_logger.go ├── stdlib.go ├── sync.go ├── syslog │ ├── example_test.go │ └── syslog.go ├── term │ ├── colorlogger.go │ ├── colorwriter.go │ ├── example_test.go │ └── term.go ├── value.go └── zap │ ├── zap_sugar_logger.go │ └── zap_sugar_logger_test.go ├── metrics ├── README.md ├── cloudwatch │ ├── cloudwatch.go │ └── cloudwatch_test.go ├── cloudwatch2 │ ├── cloudwatch2.go │ └── cloudwatch2_test.go ├── discard │ └── discard.go ├── doc.go ├── dogstatsd │ ├── dogstatsd.go │ └── dogstatsd_test.go ├── expvar │ ├── expvar.go │ └── expvar_test.go ├── generic │ ├── generic.go │ └── generic_test.go ├── graphite │ ├── graphite.go │ └── graphite_test.go ├── influx │ ├── example_test.go │ ├── influx.go │ └── influx_test.go ├── influxstatsd │ ├── influxstatsd.go │ └── influxstatsd_test.go ├── internal │ ├── convert │ │ ├── convert.go │ │ └── convert_test.go │ ├── lv │ │ ├── labelvalues.go │ │ ├── labelvalues_test.go │ │ ├── space.go │ │ └── space_test.go │ └── ratemap │ │ └── ratemap.go ├── metrics.go ├── multi │ ├── multi.go │ └── multi_test.go ├── pcp │ ├── pcp.go │ └── pcp_test.go ├── prometheus │ ├── prometheus.go │ └── prometheus_test.go ├── provider │ ├── discard.go │ ├── dogstatsd.go │ ├── expvar.go │ ├── graphite.go │ ├── influx.go │ ├── prometheus.go │ ├── provider.go │ └── statsd.go ├── statsd │ ├── statsd.go │ └── statsd_test.go ├── teststat │ ├── buffers.go │ ├── populate.go │ └── teststat.go ├── timer.go └── timer_test.go ├── ratelimit ├── token_bucket.go └── token_bucket_test.go ├── sd ├── benchmark_test.go ├── consul │ ├── client.go │ ├── client_test.go │ ├── doc.go │ ├── instancer.go │ ├── instancer_test.go │ ├── integration_test.go │ ├── registrar.go │ └── registrar_test.go ├── dnssrv │ ├── doc.go │ ├── instancer.go │ ├── instancer_test.go │ └── lookup.go ├── doc.go ├── endpoint_cache.go ├── endpoint_cache_test.go ├── endpointer.go ├── endpointer_test.go ├── etcd │ ├── client.go │ ├── client_test.go │ ├── doc.go │ ├── example_test.go │ ├── instancer.go │ ├── instancer_test.go │ ├── integration_test.go │ ├── registrar.go │ └── registrar_test.go ├── etcdv3 │ ├── client.go │ ├── client_test.go │ ├── doc.go │ ├── example_test.go │ ├── instancer.go │ ├── instancer_test.go │ ├── integration_test.go │ ├── registrar.go │ └── registrar_test.go ├── eureka │ ├── doc.go │ ├── instancer.go │ ├── instancer_test.go │ ├── integration_test.go │ ├── registrar.go │ ├── registrar_test.go │ └── util_test.go ├── factory.go ├── instancer.go ├── internal │ └── instance │ │ ├── cache.go │ │ └── cache_test.go ├── lb │ ├── balancer.go │ ├── doc.go │ ├── random.go │ ├── random_test.go │ ├── retry.go │ ├── retry_test.go │ ├── round_robin.go │ └── round_robin_test.go ├── registrar.go └── zk │ ├── client.go │ ├── client_test.go │ ├── doc.go │ ├── instancer.go │ ├── instancer_test.go │ ├── integration_test.go │ ├── logwrapper.go │ ├── registrar.go │ └── util_test.go ├── tracing ├── README.md ├── doc.go ├── opencensus │ ├── doc.go │ ├── endpoint.go │ ├── endpoint_options.go │ ├── endpoint_test.go │ ├── grpc.go │ ├── grpc_test.go │ ├── http.go │ ├── http_test.go │ ├── jsonrpc.go │ ├── jsonrpc_test.go │ ├── opencensus_test.go │ └── tracer_options.go ├── opentracing │ ├── doc.go │ ├── endpoint.go │ ├── endpoint_options.go │ ├── endpoint_test.go │ ├── grpc.go │ ├── grpc_test.go │ ├── http.go │ └── http_test.go └── zipkin │ ├── README.md │ ├── doc.go │ ├── endpoint.go │ ├── endpoint_test.go │ ├── grpc.go │ ├── grpc_test.go │ ├── http.go │ ├── http_test.go │ └── options.go ├── transport ├── amqp │ ├── doc.go │ ├── encode_decode.go │ ├── publisher.go │ ├── publisher_test.go │ ├── request_response_func.go │ ├── subscriber.go │ ├── subscriber_test.go │ └── util.go ├── awslambda │ ├── doc.go │ ├── encode_decode.go │ ├── handler.go │ ├── handler_test.go │ └── request_response_funcs.go ├── doc.go ├── error_handler.go ├── error_handler_test.go ├── grpc │ ├── README.md │ ├── _grpc_test │ │ ├── client.go │ │ ├── context_metadata.go │ │ ├── pb │ │ │ ├── generate.go │ │ │ ├── test.pb.go │ │ │ ├── test.proto │ │ │ └── test_grpc.pb.go │ │ ├── request_response.go │ │ ├── server.go │ │ └── service.go │ ├── client.go │ ├── client_test.go │ ├── doc.go │ ├── encode_decode.go │ ├── request_response_funcs.go │ └── server.go ├── http │ ├── client.go │ ├── client_test.go │ ├── doc.go │ ├── encode_decode.go │ ├── example_test.go │ ├── intercepting_writer.go │ ├── intercepting_writer_test.go │ ├── jsonrpc │ │ ├── README.md │ │ ├── client.go │ │ ├── client_test.go │ │ ├── doc.go │ │ ├── encode_decode.go │ │ ├── error.go │ │ ├── error_test.go │ │ ├── request_response_types.go │ │ ├── request_response_types_test.go │ │ ├── server.go │ │ └── server_test.go │ ├── proto │ │ ├── client.go │ │ ├── generate.go │ │ ├── proto_pb_test.go │ │ ├── proto_test.go │ │ ├── proto_test.proto │ │ └── server.go │ ├── request_response_funcs.go │ ├── request_response_funcs_test.go │ ├── server.go │ └── server_test.go ├── httprp │ ├── doc.go │ ├── server.go │ └── server_test.go ├── nats │ ├── doc.go │ ├── encode_decode.go │ ├── publisher.go │ ├── publisher_test.go │ ├── request_response_funcs.go │ ├── subscriber.go │ └── subscriber_test.go ├── netrpc │ └── README.md └── thrift │ └── README.md └── util ├── README.md └── conn ├── doc.go ├── manager.go └── manager_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [peterbourgon] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Report a bug 3 | labels: [bug] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: What did you do? 8 | validations: 9 | required: true 10 | - type: textarea 11 | attributes: 12 | label: What did you expect? 13 | validations: 14 | required: true 15 | - type: textarea 16 | attributes: 17 | label: What happened instead? 18 | validations: 19 | required: true 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a question 4 | url: https://github.com/go-kit/kit/discussions/new?category=q-a 5 | about: Questions and discussions with the Go kit community 6 | 7 | - name: Website 8 | url: https://gokit.io/ 9 | about: Project overview, examples, frequently asked questions, etc. 10 | 11 | - name: Reference 12 | url: https://pkg.go.dev/github.com/go-kit/kit 13 | about: Go kit package documentation 14 | 15 | - name: Slack channel 16 | url: https://gophers.slack.com/messages/go-kit 17 | about: Real-time discussions and Q&A 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest new functionality or an enhancement 3 | body: 4 | - type: textarea 5 | attributes: 6 | label: What would you like? 7 | validations: 8 | required: true 9 | -------------------------------------------------------------------------------- /.github/workflows/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.yml] 2 | indent_size = 2 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: # Support latest and one minor back 15 | go: ["1.17", "1.18", "1.19"] 16 | env: 17 | GOFLAGS: -mod=readonly 18 | 19 | services: 20 | etcd: 21 | image: gcr.io/etcd-development/etcd:v3.5.0 22 | ports: 23 | - 2379 24 | env: 25 | ETCD_LISTEN_CLIENT_URLS: http://0.0.0.0:2379 26 | ETCD_ADVERTISE_CLIENT_URLS: http://0.0.0.0:2379 27 | options: --health-cmd "ETCDCTL_API=3 etcdctl --endpoints http://localhost:2379 endpoint health" --health-interval 10s --health-timeout 5s --health-retries 5 28 | 29 | consul: 30 | image: consul:1.10 31 | ports: 32 | - 8500 33 | 34 | zk: 35 | image: zookeeper:3.5 36 | ports: 37 | - 2181 38 | 39 | eureka: 40 | image: springcloud/eureka 41 | ports: 42 | - 8761 43 | env: 44 | eureka.server.responseCacheUpdateIntervalMs: 1000 45 | 46 | steps: 47 | - name: Set up Go 48 | uses: actions/setup-go@v2.1.3 49 | with: 50 | stable: "false" 51 | go-version: ${{ matrix.go }} 52 | 53 | - name: Checkout code 54 | uses: actions/checkout@v2 55 | 56 | - name: Run tests 57 | env: 58 | ETCD_ADDR: http://localhost:${{ job.services.etcd.ports[2379] }} 59 | CONSUL_ADDR: localhost:${{ job.services.consul.ports[8500] }} 60 | ZK_ADDR: localhost:${{ job.services.zk.ports[2181] }} 61 | EUREKA_ADDR: http://localhost:${{ job.services.eureka.ports[8761] }}/eureka 62 | run: go test -v -race -coverprofile=coverage.coverprofile -covermode=atomic -tags integration ./... 63 | 64 | - name: Upload coverage 65 | uses: codecov/codecov-action@v1 66 | with: 67 | token: ${{ secrets.CODECOV_TOKEN }} 68 | file: coverage.coverprofile 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.coverprofile 2 | 3 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 4 | *.o 5 | *.a 6 | *.so 7 | 8 | # Folders 9 | _obj 10 | _test 11 | _old* 12 | 13 | # Architecture specific extensions/prefixes 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | 23 | _testmain.go 24 | 25 | *.exe 26 | 27 | # https://github.com/github/gitignore/blob/master/Global/Vim.gitignore 28 | # swap 29 | [._]*.s[a-w][a-z] 30 | [._]s[a-w][a-z] 31 | # session 32 | Session.vim 33 | # temporary 34 | .netrwhist 35 | *~ 36 | # auto-generated tag files 37 | tags 38 | 39 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First, thank you for contributing! We love and encourage pull requests from everyone. 4 | 5 | Before submitting major changes, here are a few guidelines to follow: 6 | 7 | 1. Check the [open issues][issues] and [pull requests][prs] for existing discussions. 8 | 1. Open an [issue][issues] first, to discuss a new feature or enhancement. 9 | 1. Write tests, and make sure the test suite passes locally and on CI. 10 | 1. Open a pull request, and reference the relevant issue(s). 11 | 1. After receiving feedback, [squash your commits][squash] and add a [great commit message][message]. 12 | 1. Have fun! 13 | 14 | [issues]: https://github.com/go-kit/kit/issues 15 | [prs]: https://github.com/go-kit/kit/pulls 16 | [squash]: http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html 17 | [message]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Peter Bourgon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /auth/basic/README.md: -------------------------------------------------------------------------------- 1 | This package provides a Basic Authentication middleware. 2 | 3 | It'll try to compare credentials from Authentication request header to a username/password pair in middleware constructor. 4 | 5 | More details about this type of authentication can be found in [Mozilla article](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). 6 | 7 | ## Usage 8 | 9 | ```go 10 | import httptransport "github.com/go-kit/kit/transport/http" 11 | 12 | httptransport.NewServer( 13 | AuthMiddleware(cfg.auth.user, cfg.auth.password, "Example Realm")(makeUppercaseEndpoint()), 14 | decodeMappingsRequest, 15 | httptransport.EncodeJSONResponse, 16 | httptransport.ServerBefore(httptransport.PopulateRequestContext), 17 | ) 18 | ``` 19 | 20 | For AuthMiddleware to be able to pick up the Authentication header from an HTTP request we need to pass it through the context with something like ```httptransport.ServerBefore(httptransport.PopulateRequestContext)```. -------------------------------------------------------------------------------- /auth/basic/middleware.go: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/sha256" 7 | "crypto/subtle" 8 | "encoding/base64" 9 | "fmt" 10 | "net/http" 11 | "strings" 12 | 13 | "github.com/go-kit/kit/endpoint" 14 | httptransport "github.com/go-kit/kit/transport/http" 15 | ) 16 | 17 | // AuthError represents an authorization error. 18 | type AuthError struct { 19 | Realm string 20 | } 21 | 22 | // StatusCode is an implementation of the StatusCoder interface in go-kit/http. 23 | func (AuthError) StatusCode() int { 24 | return http.StatusUnauthorized 25 | } 26 | 27 | // Error is an implementation of the Error interface. 28 | func (AuthError) Error() string { 29 | return http.StatusText(http.StatusUnauthorized) 30 | } 31 | 32 | // Headers is an implementation of the Headerer interface in go-kit/http. 33 | func (e AuthError) Headers() http.Header { 34 | return http.Header{ 35 | "Content-Type": []string{"text/plain; charset=utf-8"}, 36 | "X-Content-Type-Options": []string{"nosniff"}, 37 | "WWW-Authenticate": []string{fmt.Sprintf(`Basic realm=%q`, e.Realm)}, 38 | } 39 | } 40 | 41 | // parseBasicAuth parses an HTTP Basic Authentication string. 42 | // "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ([]byte("Aladdin"), []byte("open sesame"), true). 43 | func parseBasicAuth(auth string) (username, password []byte, ok bool) { 44 | const prefix = "Basic " 45 | if !strings.HasPrefix(auth, prefix) { 46 | return 47 | } 48 | c, err := base64.StdEncoding.DecodeString(auth[len(prefix):]) 49 | if err != nil { 50 | return 51 | } 52 | 53 | s := bytes.IndexByte(c, ':') 54 | if s < 0 { 55 | return 56 | } 57 | return c[:s], c[s+1:], true 58 | } 59 | 60 | // Returns a hash of a given slice. 61 | func toHashSlice(s []byte) []byte { 62 | hash := sha256.Sum256(s) 63 | return hash[:] 64 | } 65 | 66 | // AuthMiddleware returns a Basic Authentication middleware for a particular user and password. 67 | func AuthMiddleware(requiredUser, requiredPassword, realm string) endpoint.Middleware { 68 | requiredUserBytes := toHashSlice([]byte(requiredUser)) 69 | requiredPasswordBytes := toHashSlice([]byte(requiredPassword)) 70 | 71 | return func(next endpoint.Endpoint) endpoint.Endpoint { 72 | return func(ctx context.Context, request interface{}) (interface{}, error) { 73 | auth, ok := ctx.Value(httptransport.ContextKeyRequestAuthorization).(string) 74 | if !ok { 75 | return nil, AuthError{realm} 76 | } 77 | 78 | givenUser, givenPassword, ok := parseBasicAuth(auth) 79 | if !ok { 80 | return nil, AuthError{realm} 81 | } 82 | 83 | givenUserBytes := toHashSlice(givenUser) 84 | givenPasswordBytes := toHashSlice(givenPassword) 85 | 86 | if subtle.ConstantTimeCompare(givenUserBytes, requiredUserBytes) == 0 || 87 | subtle.ConstantTimeCompare(givenPasswordBytes, requiredPasswordBytes) == 0 { 88 | return nil, AuthError{realm} 89 | } 90 | 91 | return next(ctx, request) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /auth/basic/middleware_test.go: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "fmt" 7 | "testing" 8 | 9 | httptransport "github.com/go-kit/kit/transport/http" 10 | ) 11 | 12 | func TestWithBasicAuth(t *testing.T) { 13 | requiredUser := "test-user" 14 | requiredPassword := "test-pass" 15 | realm := "test realm" 16 | 17 | type want struct { 18 | result interface{} 19 | err error 20 | } 21 | tests := []struct { 22 | name string 23 | authHeader interface{} 24 | want want 25 | }{ 26 | {"Isn't valid with nil header", nil, want{nil, AuthError{realm}}}, 27 | {"Isn't valid with non-string header", 42, want{nil, AuthError{realm}}}, 28 | {"Isn't valid without authHeader", "", want{nil, AuthError{realm}}}, 29 | {"Isn't valid for wrong user", makeAuthString("wrong-user", requiredPassword), want{nil, AuthError{realm}}}, 30 | {"Isn't valid for wrong password", makeAuthString(requiredUser, "wrong-password"), want{nil, AuthError{realm}}}, 31 | {"Is valid for correct creds", makeAuthString(requiredUser, requiredPassword), want{true, nil}}, 32 | } 33 | for _, tt := range tests { 34 | t.Run(tt.name, func(t *testing.T) { 35 | ctx := context.WithValue(context.TODO(), httptransport.ContextKeyRequestAuthorization, tt.authHeader) 36 | 37 | result, err := AuthMiddleware(requiredUser, requiredPassword, realm)(passedValidation)(ctx, nil) 38 | if result != tt.want.result || err != tt.want.err { 39 | t.Errorf("WithBasicAuth() = result: %v, err: %v, want result: %v, want error: %v", result, err, tt.want.result, tt.want.err) 40 | } 41 | }) 42 | } 43 | } 44 | 45 | func makeAuthString(user string, password string) string { 46 | data := []byte(fmt.Sprintf("%s:%s", user, password)) 47 | return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(data)) 48 | } 49 | 50 | func passedValidation(ctx context.Context, request interface{}) (response interface{}, err error) { 51 | return true, nil 52 | } 53 | -------------------------------------------------------------------------------- /auth/casbin/middleware.go: -------------------------------------------------------------------------------- 1 | package casbin 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | stdcasbin "github.com/casbin/casbin/v2" 8 | "github.com/go-kit/kit/endpoint" 9 | ) 10 | 11 | type contextKey string 12 | 13 | const ( 14 | // CasbinModelContextKey holds the key to store the access control model 15 | // in context, it can be a path to configuration file or a casbin/model 16 | // Model. 17 | CasbinModelContextKey contextKey = "CasbinModel" 18 | 19 | // CasbinPolicyContextKey holds the key to store the access control policy 20 | // in context, it can be a path to policy file or an implementation of 21 | // casbin/persist Adapter interface. 22 | CasbinPolicyContextKey contextKey = "CasbinPolicy" 23 | 24 | // CasbinEnforcerContextKey holds the key to retrieve the active casbin 25 | // Enforcer. 26 | CasbinEnforcerContextKey contextKey = "CasbinEnforcer" 27 | ) 28 | 29 | var ( 30 | // ErrModelContextMissing denotes a casbin model was not passed into 31 | // the parsing of middleware's context. 32 | ErrModelContextMissing = errors.New("CasbinModel is required in context") 33 | 34 | // ErrPolicyContextMissing denotes a casbin policy was not passed into 35 | // the parsing of middleware's context. 36 | ErrPolicyContextMissing = errors.New("CasbinPolicy is required in context") 37 | 38 | // ErrUnauthorized denotes the subject is not authorized to do the action 39 | // intended on the given object, based on the context model and policy. 40 | ErrUnauthorized = errors.New("Unauthorized Access") 41 | ) 42 | 43 | // NewEnforcer checks whether the subject is authorized to do the specified 44 | // action on the given object. If a valid access control model and policy 45 | // is given, then the generated casbin Enforcer is stored in the context 46 | // with CasbinEnforcer as the key. 47 | func NewEnforcer( 48 | subject string, object interface{}, action string, 49 | ) endpoint.Middleware { 50 | return func(next endpoint.Endpoint) endpoint.Endpoint { 51 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { 52 | casbinModel := ctx.Value(CasbinModelContextKey) 53 | casbinPolicy := ctx.Value(CasbinPolicyContextKey) 54 | enforcer, err := stdcasbin.NewEnforcer(casbinModel, casbinPolicy) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | ctx = context.WithValue(ctx, CasbinEnforcerContextKey, enforcer) 60 | ok, err := enforcer.Enforce(subject, object, action) 61 | if err != nil { 62 | return nil, err 63 | } 64 | if !ok { 65 | return nil, ErrUnauthorized 66 | } 67 | 68 | return next(ctx, request) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /auth/casbin/middleware_test.go: -------------------------------------------------------------------------------- 1 | package casbin 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | stdcasbin "github.com/casbin/casbin/v2" 8 | "github.com/casbin/casbin/v2/model" 9 | fileadapter "github.com/casbin/casbin/v2/persist/file-adapter" 10 | ) 11 | 12 | func TestStructBaseContext(t *testing.T) { 13 | e := func(ctx context.Context, i interface{}) (interface{}, error) { return ctx, nil } 14 | 15 | m := model.NewModel() 16 | m.AddDef("r", "r", "sub, obj, act") 17 | m.AddDef("p", "p", "sub, obj, act") 18 | m.AddDef("e", "e", "some(where (p.eft == allow))") 19 | m.AddDef("m", "m", "r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)") 20 | 21 | a := fileadapter.NewAdapter("testdata/keymatch_policy.csv") 22 | 23 | ctx := context.WithValue(context.Background(), CasbinModelContextKey, m) 24 | ctx = context.WithValue(ctx, CasbinPolicyContextKey, a) 25 | 26 | // positive case 27 | middleware := NewEnforcer("alice", "/alice_data/resource1", "GET")(e) 28 | ctx1, err := middleware(ctx, struct{}{}) 29 | if err != nil { 30 | t.Fatalf("Enforcer returned error: %s", err) 31 | } 32 | _, ok := ctx1.(context.Context).Value(CasbinEnforcerContextKey).(*stdcasbin.Enforcer) 33 | if !ok { 34 | t.Fatalf("context should contains the active enforcer") 35 | } 36 | 37 | // negative case 38 | middleware = NewEnforcer("alice", "/alice_data/resource2", "POST")(e) 39 | _, err = middleware(ctx, struct{}{}) 40 | if err == nil { 41 | t.Fatalf("Enforcer should return error") 42 | } 43 | } 44 | 45 | func TestFileBaseContext(t *testing.T) { 46 | e := func(ctx context.Context, i interface{}) (interface{}, error) { return ctx, nil } 47 | ctx := context.WithValue(context.Background(), CasbinModelContextKey, "testdata/basic_model.conf") 48 | ctx = context.WithValue(ctx, CasbinPolicyContextKey, "testdata/basic_policy.csv") 49 | 50 | // positive case 51 | middleware := NewEnforcer("alice", "data1", "read")(e) 52 | _, err := middleware(ctx, struct{}{}) 53 | if err != nil { 54 | t.Fatalf("Enforcer returned error: %s", err) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /auth/casbin/testdata/basic_model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, obj, act 3 | 4 | [policy_definition] 5 | p = sub, obj, act 6 | 7 | [policy_effect] 8 | e = some(where (p.eft == allow)) 9 | 10 | [matchers] 11 | m = r.sub == p.sub && r.obj == p.obj && r.act == p.act -------------------------------------------------------------------------------- /auth/casbin/testdata/basic_policy.csv: -------------------------------------------------------------------------------- 1 | p, alice, data1, read 2 | p, bob, data2, write -------------------------------------------------------------------------------- /auth/casbin/testdata/keymatch_policy.csv: -------------------------------------------------------------------------------- 1 | p, alice, /alice_data/*, GET 2 | p, alice, /alice_data/resource1, POST 3 | 4 | p, bob, /alice_data/resource2, GET 5 | p, bob, /bob_data/*, POST 6 | 7 | p, cathy, /cathy_data, (GET)|(POST) -------------------------------------------------------------------------------- /auth/jwt/transport.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | stdhttp "net/http" 7 | "strings" 8 | 9 | "google.golang.org/grpc/metadata" 10 | 11 | "github.com/go-kit/kit/transport/grpc" 12 | "github.com/go-kit/kit/transport/http" 13 | ) 14 | 15 | const ( 16 | bearer string = "bearer" 17 | bearerFormat string = "Bearer %s" 18 | ) 19 | 20 | // HTTPToContext moves a JWT from request header to context. Particularly 21 | // useful for servers. 22 | func HTTPToContext() http.RequestFunc { 23 | return func(ctx context.Context, r *stdhttp.Request) context.Context { 24 | token, ok := extractTokenFromAuthHeader(r.Header.Get("Authorization")) 25 | if !ok { 26 | return ctx 27 | } 28 | 29 | return context.WithValue(ctx, JWTContextKey, token) 30 | } 31 | } 32 | 33 | // ContextToHTTP moves a JWT from context to request header. Particularly 34 | // useful for clients. 35 | func ContextToHTTP() http.RequestFunc { 36 | return func(ctx context.Context, r *stdhttp.Request) context.Context { 37 | token, ok := ctx.Value(JWTContextKey).(string) 38 | if ok { 39 | r.Header.Add("Authorization", generateAuthHeaderFromToken(token)) 40 | } 41 | return ctx 42 | } 43 | } 44 | 45 | // GRPCToContext moves a JWT from grpc metadata to context. Particularly 46 | // userful for servers. 47 | func GRPCToContext() grpc.ServerRequestFunc { 48 | return func(ctx context.Context, md metadata.MD) context.Context { 49 | // capital "Key" is illegal in HTTP/2. 50 | authHeader, ok := md["authorization"] 51 | if !ok { 52 | return ctx 53 | } 54 | 55 | token, ok := extractTokenFromAuthHeader(authHeader[0]) 56 | if ok { 57 | ctx = context.WithValue(ctx, JWTContextKey, token) 58 | } 59 | 60 | return ctx 61 | } 62 | } 63 | 64 | // ContextToGRPC moves a JWT from context to grpc metadata. Particularly 65 | // useful for clients. 66 | func ContextToGRPC() grpc.ClientRequestFunc { 67 | return func(ctx context.Context, md *metadata.MD) context.Context { 68 | token, ok := ctx.Value(JWTContextKey).(string) 69 | if ok { 70 | // capital "Key" is illegal in HTTP/2. 71 | (*md)["authorization"] = []string{generateAuthHeaderFromToken(token)} 72 | } 73 | 74 | return ctx 75 | } 76 | } 77 | 78 | func extractTokenFromAuthHeader(val string) (token string, ok bool) { 79 | authHeaderParts := strings.Split(val, " ") 80 | if len(authHeaderParts) != 2 || !strings.EqualFold(authHeaderParts[0], bearer) { 81 | return "", false 82 | } 83 | 84 | return authHeaderParts[1], true 85 | } 86 | 87 | func generateAuthHeaderFromToken(token string) string { 88 | return fmt.Sprintf(bearerFormat, token) 89 | } 90 | -------------------------------------------------------------------------------- /circuitbreaker/doc.go: -------------------------------------------------------------------------------- 1 | // Package circuitbreaker implements the circuit breaker pattern. 2 | // 3 | // Circuit breakers prevent thundering herds, and improve resiliency against 4 | // intermittent errors. Every client-side endpoint should be wrapped in a 5 | // circuit breaker. 6 | // 7 | // We provide several implementations in this package, but if you're looking 8 | // for guidance, Gobreaker is probably the best place to start. It has a 9 | // simple and intuitive API, and is well-tested. 10 | package circuitbreaker 11 | -------------------------------------------------------------------------------- /circuitbreaker/gobreaker.go: -------------------------------------------------------------------------------- 1 | package circuitbreaker 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/sony/gobreaker" 7 | 8 | "github.com/go-kit/kit/endpoint" 9 | ) 10 | 11 | // Gobreaker returns an endpoint.Middleware that implements the circuit 12 | // breaker pattern using the sony/gobreaker package. Only errors returned by 13 | // the wrapped endpoint count against the circuit breaker's error count. 14 | // 15 | // See http://godoc.org/github.com/sony/gobreaker for more information. 16 | func Gobreaker(cb *gobreaker.CircuitBreaker) endpoint.Middleware { 17 | return func(next endpoint.Endpoint) endpoint.Endpoint { 18 | return func(ctx context.Context, request interface{}) (interface{}, error) { 19 | return cb.Execute(func() (interface{}, error) { return next(ctx, request) }) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /circuitbreaker/gobreaker_test.go: -------------------------------------------------------------------------------- 1 | package circuitbreaker_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/sony/gobreaker" 7 | 8 | "github.com/go-kit/kit/circuitbreaker" 9 | ) 10 | 11 | func TestGobreaker(t *testing.T) { 12 | var ( 13 | breaker = circuitbreaker.Gobreaker(gobreaker.NewCircuitBreaker(gobreaker.Settings{})) 14 | primeWith = 100 15 | shouldPass = func(n int) bool { return n <= 5 } // https://github.com/sony/gobreaker/blob/bfa846d/gobreaker.go#L76 16 | circuitOpenError = "circuit breaker is open" 17 | ) 18 | testFailingEndpoint(t, breaker, primeWith, shouldPass, 0, circuitOpenError) 19 | } 20 | -------------------------------------------------------------------------------- /circuitbreaker/handy_breaker.go: -------------------------------------------------------------------------------- 1 | package circuitbreaker 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/streadway/handy/breaker" 8 | 9 | "github.com/go-kit/kit/endpoint" 10 | ) 11 | 12 | // HandyBreaker returns an endpoint.Middleware that implements the circuit 13 | // breaker pattern using the streadway/handy/breaker package. Only errors 14 | // returned by the wrapped endpoint count against the circuit breaker's error 15 | // count. 16 | // 17 | // See http://godoc.org/github.com/streadway/handy/breaker for more 18 | // information. 19 | func HandyBreaker(cb breaker.Breaker) endpoint.Middleware { 20 | return func(next endpoint.Endpoint) endpoint.Endpoint { 21 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { 22 | if !cb.Allow() { 23 | return nil, breaker.ErrCircuitOpen 24 | } 25 | 26 | defer func(begin time.Time) { 27 | if err == nil { 28 | cb.Success(time.Since(begin)) 29 | } else { 30 | cb.Failure(time.Since(begin)) 31 | } 32 | }(time.Now()) 33 | 34 | response, err = next(ctx, request) 35 | return 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /circuitbreaker/handy_breaker_test.go: -------------------------------------------------------------------------------- 1 | package circuitbreaker_test 2 | 3 | import ( 4 | "testing" 5 | 6 | handybreaker "github.com/streadway/handy/breaker" 7 | 8 | "github.com/go-kit/kit/circuitbreaker" 9 | ) 10 | 11 | func TestHandyBreaker(t *testing.T) { 12 | var ( 13 | failureRatio = 0.05 14 | breaker = circuitbreaker.HandyBreaker(handybreaker.NewBreaker(failureRatio)) 15 | primeWith = handybreaker.DefaultMinObservations * 10 16 | shouldPass = func(n int) bool { return (float64(n) / float64(primeWith+n)) <= failureRatio } 17 | openCircuitError = handybreaker.ErrCircuitOpen.Error() 18 | ) 19 | testFailingEndpoint(t, breaker, primeWith, shouldPass, 0, openCircuitError) 20 | } 21 | -------------------------------------------------------------------------------- /circuitbreaker/hystrix.go: -------------------------------------------------------------------------------- 1 | package circuitbreaker 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/afex/hystrix-go/hystrix" 7 | 8 | "github.com/go-kit/kit/endpoint" 9 | ) 10 | 11 | // Hystrix returns an endpoint.Middleware that implements the circuit 12 | // breaker pattern using the afex/hystrix-go package. 13 | // 14 | // When using this circuit breaker, please configure your commands separately. 15 | // 16 | // See https://godoc.org/github.com/afex/hystrix-go/hystrix for more 17 | // information. 18 | func Hystrix(commandName string) endpoint.Middleware { 19 | return func(next endpoint.Endpoint) endpoint.Endpoint { 20 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { 21 | var resp interface{} 22 | if err := hystrix.Do(commandName, func() (err error) { 23 | resp, err = next(ctx, request) 24 | return err 25 | }, nil); err != nil { 26 | return nil, err 27 | } 28 | return resp, nil 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /circuitbreaker/hystrix_test.go: -------------------------------------------------------------------------------- 1 | package circuitbreaker_test 2 | 3 | import ( 4 | "io/ioutil" 5 | stdlog "log" 6 | "testing" 7 | "time" 8 | 9 | "github.com/afex/hystrix-go/hystrix" 10 | 11 | "github.com/go-kit/kit/circuitbreaker" 12 | ) 13 | 14 | func TestHystrix(t *testing.T) { 15 | stdlog.SetOutput(ioutil.Discard) 16 | 17 | const ( 18 | commandName = "my-endpoint" 19 | errorPercent = 5 20 | maxConcurrent = 1000 21 | ) 22 | hystrix.ConfigureCommand(commandName, hystrix.CommandConfig{ 23 | ErrorPercentThreshold: errorPercent, 24 | MaxConcurrentRequests: maxConcurrent, 25 | }) 26 | 27 | var ( 28 | breaker = circuitbreaker.Hystrix(commandName) 29 | primeWith = hystrix.DefaultVolumeThreshold * 2 30 | shouldPass = func(n int) bool { return (float64(n) / float64(primeWith+n)) <= (float64(errorPercent-1) / 100.0) } 31 | openCircuitError = hystrix.ErrCircuitOpen.Error() 32 | ) 33 | 34 | // hystrix-go uses buffered channels to receive reports on request success/failure, 35 | // and so is basically impossible to test deterministically. We have to make sure 36 | // the report buffer is emptied, by injecting a sleep between each invocation. 37 | requestDelay := 5 * time.Millisecond 38 | 39 | testFailingEndpoint(t, breaker, primeWith, shouldPass, requestDelay, openCircuitError) 40 | } 41 | -------------------------------------------------------------------------------- /circuitbreaker/util_test.go: -------------------------------------------------------------------------------- 1 | package circuitbreaker_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "path/filepath" 8 | "runtime" 9 | "testing" 10 | "time" 11 | 12 | "github.com/go-kit/kit/endpoint" 13 | ) 14 | 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 | ) { 23 | _, file, line, _ := runtime.Caller(1) 24 | caller := fmt.Sprintf("%s:%d", filepath.Base(file), line) 25 | 26 | // Create a mock endpoint and wrap it with the breaker. 27 | m := mock{} 28 | var e endpoint.Endpoint 29 | e = m.endpoint 30 | e = breaker(e) 31 | 32 | // Prime the endpoint with successful requests. 33 | for i := 0; i < primeWith; i++ { 34 | if _, err := e(context.Background(), struct{}{}); err != nil { 35 | t.Fatalf("%s: during priming, got error: %v", caller, err) 36 | } 37 | time.Sleep(requestDelay) 38 | } 39 | 40 | // Switch the endpoint to start throwing errors. 41 | m.err = errors.New("tragedy+disaster") 42 | m.through = 0 43 | 44 | // The first several should be allowed through and yield our error. 45 | for i := 0; shouldPass(i); i++ { 46 | if _, err := e(context.Background(), struct{}{}); err != m.err { 47 | t.Fatalf("%s: want %v, have %v", caller, m.err, err) 48 | } 49 | time.Sleep(requestDelay) 50 | } 51 | through := m.through 52 | 53 | // But the rest should be blocked by an open circuit. 54 | for i := 0; i < 10; i++ { 55 | if _, err := e(context.Background(), struct{}{}); err.Error() != openCircuitError { 56 | t.Fatalf("%s: want %q, have %q", caller, openCircuitError, err.Error()) 57 | } 58 | time.Sleep(requestDelay) 59 | } 60 | 61 | // Make sure none of those got through. 62 | if want, have := through, m.through; want != have { 63 | t.Errorf("%s: want %d, have %d", caller, want, have) 64 | } 65 | } 66 | 67 | type mock struct { 68 | through int 69 | err error 70 | } 71 | 72 | func (m *mock) endpoint(context.Context, interface{}) (interface{}, error) { 73 | m.through++ 74 | return struct{}{}, m.err 75 | } 76 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | -------------------------------------------------------------------------------- /docker-compose-integration.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | etcd: 4 | image: gcr.io/etcd-development/etcd:v3.5.0 5 | ports: 6 | - "2379:2379" 7 | environment: 8 | ETCD_LISTEN_CLIENT_URLS: http://0.0.0.0:2379 9 | ETCD_ADVERTISE_CLIENT_URLS: http://0.0.0.0:2379 10 | 11 | consul: 12 | image: consul:1.7 13 | ports: 14 | - "8500:8500" 15 | 16 | zk: 17 | image: zookeeper:3.5 18 | ports: 19 | - "2181:2181" 20 | 21 | eureka: 22 | image: springcloud/eureka 23 | environment: 24 | eureka.server.responseCacheUpdateIntervalMs: 1000 25 | ports: 26 | - "8761:8761" 27 | -------------------------------------------------------------------------------- /endpoint/doc.go: -------------------------------------------------------------------------------- 1 | // Package endpoint defines an abstraction for RPCs. 2 | // 3 | // Endpoints are a fundamental building block for many Go kit components. 4 | // Endpoints are implemented by servers, and called by clients. 5 | package endpoint 6 | -------------------------------------------------------------------------------- /endpoint/endpoint.go: -------------------------------------------------------------------------------- 1 | package endpoint 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // Endpoint is the fundamental building block of servers and clients. 8 | // It represents a single RPC method. 9 | type Endpoint func(ctx context.Context, request interface{}) (response interface{}, err error) 10 | 11 | // Nop is an endpoint that does nothing and returns a nil error. 12 | // Useful for tests. 13 | func Nop(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil } 14 | 15 | // Middleware is a chainable behavior modifier for endpoints. 16 | type Middleware func(Endpoint) Endpoint 17 | 18 | // Chain is a helper function for composing middlewares. Requests will 19 | // traverse them in the order they're declared. That is, the first middleware 20 | // is treated as the outermost middleware. 21 | func Chain(outer Middleware, others ...Middleware) Middleware { 22 | return func(next Endpoint) Endpoint { 23 | for i := len(others) - 1; i >= 0; i-- { // reverse 24 | next = others[i](next) 25 | } 26 | return outer(next) 27 | } 28 | } 29 | 30 | // Failer may be implemented by Go kit response types that contain business 31 | // logic error details. If Failed returns a non-nil error, the Go kit transport 32 | // layer may interpret this as a business logic error, and may encode it 33 | // differently than a regular, successful response. 34 | // 35 | // It's not necessary for your response types to implement Failer, but it may 36 | // help for more sophisticated use cases. The addsvc example shows how Failer 37 | // should be used by a complete application. 38 | type Failer interface { 39 | Failed() error 40 | } 41 | -------------------------------------------------------------------------------- /endpoint/endpoint_example_test.go: -------------------------------------------------------------------------------- 1 | package endpoint_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/go-kit/kit/endpoint" 8 | ) 9 | 10 | func ExampleChain() { 11 | e := endpoint.Chain( 12 | annotate("first"), 13 | annotate("second"), 14 | annotate("third"), 15 | )(myEndpoint) 16 | 17 | if _, err := e(ctx, req); err != nil { 18 | panic(err) 19 | } 20 | 21 | // Output: 22 | // first pre 23 | // second pre 24 | // third pre 25 | // my endpoint! 26 | // third post 27 | // second post 28 | // first post 29 | } 30 | 31 | var ( 32 | ctx = context.Background() 33 | req = struct{}{} 34 | ) 35 | 36 | func annotate(s string) endpoint.Middleware { 37 | return func(next endpoint.Endpoint) endpoint.Endpoint { 38 | return func(ctx context.Context, request interface{}) (interface{}, error) { 39 | fmt.Println(s, "pre") 40 | defer fmt.Println(s, "post") 41 | return next(ctx, request) 42 | } 43 | } 44 | } 45 | 46 | func myEndpoint(context.Context, interface{}) (interface{}, error) { 47 | fmt.Println("my endpoint!") 48 | return struct{}{}, nil 49 | } 50 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Examples have been relocated to a separate repository: https://github.com/go-kit/examples 4 | -------------------------------------------------------------------------------- /lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | if [ ! $(command -v gometalinter) ] 8 | then 9 | go get github.com/alecthomas/gometalinter 10 | gometalinter --update --install 11 | fi 12 | 13 | time gometalinter \ 14 | --exclude='error return value not checked.*(Close|Log|Print).*\(errcheck\)$' \ 15 | --exclude='.*_test\.go:.*error return value not checked.*\(errcheck\)$' \ 16 | --exclude='/thrift/' \ 17 | --exclude='/pb/' \ 18 | --exclude='no args in Log call \(vet\)' \ 19 | --disable=dupl \ 20 | --disable=aligncheck \ 21 | --disable=gotype \ 22 | --cyclo-over=20 \ 23 | --tests \ 24 | --concurrency=2 \ 25 | --deadline=300s \ 26 | ./... 27 | -------------------------------------------------------------------------------- /log/deprecated_levels/levels_test.go: -------------------------------------------------------------------------------- 1 | package levels_test 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | 8 | levels "github.com/go-kit/kit/log/deprecated_levels" 9 | "github.com/go-kit/log" 10 | ) 11 | 12 | func TestDefaultLevels(t *testing.T) { 13 | buf := bytes.Buffer{} 14 | logger := levels.New(log.NewLogfmtLogger(&buf)) 15 | 16 | logger.Debug().Log("msg", "résumé") // of course you'd want to do this 17 | if want, have := "level=debug msg=résumé\n", buf.String(); want != have { 18 | t.Errorf("want %#v, have %#v", want, have) 19 | } 20 | 21 | buf.Reset() 22 | logger.Info().Log("msg", "Åhus") 23 | if want, have := "level=info msg=Åhus\n", buf.String(); want != have { 24 | t.Errorf("want %#v, have %#v", want, have) 25 | } 26 | 27 | buf.Reset() 28 | logger.Error().Log("msg", "© violation") 29 | if want, have := "level=error msg=\"© violation\"\n", buf.String(); want != have { 30 | t.Errorf("want %#v, have %#v", want, have) 31 | } 32 | 33 | buf.Reset() 34 | logger.Crit().Log("msg", " ") 35 | if want, have := "level=crit msg=\"\\t\"\n", buf.String(); want != have { 36 | t.Errorf("want %#v, have %#v", want, have) 37 | } 38 | } 39 | 40 | func TestModifiedLevels(t *testing.T) { 41 | buf := bytes.Buffer{} 42 | logger := levels.New( 43 | log.NewJSONLogger(&buf), 44 | levels.Key("l"), 45 | levels.DebugValue("dbg"), 46 | levels.InfoValue("nfo"), 47 | levels.WarnValue("wrn"), 48 | levels.ErrorValue("err"), 49 | levels.CritValue("crt"), 50 | ) 51 | logger.With("easter_island", "176°").Debug().Log("msg", "moai") 52 | if want, have := `{"easter_island":"176°","l":"dbg","msg":"moai"}`+"\n", buf.String(); want != have { 53 | t.Errorf("want %#v, have %#v", want, have) 54 | } 55 | } 56 | 57 | func ExampleLevels() { 58 | logger := levels.New(log.NewLogfmtLogger(os.Stdout)) 59 | logger.Debug().Log("msg", "hello") 60 | logger.With("context", "foo").Warn().Log("err", "error") 61 | 62 | // Output: 63 | // level=debug msg=hello 64 | // level=warn context=foo err=error 65 | } 66 | -------------------------------------------------------------------------------- /log/json_logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/go-kit/log" 7 | ) 8 | 9 | // NewJSONLogger returns a Logger that encodes keyvals to the Writer as a 10 | // single JSON object. Each log event produces no more than one call to 11 | // w.Write. The passed Writer must be safe for concurrent use by multiple 12 | // goroutines if the returned Logger will be used concurrently. 13 | func NewJSONLogger(w io.Writer) Logger { 14 | return log.NewJSONLogger(w) 15 | } 16 | -------------------------------------------------------------------------------- /log/level/doc.go: -------------------------------------------------------------------------------- 1 | // Package level implements leveled logging on top of Go kit's log package. 2 | // 3 | // Deprecated: Use github.com/go-kit/log/level instead. 4 | // 5 | // To use the level package, create a logger as per normal in your func main, 6 | // and wrap it with level.NewFilter. 7 | // 8 | // var logger log.Logger 9 | // logger = log.NewLogfmtLogger(os.Stderr) 10 | // logger = level.NewFilter(logger, level.AllowInfo()) // <-- 11 | // logger = log.With(logger, "ts", log.DefaultTimestampUTC) 12 | // 13 | // Then, at the callsites, use one of the level.Debug, Info, Warn, or Error 14 | // helper methods to emit leveled log events. 15 | // 16 | // logger.Log("foo", "bar") // as normal, no level 17 | // level.Debug(logger).Log("request_id", reqID, "trace_data", trace.Get()) 18 | // if value > 100 { 19 | // level.Error(logger).Log("value", value) 20 | // } 21 | // 22 | // NewFilter allows precise control over what happens when a log event is 23 | // emitted without a level key, or if a squelched level is used. Check the 24 | // Option functions for details. 25 | package level 26 | -------------------------------------------------------------------------------- /log/level/example_test.go: -------------------------------------------------------------------------------- 1 | package level_test 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | 7 | "github.com/go-kit/kit/log" 8 | "github.com/go-kit/kit/log/level" 9 | ) 10 | 11 | func Example_basic() { 12 | logger := log.NewLogfmtLogger(os.Stdout) 13 | level.Debug(logger).Log("msg", "this message is at the debug level") 14 | level.Info(logger).Log("msg", "this message is at the info level") 15 | level.Warn(logger).Log("msg", "this message is at the warn level") 16 | level.Error(logger).Log("msg", "this message is at the error level") 17 | 18 | // Output: 19 | // level=debug msg="this message is at the debug level" 20 | // level=info msg="this message is at the info level" 21 | // level=warn msg="this message is at the warn level" 22 | // level=error msg="this message is at the error level" 23 | } 24 | 25 | func Example_filtered() { 26 | // Set up logger with level filter. 27 | logger := log.NewLogfmtLogger(os.Stdout) 28 | logger = level.NewFilter(logger, level.AllowInfo()) 29 | logger = log.With(logger, "caller", log.DefaultCaller) 30 | 31 | // Use level helpers to log at different levels. 32 | level.Error(logger).Log("err", errors.New("bad data")) 33 | level.Info(logger).Log("event", "data saved") 34 | level.Debug(logger).Log("next item", 17) // filtered 35 | 36 | // Output: 37 | // level=error caller=example_test.go:32 err="bad data" 38 | // level=info caller=example_test.go:33 event="data saved" 39 | } 40 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "github.com/go-kit/log" 5 | ) 6 | 7 | // Logger is the fundamental interface for all log operations. Log creates a 8 | // log event from keyvals, a variadic sequence of alternating keys and values. 9 | // Implementations must be safe for concurrent use by multiple goroutines. In 10 | // particular, any implementation of Logger that appends to keyvals or 11 | // modifies or retains any of its elements must make a copy first. 12 | type Logger = log.Logger 13 | 14 | // ErrMissingValue is appended to keyvals slices with odd length to substitute 15 | // the missing value. 16 | var ErrMissingValue = log.ErrMissingValue 17 | 18 | // With returns a new contextual logger with keyvals prepended to those passed 19 | // to calls to Log. If logger is also a contextual logger created by With, 20 | // WithPrefix, or WithSuffix, keyvals is appended to the existing context. 21 | // 22 | // The returned Logger replaces all value elements (odd indexes) containing a 23 | // Valuer with their generated value for each call to its Log method. 24 | func With(logger Logger, keyvals ...interface{}) Logger { 25 | return log.With(logger, keyvals...) 26 | } 27 | 28 | // WithPrefix returns a new contextual logger with keyvals prepended to those 29 | // passed to calls to Log. If logger is also a contextual logger created by 30 | // With, WithPrefix, or WithSuffix, keyvals is prepended to the existing context. 31 | // 32 | // The returned Logger replaces all value elements (odd indexes) containing a 33 | // Valuer with their generated value for each call to its Log method. 34 | func WithPrefix(logger Logger, keyvals ...interface{}) Logger { 35 | return log.WithPrefix(logger, keyvals...) 36 | } 37 | 38 | // WithSuffix returns a new contextual logger with keyvals appended to those 39 | // passed to calls to Log. If logger is also a contextual logger created by 40 | // With, WithPrefix, or WithSuffix, keyvals is appended to the existing context. 41 | // 42 | // The returned Logger replaces all value elements (odd indexes) containing a 43 | // Valuer with their generated value for each call to its Log method. 44 | func WithSuffix(logger Logger, keyvals ...interface{}) Logger { 45 | return log.WithSuffix(logger, keyvals...) 46 | } 47 | 48 | // LoggerFunc is an adapter to allow use of ordinary functions as Loggers. If 49 | // f is a function with the appropriate signature, LoggerFunc(f) is a Logger 50 | // object that calls f. 51 | type LoggerFunc = log.LoggerFunc 52 | -------------------------------------------------------------------------------- /log/logfmt_logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/go-kit/log" 7 | ) 8 | 9 | // NewLogfmtLogger returns a logger that encodes keyvals to the Writer in 10 | // logfmt format. Each log event produces no more than one call to w.Write. 11 | // The passed Writer must be safe for concurrent use by multiple goroutines if 12 | // the returned Logger will be used concurrently. 13 | func NewLogfmtLogger(w io.Writer) Logger { 14 | return log.NewLogfmtLogger(w) 15 | } 16 | -------------------------------------------------------------------------------- /log/logrus/logrus_logger.go: -------------------------------------------------------------------------------- 1 | // Package logrus provides an adapter to the 2 | // go-kit log.Logger interface. 3 | package logrus 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | 9 | "github.com/go-kit/log" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type Logger struct { 14 | field logrus.FieldLogger 15 | level logrus.Level 16 | } 17 | 18 | type Option func(*Logger) 19 | 20 | var errMissingValue = errors.New("(MISSING)") 21 | 22 | // NewLogger returns a Go kit log.Logger that sends log events to a logrus.Logger. 23 | func NewLogger(logger logrus.FieldLogger, options ...Option) log.Logger { 24 | l := &Logger{ 25 | field: logger, 26 | level: logrus.InfoLevel, 27 | } 28 | 29 | for _, optFunc := range options { 30 | optFunc(l) 31 | } 32 | 33 | return l 34 | } 35 | 36 | // WithLevel configures a logrus logger to log at level for all events. 37 | func WithLevel(level logrus.Level) Option { 38 | return func(c *Logger) { 39 | c.level = level 40 | } 41 | } 42 | 43 | func (l Logger) Log(keyvals ...interface{}) error { 44 | fields := logrus.Fields{} 45 | for i := 0; i < len(keyvals); i += 2 { 46 | if i+1 < len(keyvals) { 47 | fields[fmt.Sprint(keyvals[i])] = keyvals[i+1] 48 | } else { 49 | fields[fmt.Sprint(keyvals[i])] = errMissingValue 50 | } 51 | } 52 | 53 | switch l.level { 54 | case logrus.InfoLevel: 55 | l.field.WithFields(fields).Info() 56 | case logrus.ErrorLevel: 57 | l.field.WithFields(fields).Error() 58 | case logrus.DebugLevel: 59 | l.field.WithFields(fields).Debug() 60 | case logrus.WarnLevel: 61 | l.field.WithFields(fields).Warn() 62 | case logrus.TraceLevel: 63 | l.field.WithFields(fields).Trace() 64 | default: 65 | l.field.WithFields(fields).Print() 66 | } 67 | 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /log/nop_logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import "github.com/go-kit/log" 4 | 5 | // NewNopLogger returns a logger that doesn't do anything. 6 | func NewNopLogger() Logger { 7 | return log.NewNopLogger() 8 | } 9 | -------------------------------------------------------------------------------- /log/stdlib.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/go-kit/log" 7 | ) 8 | 9 | // StdlibWriter implements io.Writer by invoking the stdlib log.Print. It's 10 | // designed to be passed to a Go kit logger as the writer, for cases where 11 | // it's necessary to redirect all Go kit log output to the stdlib logger. 12 | // 13 | // If you have any choice in the matter, you shouldn't use this. Prefer to 14 | // redirect the stdlib log to the Go kit logger via NewStdlibAdapter. 15 | type StdlibWriter = log.StdlibWriter 16 | 17 | // StdlibAdapter wraps a Logger and allows it to be passed to the stdlib 18 | // logger's SetOutput. It will extract date/timestamps, filenames, and 19 | // messages, and place them under relevant keys. 20 | type StdlibAdapter = log.StdlibAdapter 21 | 22 | // StdlibAdapterOption sets a parameter for the StdlibAdapter. 23 | type StdlibAdapterOption = log.StdlibAdapterOption 24 | 25 | // TimestampKey sets the key for the timestamp field. By default, it's "ts". 26 | func TimestampKey(key string) StdlibAdapterOption { 27 | return log.TimestampKey(key) 28 | } 29 | 30 | // FileKey sets the key for the file and line field. By default, it's "caller". 31 | func FileKey(key string) StdlibAdapterOption { 32 | return log.FileKey(key) 33 | } 34 | 35 | // MessageKey sets the key for the actual log message. By default, it's "msg". 36 | func MessageKey(key string) StdlibAdapterOption { 37 | return log.MessageKey(key) 38 | } 39 | 40 | // Prefix configures the adapter to parse a prefix from stdlib log events. If 41 | // you provide a non-empty prefix to the stdlib logger, then your should provide 42 | // that same prefix to the adapter via this option. 43 | // 44 | // By default, the prefix isn't included in the msg key. Set joinPrefixToMsg to 45 | // true if you want to include the parsed prefix in the msg. 46 | func Prefix(prefix string, joinPrefixToMsg bool) StdlibAdapterOption { 47 | return log.Prefix(prefix, joinPrefixToMsg) 48 | } 49 | 50 | // NewStdlibAdapter returns a new StdlibAdapter wrapper around the passed 51 | // logger. It's designed to be passed to log.SetOutput. 52 | func NewStdlibAdapter(logger Logger, options ...StdlibAdapterOption) io.Writer { 53 | return log.NewStdlibAdapter(logger, options...) 54 | } 55 | -------------------------------------------------------------------------------- /log/sync.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/go-kit/log" 7 | ) 8 | 9 | // SwapLogger wraps another logger that may be safely replaced while other 10 | // goroutines use the SwapLogger concurrently. The zero value for a SwapLogger 11 | // will discard all log events without error. 12 | // 13 | // SwapLogger serves well as a package global logger that can be changed by 14 | // importers. 15 | type SwapLogger = log.SwapLogger 16 | 17 | // NewSyncWriter returns a new writer that is safe for concurrent use by 18 | // multiple goroutines. Writes to the returned writer are passed on to w. If 19 | // another write is already in progress, the calling goroutine blocks until 20 | // the writer is available. 21 | // 22 | // If w implements the following interface, so does the returned writer. 23 | // 24 | // interface { 25 | // Fd() uintptr 26 | // } 27 | func NewSyncWriter(w io.Writer) io.Writer { 28 | return log.NewSyncWriter(w) 29 | } 30 | 31 | // NewSyncLogger returns a logger that synchronizes concurrent use of the 32 | // wrapped logger. When multiple goroutines use the SyncLogger concurrently 33 | // only one goroutine will be allowed to log to the wrapped logger at a time. 34 | // The other goroutines will block until the logger is available. 35 | func NewSyncLogger(logger Logger) Logger { 36 | return log.NewSyncLogger(logger) 37 | } 38 | -------------------------------------------------------------------------------- /log/syslog/example_test.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | // +build !plan9 3 | // +build !nacl 4 | 5 | package syslog_test 6 | 7 | import ( 8 | "fmt" 9 | 10 | gosyslog "log/syslog" 11 | 12 | "github.com/go-kit/kit/log" 13 | "github.com/go-kit/kit/log/level" 14 | "github.com/go-kit/kit/log/syslog" 15 | ) 16 | 17 | func ExampleNewSyslogLogger_defaultPrioritySelector() { 18 | // Normal syslog writer 19 | w, err := gosyslog.New(gosyslog.LOG_INFO, "experiment") 20 | if err != nil { 21 | fmt.Println(err) 22 | return 23 | } 24 | 25 | // syslog logger with logfmt formatting 26 | logger := syslog.NewSyslogLogger(w, log.NewLogfmtLogger) 27 | logger.Log("msg", "info because of default") 28 | logger.Log(level.Key(), level.DebugValue(), "msg", "debug because of explicit level") 29 | } 30 | -------------------------------------------------------------------------------- /log/syslog/syslog.go: -------------------------------------------------------------------------------- 1 | //go:build !windows && !plan9 && !nacl 2 | // +build !windows,!plan9,!nacl 3 | 4 | // Deprecated: Use github.com/go-kit/log/syslog instead. 5 | package syslog 6 | 7 | import ( 8 | "io" 9 | 10 | "github.com/go-kit/log" 11 | "github.com/go-kit/log/syslog" 12 | ) 13 | 14 | // SyslogWriter is an interface wrapping stdlib syslog Writer. 15 | type SyslogWriter = syslog.SyslogWriter 16 | 17 | // NewSyslogLogger returns a new Logger which writes to syslog in syslog format. 18 | // The body of the log message is the formatted output from the Logger returned 19 | // by newLogger. 20 | func NewSyslogLogger(w SyslogWriter, newLogger func(io.Writer) log.Logger, options ...Option) log.Logger { 21 | return syslog.NewSyslogLogger(w, newLogger, options...) 22 | } 23 | 24 | // Option sets a parameter for syslog loggers. 25 | type Option = syslog.Option 26 | 27 | // PrioritySelector inspects the list of keyvals and selects a syslog priority. 28 | type PrioritySelector = syslog.PrioritySelector 29 | 30 | // PrioritySelectorOption sets priority selector function to choose syslog 31 | // priority. 32 | func PrioritySelectorOption(selector PrioritySelector) Option { 33 | return syslog.PrioritySelectorOption(selector) 34 | } 35 | -------------------------------------------------------------------------------- /log/term/colorlogger.go: -------------------------------------------------------------------------------- 1 | package term 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/go-kit/log" 7 | "github.com/go-kit/log/term" 8 | ) 9 | 10 | // Color represents an ANSI color. The zero value is Default. 11 | type Color = term.Color 12 | 13 | // ANSI colors. 14 | const ( 15 | Default = term.Default 16 | 17 | Black = term.Black 18 | DarkRed = term.DarkRed 19 | DarkGreen = term.DarkGreen 20 | Brown = term.Brown 21 | DarkBlue = term.DarkBlue 22 | DarkMagenta = term.DarkMagenta 23 | DarkCyan = term.DarkCyan 24 | Gray = term.Gray 25 | 26 | DarkGray = term.DarkGray 27 | Red = term.Red 28 | Green = term.Green 29 | Yellow = term.Yellow 30 | Blue = term.Blue 31 | Magenta = term.Magenta 32 | Cyan = term.Cyan 33 | White = term.White 34 | ) 35 | 36 | // FgBgColor represents a foreground and background color. 37 | type FgBgColor = term.FgBgColor 38 | 39 | // NewColorLogger returns a Logger which writes colored logs to w. ANSI color 40 | // codes for the colors returned by color are added to the formatted output 41 | // from the Logger returned by newLogger and the combined result written to w. 42 | func NewColorLogger(w io.Writer, newLogger func(io.Writer) log.Logger, color func(keyvals ...interface{}) FgBgColor) log.Logger { 43 | return term.NewColorLogger(w, newLogger, color) 44 | } 45 | -------------------------------------------------------------------------------- /log/term/colorwriter.go: -------------------------------------------------------------------------------- 1 | package term 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/go-kit/log/term" 7 | ) 8 | 9 | // NewColorWriter returns an io.Writer that writes to w and provides cross 10 | // platform support for ANSI color codes. If w is not a terminal it is 11 | // returned unmodified. 12 | func NewColorWriter(w io.Writer) io.Writer { 13 | return term.NewColorWriter(w) 14 | } 15 | -------------------------------------------------------------------------------- /log/term/example_test.go: -------------------------------------------------------------------------------- 1 | package term_test 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | 7 | "github.com/go-kit/kit/log" 8 | "github.com/go-kit/kit/log/term" 9 | ) 10 | 11 | func ExampleNewLogger_redErrors() { 12 | // Color errors red 13 | colorFn := func(keyvals ...interface{}) term.FgBgColor { 14 | for i := 1; i < len(keyvals); i += 2 { 15 | if _, ok := keyvals[i].(error); ok { 16 | return term.FgBgColor{Fg: term.White, Bg: term.Red} 17 | } 18 | } 19 | return term.FgBgColor{} 20 | } 21 | 22 | logger := term.NewLogger(os.Stdout, log.NewLogfmtLogger, colorFn) 23 | 24 | logger.Log("msg", "default color", "err", nil) 25 | logger.Log("msg", "colored because of error", "err", errors.New("coloring error")) 26 | } 27 | 28 | func ExampleNewLogger_levelColors() { 29 | // Color by level value 30 | colorFn := func(keyvals ...interface{}) term.FgBgColor { 31 | for i := 0; i < len(keyvals)-1; i += 2 { 32 | if keyvals[i] != "level" { 33 | continue 34 | } 35 | switch keyvals[i+1] { 36 | case "debug": 37 | return term.FgBgColor{Fg: term.DarkGray} 38 | case "info": 39 | return term.FgBgColor{Fg: term.Gray} 40 | case "warn": 41 | return term.FgBgColor{Fg: term.Yellow} 42 | case "error": 43 | return term.FgBgColor{Fg: term.Red} 44 | case "crit": 45 | return term.FgBgColor{Fg: term.Gray, Bg: term.DarkRed} 46 | default: 47 | return term.FgBgColor{} 48 | } 49 | } 50 | return term.FgBgColor{} 51 | } 52 | 53 | logger := term.NewLogger(os.Stdout, log.NewJSONLogger, colorFn) 54 | 55 | logger.Log("level", "warn", "msg", "yellow") 56 | logger.Log("level", "debug", "msg", "dark gray") 57 | } 58 | -------------------------------------------------------------------------------- /log/term/term.go: -------------------------------------------------------------------------------- 1 | // Package term provides tools for logging to a terminal. 2 | // 3 | // Deprecated: Use github.com/go-kit/log/term instead. 4 | package term 5 | 6 | import ( 7 | "io" 8 | 9 | "github.com/go-kit/log" 10 | "github.com/go-kit/log/term" 11 | ) 12 | 13 | // NewLogger returns a Logger that takes advantage of terminal features if 14 | // possible. Log events are formatted by the Logger returned by newLogger. If 15 | // w is a terminal each log event is colored according to the color function. 16 | func NewLogger(w io.Writer, newLogger func(io.Writer) log.Logger, color func(keyvals ...interface{}) FgBgColor) log.Logger { 17 | return term.NewLogger(w, newLogger, color) 18 | } 19 | 20 | // IsTerminal returns true if w writes to a terminal. 21 | func IsTerminal(w io.Writer) bool { 22 | return term.IsTerminal(w) 23 | } 24 | -------------------------------------------------------------------------------- /log/value.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-kit/log" 7 | ) 8 | 9 | // A Valuer generates a log value. When passed to With, WithPrefix, or 10 | // WithSuffix in a value element (odd indexes), it represents a dynamic 11 | // value which is re-evaluated with each log event. 12 | type Valuer = log.Valuer 13 | 14 | // Timestamp returns a timestamp Valuer. It invokes the t function to get the 15 | // time; unless you are doing something tricky, pass time.Now. 16 | // 17 | // Most users will want to use DefaultTimestamp or DefaultTimestampUTC, which 18 | // are TimestampFormats that use the RFC3339Nano format. 19 | func Timestamp(t func() time.Time) Valuer { 20 | return log.Timestamp(t) 21 | } 22 | 23 | // TimestampFormat returns a timestamp Valuer with a custom time format. It 24 | // invokes the t function to get the time to format; unless you are doing 25 | // something tricky, pass time.Now. The layout string is passed to 26 | // Time.Format. 27 | // 28 | // Most users will want to use DefaultTimestamp or DefaultTimestampUTC, which 29 | // are TimestampFormats that use the RFC3339Nano format. 30 | func TimestampFormat(t func() time.Time, layout string) Valuer { 31 | return log.TimestampFormat(t, layout) 32 | } 33 | 34 | // Caller returns a Valuer that returns a file and line from a specified depth 35 | // in the callstack. Users will probably want to use DefaultCaller. 36 | func Caller(depth int) Valuer { 37 | return log.Caller(depth) 38 | } 39 | 40 | var ( 41 | // DefaultTimestamp is a Valuer that returns the current wallclock time, 42 | // respecting time zones, when bound. 43 | DefaultTimestamp = log.DefaultTimestamp 44 | 45 | // DefaultTimestampUTC is a Valuer that returns the current time in UTC 46 | // when bound. 47 | DefaultTimestampUTC = log.DefaultTimestampUTC 48 | 49 | // DefaultCaller is a Valuer that returns the file and line where the Log 50 | // method was invoked. It can only be used with log.With. 51 | DefaultCaller = log.DefaultCaller 52 | ) 53 | -------------------------------------------------------------------------------- /log/zap/zap_sugar_logger.go: -------------------------------------------------------------------------------- 1 | package zap 2 | 3 | import ( 4 | "github.com/go-kit/log" 5 | "go.uber.org/zap" 6 | "go.uber.org/zap/zapcore" 7 | ) 8 | 9 | type zapSugarLogger func(msg string, keysAndValues ...interface{}) 10 | 11 | func (l zapSugarLogger) Log(kv ...interface{}) error { 12 | l("", kv...) 13 | return nil 14 | } 15 | 16 | // NewZapSugarLogger returns a Go kit log.Logger that sends 17 | // log events to a zap.Logger. 18 | func NewZapSugarLogger(logger *zap.Logger, level zapcore.Level) log.Logger { 19 | sugarLogger := logger.WithOptions(zap.AddCallerSkip(2)).Sugar() 20 | var sugar zapSugarLogger 21 | switch level { 22 | case zapcore.DebugLevel: 23 | sugar = sugarLogger.Debugw 24 | case zapcore.InfoLevel: 25 | sugar = sugarLogger.Infow 26 | case zapcore.WarnLevel: 27 | sugar = sugarLogger.Warnw 28 | case zapcore.ErrorLevel: 29 | sugar = sugarLogger.Errorw 30 | case zapcore.DPanicLevel: 31 | sugar = sugarLogger.DPanicw 32 | case zapcore.PanicLevel: 33 | sugar = sugarLogger.Panicw 34 | case zapcore.FatalLevel: 35 | sugar = sugarLogger.Fatalw 36 | default: 37 | sugar = sugarLogger.Infow 38 | } 39 | return sugar 40 | } 41 | -------------------------------------------------------------------------------- /log/zap/zap_sugar_logger_test.go: -------------------------------------------------------------------------------- 1 | package zap_test 2 | 3 | import ( 4 | "encoding/json" 5 | kitzap "github.com/go-kit/kit/log/zap" 6 | "go.uber.org/zap" 7 | "go.uber.org/zap/zapcore" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestZapSugarLogger(t *testing.T) { 13 | // logger config 14 | encoderConfig := zap.NewDevelopmentEncoderConfig() 15 | encoder := zapcore.NewJSONEncoder(encoderConfig) 16 | levelKey := encoderConfig.LevelKey 17 | // basic test cases 18 | type testCase struct { 19 | level zapcore.Level 20 | kvs []interface{} 21 | want map[string]string 22 | } 23 | testCases := []testCase{ 24 | {level: zapcore.DebugLevel, kvs: []interface{}{"key1", "value1"}, 25 | want: map[string]string{levelKey: "DEBUG", "key1": "value1"}}, 26 | 27 | {level: zapcore.InfoLevel, kvs: []interface{}{"key2", "value2"}, 28 | want: map[string]string{levelKey: "INFO", "key2": "value2"}}, 29 | 30 | {level: zapcore.WarnLevel, kvs: []interface{}{"key3", "value3"}, 31 | want: map[string]string{levelKey: "WARN", "key3": "value3"}}, 32 | 33 | {level: zapcore.ErrorLevel, kvs: []interface{}{"key4", "value4"}, 34 | want: map[string]string{levelKey: "ERROR", "key4": "value4"}}, 35 | 36 | {level: zapcore.DPanicLevel, kvs: []interface{}{"key5", "value5"}, 37 | want: map[string]string{levelKey: "DPANIC", "key5": "value5"}}, 38 | 39 | {level: zapcore.PanicLevel, kvs: []interface{}{"key6", "value6"}, 40 | want: map[string]string{levelKey: "PANIC", "key6": "value6"}}, 41 | } 42 | // test 43 | for _, testCase := range testCases { 44 | t.Run(testCase.level.String(), func(t *testing.T) { 45 | // make logger 46 | writer := &tbWriter{tb: t} 47 | logger := zap.New( 48 | zapcore.NewCore(encoder, zapcore.AddSync(writer), zap.DebugLevel), 49 | zap.Development()) 50 | // check panic 51 | shouldPanic := testCase.level >= zapcore.DPanicLevel 52 | kitLogger := kitzap.NewZapSugarLogger(logger, testCase.level) 53 | defer func() { 54 | isPanic := recover() != nil 55 | if shouldPanic != isPanic { 56 | t.Errorf("test level %v should panic(%v), but %v", testCase.level, shouldPanic, isPanic) 57 | } 58 | // check log kvs 59 | logMap := make(map[string]string) 60 | err := json.Unmarshal([]byte(writer.sb.String()), &logMap) 61 | if err != nil { 62 | t.Errorf("unmarshal error: %v", err) 63 | } else { 64 | for k, v := range testCase.want { 65 | vv, ok := logMap[k] 66 | if !ok || v != vv { 67 | t.Error("error log") 68 | } 69 | } 70 | } 71 | }() 72 | kitLogger.Log(testCase.kvs...) 73 | }) 74 | } 75 | } 76 | 77 | type tbWriter struct { 78 | tb testing.TB 79 | sb strings.Builder 80 | } 81 | 82 | func (w *tbWriter) Write(b []byte) (n int, err error) { 83 | w.tb.Logf(string(b)) 84 | w.sb.Write(b) 85 | return len(b), nil 86 | } 87 | -------------------------------------------------------------------------------- /metrics/README.md: -------------------------------------------------------------------------------- 1 | # package metrics 2 | 3 | `package metrics` provides a set of uniform interfaces for service instrumentation. 4 | It has 5 | [counters](http://prometheus.io/docs/concepts/metric_types/#counter), 6 | [gauges](http://prometheus.io/docs/concepts/metric_types/#gauge), and 7 | [histograms](http://prometheus.io/docs/concepts/metric_types/#histogram), 8 | and provides adapters to popular metrics packages, like 9 | [expvar](https://golang.org/pkg/expvar), 10 | [StatsD](https://github.com/etsy/statsd), and 11 | [Prometheus](https://prometheus.io). 12 | 13 | ## Rationale 14 | 15 | Code instrumentation is absolutely essential to achieve 16 | [observability](https://speakerdeck.com/mattheath/observability-in-micro-service-architectures) 17 | into a distributed system. 18 | Metrics and instrumentation tools have coalesced around a few well-defined idioms. 19 | `package metrics` provides a common, minimal interface those idioms for service authors. 20 | 21 | ## Usage 22 | 23 | A simple counter, exported via expvar. 24 | 25 | ```go 26 | import ( 27 | "github.com/go-kit/kit/metrics" 28 | "github.com/go-kit/kit/metrics/expvar" 29 | ) 30 | 31 | func main() { 32 | var myCount metrics.Counter 33 | myCount = expvar.NewCounter("my_count") 34 | myCount.Add(1) 35 | } 36 | ``` 37 | 38 | A histogram for request duration, 39 | exported via a Prometheus summary with dynamically-computed quantiles. 40 | 41 | ```go 42 | import ( 43 | "time" 44 | 45 | stdprometheus "github.com/prometheus/client_golang/prometheus" 46 | 47 | "github.com/go-kit/kit/metrics" 48 | "github.com/go-kit/kit/metrics/prometheus" 49 | ) 50 | 51 | func main() { 52 | var dur metrics.Histogram = prometheus.NewSummaryFrom(stdprometheus.SummaryOpts{ 53 | Namespace: "myservice", 54 | Subsystem: "api", 55 | Name: "request_duration_seconds", 56 | Help: "Total time spent serving requests.", 57 | }, []string{}) 58 | // ... 59 | } 60 | 61 | func handleRequest(dur metrics.Histogram) { 62 | defer func(begin time.Time) { dur.Observe(time.Since(begin).Seconds()) }(time.Now()) 63 | // handle request 64 | } 65 | ``` 66 | 67 | A gauge for the number of goroutines currently running, exported via StatsD. 68 | 69 | ```go 70 | import ( 71 | "context" 72 | "net" 73 | "os" 74 | "runtime" 75 | "time" 76 | 77 | "github.com/go-kit/kit/metrics" 78 | "github.com/go-kit/kit/metrics/statsd" 79 | ) 80 | 81 | func main() { 82 | statsd := statsd.New("foo_svc.", log.NewNopLogger()) 83 | report := time.NewTicker(5 * time.Second) 84 | defer report.Stop() 85 | go statsd.SendLoop(context.Background(), report.C, "tcp", "statsd.internal:8125") 86 | goroutines := statsd.NewGauge("goroutine_count") 87 | go exportGoroutines(goroutines) 88 | // ... 89 | } 90 | 91 | func exportGoroutines(g metrics.Gauge) { 92 | for range time.Tick(time.Second) { 93 | g.Set(float64(runtime.NumGoroutine())) 94 | } 95 | } 96 | ``` 97 | 98 | For more information, see [the package documentation](https://godoc.org/github.com/go-kit/kit/metrics). 99 | -------------------------------------------------------------------------------- /metrics/discard/discard.go: -------------------------------------------------------------------------------- 1 | // Package discard provides a no-op metrics backend. 2 | package discard 3 | 4 | import "github.com/go-kit/kit/metrics" 5 | 6 | type counter struct{} 7 | 8 | // NewCounter returns a new no-op counter. 9 | func NewCounter() metrics.Counter { return counter{} } 10 | 11 | // With implements Counter. 12 | func (c counter) With(labelValues ...string) metrics.Counter { return c } 13 | 14 | // Add implements Counter. 15 | func (c counter) Add(delta float64) {} 16 | 17 | type gauge struct{} 18 | 19 | // NewGauge returns a new no-op gauge. 20 | func NewGauge() metrics.Gauge { return gauge{} } 21 | 22 | // With implements Gauge. 23 | func (g gauge) With(labelValues ...string) metrics.Gauge { return g } 24 | 25 | // Set implements Gauge. 26 | func (g gauge) Set(value float64) {} 27 | 28 | // Add implements metrics.Gauge. 29 | func (g gauge) Add(delta float64) {} 30 | 31 | type histogram struct{} 32 | 33 | // NewHistogram returns a new no-op histogram. 34 | func NewHistogram() metrics.Histogram { return histogram{} } 35 | 36 | // With implements Histogram. 37 | func (h histogram) With(labelValues ...string) metrics.Histogram { return h } 38 | 39 | // Observe implements histogram. 40 | func (h histogram) Observe(value float64) {} 41 | -------------------------------------------------------------------------------- /metrics/expvar/expvar.go: -------------------------------------------------------------------------------- 1 | // Package expvar provides expvar backends for metrics. 2 | // Label values are not supported. 3 | package expvar 4 | 5 | import ( 6 | "expvar" 7 | "sync" 8 | 9 | "github.com/go-kit/kit/metrics" 10 | "github.com/go-kit/kit/metrics/generic" 11 | ) 12 | 13 | // Counter implements the counter metric with an expvar float. 14 | // Label values are not supported. 15 | type Counter struct { 16 | f *expvar.Float 17 | } 18 | 19 | // NewCounter creates an expvar Float with the given name, and returns an object 20 | // that implements the Counter interface. 21 | func NewCounter(name string) *Counter { 22 | return &Counter{ 23 | f: expvar.NewFloat(name), 24 | } 25 | } 26 | 27 | // With is a no-op. 28 | func (c *Counter) With(labelValues ...string) metrics.Counter { return c } 29 | 30 | // Add implements Counter. 31 | func (c *Counter) Add(delta float64) { c.f.Add(delta) } 32 | 33 | // Gauge implements the gauge metric with an expvar float. 34 | // Label values are not supported. 35 | type Gauge struct { 36 | f *expvar.Float 37 | } 38 | 39 | // NewGauge creates an expvar Float with the given name, and returns an object 40 | // that implements the Gauge interface. 41 | func NewGauge(name string) *Gauge { 42 | return &Gauge{ 43 | f: expvar.NewFloat(name), 44 | } 45 | } 46 | 47 | // With is a no-op. 48 | func (g *Gauge) With(labelValues ...string) metrics.Gauge { return g } 49 | 50 | // Set implements Gauge. 51 | func (g *Gauge) Set(value float64) { g.f.Set(value) } 52 | 53 | // Add implements metrics.Gauge. 54 | func (g *Gauge) Add(delta float64) { g.f.Add(delta) } 55 | 56 | // Histogram implements the histogram metric with a combination of the generic 57 | // Histogram object and several expvar Floats, one for each of the 50th, 90th, 58 | // 95th, and 99th quantiles of observed values, with the quantile attached to 59 | // the name as a suffix. Label values are not supported. 60 | type Histogram struct { 61 | mtx sync.Mutex 62 | h *generic.Histogram 63 | p50 *expvar.Float 64 | p90 *expvar.Float 65 | p95 *expvar.Float 66 | p99 *expvar.Float 67 | } 68 | 69 | // NewHistogram returns a Histogram object with the given name and number of 70 | // buckets in the underlying histogram object. 50 is a good default number of 71 | // buckets. 72 | func NewHistogram(name string, buckets int) *Histogram { 73 | return &Histogram{ 74 | h: generic.NewHistogram(name, buckets), 75 | p50: expvar.NewFloat(name + ".p50"), 76 | p90: expvar.NewFloat(name + ".p90"), 77 | p95: expvar.NewFloat(name + ".p95"), 78 | p99: expvar.NewFloat(name + ".p99"), 79 | } 80 | } 81 | 82 | // With is a no-op. 83 | func (h *Histogram) With(labelValues ...string) metrics.Histogram { return h } 84 | 85 | // Observe implements Histogram. 86 | func (h *Histogram) Observe(value float64) { 87 | h.mtx.Lock() 88 | defer h.mtx.Unlock() 89 | h.h.Observe(value) 90 | h.p50.Set(h.h.Quantile(0.50)) 91 | h.p90.Set(h.h.Quantile(0.90)) 92 | h.p95.Set(h.h.Quantile(0.95)) 93 | h.p99.Set(h.h.Quantile(0.99)) 94 | } 95 | -------------------------------------------------------------------------------- /metrics/expvar/expvar_test.go: -------------------------------------------------------------------------------- 1 | package expvar 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/go-kit/kit/metrics/teststat" 8 | ) 9 | 10 | func TestCounter(t *testing.T) { 11 | counter := NewCounter("expvar_counter").With("label values", "not supported").(*Counter) 12 | value := func() float64 { f, _ := strconv.ParseFloat(counter.f.String(), 64); return f } 13 | if err := teststat.TestCounter(counter, value); err != nil { 14 | t.Fatal(err) 15 | } 16 | } 17 | 18 | func TestGauge(t *testing.T) { 19 | gauge := NewGauge("expvar_gauge").With("label values", "not supported").(*Gauge) 20 | value := func() []float64 { f, _ := strconv.ParseFloat(gauge.f.String(), 64); return []float64{f} } 21 | if err := teststat.TestGauge(gauge, value); err != nil { 22 | t.Fatal(err) 23 | } 24 | } 25 | 26 | func TestHistogram(t *testing.T) { 27 | histogram := NewHistogram("expvar_histogram", 50).With("label values", "not supported").(*Histogram) 28 | quantiles := func() (float64, float64, float64, float64) { 29 | p50, _ := strconv.ParseFloat(histogram.p50.String(), 64) 30 | p90, _ := strconv.ParseFloat(histogram.p90.String(), 64) 31 | p95, _ := strconv.ParseFloat(histogram.p95.String(), 64) 32 | p99, _ := strconv.ParseFloat(histogram.p99.String(), 64) 33 | return p50, p90, p95, p99 34 | } 35 | if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil { 36 | t.Fatal(err) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /metrics/graphite/graphite_test.go: -------------------------------------------------------------------------------- 1 | package graphite 2 | 3 | import ( 4 | "bytes" 5 | "regexp" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/go-kit/kit/metrics/teststat" 10 | "github.com/go-kit/log" 11 | ) 12 | 13 | func TestCounter(t *testing.T) { 14 | prefix, name := "abc.", "def" 15 | label, value := "label", "value" // ignored for Graphite 16 | regex := `^` + prefix + name + ` ([0-9\.]+) [0-9]+$` 17 | g := New(prefix, log.NewNopLogger()) 18 | counter := g.NewCounter(name).With(label, value) 19 | valuef := teststat.SumLines(g, regex) 20 | if err := teststat.TestCounter(counter, valuef); err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | 25 | func TestGauge(t *testing.T) { 26 | prefix, name := "ghi.", "jkl" 27 | label, value := "xyz", "abc" // ignored for Graphite 28 | regex := `^` + prefix + name + ` ([0-9\.]+) [0-9]+$` 29 | g := New(prefix, log.NewNopLogger()) 30 | gauge := g.NewGauge(name).With(label, value) 31 | valuef := teststat.LastLine(g, regex) 32 | if err := teststat.TestGauge(gauge, valuef); err != nil { 33 | t.Fatal(err) 34 | } 35 | } 36 | 37 | func TestHistogram(t *testing.T) { 38 | // The histogram test is actually like 4 gauge tests. 39 | prefix, name := "graphite.", "histogram_test" 40 | label, value := "abc", "def" // ignored for Graphite 41 | re50 := regexp.MustCompile(prefix + name + `.p50 ([0-9\.]+) [0-9]+`) 42 | re90 := regexp.MustCompile(prefix + name + `.p90 ([0-9\.]+) [0-9]+`) 43 | re95 := regexp.MustCompile(prefix + name + `.p95 ([0-9\.]+) [0-9]+`) 44 | re99 := regexp.MustCompile(prefix + name + `.p99 ([0-9\.]+) [0-9]+`) 45 | g := New(prefix, log.NewNopLogger()) 46 | histogram := g.NewHistogram(name, 50).With(label, value) 47 | quantiles := func() (float64, float64, float64, float64) { 48 | var buf bytes.Buffer 49 | g.WriteTo(&buf) 50 | match50 := re50.FindStringSubmatch(buf.String()) 51 | p50, _ := strconv.ParseFloat(match50[1], 64) 52 | match90 := re90.FindStringSubmatch(buf.String()) 53 | p90, _ := strconv.ParseFloat(match90[1], 64) 54 | match95 := re95.FindStringSubmatch(buf.String()) 55 | p95, _ := strconv.ParseFloat(match95[1], 64) 56 | match99 := re99.FindStringSubmatch(buf.String()) 57 | p99, _ := strconv.ParseFloat(match99[1], 64) 58 | return p50, p90, p95, p99 59 | } 60 | if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil { 61 | t.Fatal(err) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /metrics/internal/convert/convert_test.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-kit/kit/metrics/generic" 7 | "github.com/go-kit/kit/metrics/teststat" 8 | ) 9 | 10 | func TestCounterHistogramConversion(t *testing.T) { 11 | name := "my_counter" 12 | c := generic.NewCounter(name) 13 | h := NewCounterAsHistogram(c) 14 | top := NewHistogramAsCounter(h).With("label", "counter").(histogramCounter) 15 | mid := top.h.(counterHistogram) 16 | low := mid.c.(*generic.Counter) 17 | if want, have := name, low.Name; want != have { 18 | t.Errorf("Name: want %q, have %q", want, have) 19 | } 20 | if err := teststat.TestCounter(top, low.Value); err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | 25 | func TestCounterGaugeConversion(t *testing.T) { 26 | name := "my_counter" 27 | c := generic.NewCounter(name) 28 | g := NewCounterAsGauge(c) 29 | top := NewGaugeAsCounter(g).With("label", "counter").(gaugeCounter) 30 | mid := top.g.(counterGauge) 31 | low := mid.c.(*generic.Counter) 32 | if want, have := name, low.Name; want != have { 33 | t.Errorf("Name: want %q, have %q", want, have) 34 | } 35 | if err := teststat.TestCounter(top, low.Value); err != nil { 36 | t.Fatal(err) 37 | } 38 | } 39 | 40 | func TestHistogramGaugeConversion(t *testing.T) { 41 | name := "my_histogram" 42 | h := generic.NewHistogram(name, 50) 43 | g := NewHistogramAsGauge(h) 44 | top := NewGaugeAsHistogram(g).With("label", "histogram").(gaugeHistogram) 45 | mid := top.g.(histogramGauge) 46 | low := mid.h.(*generic.Histogram) 47 | if want, have := name, low.Name; want != have { 48 | t.Errorf("Name: want %q, have %q", want, have) 49 | } 50 | quantiles := func() (float64, float64, float64, float64) { 51 | return low.Quantile(0.50), low.Quantile(0.90), low.Quantile(0.95), low.Quantile(0.99) 52 | } 53 | if err := teststat.TestHistogram(top, quantiles, 0.01); err != nil { 54 | t.Fatal(err) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /metrics/internal/lv/labelvalues.go: -------------------------------------------------------------------------------- 1 | package lv 2 | 3 | // LabelValues is a type alias that provides validation on its With method. 4 | // Metrics may include it as a member to help them satisfy With semantics and 5 | // save some code duplication. 6 | type LabelValues []string 7 | 8 | // With validates the input, and returns a new aggregate labelValues. 9 | func (lvs LabelValues) With(labelValues ...string) LabelValues { 10 | if len(labelValues)%2 != 0 { 11 | labelValues = append(labelValues, "unknown") 12 | } 13 | return append(lvs, labelValues...) 14 | } 15 | -------------------------------------------------------------------------------- /metrics/internal/lv/labelvalues_test.go: -------------------------------------------------------------------------------- 1 | package lv 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestWith(t *testing.T) { 9 | var a LabelValues 10 | b := a.With("a", "1") 11 | c := a.With("b", "2", "c", "3") 12 | 13 | if want, have := "", strings.Join(a, ""); want != have { 14 | t.Errorf("With appears to mutate the original LabelValues: want %q, have %q", want, have) 15 | } 16 | if want, have := "a1", strings.Join(b, ""); want != have { 17 | t.Errorf("With does not appear to return the right thing: want %q, have %q", want, have) 18 | } 19 | if want, have := "b2c3", strings.Join(c, ""); want != have { 20 | t.Errorf("With does not appear to return the right thing: want %q, have %q", want, have) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /metrics/internal/lv/space_test.go: -------------------------------------------------------------------------------- 1 | package lv 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestSpaceWalkAbort(t *testing.T) { 9 | s := NewSpace() 10 | s.Observe("a", LabelValues{"a", "b"}, 1) 11 | s.Observe("a", LabelValues{"c", "d"}, 2) 12 | s.Observe("a", LabelValues{"e", "f"}, 4) 13 | s.Observe("a", LabelValues{"g", "h"}, 8) 14 | s.Observe("b", LabelValues{"a", "b"}, 16) 15 | s.Observe("b", LabelValues{"c", "d"}, 32) 16 | s.Observe("b", LabelValues{"e", "f"}, 64) 17 | s.Observe("b", LabelValues{"g", "h"}, 128) 18 | 19 | var count int 20 | s.Walk(func(name string, lvs LabelValues, obs []float64) bool { 21 | count++ 22 | return false 23 | }) 24 | if want, have := 1, count; want != have { 25 | t.Errorf("want %d, have %d", want, have) 26 | } 27 | } 28 | 29 | func TestSpaceWalkSums(t *testing.T) { 30 | s := NewSpace() 31 | s.Observe("metric_one", LabelValues{}, 1) 32 | s.Observe("metric_one", LabelValues{}, 2) 33 | s.Observe("metric_one", LabelValues{"a", "1", "b", "2"}, 4) 34 | s.Observe("metric_one", LabelValues{"a", "1", "b", "2"}, 8) 35 | s.Observe("metric_one", LabelValues{}, 16) 36 | s.Observe("metric_one", LabelValues{"a", "1", "b", "3"}, 32) 37 | s.Observe("metric_two", LabelValues{}, 64) 38 | s.Observe("metric_two", LabelValues{}, 128) 39 | s.Observe("metric_two", LabelValues{"a", "1", "b", "2"}, 256) 40 | 41 | have := map[string]float64{} 42 | s.Walk(func(name string, lvs LabelValues, obs []float64) bool { 43 | have[name+" ["+strings.Join(lvs, "")+"]"] += sum(obs) 44 | return true 45 | }) 46 | 47 | want := map[string]float64{ 48 | "metric_one []": 1 + 2 + 16, 49 | "metric_one [a1b2]": 4 + 8, 50 | "metric_one [a1b3]": 32, 51 | "metric_two []": 64 + 128, 52 | "metric_two [a1b2]": 256, 53 | } 54 | for keystr, wantsum := range want { 55 | if havesum := have[keystr]; wantsum != havesum { 56 | t.Errorf("%q: want %.1f, have %.1f", keystr, wantsum, havesum) 57 | } 58 | delete(want, keystr) 59 | delete(have, keystr) 60 | } 61 | for keystr, havesum := range have { 62 | t.Errorf("%q: unexpected observations recorded: %.1f", keystr, havesum) 63 | } 64 | } 65 | 66 | func TestSpaceWalkSkipsEmptyDimensions(t *testing.T) { 67 | s := NewSpace() 68 | s.Observe("foo", LabelValues{"bar", "1", "baz", "2"}, 123) 69 | 70 | var count int 71 | s.Walk(func(name string, lvs LabelValues, obs []float64) bool { 72 | count++ 73 | return true 74 | }) 75 | if want, have := 1, count; want != have { 76 | t.Errorf("want %d, have %d", want, have) 77 | } 78 | } 79 | 80 | func sum(a []float64) (v float64) { 81 | for _, f := range a { 82 | v += f 83 | } 84 | return 85 | } 86 | -------------------------------------------------------------------------------- /metrics/internal/ratemap/ratemap.go: -------------------------------------------------------------------------------- 1 | // Package ratemap implements a goroutine-safe map of string to float64. It can 2 | // be embedded in implementations whose metrics support fixed sample rates, so 3 | // that an additional parameter doesn't have to be tracked through the e.g. 4 | // lv.Space object. 5 | package ratemap 6 | 7 | import "sync" 8 | 9 | // RateMap is a simple goroutine-safe map of string to float64. 10 | type RateMap struct { 11 | mtx sync.RWMutex 12 | m map[string]float64 13 | } 14 | 15 | // New returns a new RateMap. 16 | func New() *RateMap { 17 | return &RateMap{ 18 | m: map[string]float64{}, 19 | } 20 | } 21 | 22 | // Set writes the given name/rate pair to the map. 23 | // Set is safe for concurrent access by multiple goroutines. 24 | func (m *RateMap) Set(name string, rate float64) { 25 | m.mtx.Lock() 26 | defer m.mtx.Unlock() 27 | m.m[name] = rate 28 | } 29 | 30 | // Get retrieves the rate for the given name, or 1.0 if none is set. 31 | // Get is safe for concurrent access by multiple goroutines. 32 | func (m *RateMap) Get(name string) float64 { 33 | m.mtx.RLock() 34 | defer m.mtx.RUnlock() 35 | f, ok := m.m[name] 36 | if !ok { 37 | f = 1.0 38 | } 39 | return f 40 | } 41 | -------------------------------------------------------------------------------- /metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | // Counter describes a metric that accumulates values monotonically. 4 | // An example of a counter is the number of received HTTP requests. 5 | type Counter interface { 6 | With(labelValues ...string) Counter 7 | Add(delta float64) 8 | } 9 | 10 | // Gauge describes a metric that takes specific values over time. 11 | // An example of a gauge is the current depth of a job queue. 12 | type Gauge interface { 13 | With(labelValues ...string) Gauge 14 | Set(value float64) 15 | Add(delta float64) 16 | } 17 | 18 | // Histogram describes a metric that takes repeated observations of the same 19 | // kind of thing, and produces a statistical summary of those observations, 20 | // typically expressed as quantiles or buckets. An example of a histogram is 21 | // HTTP request latencies. 22 | type Histogram interface { 23 | With(labelValues ...string) Histogram 24 | Observe(value float64) 25 | } 26 | -------------------------------------------------------------------------------- /metrics/multi/multi.go: -------------------------------------------------------------------------------- 1 | // Package multi provides adapters that send observations to multiple metrics 2 | // simultaneously. This is useful if your service needs to emit to multiple 3 | // instrumentation systems at the same time, for example if your organization is 4 | // transitioning from one system to another. 5 | package multi 6 | 7 | import "github.com/go-kit/kit/metrics" 8 | 9 | // Counter collects multiple individual counters and treats them as a unit. 10 | type Counter []metrics.Counter 11 | 12 | // NewCounter returns a multi-counter, wrapping the passed counters. 13 | func NewCounter(c ...metrics.Counter) Counter { 14 | return Counter(c) 15 | } 16 | 17 | // Add implements counter. 18 | func (c Counter) Add(delta float64) { 19 | for _, counter := range c { 20 | counter.Add(delta) 21 | } 22 | } 23 | 24 | // With implements counter. 25 | func (c Counter) With(labelValues ...string) metrics.Counter { 26 | next := make(Counter, len(c)) 27 | for i := range c { 28 | next[i] = c[i].With(labelValues...) 29 | } 30 | return next 31 | } 32 | 33 | // Gauge collects multiple individual gauges and treats them as a unit. 34 | type Gauge []metrics.Gauge 35 | 36 | // NewGauge returns a multi-gauge, wrapping the passed gauges. 37 | func NewGauge(g ...metrics.Gauge) Gauge { 38 | return Gauge(g) 39 | } 40 | 41 | // Set implements Gauge. 42 | func (g Gauge) Set(value float64) { 43 | for _, gauge := range g { 44 | gauge.Set(value) 45 | } 46 | } 47 | 48 | // With implements gauge. 49 | func (g Gauge) With(labelValues ...string) metrics.Gauge { 50 | next := make(Gauge, len(g)) 51 | for i := range g { 52 | next[i] = g[i].With(labelValues...) 53 | } 54 | return next 55 | } 56 | 57 | // Add implements metrics.Gauge. 58 | func (g Gauge) Add(delta float64) { 59 | for _, gauge := range g { 60 | gauge.Add(delta) 61 | } 62 | } 63 | 64 | // Histogram collects multiple individual histograms and treats them as a unit. 65 | type Histogram []metrics.Histogram 66 | 67 | // NewHistogram returns a multi-histogram, wrapping the passed histograms. 68 | func NewHistogram(h ...metrics.Histogram) Histogram { 69 | return Histogram(h) 70 | } 71 | 72 | // Observe implements Histogram. 73 | func (h Histogram) Observe(value float64) { 74 | for _, histogram := range h { 75 | histogram.Observe(value) 76 | } 77 | } 78 | 79 | // With implements histogram. 80 | func (h Histogram) With(labelValues ...string) metrics.Histogram { 81 | next := make(Histogram, len(h)) 82 | for i := range h { 83 | next[i] = h[i].With(labelValues...) 84 | } 85 | return next 86 | } 87 | -------------------------------------------------------------------------------- /metrics/multi/multi_test.go: -------------------------------------------------------------------------------- 1 | package multi 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/go-kit/kit/metrics" 8 | ) 9 | 10 | func TestMultiCounter(t *testing.T) { 11 | c1 := &mockCounter{} 12 | c2 := &mockCounter{} 13 | c3 := &mockCounter{} 14 | mc := NewCounter(c1, c2, c3) 15 | 16 | mc.Add(123) 17 | mc.Add(456) 18 | 19 | want := "[123 456]" 20 | for i, m := range []fmt.Stringer{c1, c2, c3} { 21 | if have := m.String(); want != have { 22 | t.Errorf("c%d: want %q, have %q", i+1, want, have) 23 | } 24 | } 25 | } 26 | 27 | func TestMultiGauge(t *testing.T) { 28 | g1 := &mockGauge{} 29 | g2 := &mockGauge{} 30 | g3 := &mockGauge{} 31 | mg := NewGauge(g1, g2, g3) 32 | 33 | mg.Set(9) 34 | mg.Set(8) 35 | mg.Set(7) 36 | mg.Add(3) 37 | 38 | want := "[9 8 7 10]" 39 | for i, m := range []fmt.Stringer{g1, g2, g3} { 40 | if have := m.String(); want != have { 41 | t.Errorf("g%d: want %q, have %q", i+1, want, have) 42 | } 43 | } 44 | } 45 | 46 | func TestMultiHistogram(t *testing.T) { 47 | h1 := &mockHistogram{} 48 | h2 := &mockHistogram{} 49 | h3 := &mockHistogram{} 50 | mh := NewHistogram(h1, h2, h3) 51 | 52 | mh.Observe(1) 53 | mh.Observe(2) 54 | mh.Observe(4) 55 | mh.Observe(8) 56 | 57 | want := "[1 2 4 8]" 58 | for i, m := range []fmt.Stringer{h1, h2, h3} { 59 | if have := m.String(); want != have { 60 | t.Errorf("g%d: want %q, have %q", i+1, want, have) 61 | } 62 | } 63 | } 64 | 65 | type mockCounter struct { 66 | obs []float64 67 | } 68 | 69 | func (c *mockCounter) Add(delta float64) { c.obs = append(c.obs, delta) } 70 | func (c *mockCounter) With(...string) metrics.Counter { return c } 71 | func (c *mockCounter) String() string { return fmt.Sprintf("%v", c.obs) } 72 | 73 | type mockGauge struct { 74 | obs []float64 75 | } 76 | 77 | func (g *mockGauge) Set(value float64) { g.obs = append(g.obs, value) } 78 | func (g *mockGauge) With(...string) metrics.Gauge { return g } 79 | func (g *mockGauge) String() string { return fmt.Sprintf("%v", g.obs) } 80 | func (g *mockGauge) Add(delta float64) { 81 | var value float64 82 | if len(g.obs) > 0 { 83 | value = g.obs[len(g.obs)-1] + delta 84 | } else { 85 | value = delta 86 | } 87 | g.obs = append(g.obs, value) 88 | } 89 | 90 | type mockHistogram struct { 91 | obs []float64 92 | } 93 | 94 | func (h *mockHistogram) Observe(value float64) { h.obs = append(h.obs, value) } 95 | func (h *mockHistogram) With(...string) metrics.Histogram { return h } 96 | func (h *mockHistogram) String() string { return fmt.Sprintf("%v", h.obs) } 97 | -------------------------------------------------------------------------------- /metrics/pcp/pcp_test.go: -------------------------------------------------------------------------------- 1 | package pcp 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/performancecopilot/speed/v4" 7 | 8 | "github.com/go-kit/kit/metrics/teststat" 9 | ) 10 | 11 | func TestCounter(t *testing.T) { 12 | r, err := NewReporter("test_counter") 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | 17 | counter, err := r.NewCounter("speed_counter") 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | counter = counter.With("label values", "not supported").(*Counter) 23 | 24 | value := func() float64 { f := counter.c.Val(); return float64(f) } 25 | if err := teststat.TestCounter(counter, value); err != nil { 26 | t.Fatal(err) 27 | } 28 | } 29 | 30 | func TestGauge(t *testing.T) { 31 | r, err := NewReporter("test_gauge") 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | gauge, err := r.NewGauge("speed_gauge") 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | 41 | gauge = gauge.With("label values", "not supported").(*Gauge) 42 | 43 | value := func() []float64 { f := gauge.g.Val(); return []float64{f} } 44 | if err := teststat.TestGauge(gauge, value); err != nil { 45 | t.Fatal(err) 46 | } 47 | } 48 | 49 | func TestHistogram(t *testing.T) { 50 | r, err := NewReporter("test_histogram") 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | histogram, err := r.NewHistogram("speed_histogram", 0, 3600000000, speed.OneUnit) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | histogram = histogram.With("label values", "not supported").(*Histogram) 61 | 62 | quantiles := func() (float64, float64, float64, float64) { 63 | p50 := float64(histogram.Percentile(50)) 64 | p90 := float64(histogram.Percentile(90)) 65 | p95 := float64(histogram.Percentile(95)) 66 | p99 := float64(histogram.Percentile(99)) 67 | return p50, p90, p95, p99 68 | } 69 | if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil { 70 | t.Fatal(err) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /metrics/provider/discard.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "github.com/go-kit/kit/metrics" 5 | "github.com/go-kit/kit/metrics/discard" 6 | ) 7 | 8 | type discardProvider struct{} 9 | 10 | // NewDiscardProvider returns a provider that produces no-op metrics via the 11 | // discarding backend. 12 | func NewDiscardProvider() Provider { return discardProvider{} } 13 | 14 | // NewCounter implements Provider. 15 | func (discardProvider) NewCounter(string) metrics.Counter { return discard.NewCounter() } 16 | 17 | // NewGauge implements Provider. 18 | func (discardProvider) NewGauge(string) metrics.Gauge { return discard.NewGauge() } 19 | 20 | // NewHistogram implements Provider. 21 | func (discardProvider) NewHistogram(string, int) metrics.Histogram { return discard.NewHistogram() } 22 | 23 | // Stop implements Provider. 24 | func (discardProvider) Stop() {} 25 | -------------------------------------------------------------------------------- /metrics/provider/dogstatsd.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "github.com/go-kit/kit/metrics" 5 | "github.com/go-kit/kit/metrics/dogstatsd" 6 | ) 7 | 8 | type dogstatsdProvider struct { 9 | d *dogstatsd.Dogstatsd 10 | stop func() 11 | } 12 | 13 | // NewDogstatsdProvider wraps the given Dogstatsd object and stop func and 14 | // returns a Provider that produces Dogstatsd metrics. A typical stop function 15 | // would be ticker.Stop from the ticker passed to the SendLoop helper method. 16 | func NewDogstatsdProvider(d *dogstatsd.Dogstatsd, stop func()) Provider { 17 | return &dogstatsdProvider{ 18 | d: d, 19 | stop: stop, 20 | } 21 | } 22 | 23 | // NewCounter implements Provider, returning a new Dogstatsd Counter with a 24 | // sample rate of 1.0. 25 | func (p *dogstatsdProvider) NewCounter(name string) metrics.Counter { 26 | return p.d.NewCounter(name, 1.0) 27 | } 28 | 29 | // NewGauge implements Provider. 30 | func (p *dogstatsdProvider) NewGauge(name string) metrics.Gauge { 31 | return p.d.NewGauge(name) 32 | } 33 | 34 | // NewHistogram implements Provider, returning a new Dogstatsd Histogram (note: 35 | // not a Timing) with a sample rate of 1.0. The buckets argument is ignored. 36 | func (p *dogstatsdProvider) NewHistogram(name string, _ int) metrics.Histogram { 37 | return p.d.NewHistogram(name, 1.0) 38 | } 39 | 40 | // Stop implements Provider, invoking the stop function passed at construction. 41 | func (p *dogstatsdProvider) Stop() { 42 | p.stop() 43 | } 44 | -------------------------------------------------------------------------------- /metrics/provider/expvar.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "github.com/go-kit/kit/metrics" 5 | "github.com/go-kit/kit/metrics/expvar" 6 | ) 7 | 8 | type expvarProvider struct{} 9 | 10 | // NewExpvarProvider returns a Provider that produces expvar metrics. 11 | func NewExpvarProvider() Provider { 12 | return expvarProvider{} 13 | } 14 | 15 | // NewCounter implements Provider. 16 | func (p expvarProvider) NewCounter(name string) metrics.Counter { 17 | return expvar.NewCounter(name) 18 | } 19 | 20 | // NewGauge implements Provider. 21 | func (p expvarProvider) NewGauge(name string) metrics.Gauge { 22 | return expvar.NewGauge(name) 23 | } 24 | 25 | // NewHistogram implements Provider. 26 | func (p expvarProvider) NewHistogram(name string, buckets int) metrics.Histogram { 27 | return expvar.NewHistogram(name, buckets) 28 | } 29 | 30 | // Stop implements Provider, but is a no-op. 31 | func (p expvarProvider) Stop() {} 32 | -------------------------------------------------------------------------------- /metrics/provider/graphite.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "github.com/go-kit/kit/metrics" 5 | "github.com/go-kit/kit/metrics/graphite" 6 | ) 7 | 8 | type graphiteProvider struct { 9 | g *graphite.Graphite 10 | stop func() 11 | } 12 | 13 | // NewGraphiteProvider wraps the given Graphite object and stop func and returns 14 | // a Provider that produces Graphite metrics. A typical stop function would be 15 | // ticker.Stop from the ticker passed to the SendLoop helper method. 16 | func NewGraphiteProvider(g *graphite.Graphite, stop func()) Provider { 17 | return &graphiteProvider{ 18 | g: g, 19 | stop: stop, 20 | } 21 | } 22 | 23 | // NewCounter implements Provider. 24 | func (p *graphiteProvider) NewCounter(name string) metrics.Counter { 25 | return p.g.NewCounter(name) 26 | } 27 | 28 | // NewGauge implements Provider. 29 | func (p *graphiteProvider) NewGauge(name string) metrics.Gauge { 30 | return p.g.NewGauge(name) 31 | } 32 | 33 | // NewHistogram implements Provider. 34 | func (p *graphiteProvider) NewHistogram(name string, buckets int) metrics.Histogram { 35 | return p.g.NewHistogram(name, buckets) 36 | } 37 | 38 | // Stop implements Provider, invoking the stop function passed at construction. 39 | func (p *graphiteProvider) Stop() { 40 | p.stop() 41 | } 42 | -------------------------------------------------------------------------------- /metrics/provider/influx.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "github.com/go-kit/kit/metrics" 5 | "github.com/go-kit/kit/metrics/influx" 6 | ) 7 | 8 | type influxProvider struct { 9 | in *influx.Influx 10 | stop func() 11 | } 12 | 13 | // NewInfluxProvider takes the given Influx object and stop func, and returns 14 | // a Provider that produces Influx metrics. 15 | func NewInfluxProvider(in *influx.Influx, stop func()) Provider { 16 | return &influxProvider{ 17 | in: in, 18 | stop: stop, 19 | } 20 | } 21 | 22 | // NewCounter implements Provider. Per-metric tags are not supported. 23 | func (p *influxProvider) NewCounter(name string) metrics.Counter { 24 | return p.in.NewCounter(name) 25 | } 26 | 27 | // NewGauge implements Provider. Per-metric tags are not supported. 28 | func (p *influxProvider) NewGauge(name string) metrics.Gauge { 29 | return p.in.NewGauge(name) 30 | } 31 | 32 | // NewHistogram implements Provider. Per-metric tags are not supported. 33 | func (p *influxProvider) NewHistogram(name string, buckets int) metrics.Histogram { 34 | return p.in.NewHistogram(name) 35 | } 36 | 37 | // Stop implements Provider, invoking the stop function passed at construction. 38 | func (p *influxProvider) Stop() { 39 | p.stop() 40 | } 41 | -------------------------------------------------------------------------------- /metrics/provider/prometheus.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | stdprometheus "github.com/prometheus/client_golang/prometheus" 5 | 6 | "github.com/go-kit/kit/metrics" 7 | "github.com/go-kit/kit/metrics/prometheus" 8 | ) 9 | 10 | type prometheusProvider struct { 11 | namespace string 12 | subsystem string 13 | } 14 | 15 | // NewPrometheusProvider returns a Provider that produces Prometheus metrics. 16 | // Namespace and subsystem are applied to all produced metrics. 17 | func NewPrometheusProvider(namespace, subsystem string) Provider { 18 | return &prometheusProvider{ 19 | namespace: namespace, 20 | subsystem: subsystem, 21 | } 22 | } 23 | 24 | // NewCounter implements Provider via prometheus.NewCounterFrom, i.e. the 25 | // counter is registered. The metric's namespace and subsystem are taken from 26 | // the Provider. Help is set to the name of the metric, and no const label names 27 | // are set. 28 | func (p *prometheusProvider) NewCounter(name string) metrics.Counter { 29 | return prometheus.NewCounterFrom(stdprometheus.CounterOpts{ 30 | Namespace: p.namespace, 31 | Subsystem: p.subsystem, 32 | Name: name, 33 | Help: name, 34 | }, []string{}) 35 | } 36 | 37 | // NewGauge implements Provider via prometheus.NewGaugeFrom, i.e. the gauge is 38 | // registered. The metric's namespace and subsystem are taken from the Provider. 39 | // Help is set to the name of the metric, and no const label names are set. 40 | func (p *prometheusProvider) NewGauge(name string) metrics.Gauge { 41 | return prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ 42 | Namespace: p.namespace, 43 | Subsystem: p.subsystem, 44 | Name: name, 45 | Help: name, 46 | }, []string{}) 47 | } 48 | 49 | // NewHistogram implements Provider via prometheus.NewSummaryFrom, i.e. the summary 50 | // is registered. The metric's namespace and subsystem are taken from the 51 | // Provider. Help is set to the name of the metric, and no const label names are 52 | // set. Buckets are ignored. 53 | func (p *prometheusProvider) NewHistogram(name string, _ int) metrics.Histogram { 54 | return prometheus.NewSummaryFrom(stdprometheus.SummaryOpts{ 55 | Namespace: p.namespace, 56 | Subsystem: p.subsystem, 57 | Name: name, 58 | Help: name, 59 | }, []string{}) 60 | } 61 | 62 | // Stop implements Provider, but is a no-op. 63 | func (p *prometheusProvider) Stop() {} 64 | -------------------------------------------------------------------------------- /metrics/provider/provider.go: -------------------------------------------------------------------------------- 1 | // Package provider provides a factory-like abstraction for metrics backends. 2 | // This package is provided specifically for the needs of the NY Times framework 3 | // Gizmo. Most normal Go kit users shouldn't need to use it. 4 | // 5 | // Normally, if your microservice needs to support different metrics backends, 6 | // you can simply do different construction based on a flag. For example, 7 | // 8 | // var latency metrics.Histogram 9 | // var requests metrics.Counter 10 | // switch *metricsBackend { 11 | // case "prometheus": 12 | // latency = prometheus.NewSummaryVec(...) 13 | // requests = prometheus.NewCounterVec(...) 14 | // case "statsd": 15 | // s := statsd.New(...) 16 | // t := time.NewTicker(5*time.Second) 17 | // go s.SendLoop(ctx, t.C, "tcp", "statsd.local:8125") 18 | // latency = s.NewHistogram(...) 19 | // requests = s.NewCounter(...) 20 | // default: 21 | // log.Fatal("unsupported metrics backend %q", *metricsBackend) 22 | // } 23 | // 24 | package provider 25 | 26 | import ( 27 | "github.com/go-kit/kit/metrics" 28 | ) 29 | 30 | // Provider abstracts over constructors and lifecycle management functions for 31 | // each supported metrics backend. It should only be used by those who need to 32 | // swap out implementations dynamically. 33 | // 34 | // This is primarily useful for intermediating frameworks, and is likely 35 | // unnecessary for most Go kit services. See the package-level doc comment for 36 | // more typical usage instructions. 37 | type Provider interface { 38 | NewCounter(name string) metrics.Counter 39 | NewGauge(name string) metrics.Gauge 40 | NewHistogram(name string, buckets int) metrics.Histogram 41 | Stop() 42 | } 43 | -------------------------------------------------------------------------------- /metrics/provider/statsd.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "github.com/go-kit/kit/metrics" 5 | "github.com/go-kit/kit/metrics/statsd" 6 | ) 7 | 8 | type statsdProvider struct { 9 | s *statsd.Statsd 10 | stop func() 11 | } 12 | 13 | // NewStatsdProvider wraps the given Statsd object and stop func and returns a 14 | // Provider that produces Statsd metrics. A typical stop function would be 15 | // ticker.Stop from the ticker passed to the SendLoop helper method. 16 | func NewStatsdProvider(s *statsd.Statsd, stop func()) Provider { 17 | return &statsdProvider{ 18 | s: s, 19 | stop: stop, 20 | } 21 | } 22 | 23 | // NewCounter implements Provider. 24 | func (p *statsdProvider) NewCounter(name string) metrics.Counter { 25 | return p.s.NewCounter(name, 1.0) 26 | } 27 | 28 | // NewGauge implements Provider. 29 | func (p *statsdProvider) NewGauge(name string) metrics.Gauge { 30 | return p.s.NewGauge(name) 31 | } 32 | 33 | // NewHistogram implements Provider, returning a StatsD Timing that accepts 34 | // observations in milliseconds. The sample rate is fixed at 1.0. The bucket 35 | // parameter is ignored. 36 | func (p *statsdProvider) NewHistogram(name string, _ int) metrics.Histogram { 37 | return p.s.NewTiming(name, 1.0) 38 | } 39 | 40 | // Stop implements Provider, invoking the stop function passed at construction. 41 | func (p *statsdProvider) Stop() { 42 | p.stop() 43 | } 44 | -------------------------------------------------------------------------------- /metrics/statsd/statsd_test.go: -------------------------------------------------------------------------------- 1 | package statsd 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-kit/kit/metrics/teststat" 7 | "github.com/go-kit/log" 8 | ) 9 | 10 | func TestCounter(t *testing.T) { 11 | prefix, name := "abc.", "def" 12 | label, value := "label", "value" // ignored 13 | regex := `^` + prefix + name + `:([0-9\.]+)\|c$` 14 | s := New(prefix, log.NewNopLogger()) 15 | counter := s.NewCounter(name, 1.0).With(label, value) 16 | valuef := teststat.SumLines(s, regex) 17 | if err := teststat.TestCounter(counter, valuef); err != nil { 18 | t.Fatal(err) 19 | } 20 | } 21 | 22 | func TestCounterSampled(t *testing.T) { 23 | // This will involve multiplying the observed sum by the inverse of the 24 | // sample rate and checking against the expected value within some 25 | // tolerance. 26 | t.Skip("TODO") 27 | } 28 | 29 | func TestGauge(t *testing.T) { 30 | prefix, name := "ghi.", "jkl" 31 | label, value := "xyz", "abc" // ignored 32 | regex := `^` + prefix + name + `:([0-9\.]+)\|g$` 33 | s := New(prefix, log.NewNopLogger()) 34 | gauge := s.NewGauge(name).With(label, value) 35 | valuef := teststat.LastLine(s, regex) 36 | if err := teststat.TestGauge(gauge, valuef); err != nil { 37 | t.Fatal(err) 38 | } 39 | } 40 | 41 | // StatsD timings just emit all observations. So, we collect them into a generic 42 | // histogram, and run the statistics test on that. 43 | 44 | func TestTiming(t *testing.T) { 45 | prefix, name := "statsd.", "timing_test" 46 | label, value := "abc", "def" // ignored 47 | regex := `^` + prefix + name + `:([0-9\.]+)\|ms$` 48 | s := New(prefix, log.NewNopLogger()) 49 | timing := s.NewTiming(name, 1.0).With(label, value) 50 | quantiles := teststat.Quantiles(s, regex, 50) // no |@0.X 51 | if err := teststat.TestHistogram(timing, quantiles, 0.01); err != nil { 52 | t.Fatal(err) 53 | } 54 | } 55 | 56 | func TestTimingSampled(t *testing.T) { 57 | prefix, name := "statsd.", "sampled_timing_test" 58 | label, value := "foo", "bar" // ignored 59 | regex := `^` + prefix + name + `:([0-9\.]+)\|ms\|@0\.01[0]*$` 60 | s := New(prefix, log.NewNopLogger()) 61 | timing := s.NewTiming(name, 0.01).With(label, value) 62 | quantiles := teststat.Quantiles(s, regex, 50) 63 | if err := teststat.TestHistogram(timing, quantiles, 0.02); err != nil { 64 | t.Fatal(err) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /metrics/teststat/buffers.go: -------------------------------------------------------------------------------- 1 | package teststat 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "regexp" 8 | "strconv" 9 | 10 | "github.com/go-kit/kit/metrics/generic" 11 | ) 12 | 13 | // SumLines expects a regex whose first capture group can be parsed as a 14 | // float64. It will dump the WriterTo and parse each line, expecting to find a 15 | // match. It returns the sum of all captured floats. 16 | func SumLines(w io.WriterTo, regex string) func() float64 { 17 | return func() float64 { 18 | sum, _ := stats(w, regex, nil) 19 | return sum 20 | } 21 | } 22 | 23 | // LastLine expects a regex whose first capture group can be parsed as a 24 | // float64. It will dump the WriterTo and parse each line, expecting to find a 25 | // match. It returns the final captured float. 26 | func LastLine(w io.WriterTo, regex string) func() []float64 { 27 | return func() []float64 { 28 | _, final := stats(w, regex, nil) 29 | return []float64{final} 30 | } 31 | } 32 | 33 | // Quantiles expects a regex whose first capture group can be parsed as a 34 | // float64. It will dump the WriterTo and parse each line, expecting to find a 35 | // match. It observes all captured floats into a generic.Histogram with the 36 | // given number of buckets, and returns the 50th, 90th, 95th, and 99th quantiles 37 | // from that histogram. 38 | func Quantiles(w io.WriterTo, regex string, buckets int) func() (float64, float64, float64, float64) { 39 | return func() (float64, float64, float64, float64) { 40 | h := generic.NewHistogram("quantile-test", buckets) 41 | stats(w, regex, h) 42 | return h.Quantile(0.50), h.Quantile(0.90), h.Quantile(0.95), h.Quantile(0.99) 43 | } 44 | } 45 | 46 | func stats(w io.WriterTo, regex string, h *generic.Histogram) (sum, final float64) { 47 | re := regexp.MustCompile(regex) 48 | buf := &bytes.Buffer{} 49 | w.WriteTo(buf) 50 | s := bufio.NewScanner(buf) 51 | for s.Scan() { 52 | match := re.FindStringSubmatch(s.Text()) 53 | f, err := strconv.ParseFloat(match[1], 64) 54 | if err != nil { 55 | panic(err) 56 | } 57 | sum += f 58 | final = f 59 | if h != nil { 60 | h.Observe(f) 61 | } 62 | } 63 | return sum, final 64 | } 65 | -------------------------------------------------------------------------------- /metrics/teststat/populate.go: -------------------------------------------------------------------------------- 1 | package teststat 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | 7 | "github.com/go-kit/kit/metrics" 8 | ) 9 | 10 | // PopulateNormalHistogram makes a series of normal random observations into the 11 | // histogram. The number of observations is determined by Count. The randomness 12 | // is determined by Mean, Stdev, and the seed parameter. 13 | // 14 | // This is a low-level function, exported only for metrics that don't perform 15 | // dynamic quantile computation, like a Prometheus Histogram (c.f. Summary). In 16 | // most cases, you don't need to use this function, and can use TestHistogram 17 | // instead. 18 | func PopulateNormalHistogram(h metrics.Histogram, seed int) { 19 | r := rand.New(rand.NewSource(int64(seed))) 20 | for i := 0; i < Count; i++ { 21 | sample := r.NormFloat64()*float64(Stdev) + float64(Mean) 22 | if sample < 0 { 23 | sample = 0 24 | } 25 | h.Observe(sample) 26 | } 27 | } 28 | 29 | func normalQuantiles() (p50, p90, p95, p99 float64) { 30 | return nvq(50), nvq(90), nvq(95), nvq(99) 31 | } 32 | 33 | func nvq(quantile int) float64 { 34 | // https://en.wikipedia.org/wiki/Normal_distribution#Quantile_function 35 | return float64(Mean) + float64(Stdev)*math.Sqrt2*erfinv(2*(float64(quantile)/100)-1) 36 | } 37 | 38 | func erfinv(y float64) float64 { 39 | // https://stackoverflow.com/questions/5971830/need-code-for-inverse-error-function 40 | if y < -1.0 || y > 1.0 { 41 | panic("invalid input") 42 | } 43 | 44 | var ( 45 | a = [4]float64{0.886226899, -1.645349621, 0.914624893, -0.140543331} 46 | b = [4]float64{-2.118377725, 1.442710462, -0.329097515, 0.012229801} 47 | c = [4]float64{-1.970840454, -1.624906493, 3.429567803, 1.641345311} 48 | d = [2]float64{3.543889200, 1.637067800} 49 | ) 50 | 51 | const y0 = 0.7 52 | var x, z float64 53 | 54 | if math.Abs(y) == 1.0 { 55 | x = -y * math.Log(0.0) 56 | } else if y < -y0 { 57 | z = math.Sqrt(-math.Log((1.0 + y) / 2.0)) 58 | x = -(((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0) 59 | } else { 60 | if y < y0 { 61 | z = y * y 62 | x = y * (((a[3]*z+a[2])*z+a[1])*z + a[0]) / ((((b[3]*z+b[3])*z+b[1])*z+b[0])*z + 1.0) 63 | } else { 64 | z = math.Sqrt(-math.Log((1.0 - y) / 2.0)) 65 | x = (((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0) 66 | } 67 | x -= (math.Erf(x) - y) / (2.0 / math.SqrtPi * math.Exp(-x*x)) 68 | x -= (math.Erf(x) - y) / (2.0 / math.SqrtPi * math.Exp(-x*x)) 69 | } 70 | 71 | return x 72 | } 73 | -------------------------------------------------------------------------------- /metrics/timer.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import "time" 4 | 5 | // Timer acts as a stopwatch, sending observations to a wrapped histogram. 6 | // It's a bit of helpful syntax sugar for h.Observe(time.Since(x)). 7 | type Timer struct { 8 | h Histogram 9 | t time.Time 10 | u time.Duration 11 | } 12 | 13 | // NewTimer wraps the given histogram and records the current time. 14 | func NewTimer(h Histogram) *Timer { 15 | return &Timer{ 16 | h: h, 17 | t: time.Now(), 18 | u: time.Second, 19 | } 20 | } 21 | 22 | // ObserveDuration captures the number of seconds since the timer was 23 | // constructed, and forwards that observation to the histogram. 24 | func (t *Timer) ObserveDuration() { 25 | d := float64(time.Since(t.t).Nanoseconds()) / float64(t.u) 26 | if d < 0 { 27 | d = 0 28 | } 29 | t.h.Observe(d) 30 | } 31 | 32 | // Unit sets the unit of the float64 emitted by the timer. 33 | // By default, the timer emits seconds. 34 | func (t *Timer) Unit(u time.Duration) { 35 | t.u = u 36 | } 37 | -------------------------------------------------------------------------------- /metrics/timer_test.go: -------------------------------------------------------------------------------- 1 | package metrics_test 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "time" 8 | 9 | "github.com/go-kit/kit/metrics" 10 | "github.com/go-kit/kit/metrics/generic" 11 | ) 12 | 13 | func TestTimerFast(t *testing.T) { 14 | h := generic.NewSimpleHistogram() 15 | metrics.NewTimer(h).ObserveDuration() 16 | 17 | tolerance := 0.050 18 | if want, have := 0.000, h.ApproximateMovingAverage(); math.Abs(want-have) > tolerance { 19 | t.Errorf("want %.3f, have %.3f", want, have) 20 | } 21 | } 22 | 23 | func TestTimerSlow(t *testing.T) { 24 | h := generic.NewSimpleHistogram() 25 | timer := metrics.NewTimer(h) 26 | time.Sleep(250 * time.Millisecond) 27 | timer.ObserveDuration() 28 | 29 | tolerance := 0.050 30 | if want, have := 0.250, h.ApproximateMovingAverage(); math.Abs(want-have) > tolerance { 31 | t.Errorf("want %.3f, have %.3f", want, have) 32 | } 33 | } 34 | 35 | func TestTimerUnit(t *testing.T) { 36 | for _, tc := range []struct { 37 | name string 38 | unit time.Duration 39 | tolerance float64 40 | want float64 41 | }{ 42 | {"Seconds", time.Second, 0.010, 0.100}, 43 | {"Milliseconds", time.Millisecond, 10, 100}, 44 | {"Nanoseconds", time.Nanosecond, 10000000, 100000000}, 45 | } { 46 | t.Run(tc.name, func(t *testing.T) { 47 | h := generic.NewSimpleHistogram() 48 | timer := metrics.NewTimer(h) 49 | time.Sleep(100 * time.Millisecond) 50 | timer.Unit(tc.unit) 51 | timer.ObserveDuration() 52 | 53 | if want, have := tc.want, h.ApproximateMovingAverage(); math.Abs(want-have) > tc.tolerance { 54 | t.Errorf("want %.3f, have %.3f", want, have) 55 | } 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ratelimit/token_bucket.go: -------------------------------------------------------------------------------- 1 | package ratelimit 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/go-kit/kit/endpoint" 8 | ) 9 | 10 | // ErrLimited is returned in the request path when the rate limiter is 11 | // triggered and the request is rejected. 12 | var ErrLimited = errors.New("rate limit exceeded") 13 | 14 | // Allower dictates whether or not a request is acceptable to run. 15 | // The Limiter from "golang.org/x/time/rate" already implements this interface, 16 | // one is able to use that in NewErroringLimiter without any modifications. 17 | type Allower interface { 18 | Allow() bool 19 | } 20 | 21 | // NewErroringLimiter returns an endpoint.Middleware that acts as a rate 22 | // limiter. Requests that would exceed the 23 | // maximum request rate are simply rejected with an error. 24 | func NewErroringLimiter(limit Allower) endpoint.Middleware { 25 | return func(next endpoint.Endpoint) endpoint.Endpoint { 26 | return func(ctx context.Context, request interface{}) (interface{}, error) { 27 | if !limit.Allow() { 28 | return nil, ErrLimited 29 | } 30 | return next(ctx, request) 31 | } 32 | } 33 | } 34 | 35 | // Waiter dictates how long a request must be delayed. 36 | // The Limiter from "golang.org/x/time/rate" already implements this interface, 37 | // one is able to use that in NewDelayingLimiter without any modifications. 38 | type Waiter interface { 39 | Wait(ctx context.Context) error 40 | } 41 | 42 | // NewDelayingLimiter returns an endpoint.Middleware that acts as a 43 | // request throttler. Requests that would 44 | // exceed the maximum request rate are delayed via the Waiter function 45 | func NewDelayingLimiter(limit Waiter) endpoint.Middleware { 46 | return func(next endpoint.Endpoint) endpoint.Endpoint { 47 | return func(ctx context.Context, request interface{}) (interface{}, error) { 48 | if err := limit.Wait(ctx); err != nil { 49 | return nil, err 50 | } 51 | return next(ctx, request) 52 | } 53 | } 54 | } 55 | 56 | // AllowerFunc is an adapter that lets a function operate as if 57 | // it implements Allower 58 | type AllowerFunc func() bool 59 | 60 | // Allow makes the adapter implement Allower 61 | func (f AllowerFunc) Allow() bool { 62 | return f() 63 | } 64 | 65 | // WaiterFunc is an adapter that lets a function operate as if 66 | // it implements Waiter 67 | type WaiterFunc func(ctx context.Context) error 68 | 69 | // Wait makes the adapter implement Waiter 70 | func (f WaiterFunc) Wait(ctx context.Context) error { 71 | return f(ctx) 72 | } 73 | -------------------------------------------------------------------------------- /ratelimit/token_bucket_test.go: -------------------------------------------------------------------------------- 1 | package ratelimit_test 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "golang.org/x/time/rate" 10 | 11 | "github.com/go-kit/kit/endpoint" 12 | "github.com/go-kit/kit/ratelimit" 13 | ) 14 | 15 | var nopEndpoint = func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil } 16 | 17 | func TestXRateErroring(t *testing.T) { 18 | limit := rate.NewLimiter(rate.Every(time.Minute), 1) 19 | testSuccessThenFailure( 20 | t, 21 | ratelimit.NewErroringLimiter(limit)(nopEndpoint), 22 | ratelimit.ErrLimited.Error()) 23 | } 24 | 25 | func TestXRateDelaying(t *testing.T) { 26 | limit := rate.NewLimiter(rate.Every(time.Minute), 1) 27 | testSuccessThenFailure( 28 | t, 29 | ratelimit.NewDelayingLimiter(limit)(nopEndpoint), 30 | "exceed context deadline") 31 | } 32 | 33 | func testSuccessThenFailure(t *testing.T, e endpoint.Endpoint, failContains string) { 34 | ctx, cxl := context.WithTimeout(context.Background(), 500*time.Millisecond) 35 | defer cxl() 36 | 37 | // First request should succeed. 38 | if _, err := e(ctx, struct{}{}); err != nil { 39 | t.Errorf("unexpected: %v\n", err) 40 | } 41 | 42 | // Next request should fail. 43 | if _, err := e(ctx, struct{}{}); !strings.Contains(err.Error(), failContains) { 44 | t.Errorf("expected `%s`: %v\n", failContains, err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /sd/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package sd 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | 7 | "github.com/go-kit/kit/endpoint" 8 | "github.com/go-kit/log" 9 | ) 10 | 11 | func BenchmarkEndpoints(b *testing.B) { 12 | var ( 13 | ca = make(closer) 14 | cb = make(closer) 15 | cmap = map[string]io.Closer{"a": ca, "b": cb} 16 | factory = func(instance string) (endpoint.Endpoint, io.Closer, error) { return endpoint.Nop, cmap[instance], nil } 17 | c = newEndpointCache(factory, log.NewNopLogger(), endpointerOptions{}) 18 | ) 19 | 20 | b.ReportAllocs() 21 | 22 | c.Update(Event{Instances: []string{"a", "b"}}) 23 | 24 | b.RunParallel(func(pb *testing.PB) { 25 | for pb.Next() { 26 | c.Endpoints() 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /sd/consul/client.go: -------------------------------------------------------------------------------- 1 | package consul 2 | 3 | import ( 4 | consul "github.com/hashicorp/consul/api" 5 | ) 6 | 7 | // Client is a wrapper around the Consul API. 8 | type Client interface { 9 | // Register a service with the local agent. 10 | Register(r *consul.AgentServiceRegistration) error 11 | 12 | // Deregister a service with the local agent. 13 | Deregister(r *consul.AgentServiceRegistration) error 14 | 15 | // Service 16 | Service(service, tag string, passingOnly bool, queryOpts *consul.QueryOptions) ([]*consul.ServiceEntry, *consul.QueryMeta, error) 17 | } 18 | 19 | type client struct { 20 | consul *consul.Client 21 | } 22 | 23 | // NewClient returns an implementation of the Client interface, wrapping a 24 | // concrete Consul client. 25 | func NewClient(c *consul.Client) Client { 26 | return &client{consul: c} 27 | } 28 | 29 | func (c *client) Register(r *consul.AgentServiceRegistration) error { 30 | return c.consul.Agent().ServiceRegister(r) 31 | } 32 | 33 | func (c *client) Deregister(r *consul.AgentServiceRegistration) error { 34 | return c.consul.Agent().ServiceDeregister(r.ID) 35 | } 36 | 37 | func (c *client) Service(service, tag string, passingOnly bool, queryOpts *consul.QueryOptions) ([]*consul.ServiceEntry, *consul.QueryMeta, error) { 38 | return c.consul.Health().Service(service, tag, passingOnly, queryOpts) 39 | } 40 | -------------------------------------------------------------------------------- /sd/consul/doc.go: -------------------------------------------------------------------------------- 1 | // Package consul provides Instancer and Registrar implementations for Consul. 2 | package consul 3 | -------------------------------------------------------------------------------- /sd/consul/integration_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | package consul 5 | 6 | import ( 7 | "io" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | "github.com/go-kit/kit/endpoint" 13 | "github.com/go-kit/kit/sd" 14 | "github.com/go-kit/log" 15 | stdconsul "github.com/hashicorp/consul/api" 16 | ) 17 | 18 | func TestIntegration(t *testing.T) { 19 | consulAddr := os.Getenv("CONSUL_ADDR") 20 | if consulAddr == "" { 21 | t.Skip("CONSUL_ADDR not set; skipping integration test") 22 | } 23 | stdClient, err := stdconsul.NewClient(&stdconsul.Config{ 24 | Address: consulAddr, 25 | }) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | client := NewClient(stdClient) 30 | logger := log.NewLogfmtLogger(os.Stderr) 31 | 32 | // Produce a fake service registration. 33 | r := &stdconsul.AgentServiceRegistration{ 34 | ID: "my-service-ID", 35 | Name: "my-service-name", 36 | Tags: []string{"alpha", "beta"}, 37 | Port: 12345, 38 | Address: "my-address", 39 | EnableTagOverride: false, 40 | // skipping check(s) 41 | } 42 | 43 | // Build an Instancer on r.Name + r.Tags. 44 | factory := func(instance string) (endpoint.Endpoint, io.Closer, error) { 45 | t.Logf("factory invoked for %q", instance) 46 | return endpoint.Nop, nil, nil 47 | } 48 | instancer := NewInstancer( 49 | client, 50 | log.With(logger, "component", "instancer"), 51 | r.Name, 52 | r.Tags, 53 | true, 54 | ) 55 | endpointer := sd.NewEndpointer( 56 | instancer, 57 | factory, 58 | log.With(logger, "component", "endpointer"), 59 | ) 60 | 61 | time.Sleep(time.Second) 62 | 63 | // Before we publish, we should have no endpoints. 64 | endpoints, err := endpointer.Endpoints() 65 | if err != nil { 66 | t.Error(err) 67 | } 68 | if want, have := 0, len(endpoints); want != have { 69 | t.Errorf("want %d, have %d", want, have) 70 | } 71 | 72 | // Build a registrar for r. 73 | registrar := NewRegistrar(client, r, log.With(logger, "component", "registrar")) 74 | registrar.Register() 75 | defer registrar.Deregister() 76 | 77 | time.Sleep(time.Second) 78 | 79 | // Now we should have one active endpoints. 80 | endpoints, err = endpointer.Endpoints() 81 | if err != nil { 82 | t.Error(err) 83 | } 84 | if want, have := 1, len(endpoints); want != have { 85 | t.Errorf("want %d, have %d", want, have) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /sd/consul/registrar.go: -------------------------------------------------------------------------------- 1 | package consul 2 | 3 | import ( 4 | "fmt" 5 | 6 | stdconsul "github.com/hashicorp/consul/api" 7 | 8 | "github.com/go-kit/log" 9 | ) 10 | 11 | // Registrar registers service instance liveness information to Consul. 12 | type Registrar struct { 13 | client Client 14 | registration *stdconsul.AgentServiceRegistration 15 | logger log.Logger 16 | } 17 | 18 | // NewRegistrar returns a Consul Registrar acting on the provided catalog 19 | // registration. 20 | func NewRegistrar(client Client, r *stdconsul.AgentServiceRegistration, logger log.Logger) *Registrar { 21 | return &Registrar{ 22 | client: client, 23 | registration: r, 24 | logger: log.With(logger, "service", r.Name, "tags", fmt.Sprint(r.Tags), "address", r.Address), 25 | } 26 | } 27 | 28 | // Register implements sd.Registrar interface. 29 | func (p *Registrar) Register() { 30 | if err := p.client.Register(p.registration); err != nil { 31 | p.logger.Log("err", err) 32 | } else { 33 | p.logger.Log("action", "register") 34 | } 35 | } 36 | 37 | // Deregister implements sd.Registrar interface. 38 | func (p *Registrar) Deregister() { 39 | if err := p.client.Deregister(p.registration); err != nil { 40 | p.logger.Log("err", err) 41 | } else { 42 | p.logger.Log("action", "deregister") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /sd/consul/registrar_test.go: -------------------------------------------------------------------------------- 1 | package consul 2 | 3 | import ( 4 | "testing" 5 | 6 | stdconsul "github.com/hashicorp/consul/api" 7 | 8 | "github.com/go-kit/log" 9 | ) 10 | 11 | func TestRegistrar(t *testing.T) { 12 | client := newTestClient([]*stdconsul.ServiceEntry{}) 13 | p := NewRegistrar(client, testRegistration, log.NewNopLogger()) 14 | if want, have := 0, len(client.entries); want != have { 15 | t.Errorf("want %d, have %d", want, have) 16 | } 17 | 18 | p.Register() 19 | if want, have := 1, len(client.entries); want != have { 20 | t.Errorf("want %d, have %d", want, have) 21 | } 22 | 23 | p.Deregister() 24 | if want, have := 0, len(client.entries); want != have { 25 | t.Errorf("want %d, have %d", want, have) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /sd/dnssrv/doc.go: -------------------------------------------------------------------------------- 1 | // Package dnssrv provides an Instancer implementation for DNS SRV records. 2 | package dnssrv 3 | -------------------------------------------------------------------------------- /sd/dnssrv/lookup.go: -------------------------------------------------------------------------------- 1 | package dnssrv 2 | 3 | import "net" 4 | 5 | // Lookup is a function that resolves a DNS SRV record to multiple addresses. 6 | // It has the same signature as net.LookupSRV. 7 | type Lookup func(service, proto, name string) (cname string, addrs []*net.SRV, err error) 8 | -------------------------------------------------------------------------------- /sd/doc.go: -------------------------------------------------------------------------------- 1 | // Package sd provides utilities related to service discovery. That includes the 2 | // client-side loadbalancer pattern, where a microservice subscribes to a 3 | // service discovery system in order to reach remote instances; as well as the 4 | // registrator pattern, where a microservice registers itself in a service 5 | // discovery system. Implementations are provided for most common systems. 6 | package sd 7 | -------------------------------------------------------------------------------- /sd/endpointer_test.go: -------------------------------------------------------------------------------- 1 | package sd_test 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | "time" 7 | 8 | "github.com/go-kit/kit/endpoint" 9 | "github.com/go-kit/kit/sd" 10 | "github.com/go-kit/kit/sd/internal/instance" 11 | "github.com/go-kit/log" 12 | ) 13 | 14 | func TestDefaultEndpointer(t *testing.T) { 15 | var ( 16 | ca = make(closer) 17 | cb = make(closer) 18 | c = map[string]io.Closer{"a": ca, "b": cb} 19 | f = func(instance string) (endpoint.Endpoint, io.Closer, error) { 20 | return endpoint.Nop, c[instance], nil 21 | } 22 | instancer = &mockInstancer{instance.NewCache()} 23 | ) 24 | // set initial state 25 | instancer.Update(sd.Event{Instances: []string{"a", "b"}}) 26 | 27 | endpointer := sd.NewEndpointer(instancer, f, log.NewNopLogger(), sd.InvalidateOnError(time.Minute)) 28 | 29 | var ( 30 | endpoints []endpoint.Endpoint 31 | err error 32 | ) 33 | if !within(time.Second, func() bool { 34 | endpoints, err = endpointer.Endpoints() 35 | return err == nil && len(endpoints) == 2 36 | }) { 37 | t.Errorf("wanted 2 endpoints, got %d (%v)", len(endpoints), err) 38 | } 39 | 40 | instancer.Update(sd.Event{Instances: []string{}}) 41 | 42 | select { 43 | case <-ca: 44 | t.Logf("endpoint a closed, good") 45 | case <-time.After(time.Millisecond): 46 | t.Errorf("didn't close the deleted instance in time") 47 | } 48 | 49 | select { 50 | case <-cb: 51 | t.Logf("endpoint b closed, good") 52 | case <-time.After(time.Millisecond): 53 | t.Errorf("didn't close the deleted instance in time") 54 | } 55 | 56 | if endpoints, err := endpointer.Endpoints(); err != nil { 57 | t.Errorf("unepected error %v", err) 58 | } else if want, have := 0, len(endpoints); want != have { 59 | t.Errorf("want %d, have %d", want, have) 60 | } 61 | 62 | endpointer.Close() 63 | 64 | instancer.Update(sd.Event{Instances: []string{"a"}}) 65 | // TODO verify that on Close the endpointer fully disconnects from the instancer. 66 | // Unfortunately, because we use instance.Cache, this test cannot be in the sd package, 67 | // and therefore does not have access to the endpointer's private members. 68 | } 69 | 70 | type mockInstancer struct{ *instance.Cache } 71 | 72 | type closer chan struct{} 73 | 74 | func (c closer) Close() error { close(c); return nil } 75 | 76 | func within(d time.Duration, f func() bool) bool { 77 | deadline := time.Now().Add(d) 78 | for time.Now().Before(deadline) { 79 | if f() { 80 | return true 81 | } 82 | time.Sleep(d / 10) 83 | } 84 | return false 85 | } 86 | -------------------------------------------------------------------------------- /sd/etcd/doc.go: -------------------------------------------------------------------------------- 1 | // Package etcd provides an Instancer and Registrar implementation for etcd. If 2 | // you use etcd as your service discovery system, this package will help you 3 | // implement the registration and client-side load balancing patterns. 4 | package etcd 5 | -------------------------------------------------------------------------------- /sd/etcd/example_test.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "time" 7 | 8 | "github.com/go-kit/kit/endpoint" 9 | "github.com/go-kit/kit/sd" 10 | "github.com/go-kit/kit/sd/lb" 11 | "github.com/go-kit/log" 12 | ) 13 | 14 | func Example() { 15 | // Let's say this is a service that means to register itself. 16 | // First, we will set up some context. 17 | var ( 18 | etcdServer = "http://10.0.0.1:2379" // don't forget schema and port! 19 | prefix = "/services/foosvc/" // known at compile time 20 | instance = "1.2.3.4:8080" // taken from runtime or platform, somehow 21 | key = prefix + instance // should be globally unique 22 | value = "http://" + instance // based on our transport 23 | ctx = context.Background() 24 | ) 25 | 26 | // Build the client. 27 | client, err := NewClient(ctx, []string{etcdServer}, ClientOptions{}) 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | // Build the registrar. 33 | registrar := NewRegistrar(client, Service{ 34 | Key: key, 35 | Value: value, 36 | }, log.NewNopLogger()) 37 | 38 | // Register our instance. 39 | registrar.Register() 40 | 41 | // At the end of our service lifecycle, for example at the end of func main, 42 | // we should make sure to deregister ourselves. This is important! Don't 43 | // accidentally skip this step by invoking a log.Fatal or os.Exit in the 44 | // interim, which bypasses the defer stack. 45 | defer registrar.Deregister() 46 | 47 | // It's likely that we'll also want to connect to other services and call 48 | // their methods. We can build an Instancer to listen for changes from etcd, 49 | // create Endpointer, wrap it with a load-balancer to pick a single 50 | // endpoint, and finally wrap it with a retry strategy to get something that 51 | // can be used as an endpoint directly. 52 | barPrefix := "/services/barsvc" 53 | logger := log.NewNopLogger() 54 | instancer, err := NewInstancer(client, barPrefix, logger) 55 | if err != nil { 56 | panic(err) 57 | } 58 | endpointer := sd.NewEndpointer(instancer, barFactory, logger) 59 | balancer := lb.NewRoundRobin(endpointer) 60 | retry := lb.Retry(3, 3*time.Second, balancer) 61 | 62 | // And now retry can be used like any other endpoint. 63 | req := struct{}{} 64 | if _, err = retry(ctx, req); err != nil { 65 | panic(err) 66 | } 67 | } 68 | 69 | func barFactory(string) (endpoint.Endpoint, io.Closer, error) { return endpoint.Nop, nil, nil } 70 | -------------------------------------------------------------------------------- /sd/etcd/instancer.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "github.com/go-kit/kit/sd" 5 | "github.com/go-kit/kit/sd/internal/instance" 6 | "github.com/go-kit/log" 7 | ) 8 | 9 | // Instancer yields instances stored in a certain etcd keyspace. Any kind of 10 | // change in that keyspace is watched and will update the Instancer's Instancers. 11 | type Instancer struct { 12 | cache *instance.Cache 13 | client Client 14 | prefix string 15 | logger log.Logger 16 | quitc chan struct{} 17 | } 18 | 19 | // NewInstancer returns an etcd instancer. It will start watching the given 20 | // prefix for changes, and update the subscribers. 21 | func NewInstancer(c Client, prefix string, logger log.Logger) (*Instancer, error) { 22 | s := &Instancer{ 23 | client: c, 24 | prefix: prefix, 25 | cache: instance.NewCache(), 26 | logger: logger, 27 | quitc: make(chan struct{}), 28 | } 29 | 30 | instances, err := s.client.GetEntries(s.prefix) 31 | if err == nil { 32 | logger.Log("prefix", s.prefix, "instances", len(instances)) 33 | } else { 34 | logger.Log("prefix", s.prefix, "err", err) 35 | } 36 | s.cache.Update(sd.Event{Instances: instances, Err: err}) 37 | 38 | go s.loop() 39 | return s, nil 40 | } 41 | 42 | func (s *Instancer) loop() { 43 | ch := make(chan struct{}) 44 | go s.client.WatchPrefix(s.prefix, ch) 45 | for { 46 | select { 47 | case <-ch: 48 | instances, err := s.client.GetEntries(s.prefix) 49 | if err != nil { 50 | s.logger.Log("msg", "failed to retrieve entries", "err", err) 51 | s.cache.Update(sd.Event{Err: err}) 52 | continue 53 | } 54 | s.cache.Update(sd.Event{Instances: instances}) 55 | 56 | case <-s.quitc: 57 | return 58 | } 59 | } 60 | } 61 | 62 | // Stop terminates the Instancer. 63 | func (s *Instancer) Stop() { 64 | close(s.quitc) 65 | } 66 | 67 | // Register implements Instancer. 68 | func (s *Instancer) Register(ch chan<- sd.Event) { 69 | s.cache.Register(ch) 70 | } 71 | 72 | // Deregister implements Instancer. 73 | func (s *Instancer) Deregister(ch chan<- sd.Event) { 74 | s.cache.Deregister(ch) 75 | } 76 | -------------------------------------------------------------------------------- /sd/etcd/instancer_test.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | stdetcd "go.etcd.io/etcd/client/v2" 8 | 9 | "github.com/go-kit/kit/sd" 10 | "github.com/go-kit/log" 11 | ) 12 | 13 | var _ sd.Instancer = (*Instancer)(nil) // API check 14 | 15 | var ( 16 | node = &stdetcd.Node{ 17 | Key: "/foo", 18 | Nodes: []*stdetcd.Node{ 19 | {Key: "/foo/1", Value: "1:1"}, 20 | {Key: "/foo/2", Value: "1:2"}, 21 | }, 22 | } 23 | fakeResponse = &stdetcd.Response{ 24 | Node: node, 25 | } 26 | ) 27 | 28 | var _ sd.Instancer = &Instancer{} // API check 29 | 30 | func TestInstancer(t *testing.T) { 31 | client := &fakeClient{ 32 | responses: map[string]*stdetcd.Response{"/foo": fakeResponse}, 33 | } 34 | 35 | s, err := NewInstancer(client, "/foo", log.NewNopLogger()) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | defer s.Stop() 40 | 41 | if state := s.cache.State(); state.Err != nil { 42 | t.Fatal(state.Err) 43 | } 44 | } 45 | 46 | type fakeClient struct { 47 | responses map[string]*stdetcd.Response 48 | } 49 | 50 | func (c *fakeClient) GetEntries(prefix string) ([]string, error) { 51 | response, ok := c.responses[prefix] 52 | if !ok { 53 | return nil, errors.New("key not exist") 54 | } 55 | 56 | entries := make([]string, len(response.Node.Nodes)) 57 | for i, node := range response.Node.Nodes { 58 | entries[i] = node.Value 59 | } 60 | return entries, nil 61 | } 62 | 63 | func (c *fakeClient) WatchPrefix(prefix string, ch chan struct{}) {} 64 | 65 | func (c *fakeClient) Register(Service) error { 66 | return nil 67 | } 68 | func (c *fakeClient) Deregister(Service) error { 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /sd/etcdv3/client_test.go: -------------------------------------------------------------------------------- 1 | package etcdv3 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "google.golang.org/grpc" 9 | ) 10 | 11 | const ( 12 | // irrelevantEndpoint is an address which does not exists. 13 | irrelevantEndpoint = "http://irrelevant:12345" 14 | ) 15 | 16 | func TestNewClient(t *testing.T) { 17 | client, err := NewClient( 18 | context.Background(), 19 | []string{irrelevantEndpoint}, 20 | ClientOptions{ 21 | DialTimeout: 3 * time.Second, 22 | DialKeepAlive: 3 * time.Second, 23 | }, 24 | ) 25 | if err != nil { 26 | t.Fatalf("unexpected error creating client: %v", err) 27 | } 28 | if client == nil { 29 | t.Fatal("expected new Client, got nil") 30 | } 31 | } 32 | 33 | func TestClientOptions(t *testing.T) { 34 | client, err := NewClient( 35 | context.Background(), 36 | []string{}, 37 | ClientOptions{ 38 | Cert: "", 39 | Key: "", 40 | CACert: "", 41 | DialTimeout: 3 * time.Second, 42 | DialKeepAlive: 3 * time.Second, 43 | }, 44 | ) 45 | if err == nil { 46 | t.Errorf("expected error: %v", err) 47 | } 48 | if client != nil { 49 | t.Fatalf("expected client to be nil on failure") 50 | } 51 | 52 | _, err = NewClient( 53 | context.Background(), 54 | []string{irrelevantEndpoint}, 55 | ClientOptions{ 56 | Cert: "does-not-exist.crt", 57 | Key: "does-not-exist.key", 58 | CACert: "does-not-exist.CACert", 59 | DialTimeout: 3 * time.Second, 60 | DialKeepAlive: 3 * time.Second, 61 | }, 62 | ) 63 | if err == nil { 64 | t.Errorf("expected error: %v", err) 65 | } 66 | 67 | client, err = NewClient( 68 | context.Background(), 69 | []string{irrelevantEndpoint}, 70 | ClientOptions{ 71 | DialOptions: []grpc.DialOption{grpc.WithBlock()}, 72 | }, 73 | ) 74 | if err == nil { 75 | t.Errorf("expected connection should fail") 76 | } 77 | if client != nil { 78 | t.Errorf("expected client to be nil on failure") 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /sd/etcdv3/doc.go: -------------------------------------------------------------------------------- 1 | // Package etcdv3 provides an Instancer and Registrar implementation for etcd v3. If 2 | // you use etcd v3 as your service discovery system, this package will help you 3 | // implement the registration and client-side load balancing patterns. 4 | package etcdv3 5 | -------------------------------------------------------------------------------- /sd/etcdv3/instancer.go: -------------------------------------------------------------------------------- 1 | package etcdv3 2 | 3 | import ( 4 | "github.com/go-kit/kit/sd" 5 | "github.com/go-kit/kit/sd/internal/instance" 6 | "github.com/go-kit/log" 7 | ) 8 | 9 | // Instancer yields instances stored in a certain etcd keyspace. Any kind of 10 | // change in that keyspace is watched and will update the Instancer's Instancers. 11 | type Instancer struct { 12 | cache *instance.Cache 13 | client Client 14 | prefix string 15 | logger log.Logger 16 | quitc chan struct{} 17 | } 18 | 19 | // NewInstancer returns an etcd instancer. It will start watching the given 20 | // prefix for changes, and update the subscribers. 21 | func NewInstancer(c Client, prefix string, logger log.Logger) (*Instancer, error) { 22 | s := &Instancer{ 23 | client: c, 24 | prefix: prefix, 25 | cache: instance.NewCache(), 26 | logger: logger, 27 | quitc: make(chan struct{}), 28 | } 29 | 30 | instances, err := s.client.GetEntries(s.prefix) 31 | if err == nil { 32 | logger.Log("prefix", s.prefix, "instances", len(instances)) 33 | } else { 34 | logger.Log("prefix", s.prefix, "err", err) 35 | } 36 | s.cache.Update(sd.Event{Instances: instances, Err: err}) 37 | 38 | go s.loop() 39 | return s, nil 40 | } 41 | 42 | func (s *Instancer) loop() { 43 | ch := make(chan struct{}) 44 | go s.client.WatchPrefix(s.prefix, ch) 45 | 46 | for { 47 | select { 48 | case <-ch: 49 | instances, err := s.client.GetEntries(s.prefix) 50 | if err != nil { 51 | s.logger.Log("msg", "failed to retrieve entries", "err", err) 52 | s.cache.Update(sd.Event{Err: err}) 53 | continue 54 | } 55 | s.cache.Update(sd.Event{Instances: instances}) 56 | 57 | case <-s.quitc: 58 | return 59 | } 60 | } 61 | } 62 | 63 | // Stop terminates the Instancer. 64 | func (s *Instancer) Stop() { 65 | close(s.quitc) 66 | } 67 | 68 | // Register implements Instancer. 69 | func (s *Instancer) Register(ch chan<- sd.Event) { 70 | s.cache.Register(ch) 71 | } 72 | 73 | // Deregister implements Instancer. 74 | func (s *Instancer) Deregister(ch chan<- sd.Event) { 75 | s.cache.Deregister(ch) 76 | } 77 | -------------------------------------------------------------------------------- /sd/etcdv3/instancer_test.go: -------------------------------------------------------------------------------- 1 | package etcdv3 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/go-kit/kit/sd" 8 | "github.com/go-kit/log" 9 | ) 10 | 11 | var _ sd.Instancer = (*Instancer)(nil) // API check 12 | 13 | type testKV struct { 14 | Key []byte 15 | Value []byte 16 | } 17 | 18 | type testResponse struct { 19 | Kvs []testKV 20 | } 21 | 22 | var ( 23 | fakeResponse = testResponse{ 24 | Kvs: []testKV{ 25 | { 26 | Key: []byte("/foo/1"), 27 | Value: []byte("1:1"), 28 | }, 29 | { 30 | Key: []byte("/foo/2"), 31 | Value: []byte("2:2"), 32 | }, 33 | }, 34 | } 35 | ) 36 | 37 | var _ sd.Instancer = &Instancer{} // API check 38 | 39 | func TestInstancer(t *testing.T) { 40 | client := &fakeClient{ 41 | responses: map[string]testResponse{"/foo": fakeResponse}, 42 | } 43 | 44 | s, err := NewInstancer(client, "/foo", log.NewNopLogger()) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | defer s.Stop() 49 | 50 | if state := s.cache.State(); state.Err != nil { 51 | t.Fatal(state.Err) 52 | } 53 | } 54 | 55 | type fakeClient struct { 56 | responses map[string]testResponse 57 | } 58 | 59 | func (c *fakeClient) GetEntries(prefix string) ([]string, error) { 60 | response, ok := c.responses[prefix] 61 | if !ok { 62 | return nil, errors.New("key not exist") 63 | } 64 | 65 | entries := make([]string, len(response.Kvs)) 66 | for i, node := range response.Kvs { 67 | entries[i] = string(node.Value) 68 | } 69 | return entries, nil 70 | } 71 | 72 | func (c *fakeClient) WatchPrefix(prefix string, ch chan struct{}) { 73 | } 74 | 75 | func (c *fakeClient) LeaseID() int64 { 76 | return 0 77 | } 78 | 79 | func (c *fakeClient) Register(Service) error { 80 | return nil 81 | } 82 | func (c *fakeClient) Deregister(Service) error { 83 | return nil 84 | } 85 | -------------------------------------------------------------------------------- /sd/eureka/doc.go: -------------------------------------------------------------------------------- 1 | // Package eureka provides Instancer and Registrar implementations for Netflix OSS's Eureka 2 | package eureka 3 | -------------------------------------------------------------------------------- /sd/eureka/instancer_test.go: -------------------------------------------------------------------------------- 1 | package eureka 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/hudl/fargo" 8 | 9 | "github.com/go-kit/kit/sd" 10 | ) 11 | 12 | var _ sd.Instancer = (*Instancer)(nil) // API check 13 | 14 | func TestInstancer(t *testing.T) { 15 | connection := &testConnection{ 16 | instances: []*fargo.Instance{instanceTest1, instanceTest2}, 17 | errApplication: nil, 18 | } 19 | 20 | instancer := NewInstancer(connection, appNameTest, loggerTest) 21 | defer instancer.Stop() 22 | 23 | state := instancer.state() 24 | if state.Err != nil { 25 | t.Fatal(state.Err) 26 | } 27 | 28 | if want, have := 2, len(state.Instances); want != have { 29 | t.Errorf("want %d, have %d", want, have) 30 | } 31 | } 32 | 33 | func TestInstancerReceivesUpdates(t *testing.T) { 34 | connection := &testConnection{ 35 | instances: []*fargo.Instance{instanceTest1}, 36 | errApplication: nil, 37 | } 38 | 39 | instancer := NewInstancer(connection, appNameTest, loggerTest) 40 | defer instancer.Stop() 41 | 42 | verifyCount := func(want int) (have int, converged bool) { 43 | const maxPollAttempts = 5 44 | const delayPerAttempt = 200 * time.Millisecond 45 | for i := 1; ; i++ { 46 | state := instancer.state() 47 | if have := len(state.Instances); want == have { 48 | return have, true 49 | } else if i == maxPollAttempts { 50 | return have, false 51 | } 52 | time.Sleep(delayPerAttempt) 53 | } 54 | } 55 | 56 | if have, converged := verifyCount(1); !converged { 57 | t.Fatalf("initial: want %d, have %d", 1, have) 58 | } 59 | 60 | if err := connection.RegisterInstance(instanceTest2); err != nil { 61 | t.Fatalf("failed to register an instance: %v", err) 62 | } 63 | if have, converged := verifyCount(2); !converged { 64 | t.Fatalf("after registration: want %d, have %d", 2, have) 65 | } 66 | 67 | if err := connection.DeregisterInstance(instanceTest1); err != nil { 68 | t.Fatalf("failed to unregister an instance: %v", err) 69 | } 70 | if have, converged := verifyCount(1); !converged { 71 | t.Fatalf("after deregistration: want %d, have %d", 1, have) 72 | } 73 | } 74 | 75 | func TestBadInstancerScheduleUpdates(t *testing.T) { 76 | connection := &testConnection{ 77 | instances: []*fargo.Instance{instanceTest1}, 78 | errApplication: errTest, 79 | } 80 | 81 | instancer := NewInstancer(connection, appNameTest, loggerTest) 82 | defer instancer.Stop() 83 | 84 | state := instancer.state() 85 | if state.Err == nil { 86 | t.Fatal("expecting error") 87 | } 88 | 89 | if want, have := 0, len(state.Instances); want != have { 90 | t.Errorf("want %d, have %d", want, have) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /sd/factory.go: -------------------------------------------------------------------------------- 1 | package sd 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/go-kit/kit/endpoint" 7 | ) 8 | 9 | // Factory is a function that converts an instance string (e.g. host:port) to a 10 | // specific endpoint. Instances that provide multiple endpoints require multiple 11 | // factories. A factory also returns an io.Closer that's invoked when the 12 | // instance goes away and needs to be cleaned up. Factories may return nil 13 | // closers. 14 | // 15 | // Users are expected to provide their own factory functions that assume 16 | // specific transports, or can deduce transports by parsing the instance string. 17 | type Factory func(instance string) (endpoint.Endpoint, io.Closer, error) 18 | -------------------------------------------------------------------------------- /sd/instancer.go: -------------------------------------------------------------------------------- 1 | package sd 2 | 3 | // Event represents a push notification generated from the underlying service discovery 4 | // implementation. It contains either a full set of available resource instances, or 5 | // an error indicating some issue with obtaining information from discovery backend. 6 | // Examples of errors may include loosing connection to the discovery backend, or 7 | // trying to look up resource instances using an incorrectly formatted key. 8 | // After receiving an Event with an error the listenter should treat previously discovered 9 | // resource instances as stale (although it may choose to continue using them). 10 | // If the Instancer is able to restore connection to the discovery backend it must push 11 | // another Event with the current set of resource instances. 12 | type Event struct { 13 | Instances []string 14 | Err error 15 | } 16 | 17 | // Instancer listens to a service discovery system and notifies registered 18 | // observers of changes in the resource instances. Every event sent to the channels 19 | // contains a complete set of instances known to the Instancer. That complete set is 20 | // sent immediately upon registering the channel, and on any future updates from 21 | // discovery system. 22 | type Instancer interface { 23 | Register(chan<- Event) 24 | Deregister(chan<- Event) 25 | Stop() 26 | } 27 | 28 | // FixedInstancer yields a fixed set of instances. 29 | type FixedInstancer []string 30 | 31 | // Register implements Instancer. 32 | func (d FixedInstancer) Register(ch chan<- Event) { ch <- Event{Instances: d} } 33 | 34 | // Deregister implements Instancer. 35 | func (d FixedInstancer) Deregister(ch chan<- Event) {} 36 | 37 | // Stop implements Instancer. 38 | func (d FixedInstancer) Stop() {} 39 | -------------------------------------------------------------------------------- /sd/internal/instance/cache.go: -------------------------------------------------------------------------------- 1 | package instance 2 | 3 | import ( 4 | "reflect" 5 | "sort" 6 | "sync" 7 | 8 | "github.com/go-kit/kit/sd" 9 | ) 10 | 11 | // Cache keeps track of resource instances provided to it via Update method 12 | // and implements the Instancer interface 13 | type Cache struct { 14 | mtx sync.RWMutex 15 | state sd.Event 16 | reg registry 17 | } 18 | 19 | // NewCache creates a new Cache. 20 | func NewCache() *Cache { 21 | return &Cache{ 22 | reg: registry{}, 23 | } 24 | } 25 | 26 | // Update receives new instances from service discovery, stores them internally, 27 | // and notifies all registered listeners. 28 | func (c *Cache) Update(event sd.Event) { 29 | c.mtx.Lock() 30 | defer c.mtx.Unlock() 31 | 32 | sort.Strings(event.Instances) 33 | if reflect.DeepEqual(c.state, event) { 34 | return // no need to broadcast the same instances 35 | } 36 | 37 | c.state = event 38 | c.reg.broadcast(event) 39 | } 40 | 41 | // State returns the current state of discovery (instances or error) as sd.Event 42 | func (c *Cache) State() sd.Event { 43 | c.mtx.RLock() 44 | event := c.state 45 | c.mtx.RUnlock() 46 | eventCopy := copyEvent(event) 47 | return eventCopy 48 | } 49 | 50 | // Stop implements Instancer. Since the cache is just a plain-old store of data, 51 | // Stop is a no-op. 52 | func (c *Cache) Stop() {} 53 | 54 | // Register implements Instancer. 55 | func (c *Cache) Register(ch chan<- sd.Event) { 56 | c.mtx.Lock() 57 | defer c.mtx.Unlock() 58 | c.reg.register(ch) 59 | event := c.state 60 | eventCopy := copyEvent(event) 61 | // always push the current state to new channels 62 | ch <- eventCopy 63 | } 64 | 65 | // Deregister implements Instancer. 66 | func (c *Cache) Deregister(ch chan<- sd.Event) { 67 | c.mtx.Lock() 68 | defer c.mtx.Unlock() 69 | c.reg.deregister(ch) 70 | } 71 | 72 | // registry is not goroutine-safe. 73 | type registry map[chan<- sd.Event]struct{} 74 | 75 | func (r registry) broadcast(event sd.Event) { 76 | for c := range r { 77 | eventCopy := copyEvent(event) 78 | c <- eventCopy 79 | } 80 | } 81 | 82 | func (r registry) register(c chan<- sd.Event) { 83 | r[c] = struct{}{} 84 | } 85 | 86 | func (r registry) deregister(c chan<- sd.Event) { 87 | delete(r, c) 88 | } 89 | 90 | // copyEvent does a deep copy on sd.Event 91 | func copyEvent(e sd.Event) sd.Event { 92 | // observers all need their own copy of event 93 | // because they can directly modify event.Instances 94 | // for example, by calling sort.Strings 95 | if e.Instances == nil { 96 | return e 97 | } 98 | instances := make([]string, len(e.Instances)) 99 | copy(instances, e.Instances) 100 | e.Instances = instances 101 | return e 102 | } 103 | -------------------------------------------------------------------------------- /sd/lb/balancer.go: -------------------------------------------------------------------------------- 1 | package lb 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/go-kit/kit/endpoint" 7 | ) 8 | 9 | // Balancer yields endpoints according to some heuristic. 10 | type Balancer interface { 11 | Endpoint() (endpoint.Endpoint, error) 12 | } 13 | 14 | // ErrNoEndpoints is returned when no qualifying endpoints are available. 15 | var ErrNoEndpoints = errors.New("no endpoints available") 16 | -------------------------------------------------------------------------------- /sd/lb/doc.go: -------------------------------------------------------------------------------- 1 | // Package lb implements the client-side load balancer pattern. When combined 2 | // with a service discovery system of record, it enables a more decentralized 3 | // architecture, removing the need for separate load balancers like HAProxy. 4 | package lb 5 | -------------------------------------------------------------------------------- /sd/lb/random.go: -------------------------------------------------------------------------------- 1 | package lb 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/go-kit/kit/endpoint" 7 | "github.com/go-kit/kit/sd" 8 | ) 9 | 10 | // NewRandom returns a load balancer that selects services randomly. 11 | func NewRandom(s sd.Endpointer, seed int64) Balancer { 12 | return &random{ 13 | s: s, 14 | r: rand.New(rand.NewSource(seed)), 15 | } 16 | } 17 | 18 | type random struct { 19 | s sd.Endpointer 20 | r *rand.Rand 21 | } 22 | 23 | func (r *random) Endpoint() (endpoint.Endpoint, error) { 24 | endpoints, err := r.s.Endpoints() 25 | if err != nil { 26 | return nil, err 27 | } 28 | if len(endpoints) <= 0 { 29 | return nil, ErrNoEndpoints 30 | } 31 | return endpoints[r.r.Intn(len(endpoints))], nil 32 | } 33 | -------------------------------------------------------------------------------- /sd/lb/random_test.go: -------------------------------------------------------------------------------- 1 | package lb 2 | 3 | import ( 4 | "context" 5 | "math" 6 | "testing" 7 | 8 | "github.com/go-kit/kit/endpoint" 9 | "github.com/go-kit/kit/sd" 10 | ) 11 | 12 | func TestRandom(t *testing.T) { 13 | var ( 14 | n = 7 15 | endpoints = make([]endpoint.Endpoint, n) 16 | counts = make([]int, n) 17 | seed = int64(12345) 18 | iterations = 1000000 19 | want = iterations / n 20 | tolerance = want / 100 // 1% 21 | ) 22 | 23 | for i := 0; i < n; i++ { 24 | i0 := i 25 | endpoints[i] = func(context.Context, interface{}) (interface{}, error) { counts[i0]++; return struct{}{}, nil } 26 | } 27 | 28 | endpointer := sd.FixedEndpointer(endpoints) 29 | balancer := NewRandom(endpointer, seed) 30 | 31 | for i := 0; i < iterations; i++ { 32 | endpoint, _ := balancer.Endpoint() 33 | endpoint(context.Background(), struct{}{}) 34 | } 35 | 36 | for i, have := range counts { 37 | delta := int(math.Abs(float64(want - have))) 38 | if delta > tolerance { 39 | t.Errorf("%d: want %d, have %d, delta %d > %d tolerance", i, want, have, delta, tolerance) 40 | } 41 | } 42 | } 43 | 44 | func TestRandomNoEndpoints(t *testing.T) { 45 | endpointer := sd.FixedEndpointer{} 46 | balancer := NewRandom(endpointer, 1415926) 47 | _, err := balancer.Endpoint() 48 | if want, have := ErrNoEndpoints, err; want != have { 49 | t.Errorf("want %v, have %v", want, have) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /sd/lb/round_robin.go: -------------------------------------------------------------------------------- 1 | package lb 2 | 3 | import ( 4 | "sync/atomic" 5 | 6 | "github.com/go-kit/kit/endpoint" 7 | "github.com/go-kit/kit/sd" 8 | ) 9 | 10 | // NewRoundRobin returns a load balancer that returns services in sequence. 11 | func NewRoundRobin(s sd.Endpointer) Balancer { 12 | return &roundRobin{ 13 | s: s, 14 | c: 0, 15 | } 16 | } 17 | 18 | type roundRobin struct { 19 | s sd.Endpointer 20 | c uint64 21 | } 22 | 23 | func (rr *roundRobin) Endpoint() (endpoint.Endpoint, error) { 24 | endpoints, err := rr.s.Endpoints() 25 | if err != nil { 26 | return nil, err 27 | } 28 | if len(endpoints) <= 0 { 29 | return nil, ErrNoEndpoints 30 | } 31 | old := atomic.AddUint64(&rr.c, 1) - 1 32 | idx := old % uint64(len(endpoints)) 33 | return endpoints[idx], nil 34 | } 35 | -------------------------------------------------------------------------------- /sd/lb/round_robin_test.go: -------------------------------------------------------------------------------- 1 | package lb 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "sync" 7 | "sync/atomic" 8 | "testing" 9 | "time" 10 | 11 | "github.com/go-kit/kit/endpoint" 12 | "github.com/go-kit/kit/sd" 13 | ) 14 | 15 | func TestRoundRobin(t *testing.T) { 16 | var ( 17 | counts = []int{0, 0, 0} 18 | endpoints = []endpoint.Endpoint{ 19 | func(context.Context, interface{}) (interface{}, error) { counts[0]++; return struct{}{}, nil }, 20 | func(context.Context, interface{}) (interface{}, error) { counts[1]++; return struct{}{}, nil }, 21 | func(context.Context, interface{}) (interface{}, error) { counts[2]++; return struct{}{}, nil }, 22 | } 23 | ) 24 | 25 | endpointer := sd.FixedEndpointer(endpoints) 26 | balancer := NewRoundRobin(endpointer) 27 | 28 | for i, want := range [][]int{ 29 | {1, 0, 0}, 30 | {1, 1, 0}, 31 | {1, 1, 1}, 32 | {2, 1, 1}, 33 | {2, 2, 1}, 34 | {2, 2, 2}, 35 | {3, 2, 2}, 36 | } { 37 | endpoint, err := balancer.Endpoint() 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | endpoint(context.Background(), struct{}{}) 42 | if have := counts; !reflect.DeepEqual(want, have) { 43 | t.Fatalf("%d: want %v, have %v", i, want, have) 44 | } 45 | } 46 | } 47 | 48 | func TestRoundRobinNoEndpoints(t *testing.T) { 49 | endpointer := sd.FixedEndpointer{} 50 | balancer := NewRoundRobin(endpointer) 51 | _, err := balancer.Endpoint() 52 | if want, have := ErrNoEndpoints, err; want != have { 53 | t.Errorf("want %v, have %v", want, have) 54 | } 55 | } 56 | 57 | func TestRoundRobinNoRace(t *testing.T) { 58 | balancer := NewRoundRobin(sd.FixedEndpointer([]endpoint.Endpoint{ 59 | endpoint.Nop, 60 | endpoint.Nop, 61 | endpoint.Nop, 62 | endpoint.Nop, 63 | endpoint.Nop, 64 | })) 65 | 66 | var ( 67 | n = 100 68 | done = make(chan struct{}) 69 | wg sync.WaitGroup 70 | count uint64 71 | ) 72 | 73 | wg.Add(n) 74 | 75 | for i := 0; i < n; i++ { 76 | go func() { 77 | defer wg.Done() 78 | for { 79 | select { 80 | case <-done: 81 | return 82 | default: 83 | _, _ = balancer.Endpoint() 84 | atomic.AddUint64(&count, 1) 85 | } 86 | } 87 | }() 88 | } 89 | 90 | time.Sleep(time.Second) 91 | close(done) 92 | wg.Wait() 93 | 94 | t.Logf("made %d calls", atomic.LoadUint64(&count)) 95 | } 96 | -------------------------------------------------------------------------------- /sd/registrar.go: -------------------------------------------------------------------------------- 1 | package sd 2 | 3 | // Registrar registers instance information to a service discovery system when 4 | // an instance becomes alive and healthy, and deregisters that information when 5 | // the service becomes unhealthy or goes away. 6 | // 7 | // Registrar implementations exist for various service discovery systems. Note 8 | // that identifying instance information (e.g. host:port) must be given via the 9 | // concrete constructor; this interface merely signals lifecycle changes. 10 | type Registrar interface { 11 | Register() 12 | Deregister() 13 | } 14 | -------------------------------------------------------------------------------- /sd/zk/doc.go: -------------------------------------------------------------------------------- 1 | // Package zk provides Instancer and Registrar implementations for ZooKeeper. 2 | package zk 3 | -------------------------------------------------------------------------------- /sd/zk/instancer.go: -------------------------------------------------------------------------------- 1 | package zk 2 | 3 | import ( 4 | "github.com/go-zookeeper/zk" 5 | 6 | "github.com/go-kit/kit/sd" 7 | "github.com/go-kit/kit/sd/internal/instance" 8 | "github.com/go-kit/log" 9 | ) 10 | 11 | // Instancer yield instances stored in a certain ZooKeeper path. Any kind of 12 | // change in that path is watched and will update the subscribers. 13 | type Instancer struct { 14 | cache *instance.Cache 15 | client Client 16 | path string 17 | logger log.Logger 18 | quitc chan struct{} 19 | } 20 | 21 | // NewInstancer returns a ZooKeeper Instancer. ZooKeeper will start watching 22 | // the given path for changes and update the Instancer endpoints. 23 | func NewInstancer(c Client, path string, logger log.Logger) (*Instancer, error) { 24 | s := &Instancer{ 25 | cache: instance.NewCache(), 26 | client: c, 27 | path: path, 28 | logger: logger, 29 | quitc: make(chan struct{}), 30 | } 31 | 32 | err := s.client.CreateParentNodes(s.path) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | instances, eventc, err := s.client.GetEntries(s.path) 38 | if err != nil { 39 | logger.Log("path", s.path, "msg", "failed to retrieve entries", "err", err) 40 | // other implementations continue here, but we exit because we don't know if eventc is valid 41 | return nil, err 42 | } 43 | logger.Log("path", s.path, "instances", len(instances)) 44 | s.cache.Update(sd.Event{Instances: instances}) 45 | 46 | go s.loop(eventc) 47 | 48 | return s, nil 49 | } 50 | 51 | func (s *Instancer) loop(eventc <-chan zk.Event) { 52 | var ( 53 | instances []string 54 | err error 55 | ) 56 | for { 57 | select { 58 | case <-eventc: 59 | // We received a path update notification. Call GetEntries to 60 | // retrieve child node data, and set a new watch, as ZK watches are 61 | // one-time triggers. 62 | instances, eventc, err = s.client.GetEntries(s.path) 63 | if err != nil { 64 | s.logger.Log("path", s.path, "msg", "failed to retrieve entries", "err", err) 65 | s.cache.Update(sd.Event{Err: err}) 66 | continue 67 | } 68 | s.logger.Log("path", s.path, "instances", len(instances)) 69 | s.cache.Update(sd.Event{Instances: instances}) 70 | 71 | case <-s.quitc: 72 | return 73 | } 74 | } 75 | } 76 | 77 | // Stop terminates the Instancer. 78 | func (s *Instancer) Stop() { 79 | close(s.quitc) 80 | } 81 | 82 | // Register implements Instancer. 83 | func (s *Instancer) Register(ch chan<- sd.Event) { 84 | s.cache.Register(ch) 85 | } 86 | 87 | // Deregister implements Instancer. 88 | func (s *Instancer) Deregister(ch chan<- sd.Event) { 89 | s.cache.Deregister(ch) 90 | } 91 | 92 | // state returns the current state of instance.Cache, only for testing 93 | func (s *Instancer) state() sd.Event { 94 | return s.cache.State() 95 | } 96 | -------------------------------------------------------------------------------- /sd/zk/logwrapper.go: -------------------------------------------------------------------------------- 1 | package zk 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/go-zookeeper/zk" 7 | 8 | "github.com/go-kit/log" 9 | ) 10 | 11 | // wrapLogger wraps a Go kit logger so we can use it as the logging service for 12 | // the ZooKeeper library, which expects a Printf method to be available. 13 | type wrapLogger struct { 14 | log.Logger 15 | } 16 | 17 | func (logger wrapLogger) Printf(format string, args ...interface{}) { 18 | logger.Log("msg", fmt.Sprintf(format, args...)) 19 | } 20 | 21 | // withLogger replaces the ZooKeeper library's default logging service with our 22 | // own Go kit logger. 23 | func withLogger(logger log.Logger) func(c *zk.Conn) { 24 | return func(c *zk.Conn) { 25 | c.SetLogger(wrapLogger{logger}) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /sd/zk/registrar.go: -------------------------------------------------------------------------------- 1 | package zk 2 | 3 | import "github.com/go-kit/log" 4 | 5 | // Registrar registers service instance liveness information to ZooKeeper. 6 | type Registrar struct { 7 | client Client 8 | service Service 9 | logger log.Logger 10 | } 11 | 12 | // Service holds the root path, service name and instance identifying data you 13 | // want to publish to ZooKeeper. 14 | type Service struct { 15 | Path string // discovery namespace, example: /myorganization/myplatform/ 16 | Name string // service name, example: addscv 17 | Data []byte // instance data to store for discovery, example: 10.0.2.10:80 18 | node string // Client will record the ephemeral node name so we can deregister 19 | } 20 | 21 | // NewRegistrar returns a ZooKeeper Registrar acting on the provided catalog 22 | // registration. 23 | func NewRegistrar(client Client, service Service, logger log.Logger) *Registrar { 24 | return &Registrar{ 25 | client: client, 26 | service: service, 27 | logger: log.With(logger, 28 | "service", service.Name, 29 | "path", service.Path, 30 | "data", string(service.Data), 31 | ), 32 | } 33 | } 34 | 35 | // Register implements sd.Registrar interface. 36 | func (r *Registrar) Register() { 37 | if err := r.client.Register(&r.service); err != nil { 38 | r.logger.Log("err", err) 39 | } else { 40 | r.logger.Log("action", "register") 41 | } 42 | } 43 | 44 | // Deregister implements sd.Registrar interface. 45 | func (r *Registrar) Deregister() { 46 | if err := r.client.Deregister(&r.service); err != nil { 47 | r.logger.Log("err", err) 48 | } else { 49 | r.logger.Log("action", "deregister") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tracing/doc.go: -------------------------------------------------------------------------------- 1 | // Package tracing provides helpers and bindings for distributed tracing. 2 | // 3 | // As your infrastructure grows, it becomes important to be able to trace a 4 | // request, as it travels through multiple services and back to the user. 5 | // Package tracing provides endpoints and transport helpers and middlewares to 6 | // capture and emit request-scoped information. 7 | package tracing 8 | -------------------------------------------------------------------------------- /tracing/opencensus/doc.go: -------------------------------------------------------------------------------- 1 | // Package opencensus provides Go kit integration to the OpenCensus project. 2 | // OpenCensus is a single distribution of libraries for metrics and distributed 3 | // tracing with minimal overhead that allows you to export data to multiple 4 | // backends. The Go kit OpenCencus package as provided here contains middlewares 5 | // for tracing. 6 | package opencensus 7 | -------------------------------------------------------------------------------- /tracing/opencensus/endpoint_options.go: -------------------------------------------------------------------------------- 1 | package opencensus 2 | 3 | import ( 4 | "context" 5 | 6 | "go.opencensus.io/trace" 7 | ) 8 | 9 | // EndpointOptions holds the options for tracing an endpoint 10 | type EndpointOptions struct { 11 | // IgnoreBusinessError if set to true will not treat a business error 12 | // identified through the endpoint.Failer interface as a span error. 13 | IgnoreBusinessError bool 14 | 15 | // Attributes holds the default attributes which will be set on span 16 | // creation by our Endpoint middleware. 17 | Attributes []trace.Attribute 18 | 19 | // GetName is an optional function that can set the span name based on the existing name 20 | // for the endpoint and information in the context. 21 | // 22 | // If the function is nil, or the returned name is empty, the existing name for the endpoint is used. 23 | GetName func(ctx context.Context, name string) string 24 | 25 | // GetAttributes is an optional function that can extract trace attributes 26 | // from the context and add them to the span. 27 | GetAttributes func(ctx context.Context) []trace.Attribute 28 | } 29 | 30 | // EndpointOption allows for functional options to our OpenCensus endpoint 31 | // tracing middleware. 32 | type EndpointOption func(*EndpointOptions) 33 | 34 | // WithEndpointConfig sets all configuration options at once by use of the 35 | // EndpointOptions struct. 36 | func WithEndpointConfig(options EndpointOptions) EndpointOption { 37 | return func(o *EndpointOptions) { 38 | *o = options 39 | } 40 | } 41 | 42 | // WithEndpointAttributes sets the default attributes for the spans created by 43 | // the Endpoint tracer. 44 | func WithEndpointAttributes(attrs ...trace.Attribute) EndpointOption { 45 | return func(o *EndpointOptions) { 46 | o.Attributes = attrs 47 | } 48 | } 49 | 50 | // WithIgnoreBusinessError if set to true will not treat a business error 51 | // identified through the endpoint.Failer interface as a span error. 52 | func WithIgnoreBusinessError(val bool) EndpointOption { 53 | return func(o *EndpointOptions) { 54 | o.IgnoreBusinessError = val 55 | } 56 | } 57 | 58 | // WithSpanName extracts additional attributes from the request context. 59 | func WithSpanName(fn func(ctx context.Context, name string) string) EndpointOption { 60 | return func(o *EndpointOptions) { 61 | o.GetName = fn 62 | } 63 | } 64 | 65 | // WithSpanAttributes extracts additional attributes from the request context. 66 | func WithSpanAttributes(fn func(ctx context.Context) []trace.Attribute) EndpointOption { 67 | return func(o *EndpointOptions) { 68 | o.GetAttributes = fn 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tracing/opencensus/opencensus_test.go: -------------------------------------------------------------------------------- 1 | package opencensus_test 2 | 3 | import ( 4 | "sync" 5 | 6 | "go.opencensus.io/trace" 7 | ) 8 | 9 | type recordingExporter struct { 10 | mu sync.Mutex 11 | data []*trace.SpanData 12 | } 13 | 14 | func (e *recordingExporter) ExportSpan(d *trace.SpanData) { 15 | e.mu.Lock() 16 | defer e.mu.Unlock() 17 | 18 | e.data = append(e.data, d) 19 | } 20 | 21 | func (e *recordingExporter) Flush() (data []*trace.SpanData) { 22 | e.mu.Lock() 23 | defer e.mu.Unlock() 24 | 25 | data = e.data 26 | e.data = nil 27 | return 28 | } 29 | -------------------------------------------------------------------------------- /tracing/opencensus/tracer_options.go: -------------------------------------------------------------------------------- 1 | package opencensus 2 | 3 | import ( 4 | "go.opencensus.io/plugin/ochttp/propagation/b3" 5 | "go.opencensus.io/trace" 6 | "go.opencensus.io/trace/propagation" 7 | ) 8 | 9 | // defaultHTTPPropagate holds OpenCensus' default HTTP propagation format which 10 | // currently is Zipkin's B3. 11 | var defaultHTTPPropagate propagation.HTTPFormat = &b3.HTTPFormat{} 12 | 13 | // TracerOption allows for functional options to our OpenCensus tracing 14 | // middleware. 15 | type TracerOption func(o *TracerOptions) 16 | 17 | // WithTracerConfig sets all configuration options at once. 18 | func WithTracerConfig(options TracerOptions) TracerOption { 19 | return func(o *TracerOptions) { 20 | *o = options 21 | } 22 | } 23 | 24 | // WithSampler sets the sampler to use by our OpenCensus Tracer. 25 | func WithSampler(sampler trace.Sampler) TracerOption { 26 | return func(o *TracerOptions) { 27 | o.Sampler = sampler 28 | } 29 | } 30 | 31 | // WithName sets the name for an instrumented transport endpoint. If name is omitted 32 | // at tracing middleware creation, the method of the transport or transport rpc 33 | // name is used. 34 | func WithName(name string) TracerOption { 35 | return func(o *TracerOptions) { 36 | o.Name = name 37 | } 38 | } 39 | 40 | // IsPublic should be set to true for publicly accessible servers and for 41 | // clients that should not propagate their current trace metadata. 42 | // On the server side a new trace will always be started regardless of any 43 | // trace metadata being found in the incoming request. If any trace metadata 44 | // is found, it will be added as a linked trace instead. 45 | func IsPublic(isPublic bool) TracerOption { 46 | return func(o *TracerOptions) { 47 | o.Public = isPublic 48 | } 49 | } 50 | 51 | // WithHTTPPropagation sets the propagation handlers for the HTTP transport 52 | // middlewares. If used on a non HTTP transport this is a noop. 53 | func WithHTTPPropagation(p propagation.HTTPFormat) TracerOption { 54 | return func(o *TracerOptions) { 55 | if p == nil { 56 | // reset to default OC HTTP format 57 | o.HTTPPropagate = defaultHTTPPropagate 58 | return 59 | } 60 | o.HTTPPropagate = p 61 | } 62 | } 63 | 64 | // TracerOptions holds configuration for our tracing middlewares 65 | type TracerOptions struct { 66 | Sampler trace.Sampler 67 | Name string 68 | Public bool 69 | HTTPPropagate propagation.HTTPFormat 70 | } 71 | -------------------------------------------------------------------------------- /tracing/opentracing/doc.go: -------------------------------------------------------------------------------- 1 | // Package opentracing provides Go kit integration to the OpenTracing project. 2 | // OpenTracing implements a general purpose interface that microservices can 3 | // program against, and which adapts to all major distributed tracing systems. 4 | package opentracing 5 | -------------------------------------------------------------------------------- /tracing/opentracing/endpoint_options.go: -------------------------------------------------------------------------------- 1 | package opentracing 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/opentracing/opentracing-go" 7 | ) 8 | 9 | // EndpointOptions holds the options for tracing an endpoint 10 | type EndpointOptions struct { 11 | // IgnoreBusinessError if set to true will not treat a business error 12 | // identified through the endpoint.Failer interface as a span error. 13 | IgnoreBusinessError bool 14 | 15 | // GetOperationName is an optional function that can set the span operation name based on the existing one 16 | // for the endpoint and information in the context. 17 | // 18 | // If the function is nil, or the returned name is empty, the existing name for the endpoint is used. 19 | GetOperationName func(ctx context.Context, name string) string 20 | 21 | // Tags holds the default tags which will be set on span 22 | // creation by our Endpoint middleware. 23 | Tags opentracing.Tags 24 | 25 | // GetTags is an optional function that can extract tags 26 | // from the context and add them to the span. 27 | GetTags func(ctx context.Context) opentracing.Tags 28 | } 29 | 30 | // EndpointOption allows for functional options to endpoint tracing middleware. 31 | type EndpointOption func(*EndpointOptions) 32 | 33 | // WithOptions sets all configuration options at once by use of the EndpointOptions struct. 34 | func WithOptions(options EndpointOptions) EndpointOption { 35 | return func(o *EndpointOptions) { 36 | *o = options 37 | } 38 | } 39 | 40 | // WithIgnoreBusinessError if set to true will not treat a business error 41 | // identified through the endpoint.Failer interface as a span error. 42 | func WithIgnoreBusinessError(ignoreBusinessError bool) EndpointOption { 43 | return func(o *EndpointOptions) { 44 | o.IgnoreBusinessError = ignoreBusinessError 45 | } 46 | } 47 | 48 | // WithOperationNameFunc allows to set function that can set the span operation name based on the existing one 49 | // for the endpoint and information in the context. 50 | func WithOperationNameFunc(getOperationName func(ctx context.Context, name string) string) EndpointOption { 51 | return func(o *EndpointOptions) { 52 | o.GetOperationName = getOperationName 53 | } 54 | } 55 | 56 | // WithTags adds default tags for the spans created by the Endpoint tracer. 57 | func WithTags(tags opentracing.Tags) EndpointOption { 58 | return func(o *EndpointOptions) { 59 | if o.Tags == nil { 60 | o.Tags = make(opentracing.Tags) 61 | } 62 | 63 | for key, value := range tags { 64 | o.Tags[key] = value 65 | } 66 | } 67 | } 68 | 69 | // WithTagsFunc set the func to extracts additional tags from the context. 70 | func WithTagsFunc(getTags func(ctx context.Context) opentracing.Tags) EndpointOption { 71 | return func(o *EndpointOptions) { 72 | o.GetTags = getTags 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tracing/opentracing/grpc.go: -------------------------------------------------------------------------------- 1 | package opentracing 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "strings" 7 | 8 | "github.com/opentracing/opentracing-go" 9 | "github.com/opentracing/opentracing-go/ext" 10 | "google.golang.org/grpc/metadata" 11 | 12 | "github.com/go-kit/log" 13 | ) 14 | 15 | // ContextToGRPC returns a grpc RequestFunc that injects an OpenTracing Span 16 | // found in `ctx` into the grpc Metadata. If no such Span can be found, the 17 | // RequestFunc is a noop. 18 | func ContextToGRPC(tracer opentracing.Tracer, logger log.Logger) func(ctx context.Context, md *metadata.MD) context.Context { 19 | return func(ctx context.Context, md *metadata.MD) context.Context { 20 | if span := opentracing.SpanFromContext(ctx); span != nil { 21 | // There's nothing we can do with an error here. 22 | if err := tracer.Inject(span.Context(), opentracing.TextMap, metadataReaderWriter{md}); err != nil { 23 | logger.Log("err", err) 24 | } 25 | } 26 | return ctx 27 | } 28 | } 29 | 30 | // GRPCToContext returns a grpc RequestFunc that tries to join with an 31 | // OpenTracing trace found in `req` and starts a new Span called 32 | // `operationName` accordingly. If no trace could be found in `req`, the Span 33 | // will be a trace root. The Span is incorporated in the returned Context and 34 | // can be retrieved with opentracing.SpanFromContext(ctx). 35 | func GRPCToContext(tracer opentracing.Tracer, operationName string, logger log.Logger) func(ctx context.Context, md metadata.MD) context.Context { 36 | return func(ctx context.Context, md metadata.MD) context.Context { 37 | var span opentracing.Span 38 | wireContext, err := tracer.Extract(opentracing.TextMap, metadataReaderWriter{&md}) 39 | if err != nil && err != opentracing.ErrSpanContextNotFound { 40 | logger.Log("err", err) 41 | } 42 | span = tracer.StartSpan(operationName, ext.RPCServerOption(wireContext)) 43 | return opentracing.ContextWithSpan(ctx, span) 44 | } 45 | } 46 | 47 | // A type that conforms to opentracing.TextMapReader and 48 | // opentracing.TextMapWriter. 49 | type metadataReaderWriter struct { 50 | *metadata.MD 51 | } 52 | 53 | func (w metadataReaderWriter) Set(key, val string) { 54 | key = strings.ToLower(key) 55 | if strings.HasSuffix(key, "-bin") { 56 | val = base64.StdEncoding.EncodeToString([]byte(val)) 57 | } 58 | (*w.MD)[key] = append((*w.MD)[key], val) 59 | } 60 | 61 | func (w metadataReaderWriter) ForeachKey(handler func(key, val string) error) error { 62 | for k, vals := range *w.MD { 63 | for _, v := range vals { 64 | if err := handler(k, v); err != nil { 65 | return err 66 | } 67 | } 68 | } 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /tracing/opentracing/grpc_test.go: -------------------------------------------------------------------------------- 1 | package opentracing_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/opentracing/opentracing-go" 8 | "github.com/opentracing/opentracing-go/mocktracer" 9 | "google.golang.org/grpc/metadata" 10 | 11 | kitot "github.com/go-kit/kit/tracing/opentracing" 12 | "github.com/go-kit/log" 13 | ) 14 | 15 | func TestTraceGRPCRequestRoundtrip(t *testing.T) { 16 | logger := log.NewNopLogger() 17 | tracer := mocktracer.New() 18 | 19 | // Initialize the ctx with a Span to inject. 20 | beforeSpan := tracer.StartSpan("to_inject").(*mocktracer.MockSpan) 21 | defer beforeSpan.Finish() 22 | beforeSpan.SetBaggageItem("baggage", "check") 23 | beforeCtx := opentracing.ContextWithSpan(context.Background(), beforeSpan) 24 | 25 | toGRPCFunc := kitot.ContextToGRPC(tracer, logger) 26 | md := metadata.Pairs() 27 | // Call the RequestFunc. 28 | afterCtx := toGRPCFunc(beforeCtx, &md) 29 | 30 | // The Span should not have changed. 31 | afterSpan := opentracing.SpanFromContext(afterCtx) 32 | if beforeSpan != afterSpan { 33 | t.Error("Should not swap in a new span") 34 | } 35 | 36 | // No spans should have finished yet. 37 | finishedSpans := tracer.FinishedSpans() 38 | if want, have := 0, len(finishedSpans); want != have { 39 | t.Errorf("Want %v span(s), found %v", want, have) 40 | } 41 | 42 | // Use GRPCToContext to verify that we can join with the trace given MD. 43 | fromGRPCFunc := kitot.GRPCToContext(tracer, "joined", logger) 44 | joinCtx := fromGRPCFunc(afterCtx, md) 45 | joinedSpan := opentracing.SpanFromContext(joinCtx).(*mocktracer.MockSpan) 46 | 47 | joinedContext := joinedSpan.Context().(mocktracer.MockSpanContext) 48 | beforeContext := beforeSpan.Context().(mocktracer.MockSpanContext) 49 | 50 | if joinedContext.SpanID == beforeContext.SpanID { 51 | t.Error("SpanID should have changed", joinedContext.SpanID, beforeContext.SpanID) 52 | } 53 | 54 | // Check that the parent/child relationship is as expected for the joined span. 55 | if want, have := beforeContext.SpanID, joinedSpan.ParentID; want != have { 56 | t.Errorf("Want ParentID %q, have %q", want, have) 57 | } 58 | if want, have := "joined", joinedSpan.OperationName; want != have { 59 | t.Errorf("Want %q, have %q", want, have) 60 | } 61 | if want, have := "check", joinedSpan.BaggageItem("baggage"); want != have { 62 | t.Errorf("Want %q, have %q", want, have) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tracing/opentracing/http.go: -------------------------------------------------------------------------------- 1 | package opentracing 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | "strconv" 8 | 9 | opentracing "github.com/opentracing/opentracing-go" 10 | "github.com/opentracing/opentracing-go/ext" 11 | 12 | kithttp "github.com/go-kit/kit/transport/http" 13 | "github.com/go-kit/log" 14 | ) 15 | 16 | // ContextToHTTP returns an http RequestFunc that injects an OpenTracing Span 17 | // found in `ctx` into the http headers. If no such Span can be found, the 18 | // RequestFunc is a noop. 19 | func ContextToHTTP(tracer opentracing.Tracer, logger log.Logger) kithttp.RequestFunc { 20 | return func(ctx context.Context, req *http.Request) context.Context { 21 | // Try to find a Span in the Context. 22 | if span := opentracing.SpanFromContext(ctx); span != nil { 23 | // Add standard OpenTracing tags. 24 | ext.HTTPMethod.Set(span, req.Method) 25 | ext.HTTPUrl.Set(span, req.URL.String()) 26 | host, portString, err := net.SplitHostPort(req.URL.Host) 27 | if err == nil { 28 | ext.PeerHostname.Set(span, host) 29 | if port, err := strconv.Atoi(portString); err == nil { 30 | ext.PeerPort.Set(span, uint16(port)) 31 | } 32 | } else { 33 | ext.PeerHostname.Set(span, req.URL.Host) 34 | } 35 | 36 | // There's nothing we can do with any errors here. 37 | if err = tracer.Inject( 38 | span.Context(), 39 | opentracing.HTTPHeaders, 40 | opentracing.HTTPHeadersCarrier(req.Header), 41 | ); err != nil { 42 | logger.Log("err", err) 43 | } 44 | } 45 | return ctx 46 | } 47 | } 48 | 49 | // HTTPToContext returns an http RequestFunc that tries to join with an 50 | // OpenTracing trace found in `req` and starts a new Span called 51 | // `operationName` accordingly. If no trace could be found in `req`, the Span 52 | // will be a trace root. The Span is incorporated in the returned Context and 53 | // can be retrieved with opentracing.SpanFromContext(ctx). 54 | func HTTPToContext(tracer opentracing.Tracer, operationName string, logger log.Logger) kithttp.RequestFunc { 55 | return func(ctx context.Context, req *http.Request) context.Context { 56 | // Try to join to a trace propagated in `req`. 57 | var span opentracing.Span 58 | wireContext, err := tracer.Extract( 59 | opentracing.HTTPHeaders, 60 | opentracing.HTTPHeadersCarrier(req.Header), 61 | ) 62 | if err != nil && err != opentracing.ErrSpanContextNotFound { 63 | logger.Log("err", err) 64 | } 65 | 66 | span = tracer.StartSpan(operationName, ext.RPCServerOption(wireContext)) 67 | ext.HTTPMethod.Set(span, req.Method) 68 | ext.HTTPUrl.Set(span, req.URL.String()) 69 | return opentracing.ContextWithSpan(ctx, span) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tracing/zipkin/doc.go: -------------------------------------------------------------------------------- 1 | // Package zipkin provides Go kit integration to the OpenZipkin project through 2 | // the use of zipkin-go, the official OpenZipkin tracer implementation for Go. 3 | // OpenZipkin is the most used open source distributed tracing ecosystem with 4 | // many different libraries and interoperability options. 5 | package zipkin 6 | -------------------------------------------------------------------------------- /tracing/zipkin/endpoint.go: -------------------------------------------------------------------------------- 1 | package zipkin 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/openzipkin/zipkin-go" 7 | "github.com/openzipkin/zipkin-go/model" 8 | 9 | "github.com/go-kit/kit/endpoint" 10 | ) 11 | 12 | // TraceEndpoint returns an Endpoint middleware, tracing a Go kit endpoint. 13 | // This endpoint tracer should be used in combination with a Go kit Transport 14 | // tracing middleware or custom before and after transport functions as 15 | // propagation of SpanContext is not provided in this middleware. 16 | func TraceEndpoint(tracer *zipkin.Tracer, name string) endpoint.Middleware { 17 | return func(next endpoint.Endpoint) endpoint.Endpoint { 18 | return func(ctx context.Context, request interface{}) (interface{}, error) { 19 | var sc model.SpanContext 20 | if parentSpan := zipkin.SpanFromContext(ctx); parentSpan != nil { 21 | sc = parentSpan.Context() 22 | } 23 | sp := tracer.StartSpan(name, zipkin.Parent(sc)) 24 | defer sp.Finish() 25 | 26 | ctx = zipkin.NewContext(ctx, sp) 27 | return next(ctx, request) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tracing/zipkin/endpoint_test.go: -------------------------------------------------------------------------------- 1 | package zipkin_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/openzipkin/zipkin-go" 8 | "github.com/openzipkin/zipkin-go/reporter/recorder" 9 | 10 | "github.com/go-kit/kit/endpoint" 11 | zipkinkit "github.com/go-kit/kit/tracing/zipkin" 12 | ) 13 | 14 | const spanName = "test" 15 | 16 | func TestTraceEndpoint(t *testing.T) { 17 | rec := recorder.NewReporter() 18 | tr, _ := zipkin.NewTracer(rec) 19 | mw := zipkinkit.TraceEndpoint(tr, spanName) 20 | mw(endpoint.Nop)(context.Background(), nil) 21 | 22 | spans := rec.Flush() 23 | 24 | if want, have := 1, len(spans); want != have { 25 | t.Fatalf("incorrect number of spans, wanted %d, got %d", want, have) 26 | } 27 | 28 | if want, have := spanName, spans[0].Name; want != have { 29 | t.Fatalf("incorrect span name, wanted %s, got %s", want, have) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tracing/zipkin/options.go: -------------------------------------------------------------------------------- 1 | package zipkin 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-kit/log" 7 | ) 8 | 9 | // TracerOption allows for functional options to our Zipkin tracing middleware. 10 | type TracerOption func(o *tracerOptions) 11 | 12 | // Name sets the name for an instrumented transport endpoint. If name is omitted 13 | // at tracing middleware creation, the method of the transport or transport rpc 14 | // name is used. 15 | func Name(name string) TracerOption { 16 | return func(o *tracerOptions) { 17 | o.name = name 18 | } 19 | } 20 | 21 | // Tags adds default tags to our Zipkin transport spans. 22 | func Tags(tags map[string]string) TracerOption { 23 | return func(o *tracerOptions) { 24 | for k, v := range tags { 25 | o.tags[k] = v 26 | } 27 | } 28 | } 29 | 30 | // Logger adds a Go kit logger to our Zipkin Middleware to log SpanContext 31 | // extract / inject errors if they occur. Default is Noop. 32 | func Logger(logger log.Logger) TracerOption { 33 | return func(o *tracerOptions) { 34 | if logger != nil { 35 | o.logger = logger 36 | } 37 | } 38 | } 39 | 40 | // AllowPropagation instructs the tracer to allow or deny propagation of the 41 | // span context between this instrumented client or service and its peers. If 42 | // the instrumented client connects to services outside its own platform or if 43 | // the instrumented service receives requests from untrusted clients it is 44 | // strongly advised to disallow propagation. Propagation between services inside 45 | // your own platform benefit from propagation. Default for both TraceClient and 46 | // TraceServer is to allow propagation. 47 | func AllowPropagation(propagate bool) TracerOption { 48 | return func(o *tracerOptions) { 49 | o.propagate = propagate 50 | } 51 | } 52 | 53 | // RequestSampler allows one to set the sampling decision based on the details 54 | // found in the http.Request. 55 | func RequestSampler(sampleFunc func(r *http.Request) bool) TracerOption { 56 | return func(o *tracerOptions) { 57 | o.requestSampler = sampleFunc 58 | } 59 | } 60 | 61 | type tracerOptions struct { 62 | tags map[string]string 63 | name string 64 | logger log.Logger 65 | propagate bool 66 | requestSampler func(r *http.Request) bool 67 | } 68 | -------------------------------------------------------------------------------- /transport/amqp/doc.go: -------------------------------------------------------------------------------- 1 | // Package amqp implements an AMQP transport. 2 | package amqp 3 | -------------------------------------------------------------------------------- /transport/amqp/encode_decode.go: -------------------------------------------------------------------------------- 1 | package amqp 2 | 3 | import ( 4 | "context" 5 | 6 | amqp "github.com/rabbitmq/amqp091-go" 7 | ) 8 | 9 | // DecodeRequestFunc extracts a user-domain request object from 10 | // an AMQP Delivery object. It is designed to be used in AMQP Subscribers. 11 | type DecodeRequestFunc func(context.Context, *amqp.Delivery) (request interface{}, err error) 12 | 13 | // EncodeRequestFunc encodes the passed request object into 14 | // an AMQP Publishing object. It is designed to be used in AMQP Publishers. 15 | type EncodeRequestFunc func(context.Context, *amqp.Publishing, interface{}) error 16 | 17 | // EncodeResponseFunc encodes the passed response object to 18 | // an AMQP Publishing object. It is designed to be used in AMQP Subscribers. 19 | type EncodeResponseFunc func(context.Context, *amqp.Publishing, interface{}) error 20 | 21 | // DecodeResponseFunc extracts a user-domain response object from 22 | // an AMQP Delivery object. It is designed to be used in AMQP Publishers. 23 | type DecodeResponseFunc func(context.Context, *amqp.Delivery) (response interface{}, err error) 24 | -------------------------------------------------------------------------------- /transport/amqp/util.go: -------------------------------------------------------------------------------- 1 | package amqp 2 | 3 | import ( 4 | "math/rand" 5 | ) 6 | 7 | func randomString(l int) string { 8 | bytes := make([]byte, l) 9 | for i := 0; i < l; i++ { 10 | bytes[i] = byte(randInt(65, 90)) 11 | } 12 | return string(bytes) 13 | } 14 | 15 | func randInt(min int, max int) int { 16 | return min + rand.Intn(max-min) 17 | } 18 | -------------------------------------------------------------------------------- /transport/awslambda/doc.go: -------------------------------------------------------------------------------- 1 | // Package awslambda provides an AWS Lambda transport layer. 2 | package awslambda 3 | -------------------------------------------------------------------------------- /transport/awslambda/encode_decode.go: -------------------------------------------------------------------------------- 1 | package awslambda 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // DecodeRequestFunc extracts a user-domain request object from an 8 | // AWS Lambda payload. 9 | type DecodeRequestFunc func(context.Context, []byte) (interface{}, error) 10 | 11 | // EncodeResponseFunc encodes the passed response object into []byte, 12 | // ready to be sent as AWS Lambda response. 13 | type EncodeResponseFunc func(context.Context, interface{}) ([]byte, error) 14 | 15 | // ErrorEncoder is responsible for encoding an error. 16 | type ErrorEncoder func(ctx context.Context, err error) ([]byte, error) 17 | -------------------------------------------------------------------------------- /transport/awslambda/request_response_funcs.go: -------------------------------------------------------------------------------- 1 | package awslambda 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // HandlerRequestFunc may take information from the received 8 | // payload and use it to place items in the request scoped context. 9 | // HandlerRequestFuncs are executed prior to invoking the endpoint and 10 | // decoding of the payload. 11 | type HandlerRequestFunc func(ctx context.Context, payload []byte) context.Context 12 | 13 | // HandlerResponseFunc may take information from a request context 14 | // and use it to manipulate the response before it's marshaled. 15 | // HandlerResponseFunc are executed after invoking the endpoint 16 | // but prior to returning a response. 17 | type HandlerResponseFunc func(ctx context.Context, response interface{}) context.Context 18 | 19 | // HandlerFinalizerFunc is executed at the end of Invoke. 20 | // This can be used for logging purposes. 21 | type HandlerFinalizerFunc func(ctx context.Context, resp []byte, err error) 22 | -------------------------------------------------------------------------------- /transport/doc.go: -------------------------------------------------------------------------------- 1 | // Package transport contains helpers applicable to all supported transports. 2 | package transport 3 | -------------------------------------------------------------------------------- /transport/error_handler.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-kit/log" 7 | ) 8 | 9 | // ErrorHandler receives a transport error to be processed for diagnostic purposes. 10 | // Usually this means logging the error. 11 | type ErrorHandler interface { 12 | Handle(ctx context.Context, err error) 13 | } 14 | 15 | // LogErrorHandler is a transport error handler implementation which logs an error. 16 | type LogErrorHandler struct { 17 | logger log.Logger 18 | } 19 | 20 | func NewLogErrorHandler(logger log.Logger) *LogErrorHandler { 21 | return &LogErrorHandler{ 22 | logger: logger, 23 | } 24 | } 25 | 26 | func (h *LogErrorHandler) Handle(ctx context.Context, err error) { 27 | h.logger.Log("err", err) 28 | } 29 | 30 | // The ErrorHandlerFunc type is an adapter to allow the use of 31 | // ordinary function as ErrorHandler. If f is a function 32 | // with the appropriate signature, ErrorHandlerFunc(f) is a 33 | // ErrorHandler that calls f. 34 | type ErrorHandlerFunc func(ctx context.Context, err error) 35 | 36 | // Handle calls f(ctx, err). 37 | func (f ErrorHandlerFunc) Handle(ctx context.Context, err error) { 38 | f(ctx, err) 39 | } 40 | -------------------------------------------------------------------------------- /transport/error_handler_test.go: -------------------------------------------------------------------------------- 1 | package transport_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/go-kit/kit/transport" 9 | "github.com/go-kit/log" 10 | ) 11 | 12 | func TestLogErrorHandler(t *testing.T) { 13 | var output []interface{} 14 | 15 | logger := log.Logger(log.LoggerFunc(func(keyvals ...interface{}) error { 16 | output = append(output, keyvals...) 17 | return nil 18 | })) 19 | 20 | errorHandler := transport.NewLogErrorHandler(logger) 21 | 22 | err := errors.New("error") 23 | 24 | errorHandler.Handle(context.Background(), err) 25 | 26 | if output[1] != err { 27 | t.Errorf("expected an error log event: have %v, want %v", output[1], err) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /transport/grpc/README.md: -------------------------------------------------------------------------------- 1 | # grpc 2 | 3 | [gRPC](http://www.grpc.io/) is an excellent, modern IDL and transport for 4 | microservices. If you're starting a greenfield project, go-kit strongly 5 | recommends gRPC as your default transport. 6 | 7 | One important note is that while gRPC supports streaming requests and replies, 8 | go-kit does not. You can still use streams in your service, but their 9 | implementation will not be able to take advantage of many go-kit features like middleware. 10 | 11 | Using gRPC and go-kit together is very simple. 12 | 13 | First, define your service using protobuf3. This is explained 14 | [in gRPC documentation](http://www.grpc.io/docs/#defining-a-service). 15 | See 16 | [addsvc.proto](https://github.com/go-kit/examples/blob/master/addsvc/pb/addsvc.proto) 17 | for an example. Make sure the proto definition matches your service's go-kit 18 | (interface) definition. 19 | 20 | Next, get the protoc compiler. 21 | 22 | You can download pre-compiled binaries from the 23 | [protobuf release page](https://github.com/google/protobuf/releases). 24 | You will unzip a folder called `protoc3` with a subdirectory `bin` containing 25 | an executable. Move that executable somewhere in your `$PATH` and you're good 26 | to go! 27 | 28 | It can also be built from source. 29 | 30 | ```sh 31 | brew install autoconf automake libtool 32 | git clone https://github.com/google/protobuf 33 | cd protobuf 34 | ./autogen.sh ; ./configure ; make ; make install 35 | ``` 36 | 37 | Then, compile your service definition, from .proto to .go. 38 | 39 | ```sh 40 | protoc add.proto --go_out=plugins=grpc:. 41 | ``` 42 | 43 | Finally, write a tiny binding from your service definition to the gRPC 44 | definition. It's a simple conversion from one domain to another. 45 | See 46 | [grpc.go](https://github.com/go-kit/examples/blob/master/addsvc/pkg/addtransport/grpc.go) 47 | for an example. 48 | 49 | That's it! 50 | The gRPC binding can be bound to a listener and serve normal gRPC requests. 51 | And within your service, you can use standard go-kit components and idioms. 52 | See [addsvc](https://github.com/go-kit/examples/tree/master/addsvc/) for 53 | a complete working example with gRPC support. And remember: go-kit services 54 | can support multiple transports simultaneously. 55 | -------------------------------------------------------------------------------- /transport/grpc/_grpc_test/client.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "context" 5 | 6 | "google.golang.org/grpc" 7 | 8 | "github.com/go-kit/kit/endpoint" 9 | grpctransport "github.com/go-kit/kit/transport/grpc" 10 | "github.com/go-kit/kit/transport/grpc/_grpc_test/pb" 11 | ) 12 | 13 | type clientBinding struct { 14 | test endpoint.Endpoint 15 | } 16 | 17 | func (c *clientBinding) Test(ctx context.Context, a string, b int64) (context.Context, string, error) { 18 | response, err := c.test(ctx, TestRequest{A: a, B: b}) 19 | if err != nil { 20 | return nil, "", err 21 | } 22 | r := response.(*TestResponse) 23 | return r.Ctx, r.V, nil 24 | } 25 | 26 | func NewClient(cc *grpc.ClientConn) Service { 27 | return &clientBinding{ 28 | test: grpctransport.NewClient( 29 | cc, 30 | "pb.Test", 31 | "Test", 32 | encodeRequest, 33 | decodeResponse, 34 | &pb.TestResponse{}, 35 | grpctransport.ClientBefore( 36 | injectCorrelationID, 37 | ), 38 | grpctransport.ClientBefore( 39 | displayClientRequestHeaders, 40 | ), 41 | grpctransport.ClientAfter( 42 | displayClientResponseHeaders, 43 | displayClientResponseTrailers, 44 | ), 45 | grpctransport.ClientAfter( 46 | extractConsumedCorrelationID, 47 | ), 48 | ).Endpoint(), 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /transport/grpc/_grpc_test/pb/generate.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | //go:generate protoc test.proto --go_out=. --go-grpc_out=. --go_opt=Mtest.proto=github.com/go-kit/kit/transport/grpc/_grpc_test/pb --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative --go-grpc_opt=Mtest.proto=github.com/go-kit/kit/transport/grpc/_grpc_test/pb 4 | -------------------------------------------------------------------------------- /transport/grpc/_grpc_test/pb/test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package pb; 4 | 5 | service Test { 6 | rpc Test (TestRequest) returns (TestResponse) {} 7 | } 8 | 9 | message TestRequest { 10 | string a = 1; 11 | int64 b = 2; 12 | } 13 | 14 | message TestResponse { 15 | string v = 1; 16 | } 17 | -------------------------------------------------------------------------------- /transport/grpc/_grpc_test/request_response.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-kit/kit/transport/grpc/_grpc_test/pb" 7 | ) 8 | 9 | func encodeRequest(ctx context.Context, req interface{}) (interface{}, error) { 10 | r := req.(TestRequest) 11 | return &pb.TestRequest{A: r.A, B: r.B}, nil 12 | } 13 | 14 | func decodeRequest(ctx context.Context, req interface{}) (interface{}, error) { 15 | r := req.(*pb.TestRequest) 16 | return TestRequest{A: r.A, B: r.B}, nil 17 | } 18 | 19 | func encodeResponse(ctx context.Context, resp interface{}) (interface{}, error) { 20 | r := resp.(*TestResponse) 21 | return &pb.TestResponse{V: r.V}, nil 22 | } 23 | 24 | func decodeResponse(ctx context.Context, resp interface{}) (interface{}, error) { 25 | r := resp.(*pb.TestResponse) 26 | return &TestResponse{V: r.V, Ctx: ctx}, nil 27 | } 28 | -------------------------------------------------------------------------------- /transport/grpc/_grpc_test/server.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/go-kit/kit/endpoint" 8 | grpctransport "github.com/go-kit/kit/transport/grpc" 9 | "github.com/go-kit/kit/transport/grpc/_grpc_test/pb" 10 | ) 11 | 12 | type service struct{} 13 | 14 | func (service) Test(ctx context.Context, a string, b int64) (context.Context, string, error) { 15 | return nil, fmt.Sprintf("%s = %d", a, b), nil 16 | } 17 | 18 | func NewService() Service { 19 | return service{} 20 | } 21 | 22 | func makeTestEndpoint(svc Service) endpoint.Endpoint { 23 | return func(ctx context.Context, request interface{}) (interface{}, error) { 24 | req := request.(TestRequest) 25 | newCtx, v, err := svc.Test(ctx, req.A, req.B) 26 | return &TestResponse{ 27 | V: v, 28 | Ctx: newCtx, 29 | }, err 30 | } 31 | } 32 | 33 | type serverBinding struct { 34 | pb.UnimplementedTestServer 35 | 36 | test grpctransport.Handler 37 | } 38 | 39 | func (b *serverBinding) Test(ctx context.Context, req *pb.TestRequest) (*pb.TestResponse, error) { 40 | _, response, err := b.test.ServeGRPC(ctx, req) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return response.(*pb.TestResponse), nil 45 | } 46 | 47 | func NewBinding(svc Service) *serverBinding { 48 | return &serverBinding{ 49 | test: grpctransport.NewServer( 50 | makeTestEndpoint(svc), 51 | decodeRequest, 52 | encodeResponse, 53 | grpctransport.ServerBefore( 54 | extractCorrelationID, 55 | ), 56 | grpctransport.ServerBefore( 57 | displayServerRequestHeaders, 58 | ), 59 | grpctransport.ServerAfter( 60 | injectResponseHeader, 61 | injectResponseTrailer, 62 | injectConsumedCorrelationID, 63 | ), 64 | grpctransport.ServerAfter( 65 | displayServerResponseHeaders, 66 | displayServerResponseTrailers, 67 | ), 68 | ), 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /transport/grpc/_grpc_test/service.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import "context" 4 | 5 | type Service interface { 6 | Test(ctx context.Context, a string, b int64) (context.Context, string, error) 7 | } 8 | 9 | type TestRequest struct { 10 | A string 11 | B int64 12 | } 13 | 14 | type TestResponse struct { 15 | Ctx context.Context 16 | V string 17 | } 18 | -------------------------------------------------------------------------------- /transport/grpc/client_test.go: -------------------------------------------------------------------------------- 1 | package grpc_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "testing" 8 | 9 | "google.golang.org/grpc" 10 | 11 | test "github.com/go-kit/kit/transport/grpc/_grpc_test" 12 | "github.com/go-kit/kit/transport/grpc/_grpc_test/pb" 13 | ) 14 | 15 | const ( 16 | hostPort string = "localhost:8002" 17 | ) 18 | 19 | func TestGRPCClient(t *testing.T) { 20 | var ( 21 | server = grpc.NewServer() 22 | service = test.NewService() 23 | ) 24 | 25 | sc, err := net.Listen("tcp", hostPort) 26 | if err != nil { 27 | t.Fatalf("unable to listen: %+v", err) 28 | } 29 | defer server.GracefulStop() 30 | 31 | go func() { 32 | pb.RegisterTestServer(server, test.NewBinding(service)) 33 | _ = server.Serve(sc) 34 | }() 35 | 36 | cc, err := grpc.Dial(hostPort, grpc.WithInsecure()) 37 | if err != nil { 38 | t.Fatalf("unable to Dial: %+v", err) 39 | } 40 | 41 | client := test.NewClient(cc) 42 | 43 | var ( 44 | a = "the answer to life the universe and everything" 45 | b = int64(42) 46 | cID = "request-1" 47 | ctx = test.SetCorrelationID(context.Background(), cID) 48 | ) 49 | 50 | responseCTX, v, err := client.Test(ctx, a, b) 51 | if err != nil { 52 | t.Fatalf("unable to Test: %+v", err) 53 | } 54 | if want, have := fmt.Sprintf("%s = %d", a, b), v; want != have { 55 | t.Fatalf("want %q, have %q", want, have) 56 | } 57 | 58 | if want, have := cID, test.GetConsumedCorrelationID(responseCTX); want != have { 59 | t.Fatalf("want %q, have %q", want, have) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /transport/grpc/doc.go: -------------------------------------------------------------------------------- 1 | // Package grpc provides a gRPC binding for endpoints. 2 | package grpc 3 | -------------------------------------------------------------------------------- /transport/grpc/encode_decode.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // DecodeRequestFunc extracts a user-domain request object from a gRPC request. 8 | // It's designed to be used in gRPC servers, for server-side endpoints. One 9 | // straightforward DecodeRequestFunc could be something that decodes from the 10 | // gRPC request message to the concrete request type. 11 | type DecodeRequestFunc func(context.Context, interface{}) (request interface{}, err error) 12 | 13 | // EncodeRequestFunc encodes the passed request object into the gRPC request 14 | // object. It's designed to be used in gRPC clients, for client-side endpoints. 15 | // One straightforward EncodeRequestFunc could something that encodes the object 16 | // directly to the gRPC request message. 17 | type EncodeRequestFunc func(context.Context, interface{}) (request interface{}, err error) 18 | 19 | // EncodeResponseFunc encodes the passed response object to the gRPC response 20 | // message. It's designed to be used in gRPC servers, for server-side endpoints. 21 | // One straightforward EncodeResponseFunc could be something that encodes the 22 | // object directly to the gRPC response message. 23 | type EncodeResponseFunc func(context.Context, interface{}) (response interface{}, err error) 24 | 25 | // DecodeResponseFunc extracts a user-domain response object from a gRPC 26 | // response object. It's designed to be used in gRPC clients, for client-side 27 | // endpoints. One straightforward DecodeResponseFunc could be something that 28 | // decodes from the gRPC response message to the concrete response type. 29 | type DecodeResponseFunc func(context.Context, interface{}) (response interface{}, err error) 30 | -------------------------------------------------------------------------------- /transport/http/doc.go: -------------------------------------------------------------------------------- 1 | // Package http provides a general purpose HTTP binding for endpoints. 2 | package http 3 | -------------------------------------------------------------------------------- /transport/http/encode_decode.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // DecodeRequestFunc extracts a user-domain request object from an HTTP 9 | // request object. It's designed to be used in HTTP servers, for server-side 10 | // endpoints. One straightforward DecodeRequestFunc could be something that 11 | // JSON decodes from the request body to the concrete request type. 12 | type DecodeRequestFunc func(context.Context, *http.Request) (request interface{}, err error) 13 | 14 | // EncodeRequestFunc encodes the passed request object into the HTTP request 15 | // object. It's designed to be used in HTTP clients, for client-side 16 | // endpoints. One straightforward EncodeRequestFunc could be something that JSON 17 | // encodes the object directly to the request body. 18 | type EncodeRequestFunc func(context.Context, *http.Request, interface{}) error 19 | 20 | // CreateRequestFunc creates an outgoing HTTP request based on the passed 21 | // request object. It's designed to be used in HTTP clients, for client-side 22 | // endpoints. It's a more powerful version of EncodeRequestFunc, and can be used 23 | // if more fine-grained control of the HTTP request is required. 24 | type CreateRequestFunc func(context.Context, interface{}) (*http.Request, error) 25 | 26 | // EncodeResponseFunc encodes the passed response object to the HTTP response 27 | // writer. It's designed to be used in HTTP servers, for server-side 28 | // endpoints. One straightforward EncodeResponseFunc could be something that 29 | // JSON encodes the object directly to the response body. 30 | type EncodeResponseFunc func(context.Context, http.ResponseWriter, interface{}) error 31 | 32 | // DecodeResponseFunc extracts a user-domain response object from an HTTP 33 | // response object. It's designed to be used in HTTP clients, for client-side 34 | // endpoints. One straightforward DecodeResponseFunc could be something that 35 | // JSON decodes from the response body to the concrete response type. 36 | type DecodeResponseFunc func(context.Context, *http.Response) (response interface{}, err error) 37 | -------------------------------------------------------------------------------- /transport/http/example_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | ) 9 | 10 | func ExamplePopulateRequestContext() { 11 | handler := NewServer( 12 | func(ctx context.Context, request interface{}) (response interface{}, err error) { 13 | fmt.Println("Method", ctx.Value(ContextKeyRequestMethod).(string)) 14 | fmt.Println("RequestPath", ctx.Value(ContextKeyRequestPath).(string)) 15 | fmt.Println("RequestURI", ctx.Value(ContextKeyRequestURI).(string)) 16 | fmt.Println("X-Request-ID", ctx.Value(ContextKeyRequestXRequestID).(string)) 17 | return struct{}{}, nil 18 | }, 19 | func(context.Context, *http.Request) (interface{}, error) { return struct{}{}, nil }, 20 | func(context.Context, http.ResponseWriter, interface{}) error { return nil }, 21 | ServerBefore(PopulateRequestContext), 22 | ) 23 | 24 | server := httptest.NewServer(handler) 25 | defer server.Close() 26 | 27 | req, _ := http.NewRequest("PATCH", fmt.Sprintf("%s/search?q=sympatico", server.URL), nil) 28 | req.Header.Set("X-Request-Id", "a1b2c3d4e5") 29 | http.DefaultClient.Do(req) 30 | 31 | // Output: 32 | // Method PATCH 33 | // RequestPath /search 34 | // RequestURI /search?q=sympatico 35 | // X-Request-ID a1b2c3d4e5 36 | } 37 | -------------------------------------------------------------------------------- /transport/http/jsonrpc/doc.go: -------------------------------------------------------------------------------- 1 | // Package jsonrpc provides a JSON RPC (v2.0) binding for endpoints. 2 | // See http://www.jsonrpc.org/specification 3 | package jsonrpc 4 | -------------------------------------------------------------------------------- /transport/http/jsonrpc/encode_decode.go: -------------------------------------------------------------------------------- 1 | package jsonrpc 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/go-kit/kit/endpoint" 7 | 8 | "context" 9 | ) 10 | 11 | // Server-Side Codec 12 | 13 | // EndpointCodec defines a server Endpoint and its associated codecs 14 | type EndpointCodec struct { 15 | Endpoint endpoint.Endpoint 16 | Decode DecodeRequestFunc 17 | Encode EncodeResponseFunc 18 | } 19 | 20 | // EndpointCodecMap maps the Request.Method to the proper EndpointCodec 21 | type EndpointCodecMap map[string]EndpointCodec 22 | 23 | // DecodeRequestFunc extracts a user-domain request object from raw JSON 24 | // It's designed to be used in JSON RPC servers, for server-side endpoints. 25 | // One straightforward DecodeRequestFunc could be something that unmarshals 26 | // JSON from the request body to the concrete request type. 27 | type DecodeRequestFunc func(context.Context, json.RawMessage) (request interface{}, err error) 28 | 29 | // EncodeResponseFunc encodes the passed response object to a JSON RPC result. 30 | // It's designed to be used in HTTP servers, for server-side endpoints. 31 | // One straightforward EncodeResponseFunc could be something that JSON encodes 32 | // the object directly. 33 | type EncodeResponseFunc func(context.Context, interface{}) (response json.RawMessage, err error) 34 | 35 | // Client-Side Codec 36 | 37 | // EncodeRequestFunc encodes the given request object to raw JSON. 38 | // It's designed to be used in JSON RPC clients, for client-side 39 | // endpoints. One straightforward EncodeResponseFunc could be something that 40 | // JSON encodes the object directly. 41 | type EncodeRequestFunc func(context.Context, interface{}) (request json.RawMessage, err error) 42 | 43 | // DecodeResponseFunc extracts a user-domain response object from an JSON RPC 44 | // response object. It's designed to be used in JSON RPC clients, for 45 | // client-side endpoints. It is the responsibility of this function to decide 46 | // whether any error present in the JSON RPC response should be surfaced to the 47 | // client endpoint. 48 | type DecodeResponseFunc func(context.Context, Response) (response interface{}, err error) 49 | -------------------------------------------------------------------------------- /transport/http/jsonrpc/error_test.go: -------------------------------------------------------------------------------- 1 | package jsonrpc 2 | 3 | import "testing" 4 | 5 | func TestError(t *testing.T) { 6 | wantCode := ParseError 7 | sut := Error{ 8 | Code: wantCode, 9 | } 10 | 11 | gotCode := sut.ErrorCode() 12 | if gotCode != wantCode { 13 | t.Fatalf("want=%d, got=%d", gotCode, wantCode) 14 | } 15 | 16 | if sut.Error() == "" { 17 | t.Fatal("Empty error string.") 18 | } 19 | 20 | want := "override" 21 | sut.Message = want 22 | got := sut.Error() 23 | if sut.Error() != want { 24 | t.Fatalf("overridden error message: want=%s, got=%s", want, got) 25 | } 26 | 27 | } 28 | func TestErrorsSatisfyError(t *testing.T) { 29 | errs := []interface{}{ 30 | parseError("parseError"), 31 | invalidRequestError("invalidRequestError"), 32 | methodNotFoundError("methodNotFoundError"), 33 | invalidParamsError("invalidParamsError"), 34 | internalError("internalError"), 35 | } 36 | for _, e := range errs { 37 | err, ok := e.(error) 38 | if !ok { 39 | t.Fatalf("Couldn't assert %s as error.", e) 40 | } 41 | errString := err.Error() 42 | if errString == "" { 43 | t.Fatal("Empty error string") 44 | } 45 | 46 | ec, ok := e.(ErrorCoder) 47 | if !ok { 48 | t.Fatalf("Couldn't assert %s as ErrorCoder.", e) 49 | } 50 | if ErrorMessage(ec.ErrorCode()) == "" { 51 | t.Fatalf("Error type %s returned code of %d, which does not map to error string", e, ec.ErrorCode()) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /transport/http/proto/client.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "io/ioutil" 8 | "net/http" 9 | 10 | httptransport "github.com/go-kit/kit/transport/http" 11 | "google.golang.org/protobuf/proto" 12 | ) 13 | 14 | // EncodeProtoRequest is an EncodeRequestFunc that serializes the request as Protobuf. 15 | // If the request implements Headerer, the provided headers will be applied 16 | // to the request. If the given request does not implement proto.Message, an error will 17 | // be returned. 18 | func EncodeProtoRequest(_ context.Context, r *http.Request, preq interface{}) error { 19 | r.Header.Set("Content-Type", "application/x-protobuf") 20 | if headerer, ok := preq.(httptransport.Headerer); ok { 21 | for k := range headerer.Headers() { 22 | r.Header.Set(k, headerer.Headers().Get(k)) 23 | } 24 | } 25 | req, ok := preq.(proto.Message) 26 | if !ok { 27 | return errors.New("response does not implement proto.Message") 28 | } 29 | 30 | b, err := proto.Marshal(req) 31 | if err != nil { 32 | return err 33 | } 34 | r.ContentLength = int64(len(b)) 35 | r.Body = ioutil.NopCloser(bytes.NewReader(b)) 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /transport/http/proto/generate.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | //go:generate protoc proto_test.proto --go_out=. --go_opt=Mproto_test.proto=github.com/go-kit/kit/transport/http/proto --go_opt=paths=source_relative 4 | //go:generate mv proto_test.pb.go proto_pb_test.go 5 | -------------------------------------------------------------------------------- /transport/http/proto/proto_test.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | "context" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "google.golang.org/protobuf/proto" 11 | ) 12 | 13 | func TestEncodeProtoRequest(t *testing.T) { 14 | cat := &Cat{Name: "Ziggy", Age: 13, Breed: "Lumpy"} 15 | 16 | r := httptest.NewRequest(http.MethodGet, "/cat", nil) 17 | 18 | err := EncodeProtoRequest(context.TODO(), r, cat) 19 | if err != nil { 20 | t.Errorf("expected no encoding errors but got: %s", err) 21 | return 22 | } 23 | 24 | const xproto = "application/x-protobuf" 25 | if typ := r.Header.Get("Content-Type"); typ != xproto { 26 | t.Errorf("expected content type of %q, got %q", xproto, typ) 27 | return 28 | } 29 | 30 | bod, err := ioutil.ReadAll(r.Body) 31 | if err != nil { 32 | t.Errorf("expected no read errors but got: %s", err) 33 | return 34 | } 35 | defer r.Body.Close() 36 | 37 | var got Cat 38 | err = proto.Unmarshal(bod, &got) 39 | if err != nil { 40 | t.Errorf("expected no proto errors but got: %s", err) 41 | return 42 | } 43 | 44 | if !proto.Equal(&got, cat) { 45 | t.Errorf("expected cats to be equal but got:\n\n%#v\n\nwant:\n\n%#v", got, cat) 46 | return 47 | } 48 | } 49 | 50 | func TestEncodeProtoResponse(t *testing.T) { 51 | cat := &Cat{Name: "Ziggy", Age: 13, Breed: "Lumpy"} 52 | 53 | wr := httptest.NewRecorder() 54 | 55 | err := EncodeProtoResponse(context.TODO(), wr, cat) 56 | if err != nil { 57 | t.Errorf("expected no encoding errors but got: %s", err) 58 | return 59 | } 60 | 61 | w := wr.Result() 62 | 63 | const xproto = "application/x-protobuf" 64 | if typ := w.Header.Get("Content-Type"); typ != xproto { 65 | t.Errorf("expected content type of %q, got %q", xproto, typ) 66 | return 67 | } 68 | 69 | if w.StatusCode != http.StatusTeapot { 70 | t.Errorf("expected status code of %d, got %d", http.StatusTeapot, w.StatusCode) 71 | return 72 | } 73 | 74 | bod, err := ioutil.ReadAll(w.Body) 75 | if err != nil { 76 | t.Errorf("expected no read errors but got: %s", err) 77 | return 78 | } 79 | defer w.Body.Close() 80 | 81 | var got Cat 82 | err = proto.Unmarshal(bod, &got) 83 | if err != nil { 84 | t.Errorf("expected no proto errors but got: %s", err) 85 | return 86 | } 87 | 88 | if !proto.Equal(&got, cat) { 89 | t.Errorf("expected cats to be equal but got:\n\n%#v\n\nwant:\n\n%#v", got, cat) 90 | return 91 | } 92 | } 93 | 94 | func (c *Cat) StatusCode() int { 95 | return http.StatusTeapot 96 | } 97 | -------------------------------------------------------------------------------- /transport/http/proto/proto_test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Cat { 4 | int32 Age = 1; 5 | string Breed = 2; 6 | string Name = 3; 7 | } 8 | -------------------------------------------------------------------------------- /transport/http/proto/server.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | 8 | httptransport "github.com/go-kit/kit/transport/http" 9 | "google.golang.org/protobuf/proto" 10 | ) 11 | 12 | // EncodeProtoResponse is an EncodeResponseFunc that serializes the response as Protobuf. 13 | // Many Proto-over-HTTP services can use it as a sensible default. If the response 14 | // implements Headerer, the provided headers will be applied to the response. If the 15 | // response implements StatusCoder, the provided StatusCode will be used instead of 200. 16 | func EncodeProtoResponse(ctx context.Context, w http.ResponseWriter, pres interface{}) error { 17 | res, ok := pres.(proto.Message) 18 | if !ok { 19 | return errors.New("response does not implement proto.Message") 20 | } 21 | w.Header().Set("Content-Type", "application/x-protobuf") 22 | if headerer, ok := w.(httptransport.Headerer); ok { 23 | for k := range headerer.Headers() { 24 | w.Header().Set(k, headerer.Headers().Get(k)) 25 | } 26 | } 27 | code := http.StatusOK 28 | if sc, ok := pres.(httptransport.StatusCoder); ok { 29 | code = sc.StatusCode() 30 | } 31 | w.WriteHeader(code) 32 | if code == http.StatusNoContent { 33 | return nil 34 | } 35 | if res == nil { 36 | return nil 37 | } 38 | b, err := proto.Marshal(res) 39 | if err != nil { 40 | return err 41 | } 42 | _, err = w.Write(b) 43 | if err != nil { 44 | return err 45 | } 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /transport/http/request_response_funcs_test.go: -------------------------------------------------------------------------------- 1 | package http_test 2 | 3 | import ( 4 | "context" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | httptransport "github.com/go-kit/kit/transport/http" 9 | ) 10 | 11 | func TestSetHeader(t *testing.T) { 12 | const ( 13 | key = "X-Foo" 14 | val = "12345" 15 | ) 16 | r := httptest.NewRecorder() 17 | httptransport.SetResponseHeader(key, val)(context.Background(), r) 18 | if want, have := val, r.Header().Get(key); want != have { 19 | t.Errorf("want %q, have %q", want, have) 20 | } 21 | } 22 | 23 | func TestSetContentType(t *testing.T) { 24 | const contentType = "application/json" 25 | r := httptest.NewRecorder() 26 | httptransport.SetContentType(contentType)(context.Background(), r) 27 | if want, have := contentType, r.Header().Get("Content-Type"); want != have { 28 | t.Errorf("want %q, have %q", want, have) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /transport/httprp/doc.go: -------------------------------------------------------------------------------- 1 | // Package httprp provides an HTTP reverse-proxy transport. HTTP handlers that 2 | // need to proxy requests to another HTTP service can do so with this package by 3 | // specifying the URL to forward the request to. 4 | package httprp 5 | -------------------------------------------------------------------------------- /transport/httprp/server.go: -------------------------------------------------------------------------------- 1 | package httprp 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httputil" 7 | "net/url" 8 | ) 9 | 10 | // RequestFunc may take information from an HTTP request and put it into a 11 | // request context. BeforeFuncs are executed prior to invoking the 12 | // endpoint. 13 | type RequestFunc func(context.Context, *http.Request) context.Context 14 | 15 | // Server is a proxying request handler. 16 | type Server struct { 17 | proxy http.Handler 18 | before []RequestFunc 19 | errorEncoder func(w http.ResponseWriter, err error) 20 | } 21 | 22 | // NewServer constructs a new server that implements http.Server and will proxy 23 | // requests to the given base URL using its scheme, host, and base path. 24 | // If the target's path is "/base" and the incoming request was for "/dir", 25 | // the target request will be for /base/dir. 26 | func NewServer( 27 | baseURL *url.URL, 28 | options ...ServerOption, 29 | ) *Server { 30 | s := &Server{ 31 | proxy: httputil.NewSingleHostReverseProxy(baseURL), 32 | } 33 | for _, option := range options { 34 | option(s) 35 | } 36 | return s 37 | } 38 | 39 | // ServerOption sets an optional parameter for servers. 40 | type ServerOption func(*Server) 41 | 42 | // ServerBefore functions are executed on the HTTP request object before the 43 | // request is decoded. 44 | func ServerBefore(before ...RequestFunc) ServerOption { 45 | return func(s *Server) { s.before = append(s.before, before...) } 46 | } 47 | 48 | // ServeHTTP implements http.Handler. 49 | func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 50 | ctx := r.Context() 51 | 52 | for _, f := range s.before { 53 | ctx = f(ctx, r) 54 | } 55 | 56 | s.proxy.ServeHTTP(w, r) 57 | } 58 | -------------------------------------------------------------------------------- /transport/nats/doc.go: -------------------------------------------------------------------------------- 1 | // Package nats provides a NATS transport. 2 | package nats 3 | -------------------------------------------------------------------------------- /transport/nats/encode_decode.go: -------------------------------------------------------------------------------- 1 | package nats 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/nats-io/nats.go" 7 | ) 8 | 9 | // DecodeRequestFunc extracts a user-domain request object from a publisher 10 | // request object. It's designed to be used in NATS subscribers, for subscriber-side 11 | // endpoints. One straightforward DecodeRequestFunc could be something that 12 | // JSON decodes from the request body to the concrete response type. 13 | type DecodeRequestFunc func(context.Context, *nats.Msg) (request interface{}, err error) 14 | 15 | // EncodeRequestFunc encodes the passed request object into the NATS request 16 | // object. It's designed to be used in NATS publishers, for publisher-side 17 | // endpoints. One straightforward EncodeRequestFunc could something that JSON 18 | // encodes the object directly to the request payload. 19 | type EncodeRequestFunc func(context.Context, *nats.Msg, interface{}) error 20 | 21 | // EncodeResponseFunc encodes the passed response object to the subscriber reply. 22 | // It's designed to be used in NATS subscribers, for subscriber-side 23 | // endpoints. One straightforward EncodeResponseFunc could be something that 24 | // JSON encodes the object directly to the response body. 25 | type EncodeResponseFunc func(context.Context, string, *nats.Conn, interface{}) error 26 | 27 | // DecodeResponseFunc extracts a user-domain response object from an NATS 28 | // response object. It's designed to be used in NATS publisher, for publisher-side 29 | // endpoints. One straightforward DecodeResponseFunc could be something that 30 | // JSON decodes from the response payload to the concrete response type. 31 | type DecodeResponseFunc func(context.Context, *nats.Msg) (response interface{}, err error) 32 | 33 | -------------------------------------------------------------------------------- /transport/nats/request_response_funcs.go: -------------------------------------------------------------------------------- 1 | package nats 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/nats-io/nats.go" 7 | ) 8 | 9 | // RequestFunc may take information from a publisher request and put it into a 10 | // request context. In Subscribers, RequestFuncs are executed prior to invoking the 11 | // endpoint. 12 | type RequestFunc func(context.Context, *nats.Msg) context.Context 13 | 14 | // SubscriberResponseFunc may take information from a request context and use it to 15 | // manipulate a Publisher. SubscriberResponseFuncs are only executed in 16 | // subscribers, after invoking the endpoint but prior to publishing a reply. 17 | type SubscriberResponseFunc func(context.Context, *nats.Conn) context.Context 18 | 19 | // PublisherResponseFunc may take information from an NATS request and make the 20 | // response available for consumption. ClientResponseFuncs are only executed in 21 | // clients, after a request has been made, but prior to it being decoded. 22 | type PublisherResponseFunc func(context.Context, *nats.Msg) context.Context 23 | -------------------------------------------------------------------------------- /transport/netrpc/README.md: -------------------------------------------------------------------------------- 1 | # net/rpc 2 | 3 | [net/rpc](https://golang.org/pkg/net/rpc) is an RPC transport that's part of the Go standard library. 4 | It's a simple and fast transport that's appropriate when all of your services are written in Go. 5 | 6 | Using net/rpc with Go kit is very simple. 7 | Just write a simple binding from your service definition to the net/rpc definition. 8 | See [netrpc_binding.go](https://github.com/go-kit/kit/blob/ec8b02591ee873433565a1ae9d317353412d1d27/examples/addsvc/netrpc_binding.go) for an example. 9 | 10 | That's it! 11 | The net/rpc binding can be registered to a name, and bound to an HTTP handler, the same as any other net/rpc endpoint. 12 | And within your service, you can use standard Go kit components and idioms. 13 | See [addsvc](https://github.com/go-kit/examples/tree/master/addsvc) for a complete working example with net/rpc support. 14 | And remember: Go kit services can support multiple transports simultaneously. 15 | -------------------------------------------------------------------------------- /transport/thrift/README.md: -------------------------------------------------------------------------------- 1 | # Thrift 2 | 3 | [Thrift](https://thrift.apache.org/) is a large IDL and transport package from Apache, popularized by Facebook. 4 | Thrift is well-supported in Go kit, for organizations that already have significant Thrift investment. 5 | And using Thrift with Go kit is very simple. 6 | 7 | First, define your service in the Thrift IDL. 8 | The [Thrift IDL documentation](https://thrift.apache.org/docs/idl) provides more details. 9 | See [addsvc.thrift](https://github.com/go-kit/examples/blob/master/addsvc/thrift/addsvc.thrift) for an example. 10 | Make sure the Thrift definition matches your service's Go kit (interface) definition. 11 | 12 | Next, [download Thrift](https://thrift.apache.org/download) and [install the compiler](https://thrift.apache.org/docs/install/). 13 | On a Mac, you may be able to `brew install thrift`. 14 | 15 | Then, compile your service definition, from .thrift to .go. 16 | You'll probably want to specify the package_prefix option to the --gen go flag. 17 | See [THRIFT-3021](https://issues.apache.org/jira/browse/THRIFT-3021) for more details. 18 | 19 | ``` 20 | thrift -r --gen go:package_prefix=github.com/my-org/my-repo/thrift/gen-go/ add.thrift 21 | ``` 22 | 23 | Finally, write a tiny binding from your service definition to the Thrift definition. 24 | It's a straightforward conversion from one domain to the other. 25 | See [thrift.go](https://github.com/go-kit/examples/blob/master/addsvc/pkg/addtransport/thrift.go) for an example. 26 | 27 | That's it! 28 | The Thrift binding can be bound to a listener and serve normal Thrift requests. 29 | And within your service, you can use standard Go kit components and idioms. 30 | Unfortunately, setting up a Thrift listener is rather laborious and nonidiomatic in Go. 31 | Fortunately, [addsvc](https://github.com/go-kit/examples/tree/master/addsvc) is a complete working example with Thrift support. 32 | And remember: Go kit services can support multiple transports simultaneously. 33 | -------------------------------------------------------------------------------- /util/README.md: -------------------------------------------------------------------------------- 1 | # util 2 | 3 | This directory holds packages of general utility to multiple consumers within Go kit, 4 | and potentially other consumers in the wider Go ecosystem. 5 | There is no `package util` and will never be. 6 | -------------------------------------------------------------------------------- /util/conn/doc.go: -------------------------------------------------------------------------------- 1 | // Package conn provides utilities related to connections. 2 | package conn 3 | --------------------------------------------------------------------------------