Documentation ¶
Overview ¶
Package Rye is a simple library to support http services. Rye provides a middleware handler which can be used to chain http handlers together while providing simple statsd metrics for use with a monitoring solution such as DataDog or other logging aggregators. Rye also provides some additional middleware handlers that are entirely optional but easily consumed using Rye.
Setup ¶
In order to use rye, you should vendor it and the statsd client within your project.
govendor fetch github.com/cactus/go-statsd-client/statsd # Rye is a private repo, so we should clone it first mkdir -p $GOPATH/github.com/InVisionApp cd $GOPATH/github.com/InVisionApp git clone git@github.com:InVisionApp/rye.git govendor add github.com/InVisionApp/rye
Writing custom middleware handlers ¶
Begin by importing the required libraries:
import ( "github.com/cactus/go-statsd-client/statsd" "github.com/InVisionApp/rye" )
Create a statsd client (if desired) and create a rye Config in order to pass in optional dependencies:
config := &rye.Config{ Statter: statsdClient, StatRate: DEFAULT_STATSD_RATE, }
Create a middleware handler. The purpose of the Handler is to keep Config and to provide an interface for chaining http handlers.
middlewareHandler := rye.NewMWHandler(config)
Build your http handlers using the Handler type from the **rye** package.
type Handler func(w http.ResponseWriter, r *http.Request) *rye.Response
Here are some example (custom) handlers:
func homeHandler(rw http.ResponseWriter, r *http.Request) *rye.Response { fmt.Fprint(rw, "Refer to README.md for auth-api API usage") return nil } func middlewareFirstHandler(rw http.ResponseWriter, r *http.Request) *rye.Response { fmt.Fprint(rw, "This handler fires first.") return nil } func errorHandler(rw http.ResponseWriter, r *http.Request) *rye.Response { return &rye.Response { StatusCode: http.StatusInternalServerError, Err: errors.New(message), } }
Finally, to setup your handlers in your API
routes := mux.NewRouter().StrictSlash(true) routes.Handle("/", middlewareHandler.Handle( []rye.Handler{ a.middlewareFirstHandler, a.homeHandler, })).Methods("GET") log.Infof("API server listening on %v", ListenAddress) srv := &http.Server{ Addr: ListenAddress, Handler: routes, } srv.ListenAndServe()
Statsd Generated by Rye ¶
Rye comes with built-in configurable `statsd` statistics that you could record to your favorite monitoring system. To configure that, you'll need to set up a `Statter` based on the `github.com/cactus/go-statsd-client` and set it in your instantiation of `MWHandler` through the `rye.Config`.
When a middleware is called, it's timing is recorded and a counter is recorded associated directly with the http status code returned during the call. Additionally, an `errors` counter is also sent to the statter which allows you to count any errors that occur with a code equaling or above 500.
Example: If you have a middleware handler you've created with a method named `loginHandler`, successful calls to that will be recorded to `handlers.loginHandler.2xx`. Additionally you'll receive stats such as `handlers.loginHandler.400` or `handlers.loginHandler.500`. You also will receive an increase in the `errors` count.
If you're sending your logs into a system such as DataDog, be aware that your stats from Rye can have prefixes such as `statsd.my-service.my-k8s-cluster.handlers.loginHandler.2xx` or even `statsd.my-service.my-k8s-cluster.errors`. Just keep in mind your stats could end up in the destination sink system with prefixes.
Using With Golang Context ¶
With Golang 1.7, a new feature has been added that supports a request specific context. This is a great feature that Rye supports out-of-the-box. The tricky part of this is how the context is modified on the request. In Golang, the Context is always available on a Request through `http.Request.Context()`. Great! However, if you want to add key/value pairs to the context, you will have to add the context to the request before it gets passed to the next Middleware. To support this, the `rye.Response` has a property called `Context`. This property takes a properly created context (pulled from the `request.Context()` function. When you return a `rye.Response` which has `Context`, the **rye** library will craft a new Request and make sure that the next middleware receives that request.
Here's the details of creating a middleware with a proper `Context`. You must first pull from the current request `Context`. In the example below, you see `ctx := r.Context()`. That pulls the current context. Then, you create a NEW context with your additional context key/value. Finally, you return `&rye.Response{Context:ctx}`
func addContextVar(rw http.ResponseWriter, r *http.Request) *rye.Response { // Retrieve the request's context ctx := r.Context() // Create a NEW context ctx = context.WithValue(ctx,"CONTEXT_KEY","my context value") // Return that in the Rye response // Rye will add it to the Request to // pass to the next middleware return &rye.Response{Context:ctx} }
Now in a later middleware, you can easily retrieve the value you set!
func getContextVar(rw http.ResponseWriter, r *http.Request) *rye.Response { // Retrieving the value is easy! myVal := r.Context().Value("CONTEXT_KEY") // Log it to the server log? log.Infof("Context Value: %v", myVal) return nil }
For another simple example, look in the JWT middleware - it adds the JWT into the context for use by other middlewares. It uses the `CONTEXT_JWT` key to push the JWT token into the `Context`.
Using built-in middleware handlers ¶
Rye comes with various pre-built middleware handlers. Pre-built middlewares source (and docs) can be found in the package dir following the pattern `middleware_*.go`.
To use them, specify the constructor of the middleware as one of the middleware handlers when you define your routes:
// example routes.Handle("/", middlewareHandler.Handle( []rye.Handler{ rye.MiddlewareCORS(), // to use the CORS middleware (with defaults) a.homeHandler, })).Methods("GET")
OR
routes.Handle("/", middlewareHandler.Handle( []rye.Handler{ rye.NewMiddlewareCORS("*", "GET, POST", "X-Access-Token"), // to use specific config when instantiating the middleware handler a.homeHandler, })).Methods("GET")
A Note on the JWT Middleware ¶
The JWT Middleware pushes the JWT token onto the Context for use by other middlewares in the chain. This is a convenience that allows any part of your middleware chain quick access to the JWT. Example usage might include a middleware that needs access to your user id or email address stored in the JWT. To access this `Context` variable, the code is very simple:
func getJWTfromContext(rw http.ResponseWriter, r *http.Request) *rye.Response { // Retrieving the value is easy! // Just reference the rye.CONTEXT_JWT const as a key myVal := r.Context().Value(rye.CONTEXT_JWT) // Log it to the server log? log.Infof("Context Value: %v", myVal) return nil }
Example (Basic) ¶
package main import ( "context" "errors" "fmt" "net/http" "github.com/InVisionApp/rye" "github.com/cactus/go-statsd-client/statsd" "github.com/gorilla/mux" log "github.com/sirupsen/logrus" ) func main() { statsdClient, err := statsd.NewBufferedClient("localhost:12345", "my_service", 1.0, 0) if err != nil { log.Fatalf("Unable to instantiate statsd client: %v", err.Error()) } config := rye.Config{ Statter: statsdClient, StatRate: 1.0, } middlewareHandler := rye.NewMWHandler(config) middlewareHandler.Use(beforeAllHandler) routes := mux.NewRouter().StrictSlash(true) routes.Handle("/", middlewareHandler.Handle([]rye.Handler{ middlewareFirstHandler, homeHandler, })).Methods("GET") // If you perform a `curl -i http://localhost:8181/cors -H "Origin: *.foo.com"` // you will see that the CORS middleware is adding required headers routes.Handle("/cors", middlewareHandler.Handle([]rye.Handler{ rye.MiddlewareCORS(), homeHandler, })).Methods("GET", "OPTIONS") // If you perform an `curl -i http://localhost:8181/jwt \ // -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" // you will see that we are allowed through to the handler, if the sample token is changed, we will get a 401 routes.Handle("/jwt", middlewareHandler.Handle([]rye.Handler{ rye.NewMiddlewareJWT("secret"), getJwtFromContextHandler, })).Methods("GET") routes.Handle("/error", middlewareHandler.Handle([]rye.Handler{ middlewareFirstHandler, errorHandler, homeHandler, })).Methods("GET") // In order to pass in a context variable, this set of // handlers works with "ctx" on the query string routes.Handle("/context", middlewareHandler.Handle( []rye.Handler{ stashContextHandler, logContextHandler, })).Methods("GET") log.Infof("API server listening on %v", "localhost:8181") srv := &http.Server{ Addr: "localhost:8181", Handler: routes, } srv.ListenAndServe() } func beforeAllHandler(rw http.ResponseWriter, r *http.Request) *rye.Response { log.Infof("This handler is called before every endpoint: %+v", r) return nil } func homeHandler(rw http.ResponseWriter, r *http.Request) *rye.Response { log.Infof("Home handler has fired!") fmt.Fprint(rw, "This is the home handler") return nil } func middlewareFirstHandler(rw http.ResponseWriter, r *http.Request) *rye.Response { log.Infof("Middleware handler has fired!") return nil } func errorHandler(rw http.ResponseWriter, r *http.Request) *rye.Response { log.Infof("Error handler has fired!") message := "This is the error handler" return &rye.Response{ StatusCode: http.StatusInternalServerError, Err: errors.New(message), } } func stashContextHandler(rw http.ResponseWriter, r *http.Request) *rye.Response { log.Infof("Stash Context handler has fired!") // Retrieve the request's context ctx := r.Context() // A query string value to add to the context toContext := r.URL.Query().Get("ctx") if toContext != "" { log.Infof("Adding `query-string-ctx` to request.Context(). Val: %v", toContext) } else { log.Infof("Adding default `query-string-ctx` value to context") toContext = "No value added. Add querystring param `ctx` with a value to get it mirrored through context." } // Create a NEW context ctx = context.WithValue(ctx, "query-string-ctx", toContext) // Return that in the Rye response // Rye will add it to the Request to // pass to the next middleware return &rye.Response{Context: ctx} } func logContextHandler(rw http.ResponseWriter, r *http.Request) *rye.Response { log.Infof("Log Context handler has fired!") // Retrieving a context value is EASY in subsequent middlewares fromContext := r.Context().Value("query-string-ctx") // Reflect that on the http response fmt.Fprintf(rw, "Here's the `ctx` query string value you passed. Pulled from context: %v", fromContext) return nil } // This handler pulls the JWT from the Context and echoes it through the request func getJwtFromContextHandler(rw http.ResponseWriter, r *http.Request) *rye.Response { log.Infof("Log Context handler has fired!") jwt := r.Context().Value(rye.CONTEXT_JWT) if jwt != nil { fmt.Fprintf(rw, "JWT found in Context: %v", jwt) } return nil }
Output:
Index ¶
- Constants
- func MiddlewareCORS() func(rw http.ResponseWriter, req *http.Request) *Response
- func MiddlewareRouteLogger() func(rw http.ResponseWriter, req *http.Request) *Response
- func NewMiddlewareAccessQueryToken(queryParamName string, tokens []string) func(rw http.ResponseWriter, req *http.Request) *Response
- func NewMiddlewareAccessToken(headerName string, tokens []string) func(rw http.ResponseWriter, req *http.Request) *Response
- func NewMiddlewareAuth(authFunc AuthFunc) func(rw http.ResponseWriter, req *http.Request) *Response
- func NewMiddlewareCIDR(CIDRs []string) func(rw http.ResponseWriter, req *http.Request) *Response
- func NewMiddlewareCORS(origin, methods, headers string) func(rw http.ResponseWriter, req *http.Request) *Response
- func NewMiddlewareGetHeader(headerName, contextKey string) func(rw http.ResponseWriter, req *http.Request) *Response
- func NewMiddlewareJWT(secret string) func(rw http.ResponseWriter, req *http.Request) *Response
- func NewStaticFile(path string) func(rw http.ResponseWriter, req *http.Request) *Response
- func NewStaticFilesystem(path string, stripPrefix string) func(rw http.ResponseWriter, req *http.Request) *Response
- func WriteJSONResponse(rw http.ResponseWriter, statusCode int, content []byte)
- func WriteJSONStatus(rw http.ResponseWriter, status, message string, statusCode int)
- type AuthFunc
- type Config
- type CustomStatter
- type Handler
- type JSONStatus
- type MWHandler
- type Response
Examples ¶
Constants ¶
const ( // CORS Specific constants DEFAULT_CORS_ALLOW_ORIGIN = "*" DEFAULT_CORS_ALLOW_METHODS = "POST, GET, OPTIONS, PUT, DELETE" DEFAULT_CORS_ALLOW_HEADERS = "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, X-Access-Token" )
const AUTH_USERNAME_KEY = "request-username"
const (
CONTEXT_JWT = "rye-middlewarejwt-jwt"
)
Variables ¶
This section is empty.
Functions ¶
func MiddlewareCORS ¶
func MiddlewareCORS() func(rw http.ResponseWriter, req *http.Request) *Response
MiddlewareCORS is the struct to represent configuration of the CORS handler.
func MiddlewareRouteLogger ¶
func MiddlewareRouteLogger() func(rw http.ResponseWriter, req *http.Request) *Response
MiddlewareRouteLogger creates a new handler to provide simple logging output for the specific route. You can use this middleware by specifying `rye.MiddlewareRouteLogger` when defining your routes.
Example use case:
routes.Handle("/some/route", a.Dependencies.MWHandler.Handle( []rye.Handler{ rye.MiddlewareRouteLogger(), yourHandler, })).Methods("PUT", "OPTIONS")
func NewMiddlewareAccessQueryToken ¶ added in v1.0.1
func NewMiddlewareAccessQueryToken(queryParamName string, tokens []string) func(rw http.ResponseWriter, req *http.Request) *Response
NewMiddlewareAccessQueryToken creates a new handler to verify access tokens passed as a query parameter.
Example usage:
routes.Handle("/some/route", a.Dependencies.MWHandler.Handle( []rye.Handler{ rye.NewMiddlewareAccessQueryToken(queryParamName, []string{token1, token2}), yourHandler, })).Methods("POST")
func NewMiddlewareAccessToken ¶
func NewMiddlewareAccessToken(headerName string, tokens []string) func(rw http.ResponseWriter, req *http.Request) *Response
NewMiddlewareAccessToken creates a new handler to verify access tokens passed as a header.
Example usage:
routes.Handle("/some/route", a.Dependencies.MWHandler.Handle( []rye.Handler{ rye.NewMiddlewareAccessToken(tokenHeaderName, []string{token1, token2}), yourHandler, })).Methods("POST")
func NewMiddlewareAuth ¶ added in v1.0.6
func NewMiddlewareCIDR ¶
NewMiddlewareCIDR creates a new handler to verify incoming IPs against a set of CIDR Notation strings in a rye chain. For reference on CIDR notation see https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing
Example usage:
routes.Handle("/some/route", a.Dependencies.MWHandler.Handle( []rye.Handler{ rye.NewMiddlewareCIDR(CIDRs), // []string of allowed CIDRs yourHandler, })).Methods("POST")
func NewMiddlewareCORS ¶
func NewMiddlewareCORS(origin, methods, headers string) func(rw http.ResponseWriter, req *http.Request) *Response
NewMiddlewareCORS creates a new handler to support CORS functionality. You can use this middleware by specifying `rye.MiddlewareCORS()` or `rye.NewMiddlewareCORS(origin, methods, headers)` when defining your routes.
Default CORS Values:
DEFAULT_CORS_ALLOW_ORIGIN**: "*" DEFAULT_CORS_ALLOW_METHODS**: "POST, GET, OPTIONS, PUT, DELETE" DEFAULT_CORS_ALLOW_HEADERS**: "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, X-Access-Token"
If you are planning to use this in production - you should probably use this middleware *with* params.
Example use case:
routes.Handle("/some/route", a.Dependencies.MWHandler.Handle( []rye.Handler{ rye.MiddlewareCORS(), // use defaults for allowed origin, headers, methods yourHandler, })).Methods("PUT", "OPTIONS")
OR:
routes.Handle("/some/route", a.Dependencies.MWHandler.Handle( []rye.Handler{ rye.NewMiddlewareCORS("*", "POST, GET", "SomeHeader, AnotherHeader"), yourHandler, })).Methods("PUT", "OPTIONS")
func NewMiddlewareGetHeader ¶ added in v1.0.2
func NewMiddlewareGetHeader(headerName, contextKey string) func(rw http.ResponseWriter, req *http.Request) *Response
NewMiddlewareGetHeader creates a new handler to extract any header and save its value into the context.
headerName: the name of the header you want to extract contextKey: the value key that you would like to store this header under in the context
Example usage:
routes.Handle("/some/route", a.Dependencies.MWHandler.Handle( []rye.Handler{ rye.NewMiddlewareGetHeader(headerName, contextKey), yourHandler, })).Methods("POST")
func NewMiddlewareJWT ¶
This middleware is deprecated. Use NewMiddlewareAuth with NewJWTAuthFunc instead.
This remains here as a shim for backwards compatibility.
---------------------------------------------------------------------------
This middleware provides JWT verification functionality ¶
You can use this middleware by specifying `rye.NewMiddlewareJWT(shared_secret)` when defining your routes.
This middleware has no default version, it must be configured with a shared secret.
Example use case:
routes.Handle("/some/route", a.Dependencies.MWHandler.Handle( []rye.Handler{ rye.NewMiddlewareJWT("this is a big secret"), yourHandler, })).Methods("PUT", "OPTIONS")
Additionally, this middleware puts the JWT token into the context for use by other middlewares in your chain.
Access to that is simple (using the CONTEXT_JWT constant as a key)
func getJWTfromContext(rw http.ResponseWriter, r *http.Request) *rye.Response { // Retrieving the value is easy! // Just reference the rye.CONTEXT_JWT const as a key myVal := r.Context().Value(rye.CONTEXT_JWT) // Log it to the server log? log.Infof("Context Value: %v", myVal) return nil }
func NewStaticFile ¶ added in v1.0.1
NewStaticFile creates a new handler to serve a file from a path on the local filesystem. The path should be an absolute path -> i.e., it's up to the program using Rye to correctly determine what path it should be serving from. An example is available in the `static_example.go` file which shows setting up a path relative to the go executable.
The purpose of this handler is to serve a specific file for any requests through the route handler. For instance, in the example below, any requests made to `/ui` will always be routed to /dist/index.html. This is important for single page applications which happen to use client-side routers. Therefore, you might have a webpack application with it's entrypoint `/dist/index.html`. That file may point at your `bundle.js`. Every request into the app will need to always be routed to `/dist/index.html`
Example use case:
routes.PathPrefix("/ui/").Handler(middlewareHandler.Handle([]rye.Handler{ rye.MiddlewareRouteLogger(), rye.NewStaticFile(pwd + "/dist/index.html"), }))
func NewStaticFilesystem ¶ added in v1.0.1
func NewStaticFilesystem(path string, stripPrefix string) func(rw http.ResponseWriter, req *http.Request) *Response
NewStaticFilesystem creates a new handler to serve a filesystem from a path on the local filesystem. The path should be an absolute path -> i.e., it's up to the program using Rye to correctly determine what path it should be serving from. An example is available in the `static_example.go` file which shows setting up a path relative to the go executable.
The primary benefit of this is to serve an entire set of files. You can pre-pend typical Rye middlewares to the chain. The static filesystem middleware should always be last in a chain, however. The `stripPrefix` allows you to ignore the prefix on requests so that the proper files will be matched.
Example use case:
routes.PathPrefix("/dist/").Handler(middlewareHandler.Handle([]rye.Handler{ rye.MiddlewareRouteLogger(), rye.NewStaticFilesystem(pwd+"/dist/", "/dist/"), }))
func WriteJSONResponse ¶
func WriteJSONResponse(rw http.ResponseWriter, statusCode int, content []byte)
WriteJSONResponse writes data and status code to the ResponseWriter
func WriteJSONStatus ¶
func WriteJSONStatus(rw http.ResponseWriter, status, message string, statusCode int)
WriteJSONStatus is a wrapper for WriteJSONResponse that returns a marshalled JSONStatus blob
Types ¶
type AuthFunc ¶ added in v1.0.6
func NewBasicAuthFunc ¶ added in v1.0.6
func NewJWTAuthFunc ¶ added in v1.0.6
type Config ¶
type Config struct { Statter statsd.Statter StatRate float32 // toggle types of stats sent NoErrStats bool NoDurationStats bool NoStatusCodeStats bool // Customer Statter for the client CustomStatter CustomStatter }
Config struct allows you to set a reference to a statsd.Statter and include it's stats rate.
type CustomStatter ¶ added in v1.0.8
type CustomStatter interface {
ReportStats(handlerName string, elapsedTime time.Duration, req *http.Request, resp *Response) error
}
CustomStatter allows the client to log any additional statsD metrics Rye computes around the request handler.
type Handler ¶
type Handler func(w http.ResponseWriter, r *http.Request) *Response
Handler is the primary type that any rye middleware must implement to be called in the Handle() function. In order to use this you must return a *rye.Response.
type JSONStatus ¶
JSONStatus is a simple container used for conveying status messages.
type MWHandler ¶
type MWHandler struct { Config Config // contains filtered or unexported fields }
MWHandler struct is used to configure and access rye's basic functionality.
func NewMWHandler ¶
Constructor for new instantiating new rye instances It returns a constructed *MWHandler instance.
Source Files ¶
Directories ¶
Path | Synopsis |
---|---|
fakes
|
|
statsdfakes
This file was generated by counterfeiter
|
This file was generated by counterfeiter |