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 [email protected]: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
}
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 |