diff --git a/examples/profilesvc/client/client.go b/examples/profilesvc/client/client.go new file mode 100644 index 0000000..6b1dff0 --- /dev/null +++ b/examples/profilesvc/client/client.go @@ -0,0 +1,120 @@ +// Package client provides a profilesvc client based on a predefined Consul +// service name and relevant tags. Users must only provide the address of a +// Consul server. +package client + +import ( + "io" + "time" + + consulapi "github.com/hashicorp/consul/api" + + "github.com/go-kit/kit/endpoint" + "github.com/go-kit/kit/examples/profilesvc" + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/sd" + "github.com/go-kit/kit/sd/consul" + "github.com/go-kit/kit/sd/lb" +) + +// New returns a service that's load-balanced over instances of profilesvc found +// in the provided Consul server. The mechanism of looking up profilesvc +// instances in Consul is hard-coded into the client. +func New(consulAddr string, logger log.Logger) (profilesvc.Service, error) { + apiclient, err := consulapi.NewClient(&consulapi.Config{ + Address: consulAddr, + }) + if err != nil { + return nil, err + } + + // As the implementer of profilesvc, we declare and enforce these + // parameters for all of the profilesvc consumers. + var ( + consulService = "profilesvc" + consulTags = []string{"prod"} + passingOnly = true + retryMax = 3 + retryTimeout = 500 * time.Millisecond + ) + + var ( + sdclient = consul.NewClient(apiclient) + endpoints profilesvc.Endpoints + ) + { + factory := factoryFor(profilesvc.MakePostProfileEndpoint) + subscriber := consul.NewSubscriber(sdclient, factory, logger, consulService, consulTags, passingOnly) + balancer := lb.NewRoundRobin(subscriber) + retry := lb.Retry(retryMax, retryTimeout, balancer) + endpoints.PostProfileEndpoint = retry + } + { + factory := factoryFor(profilesvc.MakeGetProfileEndpoint) + subscriber := consul.NewSubscriber(sdclient, factory, logger, consulService, consulTags, passingOnly) + balancer := lb.NewRoundRobin(subscriber) + retry := lb.Retry(retryMax, retryTimeout, balancer) + endpoints.GetProfileEndpoint = retry + } + { + factory := factoryFor(profilesvc.MakePutProfileEndpoint) + subscriber := consul.NewSubscriber(sdclient, factory, logger, consulService, consulTags, passingOnly) + balancer := lb.NewRoundRobin(subscriber) + retry := lb.Retry(retryMax, retryTimeout, balancer) + endpoints.PutProfileEndpoint = retry + } + { + factory := factoryFor(profilesvc.MakePatchProfileEndpoint) + subscriber := consul.NewSubscriber(sdclient, factory, logger, consulService, consulTags, passingOnly) + balancer := lb.NewRoundRobin(subscriber) + retry := lb.Retry(retryMax, retryTimeout, balancer) + endpoints.PatchProfileEndpoint = retry + } + { + factory := factoryFor(profilesvc.MakeDeleteProfileEndpoint) + subscriber := consul.NewSubscriber(sdclient, factory, logger, consulService, consulTags, passingOnly) + balancer := lb.NewRoundRobin(subscriber) + retry := lb.Retry(retryMax, retryTimeout, balancer) + endpoints.DeleteProfileEndpoint = retry + } + { + factory := factoryFor(profilesvc.MakeGetAddressesEndpoint) + subscriber := consul.NewSubscriber(sdclient, factory, logger, consulService, consulTags, passingOnly) + balancer := lb.NewRoundRobin(subscriber) + retry := lb.Retry(retryMax, retryTimeout, balancer) + endpoints.GetAddressesEndpoint = retry + } + { + factory := factoryFor(profilesvc.MakeGetAddressEndpoint) + subscriber := consul.NewSubscriber(sdclient, factory, logger, consulService, consulTags, passingOnly) + balancer := lb.NewRoundRobin(subscriber) + retry := lb.Retry(retryMax, retryTimeout, balancer) + endpoints.GetAddressEndpoint = retry + } + { + factory := factoryFor(profilesvc.MakePostAddressEndpoint) + subscriber := consul.NewSubscriber(sdclient, factory, logger, consulService, consulTags, passingOnly) + balancer := lb.NewRoundRobin(subscriber) + retry := lb.Retry(retryMax, retryTimeout, balancer) + endpoints.PostAddressEndpoint = retry + } + { + factory := factoryFor(profilesvc.MakeDeleteAddressEndpoint) + subscriber := consul.NewSubscriber(sdclient, factory, logger, consulService, consulTags, passingOnly) + balancer := lb.NewRoundRobin(subscriber) + retry := lb.Retry(retryMax, retryTimeout, balancer) + endpoints.DeleteAddressEndpoint = retry + } + + return endpoints, nil +} + +func factoryFor(makeEndpoint func(profilesvc.Service) endpoint.Endpoint) sd.Factory { + return func(instance string) (endpoint.Endpoint, io.Closer, error) { + service, err := profilesvc.MakeClientEndpoints(instance) + if err != nil { + return nil, nil, err + } + return makeEndpoint(service), nil, nil + } +} diff --git a/examples/profilesvc/cmd/profilesvc/main.go b/examples/profilesvc/cmd/profilesvc/main.go new file mode 100644 index 0000000..a340e69 --- /dev/null +++ b/examples/profilesvc/cmd/profilesvc/main.go @@ -0,0 +1,59 @@ +package main + +import ( + "flag" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + + "golang.org/x/net/context" + + "github.com/go-kit/kit/examples/profilesvc" + "github.com/go-kit/kit/log" +) + +func main() { + var ( + httpAddr = flag.String("http.addr", ":8080", "HTTP listen address") + ) + flag.Parse() + + var logger log.Logger + { + logger = log.NewLogfmtLogger(os.Stderr) + logger = log.NewContext(logger).With("ts", log.DefaultTimestampUTC) + logger = log.NewContext(logger).With("caller", log.DefaultCaller) + } + + var ctx context.Context + { + ctx = context.Background() + } + + var s profilesvc.Service + { + s = profilesvc.NewInmemService() + s = profilesvc.LoggingMiddleware(logger)(s) + } + + var h http.Handler + { + h = profilesvc.MakeHTTPHandler(ctx, s, log.NewContext(logger).With("component", "HTTP")) + } + + errs := make(chan error) + go func() { + c := make(chan os.Signal) + signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) + errs <- fmt.Errorf("%s", <-c) + }() + + go func() { + logger.Log("transport", "HTTP", "addr", *httpAddr) + errs <- http.ListenAndServe(*httpAddr, h) + }() + + logger.Log("exit", <-errs) +} diff --git a/examples/profilesvc/endpoints.go b/examples/profilesvc/endpoints.go index 062cf9a..6dd129f 100644 --- a/examples/profilesvc/endpoints.go +++ b/examples/profilesvc/endpoints.go @@ -1,58 +1,192 @@ -package main +package profilesvc import ( + "net/url" + "strings" + + "golang.org/x/net/context" + "github.com/go-kit/kit/endpoint" - "golang.org/x/net/context" + httptransport "github.com/go-kit/kit/transport/http" ) -type endpoints struct { - postProfileEndpoint endpoint.Endpoint - getProfileEndpoint endpoint.Endpoint - putProfileEndpoint endpoint.Endpoint - patchProfileEndpoint endpoint.Endpoint - deleteProfileEndpoint endpoint.Endpoint - getAddressesEndpoint endpoint.Endpoint - getAddressEndpoint endpoint.Endpoint - postAddressEndpoint endpoint.Endpoint - deleteAddressEndpoint endpoint.Endpoint -} - -func makeEndpoints(s ProfileService) endpoints { - return endpoints{ - postProfileEndpoint: makePostProfileEndpoint(s), - getProfileEndpoint: makeGetProfileEndpoint(s), - putProfileEndpoint: makePutProfileEndpoint(s), - patchProfileEndpoint: makePatchProfileEndpoint(s), - deleteProfileEndpoint: makeDeleteProfileEndpoint(s), - getAddressesEndpoint: makeGetAddressesEndpoint(s), - getAddressEndpoint: makeGetAddressEndpoint(s), - postAddressEndpoint: makePostAddressEndpoint(s), - deleteAddressEndpoint: makeDeleteAddressEndpoint(s), - } -} - -type postProfileRequest struct { - Profile Profile -} - -type postProfileResponse struct { - Err error `json:"err,omitempty"` -} - -func (r postProfileResponse) error() error { return r.Err } - -// Regarding errors returned from service (business logic) methods, we have two -// options. We could return the error via the endpoint itself. That makes -// certain things a little bit easier, like providing non-200 HTTP responses to -// the client. But Go kit assumes that endpoint errors are (or may be treated -// as) transport-domain errors. For example, an endpoint error will count -// against a circuit breaker error count. Therefore, it's almost certainly -// better to return service (business logic) errors in the response object. This -// means we have to do a bit more work in the HTTP response encoder to detect -// e.g. a not-found error and provide a proper HTTP status code. That work is -// done with the errorer interface, in transport.go. - -func makePostProfileEndpoint(s ProfileService) endpoint.Endpoint { +// Endpoints collects all of the endpoints that compose a profile service. It's +// meant to be used as a helper struct, to collect all of the endpoints into a +// single parameter. +// +// In a server, it's useful for functions that need to operate on a per-endpoint +// basis. For example, you might pass an Endpoints to a function that produces +// an http.Handler, with each method (endpoint) wired up to a specific path. (It +// is probably a mistake in design to invoke the Service methods on the +// Endpoints struct in a server.) +// +// In a client, it's useful to collect individually constructed endpoints into a +// single type that implements the Service interface. For example, you might +// construct individual endpoints using transport/http.NewClient, combine them +// into an Endpoints, and return it to the caller as a Service. +type Endpoints struct { + PostProfileEndpoint endpoint.Endpoint + GetProfileEndpoint endpoint.Endpoint + PutProfileEndpoint endpoint.Endpoint + PatchProfileEndpoint endpoint.Endpoint + DeleteProfileEndpoint endpoint.Endpoint + GetAddressesEndpoint endpoint.Endpoint + GetAddressEndpoint endpoint.Endpoint + PostAddressEndpoint endpoint.Endpoint + DeleteAddressEndpoint endpoint.Endpoint +} + +// MakeServerEndpoints returns an Endpoints struct where each endpoint invokes +// the corresponding method on the provided service. Useful in a profilesvc +// server. +func MakeServerEndpoints(s Service) Endpoints { + return Endpoints{ + PostProfileEndpoint: MakePostProfileEndpoint(s), + GetProfileEndpoint: MakeGetProfileEndpoint(s), + PutProfileEndpoint: MakePutProfileEndpoint(s), + PatchProfileEndpoint: MakePatchProfileEndpoint(s), + DeleteProfileEndpoint: MakeDeleteProfileEndpoint(s), + GetAddressesEndpoint: MakeGetAddressesEndpoint(s), + GetAddressEndpoint: MakeGetAddressEndpoint(s), + PostAddressEndpoint: MakePostAddressEndpoint(s), + DeleteAddressEndpoint: MakeDeleteAddressEndpoint(s), + } +} + +// MakeClientEndpoints returns an Endpoints struct where each endpoint invokes +// the corresponding method on the remote instance, via a transport/http.Client. +// Useful in a profilesvc client. +func MakeClientEndpoints(instance string) (Endpoints, error) { + if !strings.HasPrefix(instance, "http") { + instance = "http://" + instance + } + tgt, err := url.Parse(instance) + if err != nil { + return Endpoints{}, err + } + tgt.Path = "" + + options := []httptransport.ClientOption{} + + // Note that the request encoders need to modify the request URL, changing + // the path and method. That's fine: we simply need to provide specific + // encoders for each endpoint. + + return Endpoints{ + PostProfileEndpoint: httptransport.NewClient("POST", tgt, encodePostProfileRequest, decodePostProfileResponse, options...).Endpoint(), + GetProfileEndpoint: httptransport.NewClient("GET", tgt, encodeGetProfileRequest, decodeGetProfileResponse, options...).Endpoint(), + PutProfileEndpoint: httptransport.NewClient("PUT", tgt, encodePutProfileRequest, decodePutProfileResponse, options...).Endpoint(), + PatchProfileEndpoint: httptransport.NewClient("PATCH", tgt, encodePatchProfileRequest, decodePatchProfileResponse, options...).Endpoint(), + DeleteProfileEndpoint: httptransport.NewClient("DELETE", tgt, encodeDeleteProfileRequest, decodeDeleteProfileResponse, options...).Endpoint(), + GetAddressesEndpoint: httptransport.NewClient("GET", tgt, encodeGetAddressesRequest, decodeGetAddressesResponse, options...).Endpoint(), + GetAddressEndpoint: httptransport.NewClient("GET", tgt, encodeGetAddressRequest, decodeGetAddressResponse, options...).Endpoint(), + PostAddressEndpoint: httptransport.NewClient("POST", tgt, encodePostAddressRequest, decodePostAddressResponse, options...).Endpoint(), + DeleteAddressEndpoint: httptransport.NewClient("DELETE", tgt, encodeDeleteAddressRequest, decodeDeleteAddressResponse, options...).Endpoint(), + }, nil +} + +// PostProfile implements Service. Primarily useful in a client. +func (e Endpoints) PostProfile(ctx context.Context, p Profile) error { + request := postProfileRequest{Profile: p} + response, err := e.PostProfileEndpoint(ctx, request) + if err != nil { + return err + } + resp := response.(postProfileResponse) + return resp.Err +} + +// GetProfile implements Service. Primarily useful in a client. +func (e Endpoints) GetProfile(ctx context.Context, id string) (Profile, error) { + request := getProfileRequest{ID: id} + response, err := e.GetProfileEndpoint(ctx, request) + if err != nil { + return Profile{}, err + } + resp := response.(getProfileResponse) + return resp.Profile, resp.Err +} + +// PutProfile implements Service. Primarily useful in a client. +func (e Endpoints) PutProfile(ctx context.Context, id string, p Profile) error { + request := putProfileRequest{ID: id, Profile: p} + response, err := e.PutProfileEndpoint(ctx, request) + if err != nil { + return err + } + resp := response.(putProfileResponse) + return resp.Err +} + +// PatchProfile implements Service. Primarily useful in a client. +func (e Endpoints) PatchProfile(ctx context.Context, id string, p Profile) error { + request := patchProfileRequest{ID: id, Profile: p} + response, err := e.PatchProfileEndpoint(ctx, request) + if err != nil { + return err + } + resp := response.(patchProfileResponse) + return resp.Err +} + +// DeleteProfile implements Service. Primarily useful in a client. +func (e Endpoints) DeleteProfile(ctx context.Context, id string) error { + request := deleteProfileRequest{ID: id} + response, err := e.DeleteProfileEndpoint(ctx, request) + if err != nil { + return err + } + resp := response.(deleteProfileResponse) + return resp.Err +} + +// GetAddresses implements Service. Primarily useful in a client. +func (e Endpoints) GetAddresses(ctx context.Context, profileID string) ([]Address, error) { + request := getAddressesRequest{ProfileID: profileID} + response, err := e.GetAddressesEndpoint(ctx, request) + if err != nil { + return nil, err + } + resp := response.(getAddressesResponse) + return resp.Addresses, resp.Err +} + +// GetAddress implements Service. Primarily useful in a client. +func (e Endpoints) GetAddress(ctx context.Context, profileID string, addressID string) (Address, error) { + request := getAddressRequest{ProfileID: profileID, AddressID: addressID} + response, err := e.GetAddressEndpoint(ctx, request) + if err != nil { + return Address{}, err + } + resp := response.(getAddressResponse) + return resp.Address, resp.Err +} + +// PostAddress implements Service. Primarily useful in a client. +func (e Endpoints) PostAddress(ctx context.Context, profileID string, a Address) error { + request := postAddressRequest{ProfileID: profileID, Address: a} + response, err := e.PostAddressEndpoint(ctx, request) + if err != nil { + return err + } + resp := response.(postAddressResponse) + return resp.Err +} + +// DeleteAddress implements Service. Primarily useful in a client. +func (e Endpoints) DeleteAddress(ctx context.Context, profileID string, addressID string) error { + request := deleteAddressRequest{ProfileID: profileID, AddressID: addressID} + response, err := e.DeleteAddressEndpoint(ctx, request) + if err != nil { + return err + } + resp := response.(deleteAddressResponse) + return resp.Err +} + +// MakePostProfileEndpoint returns an endpoint via the passed service. +// Primarily useful in a server. +func MakePostProfileEndpoint(s Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (response interface{}, err error) { req := request.(postProfileRequest) e := s.PostProfile(ctx, req.Profile) @@ -60,6 +194,111 @@ } } +// MakeGetProfileEndpoint returns an endpoint via the passed service. +// Primarily useful in a server. +func MakeGetProfileEndpoint(s Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (response interface{}, err error) { + req := request.(getProfileRequest) + p, e := s.GetProfile(ctx, req.ID) + return getProfileResponse{Profile: p, Err: e}, nil + } +} + +// MakePutProfileEndpoint returns an endpoint via the passed service. +// Primarily useful in a server. +func MakePutProfileEndpoint(s Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (response interface{}, err error) { + req := request.(putProfileRequest) + e := s.PutProfile(ctx, req.ID, req.Profile) + return putProfileResponse{Err: e}, nil + } +} + +// MakePatchProfileEndpoint returns an endpoint via the passed service. +// Primarily useful in a server. +func MakePatchProfileEndpoint(s Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (response interface{}, err error) { + req := request.(patchProfileRequest) + e := s.PatchProfile(ctx, req.ID, req.Profile) + return patchProfileResponse{Err: e}, nil + } +} + +// MakeDeleteProfileEndpoint returns an endpoint via the passed service. +// Primarily useful in a server. +func MakeDeleteProfileEndpoint(s Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (response interface{}, err error) { + req := request.(deleteProfileRequest) + e := s.DeleteProfile(ctx, req.ID) + return deleteProfileResponse{Err: e}, nil + } +} + +// MakeGetAddressesEndpoint returns an endpoint via the passed service. +// Primarily useful in a server. +func MakeGetAddressesEndpoint(s Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (response interface{}, err error) { + req := request.(getAddressesRequest) + a, e := s.GetAddresses(ctx, req.ProfileID) + return getAddressesResponse{Addresses: a, Err: e}, nil + } +} + +// MakeGetAddressEndpoint returns an endpoint via the passed service. +// Primarily useful in a server. +func MakeGetAddressEndpoint(s Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (response interface{}, err error) { + req := request.(getAddressRequest) + a, e := s.GetAddress(ctx, req.ProfileID, req.AddressID) + return getAddressResponse{Address: a, Err: e}, nil + } +} + +// MakePostAddressEndpoint returns an endpoint via the passed service. +// Primarily useful in a server. +func MakePostAddressEndpoint(s Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (response interface{}, err error) { + req := request.(postAddressRequest) + e := s.PostAddress(ctx, req.ProfileID, req.Address) + return postAddressResponse{Err: e}, nil + } +} + +// MakeDeleteAddressEndpoint returns an endpoint via the passed service. +// Primarily useful in a server. +func MakeDeleteAddressEndpoint(s Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (response interface{}, err error) { + req := request.(deleteAddressRequest) + e := s.DeleteAddress(ctx, req.ProfileID, req.AddressID) + return deleteAddressResponse{Err: e}, nil + } +} + +// We have two options to return errors from the business logic. +// +// We could return the error via the endpoint itself. That makes certain things +// a little bit easier, like providing non-200 HTTP responses to the client. But +// Go kit assumes that endpoint errors are (or may be treated as) +// transport-domain errors. For example, an endpoint error will count against a +// circuit breaker error count. +// +// Therefore, it's often better to return service (business logic) errors in the +// response object. This means we have to do a bit more work in the HTTP +// response encoder to detect e.g. a not-found error and provide a proper HTTP +// status code. That work is done with the errorer interface, in transport.go. +// Response types that may contain business-logic errors implement that +// interface. + +type postProfileRequest struct { + Profile Profile +} + +type postProfileResponse struct { + Err error `json:"err,omitempty"` +} + +func (r postProfileResponse) error() error { return r.Err } + type getProfileRequest struct { ID string } @@ -71,14 +310,6 @@ func (r getProfileResponse) error() error { return r.Err } -func makeGetProfileEndpoint(s ProfileService) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (response interface{}, err error) { - req := request.(getProfileRequest) - p, e := s.GetProfile(ctx, req.ID) - return getProfileResponse{Profile: p, Err: e}, nil - } -} - type putProfileRequest struct { ID string Profile Profile @@ -90,14 +321,6 @@ func (r putProfileResponse) error() error { return nil } -func makePutProfileEndpoint(s ProfileService) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (response interface{}, err error) { - req := request.(putProfileRequest) - e := s.PutProfile(ctx, req.ID, req.Profile) - return putProfileResponse{Err: e}, nil - } -} - type patchProfileRequest struct { ID string Profile Profile @@ -109,14 +332,6 @@ func (r patchProfileResponse) error() error { return r.Err } -func makePatchProfileEndpoint(s ProfileService) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (response interface{}, err error) { - req := request.(patchProfileRequest) - e := s.PatchProfile(ctx, req.ID, req.Profile) - return patchProfileResponse{Err: e}, nil - } -} - type deleteProfileRequest struct { ID string } @@ -126,14 +341,6 @@ } func (r deleteProfileResponse) error() error { return r.Err } - -func makeDeleteProfileEndpoint(s ProfileService) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (response interface{}, err error) { - req := request.(deleteProfileRequest) - e := s.DeleteProfile(ctx, req.ID) - return deleteProfileResponse{Err: e}, nil - } -} type getAddressesRequest struct { ProfileID string @@ -146,14 +353,6 @@ func (r getAddressesResponse) error() error { return r.Err } -func makeGetAddressesEndpoint(s ProfileService) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (response interface{}, err error) { - req := request.(getAddressesRequest) - a, e := s.GetAddresses(ctx, req.ProfileID) - return getAddressesResponse{Addresses: a, Err: e}, nil - } -} - type getAddressRequest struct { ProfileID string AddressID string @@ -166,14 +365,6 @@ func (r getAddressResponse) error() error { return r.Err } -func makeGetAddressEndpoint(s ProfileService) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (response interface{}, err error) { - req := request.(getAddressRequest) - a, e := s.GetAddress(ctx, req.ProfileID, req.AddressID) - return getAddressResponse{Address: a, Err: e}, nil - } -} - type postAddressRequest struct { ProfileID string Address Address @@ -185,14 +376,6 @@ func (r postAddressResponse) error() error { return r.Err } -func makePostAddressEndpoint(s ProfileService) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (response interface{}, err error) { - req := request.(postAddressRequest) - e := s.PostAddress(ctx, req.ProfileID, req.Address) - return postAddressResponse{Err: e}, nil - } -} - type deleteAddressRequest struct { ProfileID string AddressID string @@ -203,11 +386,3 @@ } func (r deleteAddressResponse) error() error { return r.Err } - -func makeDeleteAddressEndpoint(s ProfileService) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (response interface{}, err error) { - req := request.(deleteAddressRequest) - e := s.DeleteAddress(ctx, req.ProfileID, req.AddressID) - return deleteAddressResponse{Err: e}, nil - } -} diff --git a/examples/profilesvc/main.go b/examples/profilesvc/main.go deleted file mode 100644 index 5dfc082..0000000 --- a/examples/profilesvc/main.go +++ /dev/null @@ -1,57 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "net/http" - "os" - "os/signal" - "syscall" - - "golang.org/x/net/context" - - "github.com/go-kit/kit/log" -) - -func main() { - var ( - httpAddr = flag.String("http.addr", ":8080", "HTTP listen address") - ) - flag.Parse() - - var logger log.Logger - { - logger = log.NewLogfmtLogger(os.Stderr) - logger = log.NewContext(logger).With("ts", log.DefaultTimestampUTC) - logger = log.NewContext(logger).With("caller", log.DefaultCaller) - } - - var ctx context.Context - { - ctx = context.Background() - } - - var s ProfileService - { - s = newInmemService() - s = loggingMiddleware{s, log.NewContext(logger).With("component", "svc")} - } - - var h http.Handler - { - h = makeHandler(ctx, s, log.NewContext(logger).With("component", "http")) - } - - errs := make(chan error, 2) - go func() { - logger.Log("transport", "http", "address", *httpAddr, "msg", "listening") - errs <- http.ListenAndServe(*httpAddr, h) - }() - go func() { - c := make(chan os.Signal) - signal.Notify(c, syscall.SIGINT) - errs <- fmt.Errorf("%s", <-c) - }() - - logger.Log("terminated", <-errs) -} diff --git a/examples/profilesvc/middlewares.go b/examples/profilesvc/middlewares.go index 698e268..76708e5 100644 --- a/examples/profilesvc/middlewares.go +++ b/examples/profilesvc/middlewares.go @@ -1,4 +1,4 @@ -package main +package profilesvc import ( "time" @@ -8,8 +8,20 @@ "github.com/go-kit/kit/log" ) +// Middleware describes a service (as opposed to endpoint) middleware. +type Middleware func(Service) Service + +func LoggingMiddleware(logger log.Logger) Middleware { + return func(next Service) Service { + return &loggingMiddleware{ + next: next, + logger: logger, + } + } +} + type loggingMiddleware struct { - next ProfileService + next Service logger log.Logger } diff --git a/examples/profilesvc/service.go b/examples/profilesvc/service.go index 9e12100..4ae6756 100644 --- a/examples/profilesvc/service.go +++ b/examples/profilesvc/service.go @@ -1,4 +1,4 @@ -package main +package profilesvc import ( "errors" @@ -7,8 +7,8 @@ "golang.org/x/net/context" ) -// ProfileService is a simple CRUD interface for user profiles. -type ProfileService interface { +// Service is a simple CRUD interface for user profiles. +type Service interface { PostProfile(ctx context.Context, p Profile) error GetProfile(ctx context.Context, id string) (Profile, error) PutProfile(ctx context.Context, id string, p Profile) error @@ -36,9 +36,9 @@ } var ( - errInconsistentIDs = errors.New("inconsistent IDs") - errAlreadyExists = errors.New("already exists") - errNotFound = errors.New("not found") + ErrInconsistentIDs = errors.New("inconsistent IDs") + ErrAlreadyExists = errors.New("already exists") + ErrNotFound = errors.New("not found") ) type inmemService struct { @@ -46,7 +46,7 @@ m map[string]Profile } -func newInmemService() ProfileService { +func NewInmemService() Service { return &inmemService{ m: map[string]Profile{}, } @@ -56,7 +56,7 @@ s.mtx.Lock() defer s.mtx.Unlock() if _, ok := s.m[p.ID]; ok { - return errAlreadyExists // POST = create, don't overwrite + return ErrAlreadyExists // POST = create, don't overwrite } s.m[p.ID] = p return nil @@ -67,14 +67,14 @@ defer s.mtx.RUnlock() p, ok := s.m[id] if !ok { - return Profile{}, errNotFound + return Profile{}, ErrNotFound } return p, nil } func (s *inmemService) PutProfile(ctx context.Context, id string, p Profile) error { if id != p.ID { - return errInconsistentIDs + return ErrInconsistentIDs } s.mtx.Lock() defer s.mtx.Unlock() @@ -84,7 +84,7 @@ func (s *inmemService) PatchProfile(ctx context.Context, id string, p Profile) error { if p.ID != "" && id != p.ID { - return errInconsistentIDs + return ErrInconsistentIDs } s.mtx.Lock() @@ -92,7 +92,7 @@ existing, ok := s.m[id] if !ok { - return errNotFound // PATCH = update existing, don't create + return ErrNotFound // PATCH = update existing, don't create } // We assume that it's not possible to PATCH the ID, and that it's not @@ -115,7 +115,7 @@ s.mtx.Lock() defer s.mtx.Unlock() if _, ok := s.m[id]; !ok { - return errNotFound + return ErrNotFound } delete(s.m, id) return nil @@ -126,7 +126,7 @@ defer s.mtx.RUnlock() p, ok := s.m[profileID] if !ok { - return []Address{}, errNotFound + return []Address{}, ErrNotFound } return p.Addresses, nil } @@ -136,14 +136,14 @@ defer s.mtx.RUnlock() p, ok := s.m[profileID] if !ok { - return Address{}, errNotFound + return Address{}, ErrNotFound } for _, address := range p.Addresses { if address.ID == addressID { return address, nil } } - return Address{}, errNotFound + return Address{}, ErrNotFound } func (s *inmemService) PostAddress(ctx context.Context, profileID string, a Address) error { @@ -151,11 +151,11 @@ defer s.mtx.Unlock() p, ok := s.m[profileID] if !ok { - return errNotFound + return ErrNotFound } for _, address := range p.Addresses { if address.ID == a.ID { - return errAlreadyExists + return ErrAlreadyExists } } p.Addresses = append(p.Addresses, a) @@ -168,7 +168,7 @@ defer s.mtx.Unlock() p, ok := s.m[profileID] if !ok { - return errNotFound + return ErrNotFound } newAddresses := make([]Address, 0, len(p.Addresses)) for _, address := range p.Addresses { @@ -178,7 +178,7 @@ newAddresses = append(newAddresses, address) } if len(newAddresses) == len(p.Addresses) { - return errNotFound + return ErrNotFound } p.Addresses = newAddresses s.m[profileID] = p diff --git a/examples/profilesvc/transport.go b/examples/profilesvc/transport.go index 0874a10..02d807c 100644 --- a/examples/profilesvc/transport.go +++ b/examples/profilesvc/transport.go @@ -1,28 +1,37 @@ -package main +package profilesvc + +// The profilesvc is just over HTTP, so we just have a single transport.go. import ( + "bytes" "encoding/json" "errors" - stdhttp "net/http" + "io/ioutil" + "net/http" "github.com/gorilla/mux" "golang.org/x/net/context" - kitlog "github.com/go-kit/kit/log" - kithttp "github.com/go-kit/kit/transport/http" + "net/url" + + "github.com/go-kit/kit/log" + httptransport "github.com/go-kit/kit/transport/http" ) var ( - errBadRouting = errors.New("inconsistent mapping between route and handler (programmer error)") + // ErrBadRouting is returned when an expected path variable is missing. + // It always indicates programmer error. + ErrBadRouting = errors.New("inconsistent mapping between route and handler (programmer error)") ) -func makeHandler(ctx context.Context, s ProfileService, logger kitlog.Logger) stdhttp.Handler { - e := makeEndpoints(s) +// MakeHTTPHandler mounts all of the service endpoints into an http.Handler. +// Useful in a profilesvc server. +func MakeHTTPHandler(ctx context.Context, s Service, logger log.Logger) http.Handler { r := mux.NewRouter() - - commonOptions := []kithttp.ServerOption{ - kithttp.ServerErrorLogger(logger), - kithttp.ServerErrorEncoder(encodeError), + e := MakeServerEndpoints(s) + options := []httptransport.ServerOption{ + httptransport.ServerErrorLogger(logger), + httptransport.ServerErrorEncoder(encodeError), } // POST /profiles adds another profile @@ -35,73 +44,73 @@ // POST /profiles/:id/addresses add a new address // DELETE /profiles/:id/addresses/:addressID remove an address - r.Methods("POST").Path("/profiles/").Handler(kithttp.NewServer( - ctx, - e.postProfileEndpoint, + r.Methods("POST").Path("/profiles/").Handler(httptransport.NewServer( + ctx, + e.PostProfileEndpoint, decodePostProfileRequest, encodeResponse, - commonOptions..., - )) - r.Methods("GET").Path("/profiles/{id}").Handler(kithttp.NewServer( - ctx, - e.getProfileEndpoint, + options..., + )) + r.Methods("GET").Path("/profiles/{id}").Handler(httptransport.NewServer( + ctx, + e.GetProfileEndpoint, decodeGetProfileRequest, encodeResponse, - commonOptions..., - )) - r.Methods("PUT").Path("/profiles/{id}").Handler(kithttp.NewServer( - ctx, - e.putProfileEndpoint, + options..., + )) + r.Methods("PUT").Path("/profiles/{id}").Handler(httptransport.NewServer( + ctx, + e.PutProfileEndpoint, decodePutProfileRequest, encodeResponse, - commonOptions..., - )) - r.Methods("PATCH").Path("/profiles/{id}").Handler(kithttp.NewServer( - ctx, - e.patchProfileEndpoint, + options..., + )) + r.Methods("PATCH").Path("/profiles/{id}").Handler(httptransport.NewServer( + ctx, + e.PatchProfileEndpoint, decodePatchProfileRequest, encodeResponse, - commonOptions..., - )) - r.Methods("DELETE").Path("/profiles/{id}").Handler(kithttp.NewServer( - ctx, - e.deleteProfileEndpoint, + options..., + )) + r.Methods("DELETE").Path("/profiles/{id}").Handler(httptransport.NewServer( + ctx, + e.DeleteProfileEndpoint, decodeDeleteProfileRequest, encodeResponse, - commonOptions..., - )) - r.Methods("GET").Path("/profiles/{id}/addresses/").Handler(kithttp.NewServer( - ctx, - e.getAddressesEndpoint, + options..., + )) + r.Methods("GET").Path("/profiles/{id}/addresses/").Handler(httptransport.NewServer( + ctx, + e.GetAddressesEndpoint, decodeGetAddressesRequest, encodeResponse, - commonOptions..., - )) - r.Methods("GET").Path("/profiles/{id}/addresses/{addressID}").Handler(kithttp.NewServer( - ctx, - e.getAddressEndpoint, + options..., + )) + r.Methods("GET").Path("/profiles/{id}/addresses/{addressID}").Handler(httptransport.NewServer( + ctx, + e.GetAddressEndpoint, decodeGetAddressRequest, encodeResponse, - commonOptions..., - )) - r.Methods("POST").Path("/profiles/{id}/addresses/").Handler(kithttp.NewServer( - ctx, - e.postAddressEndpoint, + options..., + )) + r.Methods("POST").Path("/profiles/{id}/addresses/").Handler(httptransport.NewServer( + ctx, + e.PostAddressEndpoint, decodePostAddressRequest, encodeResponse, - commonOptions..., - )) - r.Methods("DELETE").Path("/profiles/{id}/addresses/{addressID}").Handler(kithttp.NewServer( - ctx, - e.deleteAddressEndpoint, + options..., + )) + r.Methods("DELETE").Path("/profiles/{id}/addresses/{addressID}").Handler(httptransport.NewServer( + ctx, + e.DeleteAddressEndpoint, decodeDeleteAddressRequest, encodeResponse, - commonOptions..., + options..., )) return r } -func decodePostProfileRequest(_ context.Context, r *stdhttp.Request) (request interface{}, err error) { +func decodePostProfileRequest(_ context.Context, r *http.Request) (request interface{}, err error) { var req postProfileRequest if e := json.NewDecoder(r.Body).Decode(&req.Profile); e != nil { return nil, e @@ -109,20 +118,20 @@ return req, nil } -func decodeGetProfileRequest(_ context.Context, r *stdhttp.Request) (request interface{}, err error) { - vars := mux.Vars(r) - id, ok := vars["id"] - if !ok { - return nil, errBadRouting +func decodeGetProfileRequest(_ context.Context, r *http.Request) (request interface{}, err error) { + vars := mux.Vars(r) + id, ok := vars["id"] + if !ok { + return nil, ErrBadRouting } return getProfileRequest{ID: id}, nil } -func decodePutProfileRequest(_ context.Context, r *stdhttp.Request) (request interface{}, err error) { - vars := mux.Vars(r) - id, ok := vars["id"] - if !ok { - return nil, errBadRouting +func decodePutProfileRequest(_ context.Context, r *http.Request) (request interface{}, err error) { + vars := mux.Vars(r) + id, ok := vars["id"] + if !ok { + return nil, ErrBadRouting } var profile Profile if err := json.NewDecoder(r.Body).Decode(&profile); err != nil { @@ -134,11 +143,11 @@ }, nil } -func decodePatchProfileRequest(_ context.Context, r *stdhttp.Request) (request interface{}, err error) { - vars := mux.Vars(r) - id, ok := vars["id"] - if !ok { - return nil, errBadRouting +func decodePatchProfileRequest(_ context.Context, r *http.Request) (request interface{}, err error) { + vars := mux.Vars(r) + id, ok := vars["id"] + if !ok { + return nil, ErrBadRouting } var profile Profile if err := json.NewDecoder(r.Body).Decode(&profile); err != nil { @@ -150,33 +159,33 @@ }, nil } -func decodeDeleteProfileRequest(_ context.Context, r *stdhttp.Request) (request interface{}, err error) { - vars := mux.Vars(r) - id, ok := vars["id"] - if !ok { - return nil, errBadRouting +func decodeDeleteProfileRequest(_ context.Context, r *http.Request) (request interface{}, err error) { + vars := mux.Vars(r) + id, ok := vars["id"] + if !ok { + return nil, ErrBadRouting } return deleteProfileRequest{ID: id}, nil } -func decodeGetAddressesRequest(_ context.Context, r *stdhttp.Request) (request interface{}, err error) { - vars := mux.Vars(r) - id, ok := vars["id"] - if !ok { - return nil, errBadRouting +func decodeGetAddressesRequest(_ context.Context, r *http.Request) (request interface{}, err error) { + vars := mux.Vars(r) + id, ok := vars["id"] + if !ok { + return nil, ErrBadRouting } return getAddressesRequest{ProfileID: id}, nil } -func decodeGetAddressRequest(_ context.Context, r *stdhttp.Request) (request interface{}, err error) { - vars := mux.Vars(r) - id, ok := vars["id"] - if !ok { - return nil, errBadRouting +func decodeGetAddressRequest(_ context.Context, r *http.Request) (request interface{}, err error) { + vars := mux.Vars(r) + id, ok := vars["id"] + if !ok { + return nil, ErrBadRouting } addressID, ok := vars["addressID"] if !ok { - return nil, errBadRouting + return nil, ErrBadRouting } return getAddressRequest{ ProfileID: id, @@ -184,11 +193,11 @@ }, nil } -func decodePostAddressRequest(_ context.Context, r *stdhttp.Request) (request interface{}, err error) { - vars := mux.Vars(r) - id, ok := vars["id"] - if !ok { - return nil, errBadRouting +func decodePostAddressRequest(_ context.Context, r *http.Request) (request interface{}, err error) { + vars := mux.Vars(r) + id, ok := vars["id"] + if !ok { + return nil, ErrBadRouting } var address Address if err := json.NewDecoder(r.Body).Decode(&address); err != nil { @@ -200,15 +209,15 @@ }, nil } -func decodeDeleteAddressRequest(_ context.Context, r *stdhttp.Request) (request interface{}, err error) { - vars := mux.Vars(r) - id, ok := vars["id"] - if !ok { - return nil, errBadRouting +func decodeDeleteAddressRequest(_ context.Context, r *http.Request) (request interface{}, err error) { + vars := mux.Vars(r) + id, ok := vars["id"] + if !ok { + return nil, ErrBadRouting } addressID, ok := vars["addressID"] if !ok { - return nil, errBadRouting + return nil, ErrBadRouting } return deleteAddressRequest{ ProfileID: id, @@ -216,32 +225,163 @@ }, nil } -// errorer is implemented by all concrete response types. It allows us to -// change the HTTP response code without needing to trigger an endpoint -// (transport-level) error. For more information, read the big comment in -// endpoints.go. +func encodePostProfileRequest(ctx context.Context, req *http.Request, request interface{}) error { + // r.Methods("POST").Path("/profiles/") + req.Method, req.URL.Path = "POST", url.QueryEscape("/profiles/") + return encodeRequest(ctx, req, request) +} + +func encodeGetProfileRequest(ctx context.Context, req *http.Request, request interface{}) error { + // r.Methods("GET").Path("/profiles/{id}") + r := request.(getProfileRequest) + req.Method, req.URL.Path = "GET", url.QueryEscape("/profiles/"+r.ID) + return encodeRequest(ctx, req, request) +} + +func encodePutProfileRequest(ctx context.Context, req *http.Request, request interface{}) error { + // r.Methods("PUT").Path("/profiles/{id}") + r := request.(putProfileRequest) + req.Method, req.URL.Path = "PUT", url.QueryEscape("/profiles/"+r.ID) + return encodeRequest(ctx, req, request) +} + +func encodePatchProfileRequest(ctx context.Context, req *http.Request, request interface{}) error { + // r.Methods("PATCH").Path("/profiles/{id}") + r := request.(patchProfileRequest) + req.Method, req.URL.Path = "PATCH", url.QueryEscape("/profiles/"+r.ID) + return encodeRequest(ctx, req, request) +} + +func encodeDeleteProfileRequest(ctx context.Context, req *http.Request, request interface{}) error { + // r.Methods("DELETE").Path("/profiles/{id}") + r := request.(deleteProfileRequest) + req.Method, req.URL.Path = "DELETE", url.QueryEscape("/profiles/"+r.ID) + return encodeRequest(ctx, req, request) +} + +func encodeGetAddressesRequest(ctx context.Context, req *http.Request, request interface{}) error { + // r.Methods("GET").Path("/profiles/{id}/addresses/") + r := request.(getAddressesRequest) + req.Method, req.URL.Path = "GET", url.QueryEscape("/profiles/"+r.ProfileID+"/addresses/") + return encodeRequest(ctx, req, request) +} + +func encodeGetAddressRequest(ctx context.Context, req *http.Request, request interface{}) error { + // r.Methods("GET").Path("/profiles/{id}/addresses/{addressID}") + r := request.(getAddressRequest) + req.Method, req.URL.Path = "GET", url.QueryEscape("/profiles/"+r.ProfileID+"/addresses/"+r.AddressID) + return encodeRequest(ctx, req, request) +} + +func encodePostAddressRequest(ctx context.Context, req *http.Request, request interface{}) error { + // r.Methods("POST").Path("/profiles/{id}/addresses/") + r := request.(postAddressRequest) + req.Method, req.URL.Path = "POST", url.QueryEscape("/profiles/"+r.ProfileID+"/addresses/") + return encodeRequest(ctx, req, request) +} + +func encodeDeleteAddressRequest(ctx context.Context, req *http.Request, request interface{}) error { + // r.Methods("DELETE").Path("/profiles/{id}/addresses/{addressID}") + r := request.(deleteAddressRequest) + req.Method, req.URL.Path = "DELETE", url.QueryEscape("/profiles/"+r.ProfileID+"/addresses/"+r.AddressID) + return encodeRequest(ctx, req, request) +} + +func decodePostProfileResponse(_ context.Context, resp *http.Response) (interface{}, error) { + var response postProfileResponse + err := json.NewDecoder(resp.Body).Decode(&response) + return response, err +} + +func decodeGetProfileResponse(_ context.Context, resp *http.Response) (interface{}, error) { + var response getProfileResponse + err := json.NewDecoder(resp.Body).Decode(&response) + return response, err +} + +func decodePutProfileResponse(_ context.Context, resp *http.Response) (interface{}, error) { + var response putProfileResponse + err := json.NewDecoder(resp.Body).Decode(&response) + return response, err +} + +func decodePatchProfileResponse(_ context.Context, resp *http.Response) (interface{}, error) { + var response patchProfileResponse + err := json.NewDecoder(resp.Body).Decode(&response) + return response, err +} + +func decodeDeleteProfileResponse(_ context.Context, resp *http.Response) (interface{}, error) { + var response deleteProfileResponse + err := json.NewDecoder(resp.Body).Decode(&response) + return response, err +} + +func decodeGetAddressesResponse(_ context.Context, resp *http.Response) (interface{}, error) { + var response getAddressesResponse + err := json.NewDecoder(resp.Body).Decode(&response) + return response, err +} + +func decodeGetAddressResponse(_ context.Context, resp *http.Response) (interface{}, error) { + var response getAddressResponse + err := json.NewDecoder(resp.Body).Decode(&response) + return response, err +} + +func decodePostAddressResponse(_ context.Context, resp *http.Response) (interface{}, error) { + var response postAddressResponse + err := json.NewDecoder(resp.Body).Decode(&response) + return response, err +} + +func decodeDeleteAddressResponse(_ context.Context, resp *http.Response) (interface{}, error) { + var response deleteAddressResponse + err := json.NewDecoder(resp.Body).Decode(&response) + return response, err +} + +// errorer is implemented by all concrete response types that may contain +// errors. It allows us to change the HTTP response code without needing to +// trigger an endpoint (transport-level) error. For more information, read the +// big comment in endpoints.go. type errorer interface { error() error } // encodeResponse is the common method to encode all response types to the -// client. I chose to do it this way because I didn't know if something more -// specific was necessary. It's certainly possible to specialize on a -// per-response (per-method) basis. -func encodeResponse(ctx context.Context, w stdhttp.ResponseWriter, response interface{}) error { +// client. I chose to do it this way because, since we're using JSON, there's no +// reason to provide anything more specific. It's certainly possible to +// specialize on a per-response (per-method) basis. +func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error { if e, ok := response.(errorer); ok && e.error() != nil { // Not a Go kit transport error, but a business-logic error. // Provide those as HTTP errors. encodeError(ctx, e.error(), w) return nil } + w.Header().Set("Content-Type", "application/json; charset=utf-8") return json.NewEncoder(w).Encode(response) } -func encodeError(_ context.Context, err error, w stdhttp.ResponseWriter) { +// encodeRequest likewise JSON-encodes the request to the HTTP request body. +// Don't use it directly as a transport/http.Client EncodeRequestFunc: +// profilesvc endpoints require mutating the HTTP method and request path. +func encodeRequest(_ context.Context, req *http.Request, request interface{}) error { + var buf bytes.Buffer + err := json.NewEncoder(&buf).Encode(request) + if err != nil { + return err + } + req.Body = ioutil.NopCloser(&buf) + return nil +} + +func encodeError(_ context.Context, err error, w http.ResponseWriter) { if err == nil { panic("encodeError with nil error") } + w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(codeFrom(err)) json.NewEncoder(w).Encode(map[string]interface{}{ "error": err.Error(), @@ -250,21 +390,21 @@ func codeFrom(err error) int { switch err { - case errNotFound: - return stdhttp.StatusNotFound - case errAlreadyExists, errInconsistentIDs: - return stdhttp.StatusBadRequest + case ErrNotFound: + return http.StatusNotFound + case ErrAlreadyExists, ErrInconsistentIDs: + return http.StatusBadRequest default: - if e, ok := err.(kithttp.Error); ok { + if e, ok := err.(httptransport.Error); ok { switch e.Domain { - case kithttp.DomainDecode: - return stdhttp.StatusBadRequest - case kithttp.DomainDo: - return stdhttp.StatusServiceUnavailable + case httptransport.DomainDecode: + return http.StatusBadRequest + case httptransport.DomainDo: + return http.StatusServiceUnavailable default: - return stdhttp.StatusInternalServerError + return http.StatusInternalServerError } } - return stdhttp.StatusInternalServerError - } -} + return http.StatusInternalServerError + } +}