Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move all HTTP code from Server to HTTPTransport, add Transport interface #8

Merged
merged 10 commits into from
Sep 24, 2018
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 18 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Kitty is a slightly opinionated framework based on go-kit.
It's goal is to ease development of microservices deployed on Kubernetes (or any similar orchestration platform).

Kitty has an opinion on:
* transports: only HTTP is supported (additional transports might be added),
* transports: HTTP only (additional transports can be added as long as they implement kitty.Transport),
* errors: an error may be Retryable (e.g. 5XX status codes) or not (e.g. 4XX status codes),
* status codes: unless specified, request decoding errors will generate 400 HTTP status codes.

Expand All @@ -30,11 +30,12 @@ Kitty includes 2 sub-packages:

Server-side
```
kitty.NewServer().Config(kitty.Config{HTTPPort: 8081}).
t := kitty.NewHTTPTransport(kitty.Config{HTTPPort: 8081}).
Router(gorilla.Router()).
HTTPEndpoint("POST", "/foo", Foo, kitty.Decoder(decodeFooRequest)).
HTTPEndpoint("GET", "/bar", Bar).
Run(ctx)
Endpoint("POST", "/foo", Foo, kitty.Decoder(decodeFooRequest)).
Endpoint("GET", "/bar", Bar)

kitty.NewServer(t).Run(ctx)

// Foo is a go-kit Endpoint
func Foo(ctx context.Context, request interface{}) (interface{}, error) {
Expand All @@ -54,31 +55,31 @@ func decodeFooRequest(ctx context.Context, r *http.Request) (interface{}, error)

Client-side (with circuit breaker & exponential backoff)
```
u, err := url.Parse("http://example.com/foo")
e := kitty.NewClient(
b-2-83 marked this conversation as resolved.
Show resolved Hide resolved
"POST",
"/foo",
httptransport.EncodeJSONRequest,
decodeFooResponse,
u,
kithttp.EncodeJSONRequest,
decodeFooResponse
).Endpoint()
cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{Name: "foo"})
e = kittycircuitbreaker.NewCircuitBreaker(cb)(e)
bo := backoff.NewExponentialBackOff()
e = kittybackoff.NewBackoff(bo)(e)

```

## How-to

### Log requests

```
kitty.NewServer().
// Log as JSON
Logger(log.NewJSONLogger(log.NewSyncWriter(os.Stdout))).
// Add path and method to all log lines
LogContext("http-path", "http-method").
// Log request only if an error occured
Middlewares(kitty.LogEndpoint(kitty.LogErrors))
kitty.NewServer(t).
// Log as JSON
Logger(log.NewJSONLogger(log.NewSyncWriter(os.Stdout))).
// Add path and method to all log lines
LogContext("http-path", "http-method").
// Log request only if an error occurred
Middlewares(kitty.LogEndpoint(kitty.LogErrors))
```

### Integrate with Istio
Expand All @@ -93,7 +94,7 @@ health := healthcheck.NewHandler()
health.AddLivenessCheck("goroutine-threshold", healthcheck.GoroutineCountCheck(100))
health.AddReadinessCheck("database", healthcheck.DatabasePingCheck(db, 1*time.Second))

kitty.NewServer().Liveness(health.LiveEndpoint).Readiness(health.ReadyEndpoint)
t := kitty.NewTransport(kitty.Config{}).Liveness(health.LiveEndpoint).Readiness(health.ReadyEndpoint)
```

## Requirements
Expand Down
2 changes: 1 addition & 1 deletion backoff/backoff.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
)

// NewBackoff creates an exponential backoff middleware, based on github.com/cenkalti/backoff.
// Retries will be attemped if the returned error implements is retryable (see kitty.IsRetryable).
// Retries will be attempted if the returned error implements is retryable (see kitty.IsRetryable).
func NewBackoff(bo backoff.BackOff) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, finalerr error) {
Expand Down
8 changes: 4 additions & 4 deletions backoff/backoff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import (

type retryableError struct{}

func (_ *retryableError) Error() string { return "error" }
func (_ *retryableError) Retryable() bool { return true }
func (*retryableError) Error() string { return "error" }
func (*retryableError) Retryable() bool { return true }

type nonRetryableError struct{}

func (_ *nonRetryableError) Error() string { return "error" }
func (_ *nonRetryableError) Retryable() bool { return false }
func (*nonRetryableError) Error() string { return "error" }
func (*nonRetryableError) Retryable() bool { return false }

func TestBackoff(t *testing.T) {
bo := backoff.NewExponentialBackOff()
Expand Down
8 changes: 4 additions & 4 deletions circuitbreaker/circuitbreaker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import (

type retryableError struct{}

func (_ *retryableError) Error() string { return "error" }
func (_ *retryableError) Retryable() bool { return true }
func (*retryableError) Error() string { return "error" }
func (*retryableError) Retryable() bool { return true }

type nonRetryableError struct{}

func (_ *nonRetryableError) Error() string { return "error" }
func (_ *nonRetryableError) Retryable() bool { return false }
func (*nonRetryableError) Error() string { return "error" }
func (*nonRetryableError) Retryable() bool { return false }

func TestCircuitBreaker(t *testing.T) {
{
Expand Down
14 changes: 7 additions & 7 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"net/http"
"net/url"

httptransport "github.com/go-kit/kit/transport/http"
kithttp "github.com/go-kit/kit/transport/http"
)

// Client is a wrapper above the go-kit http client.
Expand All @@ -14,22 +14,22 @@ import (
// as the status code returned by a go-kit HTTP endpoint.
// When using the backoff middleware, only 429 & 5XX errors trigger a retry.
type Client struct {
*httptransport.Client
*kithttp.Client
}

// NewClient creates a kitty client.
func NewClient(
method string,
tgt *url.URL,
enc httptransport.EncodeRequestFunc,
dec httptransport.DecodeResponseFunc,
options ...httptransport.ClientOption,
enc kithttp.EncodeRequestFunc,
dec kithttp.DecodeResponseFunc,
options ...kithttp.ClientOption,
) *Client {
return &Client{Client: httptransport.NewClient(method, tgt, enc, makeDecodeResponseFunc(dec), options...)}
return &Client{Client: kithttp.NewClient(method, tgt, enc, makeDecodeResponseFunc(dec), options...)}
}

// makeDecodeResponseFunc maps HTTP errors to Go errors.
func makeDecodeResponseFunc(fn httptransport.DecodeResponseFunc) httptransport.DecodeResponseFunc {
func makeDecodeResponseFunc(fn kithttp.DecodeResponseFunc) kithttp.DecodeResponseFunc {
return func(ctx context.Context, resp *http.Response) (interface{}, error) {
if err := HTTPError(resp); err != nil {
return nil, err
Expand Down
4 changes: 2 additions & 2 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"reflect"
"testing"

httptransport "github.com/go-kit/kit/transport/http"
kithttp "github.com/go-kit/kit/transport/http"
)

func TestClient(t *testing.T) {
Expand All @@ -19,7 +19,7 @@ func TestClient(t *testing.T) {

client := ts.Client()
u, _ := url.Parse(ts.URL)
e := NewClient("GET", u, httptransport.EncodeJSONRequest, decodeTestResponse, httptransport.SetClient(client)).Endpoint()
e := NewClient("GET", u, kithttp.EncodeJSONRequest, decodeTestResponse, kithttp.SetClient(client)).Endpoint()
h.statuses = []int{http.StatusServiceUnavailable}
_, err := e(context.TODO(), nil)
if err == nil {
Expand Down
19 changes: 2 additions & 17 deletions config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package kitty

// Config holds configuration info for a kitty.Server.
// Config holds configuration info for kitty.HTTPTransport.
type Config struct {
// LivenessCheckPath is the path of the health handler (default: "/alivez").
LivenessCheckPath string
Expand All @@ -12,22 +12,7 @@ type Config struct {
EnablePProf bool
}

// Config defines the configuration to be used by a kitty server.
func (s *Server) Config(cfg Config) *Server {
if cfg.HTTPPort > 0 {
s.cfg.HTTPPort = cfg.HTTPPort
}
if cfg.LivenessCheckPath != "" {
s.cfg.LivenessCheckPath = cfg.LivenessCheckPath
}
if cfg.ReadinessCheckPath != "" {
s.cfg.ReadinessCheckPath = cfg.ReadinessCheckPath
}
s.cfg.EnablePProf = cfg.EnablePProf
return s
}

// DefaultConfig defines the default config of a kitty.Server.
// DefaultConfig defines the default config of kitty.HTTPTransport.
var DefaultConfig = Config{
HTTPPort: 8080,
LivenessCheckPath: "/alivez",
Expand Down
5 changes: 3 additions & 2 deletions doc.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// Kitty is a slightly opinionated framework based on go-kit.
// Package kitty is a slightly opinionated framework based on go-kit.
// It's goal is to ease development of services deployed on Kubernetes (or any similar orchestration platform).
//
// Kitty has an opinion on:
//
// * transports: only HTTP is supported.
// * transports: HTTP only (additional transports can be added as long as they implement kitty.Transport),
// * errors: an error may be Retryable (e.g. 5XX status codes) or not (e.g. 4XX status codes).
//
// Kitty has no opinion on:
//
Expand Down
37 changes: 18 additions & 19 deletions endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,54 @@ import (
"net/http"

"github.com/go-kit/kit/endpoint"
httptransport "github.com/go-kit/kit/transport/http"
kithttp "github.com/go-kit/kit/transport/http"
)

// httpendpoint encapsulates everything required to build
// an endpoint hosted on a kit server.
type httpendpoint struct {
method, path string
endpoint endpoint.Endpoint
decoder httptransport.DecodeRequestFunc
encoder httptransport.EncodeResponseFunc
options []httptransport.ServerOption
decoder kithttp.DecodeRequestFunc
encoder kithttp.EncodeResponseFunc
options []kithttp.ServerOption
}

// HTTPEndpointOption is an option for an HTTP endpoint
type HTTPEndpointOption func(*httpendpoint) *httpendpoint

// HTTPEndpoint registers an endpoint to a kitty.Server.
// Unless specified, the endpoint will use the POST method,
// NopRequestDecoder will decode the request (and do nothing),
// Endpoint registers an endpoint to a kitty.HTTPTransport.
// Unless specified, NopRequestDecoder will decode the request (and do nothing),
// and EncodeJSONResponse will encode the response.
func (s *Server) HTTPEndpoint(method, path string, ep endpoint.Endpoint, opts ...HTTPEndpointOption) *Server {
func (t *HTTPTransport) Endpoint(method, path string, ep endpoint.Endpoint, opts ...HTTPEndpointOption) *HTTPTransport {
e := &httpendpoint{
method: method,
path: path,
endpoint: ep,
decoder: httptransport.NopRequestDecoder,
encoder: httptransport.EncodeJSONResponse,
decoder: kithttp.NopRequestDecoder,
encoder: kithttp.EncodeJSONResponse,
}
for _, opt := range opts {
e = opt(e)
}
s.endpoints = append(s.endpoints, e)
return s
t.endpoints = append(t.endpoints, e)
return t
}

type decoderError struct {
error
}

func (e decoderError) StatusCode() int {
if err, ok := e.error.(httptransport.StatusCoder); ok {
if err, ok := e.error.(kithttp.StatusCoder); ok {
return err.StatusCode()
}
return http.StatusBadRequest
}

// Decoder defines the request decoder for an endpoint.
// Decoder defines the request decoder for a HTTP endpoint.
// If none is provided, NopRequestDecoder is used.
func Decoder(dec httptransport.DecodeRequestFunc) HTTPEndpointOption {
func Decoder(dec kithttp.DecodeRequestFunc) HTTPEndpointOption {
return func(e *httpendpoint) *httpendpoint {
e.decoder = func(ctx context.Context, r *http.Request) (interface{}, error) {
request, err := dec(ctx, r)
Expand All @@ -66,17 +65,17 @@ func Decoder(dec httptransport.DecodeRequestFunc) HTTPEndpointOption {
}
}

// Encoder defines the response encoder for an endpoint.
// Encoder defines the response encoder for a HTTP endpoint.
// If none is provided, EncodeJSONResponse is used.
func Encoder(enc httptransport.EncodeResponseFunc) HTTPEndpointOption {
func Encoder(enc kithttp.EncodeResponseFunc) HTTPEndpointOption {
return func(e *httpendpoint) *httpendpoint {
e.encoder = enc
return e
}
}

// ServerOptions defines a liste of go-kit ServerOption to be used by the endpoint.
func ServerOptions(opts ...httptransport.ServerOption) HTTPEndpointOption {
// ServerOptions defines a liste of go-kit ServerOption to be used by a HTTP endpoint.
func ServerOptions(opts ...kithttp.ServerOption) HTTPEndpointOption {
return func(e *httpendpoint) *httpendpoint {
e.options = opts
return e
Expand Down
4 changes: 2 additions & 2 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ type retryableError struct {
error
}

func (_ retryableError) Retryable() bool {
func (retryableError) Retryable() bool {
return true
}

var _ error = retryableError{}
var _ Retryabler = retryableError{}

// Retryable defines an error as retryable.
// Retryable defines any error as retryable.
func Retryable(err error) error {
return retryableError{error: err}
}
Expand Down
7 changes: 3 additions & 4 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ func ExampleServer() {
}
return request, nil
}

kitty.NewServer().Config(kitty.Config{HTTPPort: 8081}).
t := kitty.NewHTTPTransport(kitty.Config{HTTPPort: 8081}).
Router(gorilla.Router()).
HTTPEndpoint("POST", "/foo", foo, kitty.Decoder(decodeFooRequest)).
Run(context.Background())
Endpoint("POST", "/foo", foo, kitty.Decoder(decodeFooRequest))
kitty.NewServer(t).Run(context.Background())
}
Loading