bastion

package module
v2.1.2+incompatible Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 6, 2018 License: MIT Imports: 20 Imported by: 8

README

Bastion

Documentation Coverage Status Go Report Card CircleCI

Defend your API from the sieges. Bastion offers an "augmented" Router instance.

It has the minimal necessary to create an API with default handlers and middleware that help you raise your API easy and fast. Allows to have commons handlers and middleware between projects with the need for each one to do so.

Examples

Table of contents

  1. Installation
  2. Router
  3. Middleware
  4. Register on shutdown
  5. Options
    1. Structure
    2. From optionals functions
    3. From options file
  6. Testing
  7. Render
  8. Logger

Installation

go get -u github.com/ifreddyrondon/bastion

Router

Bastion use go-chi router to modularize the applications. Each instance of Bastion, will have the possibility of mounting an api router, it will define the routes and middleware of the application with the app logic.

NewRouter

NewRouter return a router as a subrouter along a routing path.

It's very useful to split up a large API as many independent routers and compose them as a single service.

package main

import (
	"fmt"
	"net/http"

	"github.com/go-chi/chi"
	"github.com/ifreddyrondon/bastion"
)

type Handler struct{}

// Routes creates a REST router for the todos resource
func (h *Handler) Routes() http.Handler {
	r := bastion.NewRouter()

	r.Get("/", h.list)    // GET /todos - read a list of todos
	r.Post("/", h.create) // POST /todos - create a new todo and persist it
	r.Route("/{id}", func(r chi.Router) {
		r.Get("/", h.get)       // GET /todos/{id} - read a single todo by :id
		r.Put("/", h.update)    // PUT /todos/{id} - update a single todo by :id
		r.Delete("/", h.delete) // DELETE /todos/{id} - delete a single todo by :id
	})

	return r
}

func (h *Handler) list(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("todos list of stuff.."))
}

func (h *Handler) create(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("todos create"))
}

func (h *Handler) get(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")
	w.Write([]byte(fmt.Sprintf("get todo with id %v", id)))
}

func (h *Handler) update(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")
	w.Write([]byte(fmt.Sprintf("update todo with id %v", id)))
}

func (h *Handler) delete(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")
	w.Write([]byte(fmt.Sprintf("delete todo with id %v", id)))
}

func main() {
	app := bastion.New()
	app.APIRouter.Mount("/todo/", new(Handler).Routes())
	app.Serve()
}
Router example
package main

import (
    "net/http"

    "github.com/ifreddyrondon/bastion"
    "github.com/ifreddyrondon/bastion/render"
)

func handler(w http.ResponseWriter, _ *http.Request) {
    res := struct {Message string `json:"message"`}{Message: "world"}
    render.NewJSON().Send(w, res)
}

func main() {
    app := bastion.New()
    app.APIRouter.Get("/hello", handler)
    app.Serve()
}

Middleware

Bastion comes equipped with a set of commons middleware handlers, providing a suite of standard net/http middleware. They are just stdlib net/http middleware handlers. There is nothing special about them, which means the router and all the tooling is designed to be compatible and friendly with any middleware in the community.

Core middleware
Name Description
Logger Logs the start and end of each request with the elapsed processing time.
Recovery Gracefully absorb panics and prints the stack trace.
RequestID Injects a request ID into the context of each request.
APIErrHandler Intercept responses to verify if his status code is >= 500. If status is >= 500, it'll response with a default error. IT allows to response with the same error without disclosure internal information, also the real error is logged.
Auxiliary middleware
Name Description
Listing Parses the url from a request and stores a listing.Listing on the context, it can be accessed through middleware.GetListing.

For more references check chi middleware

Register on shutdown

You can register a function to call on shutdown. This can be used to gracefully shutdown connections. By default the shutdown execute the server shutdown.

Bastion listens if any SIGINT, SIGTERM or SIGKILL signal is emitted and performs a graceful shutdown.

It can be added with RegisterOnShutdown method of the bastion instance, it can accept variable number of functions.

Register on shutdown example
package main

import (
    "log"

    "github.com/ifreddyrondon/bastion"
)

func onShutdown() {
    log.Printf("My registered on shutdown. Doing something...")
}

func main() {
    app := bastion.New()
    app.RegisterOnShutdown(onShutdown)
    app.Serve()
}

Options

Options are used to define how the application should run.

Structure
// Options are used to define how the application should run.
type Options struct {
	// APIBasepath path where the bastion api router is going to be mounted. Default `/`.
	APIBasepath string `yaml:"apiBasepath"`
	// API500ErrMessage message returned to the user when catch a 500 status error.
	API500ErrMessage string `yaml:"api500ErrMessage"`
	// Addr bind address provided to http.Server. Default is "127.0.0.1:8080"
	// Can be set using ENV vars "ADDR" and "PORT".
	Addr string `yaml:"addr"`
	// Env "environment" in which the App is running. Default is "development".
	Env string `yaml:"env"`
	// NoPrettyLogging don't output a colored human readable version on the out writer.
	NoPrettyLogging bool `yaml:"noPrettyLogging"`
	// LoggerLevel defines log levels. Default is DebugLevel.
	LoggerLevel Level `yaml:"loggerLevel"`
	// LoggerOutput logger output writer. Default os.Stdout
	LoggerOutput io.Writer
}
APIBasepath

Api base path value is where the bastion api router is going to be mounted. Default /. It's JSON tagged as apiBasepath

When:

"apiBasepath": "/foo/test",
API500ErrMessage

Api 500 error message represent the message returned to the user when a http 500 error is caught by the APIErrHandler middleware. Default looks like something went wrong. It's JSON tagged as api500ErrMessage

When:

"api500ErrMessage": "looks like something went wrong",

Then: http://localhost:8080/foo/test

Addr

Addr is the bind address provided to http.Server. Default is 127.0.0.1:8080. Can be set using ENV vars ADDR and PORT. It's JSON tagged as addr

Env

Env is the "environment" in which the App is running. Default is "development". Can be set using ENV vars GO_ENV It's JSON tagged as env

NoPrettyLogging

NoPrettyLogging boolean flag to don't output a colored human readable version on the out writer. Default false. It's JSON tagged as noPrettyLogging

LoggerLevel

LoggerLevel defines log levels. Allows for logging at the following levels (from highest to lowest):

  • panic (bastion.PanicLevel, 5)
  • fatal (bastion.FatalLevel, 4)
  • error (bastion.ErrorLevel, 3)
  • warn (bastion.WarnLevel, 2)
  • info (bastion.InfoLevel, 1)
  • debug (bastion.DebugLevel, 0)

Default bastion.DebugLevel, to turn off logging entirely, pass the bastion.Disabled constant. It's JSON tagged as loggerLevel.

LoggerOutput

LoggerOutput is an io.Writer where the logger output write. Default os.Stdout.

Each logging operation makes a single call to the Writer's Write method. There is no guaranty on access serialization to the Writer. If your Writer is not thread safe, you may consider using sync wrapper.

From optionals functions

Bastion can be configured with optionals funtions that are optional when using bastion.New().

  • APIBasePath(path string) set path where the bastion api router is going to be mounted.
  • API500ErrMessage(msg string) set the message returned to the user when catch a 500 status error.
  • Addr(add string) bind address provided to http.Server.
  • Env(env string) set the "environment" in which the App is running.
  • NoPrettyLogging() turn off the pretty logging.
  • LoggerLevel(lvl Level) set the logger level.
  • LoggerOutput(w io.Writer) set the logger output writer.
package main

import (
    "github.com/ifreddyrondon/bastion"
)

func main() {
	bastion.New() // defaults options

	bastion.New(bastion.NoPrettyLogging(), bastion.Addr("0.0.0.0:3000")) // turn off pretty print logger and sets address to 0.0.0.0:3000
}
From options file

Bastion comes with an util function to load a new instance of Bastion from a options file. The options file could it be in YAML or JSON format. Is some attributes are missing from the options file it'll be set with the default. Example.

FromFile takes special consideration when there are ENV vars:

  • For Addr. When it's not provided it'll search the ADDR and PORT environment variables first before set the default.

  • For Env. When it's not provided it'll search the GO_ENV environment variables first before set the default.

YAML
apiBasepath: "/"
addr: ":8080"
debug: true
env: "development"
JSON
{
  "apiBasepath": "/",
  "addr": ":8080",
  "debug": true,
  "env": "development"
}

Testing

Bastion comes with battery included testing tools to perform End-to-end test over your endpoint/handlers.

It uses github.com/gavv/httpexpect to incrementally build HTTP requests, inspect HTTP responses and inspect response payload recursively.

Quick start
  1. Create the bastion instance with the handler you want to test.
  2. Import from bastion.Tester
  3. It receive a *testing.T and *bastion.Bastion instances as params.
  4. Build http request.
  5. Inspect http response.
  6. Inspect response payload.
package main_test

import (
	"net/http"
	"testing"

	"github.com/ifreddyrondon/bastion"
	"github.com/ifreddyrondon/bastion/_examples/todo-rest/todo"
    "github.com/ifreddyrondon/bastion/render"
)

func setup() *bastion.Bastion {
	app := bastion.New()
	handler := todo.Handler{
		Render: render.NewJSON(),
	}
	app.APIRouter.Mount("/todo/", handler.Routes())
	return app
}

func TestHandlerCreate(t *testing.T) {
	app := setup()
	payload := map[string]interface{}{
		"description": "new description",
	}

	e := bastion.Tester(t, app)
	e.POST("/todo/").WithJSON(payload).Expect().
		Status(http.StatusCreated).
		JSON().Object().
		ContainsKey("id").ValueEqual("id", 0).
		ContainsKey("description").ValueEqual("description", "new description")
}

Go and check the full test for handler and complete app 🤓

Render

Render a HTTP status code and content type to the associated Response.

StringRenderer
  • render.Text response strings with text/plain Content-Type.
render.Text.Response(rr, http.StatusOK, "test")
  • render.HTML response strings with text/html Content-Type.
render.HTML.Response(rr, http.StatusOK, "<h1>Hello World</h1>")
ByteRenderer
  • render.Data response []byte with application/octet-stream Content-Type.
render.Data.Response(rr, http.StatusOK, []byte("test"))
Renderer

Handle the marshaler of structs responses to the client.

// Renderer interface for managing response payloads.
type Renderer interface {
	// Response encoded responses in the ResponseWriter with the HTTP status code.
	Response(w http.ResponseWriter, code int, response interface{})
}

APIRenderer are convenient methods for api responses.


// APIRenderer interface for managing API response payloads.
type APIRenderer interface {
	Renderer
	OKRenderer
	ClientErrRenderer
	ServerErrRenderer
}

// OKRenderer interface for managing success API response payloads.
type OKRenderer interface {
	Send(w http.ResponseWriter, response interface{})
	Created(w http.ResponseWriter, response interface{})
	NoContent(w http.ResponseWriter)
}

// ClientErrRenderer interface for managing API responses when client error.
type ClientErrRenderer interface {
	BadRequest(w http.ResponseWriter, err error)
	NotFound(w http.ResponseWriter, err error)
	MethodNotAllowed(w http.ResponseWriter, err error)
}

// ServerErrRenderer interface for managing API responses when server error.
type ServerErrRenderer interface {
	InternalServerError(w http.ResponseWriter, err error)
}

JSON and XML implements APIRenderer and they can be configured with optional functions.

E.g.

Response a JSON with a 200 HTTP status code.

package main

import (
	"net/http"

	"github.com/ifreddyrondon/bastion"
	"github.com/ifreddyrondon/bastion/render"
)

func handler(w http.ResponseWriter, _ *http.Request) {
	res := struct {
		Message string `json:"message"`
	}{Message: "world"}
	render.NewJSON().Send(w, res)
}

func main() {
	app := bastion.New()
	app.APIRouter.Get("/hello", handler)
	app.Serve()
}

Logger

Bastion commes with a JSON structured logger powered by github.com/rs/zerolog. It can be accessed through the bastion instance bastion.Logger or from the context of each request l := bastion.LoggerFromCtx(ctx)

Logging from bastion instance
package main

import (
	"net/http"

	"github.com/ifreddyrondon/bastion"
)

func main() {
	app := bastion.New()
	app.Logger.Info().Str("app", "demo").Msg("main")
	app.Serve()
}
Logging from handler
package main

import (
	"net/http"

	"github.com/ifreddyrondon/bastion"
	"github.com/ifreddyrondon/bastion/render"
)

func handler(w http.ResponseWriter, r *http.Request) {
	res := struct {
		Message string `json:"message"`
	}{Message: "world"}
	l := bastion.LoggerFromCtx(r.Context())
	l.Info().Msg("handler")

	render.NewJSON().Send(w, res)
}

func main() {
	app := bastion.New()
	app.APIRouter.Get("/hello", handler)
	app.Serve()
}

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ProductionLoggers = []func(next http.Handler) http.Handler{
	hlog.RemoteAddrHandler("ip"),
	hlog.UserAgentHandler("user_agent"),
	hlog.RefererHandler("referer"),
}

ProductionLoggers is a list of logger added for production

Functions

func LoggerFromCtx added in v1.4.0

func LoggerFromCtx(ctx context.Context) *zerolog.Logger

LoggerFromCtx returns the Logger associated with the ctx.

func NewRouter added in v1.1.0

func NewRouter() *chi.Mux

NewRouter return a router as a subrouter along a routing path. It's very useful to split up a large API as many independent routers and compose them as a single service.

func Tester added in v1.2.4

func Tester(t *testing.T, bastion *Bastion) *httpexpect.Expect

Tester is an end-to-end testing helper for bastion handlers. It receives a reporter testing.T and http.Handler as params.

Types

type Bastion

type Bastion struct {
	Logger *zerolog.Logger
	Options
	APIRouter *chi.Mux
	// contains filtered or unexported fields
}

Bastion offers an "augmented" Router instance. It has the minimal necessary to create an API with default handlers and middleware. Allows to have commons handlers and middleware between projects with the need for each one to do so. Mounted Routers It use go-chi router to modularize the applications. Each instance of GogApp, will have the possibility of mounting an API router, it will define the routes and middleware of the application with the app logic. Without a Bastion you can't do much!

func FromFile added in v1.3.3

func FromFile(path string) (*Bastion, error)

FromFile is an util function to load a new instance of Bastion from a options file. The options file could it be in YAML or JSON format. Is some attributes are missing from the config file it'll be set with the defaults. FromFile takes a special consideration for `server.address` default. When it's not provided it'll search the ADDR and PORT environment variables first before set the default.

func New

func New(opts ...Opt) *Bastion

New returns a new instance of Bastion and adds some sane, and useful, defaults.

Defaults:
	Addr: "127.0.0.1:8080"
	Env: "development"
	Debug: false
	API:
		BasePath: "/"

func (*Bastion) RegisterOnShutdown added in v1.3.0

func (app *Bastion) RegisterOnShutdown(fs ...onShutdown)

RegisterOnShutdown registers a function to call on Shutdown. This can be used to gracefully shutdown connections that have undergone NPN/ALPN protocol upgrade or that have been hijacked. This function should start protocol-specific graceful shutdown, but should not wait for shutdown to complete.

func (*Bastion) Serve

func (app *Bastion) Serve() error

Serve accepts incoming incoming connections coming from the specified address/port. It also prepare the graceful shutdown.

type Level added in v1.4.0

type Level uint8

Level defines log levels.

const (
	// DebugLevel defines debug log level.
	DebugLevel Level = iota
	// InfoLevel defines info log level.
	InfoLevel
	// WarnLevel defines warn log level.
	WarnLevel
	// ErrorLevel defines error log level.
	ErrorLevel
	// FatalLevel defines fatal log level.
	FatalLevel
	// PanicLevel defines panic log level.
	PanicLevel
	// NoLevel defines an absent log level.
	NoLevel
	// Disabled disables the logger.
	Disabled
)

type Opt

type Opt func(*Bastion)

Opt helper type to create functional options

func API500ErrMessage

func API500ErrMessage(msg string) Opt

API500ErrMessage set the message returned to the user when catch a 500 status error.

func APIBasePath

func APIBasePath(path string) Opt

APIBasePath set path where the bastion api router is going to be mounted.

func Addr

func Addr(add string) Opt

Addr bind address provided to http.Server

func Env

func Env(env string) Opt

Env set the "environment" in which the App is running.

func LoggerLevel

func LoggerLevel(lvl Level) Opt

LoggerLevel set the logger level.

func LoggerOutput

func LoggerOutput(w io.Writer) Opt

LoggerOutput set the logger output writer

func NoPrettyLogging

func NoPrettyLogging() Opt

NoPrettyLogging turn off the pretty logging.

type Options added in v1.3.3

type Options struct {
	// APIBasepath path where the bastion api router is going to be mounted. Default `/`.
	APIBasepath string `yaml:"apiBasepath"`
	// API500ErrMessage message returned to the user when catch a 500 status error.
	API500ErrMessage string `yaml:"api500ErrMessage"`
	// Addr bind address provided to http.Server. Default is "127.0.0.1:8080"
	// Can be set using ENV vars "ADDR" and "PORT".
	Addr string `yaml:"addr"`
	// Env "environment" in which the App is running. Default is "development".
	Env string `yaml:"env"`
	// NoPrettyLogging don't output a colored human readable version on the out writer.
	NoPrettyLogging bool `yaml:"prettyLogging"`
	// LoggerLevel defines log levels. Default is DebugLevel defines an absent log level.
	LoggerLevel Level `yaml:"loggerLevel"`
	// LoggerOutput logger output writer. Default os.Stdout
	LoggerOutput io.Writer
}

Options are used to define how the application should run.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL