Microservice framework following best cloud practices with a focus on productivity.
The HTTP component provides the functionality for creating an HTTP server exposing the relevant routes.
It wraps the logic and handles the boilerplate for the net.http
go package.
The way to initialise an HTTP component is through the patron http.Builder
struct.
// NewBuilder initiates the HTTP component builder chain.
// The builder instantiates the component using default values for
// HTTP Port, Alive/Ready check functions and Read/Write timeouts.
func NewBuilder() *Builder {
// ...
}
// WithSSL sets the filenames for the Certificate and Keyfile, in order to enable SSL.
func (cb *Builder) WithSSL(c, k string) *Builder {
// ..
}
// WithRoutesBuilder adds routes builder to the HTTP component.
func (cb *Builder) WithRoutesBuilder(rb *RoutesBuilder) *Builder {
// ...
}
// WithMiddlewares adds middlewares to the HTTP component.
func (cb *Builder) WithMiddlewares(mm ...MiddlewareFunc) *Builder {
// ...
}
// WithReadTimeout sets the Read Timeout for the HTTP component.
func (cb *Builder) WithReadTimeout(rt time.Duration) *Builder {
// ...
}
// WithWriteTimeout sets the write timeout for the HTTP component.
func (cb *Builder) WithWriteTimeout(wt time.Duration) *Builder {
// ...
}
// WithShutdownGracePeriod sets the Shutdown Grace Period for the HTTP component.
func (cb *Builder) WithShutdownGracePeriod(gp time.Duration) *Builder {
// ...
}
// WithPort sets the port used by the HTTP component.
func (cb *Builder) WithPort(p int) *Builder {
// ...
}
// WithAliveCheckFunc sets the AliveCheckFunc used by the HTTP component.
func (cb *Builder) WithAliveCheckFunc(acf AliveCheckFunc) *Builder {
// ...
}
// WithReadyCheckFunc sets the ReadyCheckFunc used by the HTTP component.
func (cb *Builder) WithReadyCheckFunc(rcf ReadyCheckFunc) *Builder {
// ...
}
// Create constructs the HTTP component by applying the gathered properties.
func (cb *Builder) Create() (*Component, error) {
// ...
}
When creating a new HTTP component, Patron will automatically create a liveness and readiness route, which can be used to probe the lifecycle of the application:
# liveness
GET /alive
# readiness
GET /ready
Both can return either a 200 OK
or a 503 Service Unavailable
status code (default: 200 OK
).
It is possible to customize their behaviour by injecting an http.AliveCheck
and/or an http.ReadyCheck
OptionFunc
to the HTTP component constructor.
The following metrics are automatically provided by default:
component_http_handled_total
component_http_handled_seconds
Example of the associated labels: status_code="200"
, method="GET"
, path="/hello/world"
When using WithTrace()
the following metrics are automatically provided via Jaeger (they are populated together with the spans):
{microservice_name}_http_requests_total
{microservice_name}_http_requests_latency
They have labels endpoint="GET-/hello/world"
and status_code="2xx"
.
A MiddlewareFunc
preserves the default net/http middleware pattern.
You can create new middleware functions and pass them to Service to be chained on all routes in the default HTTP Component.
type MiddlewareFunc func(next http.Handler) http.Handler
// Set up a simple middleware for CORS
newMiddleware := func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Access-Control-Allow-Origin", "*")
// Next
h.ServeHTTP(w, r)
})
}
Middlewares are invoked sequentially. The object handling this is the MiddlewareChain
// MiddlewareChain chains middlewares to a handler func.
func MiddlewareChain(f http.Handler, mm ...MiddlewareFunc) http.Handler {
for i := len(mm) - 1; i >= 0; i-- {
f = mm[i](f)
}
return f
}
Patron comes with some predefined middlewares, as helper tools to inject functionality into the HTTP endpoint or individual routes.
// NewRecoveryMiddleware creates a MiddlewareFunc that ensures recovery and no panic.
func NewRecoveryMiddleware() MiddlewareFunc {
// ...
}
// NewAuthMiddleware creates a MiddlewareFunc that implements authentication using an Authenticator.
func NewAuthMiddleware(auth auth.Authenticator) MiddlewareFunc {
// ...
}
// NewLoggingTracingMiddleware creates a MiddlewareFunc that continues a tracing span and finishes it.
// It uses Jaeger and OpenTracing and will also log the HTTP request on debug level if configured so.
func NewLoggingTracingMiddleware(path string) MiddlewareFunc {
// ...
}
// NewRequestObserverMiddleware creates a MiddlewareFunc that captures status code and duration metrics about the responses returned;
// metrics are exposed via Prometheus.
// This middleware is enabled by default.
func NewRequestObserverMiddleware(method, path string) MiddlewareFunc {
// ...
}
// NewCachingMiddleware creates a cache layer as a middleware.
// when used as part of a middleware chain any middleware later in the chain,
// will not be executed, but the headers it appends will be part of the cache.
func NewCachingMiddleware(rc *cache.RouteCache) MiddlewareFunc {
// ...
}
// NewCompressionMiddleware initializes a compression middleware.
// As per Section 3.5 of the HTTP/1.1 RFC, we support GZIP and Deflate as compression methods.
// https://tools.ietf.org/html/rfc2616#section-3.5
func NewCompressionMiddleware(deflateLevel int, ignoreRoutes ...string) MiddlewareFunc {
// NewRateLimitingMiddleware creates a MiddlewareFunc that adds a rate limit to a route.
// It uses golang in-built rate library to implement simple rate limiting
//"https://pkg.go.dev/golang.org/x/time/rate"
func NewRateLimitingMiddleware(limiter *rate.Limiter) MiddlewareFunc {
// ..
}
It is possible to configure specific status codes that, if returned by an HTTP handler, the response’s error will be logged.
This configuration must be done using the PATRON_HTTP_STATUS_ERROR_LOGGING
environment variable. The syntax of this variable is based on PostgreSQL syntax and allows providing ranges.
For example, setting this environment variable to 409;[500,600)
that an error will be logged if an HTTP handler returns either:
Be it a specific status code or a range; each element must be delimited with ;
.
To enable error logging, we enable route tracing (WithTrace
option).
Each HTTP component can contain several routes. These are injected through the RoutesBuilder
// RouteBuilder for building a route.
type RouteBuilder struct {
// ...
}
// NewRouteBuilder constructor.
func NewRouteBuilder(path string, processor ProcessorFunc) *RouteBuilder {
// ...
}
// WithTrace enables route tracing.
func (rb *RouteBuilder) WithTrace() *RouteBuilder {
// ...
}
// WithMiddlewares adds middlewares.
func (rb *RouteBuilder) WithMiddlewares(mm ...MiddlewareFunc) *RouteBuilder {
// ...
}
// WithAuth adds authenticator.
func (rb *RouteBuilder) WithAuth(auth auth.Authenticator) *RouteBuilder {
// ...
}
// WithRouteCache adds a cache to the corresponding route
func (rb *RouteBuilder) WithRouteCache(cache cache.TTLCache, ageBounds httpcache.Age) *RouteBuilder {
// ...
}
// Build a route.
func (rb *RouteBuilder) Build() (Route, error) {
// ...
}
The main components that hold the logic for a route are the processor and the middlewares
The method for each route cn be defined through the builder as well
// MethodGet HTTP method.
func (rb *RouteBuilder) MethodGet() *RouteBuilder {
// ...
}
// MethodHead HTTP method.
func (rb *RouteBuilder) MethodHead() *RouteBuilder {
// ...
}
// MethodPost HTTP method.
func (rb *RouteBuilder) MethodPost() *RouteBuilder {
// ...
}
// MethodPut HTTP method.
func (rb *RouteBuilder) MethodPut() *RouteBuilder {
// ...
}
...
and for reducing boilerplate code one can also combine this in the constructor call for the Builder
// NewGetRouteBuilder constructor
func NewGetRouteBuilder(path string, processor ProcessorFunc) *RouteBuilder {
// ...
}
// NewHeadRouteBuilder constructor.
func NewHeadRouteBuilder(path string, processor ProcessorFunc) *RouteBuilder {
// ...
}
// NewPostRouteBuilder constructor.
func NewPostRouteBuilder(path string, processor ProcessorFunc) *RouteBuilder {
// ...
}
...
The processor is responsible for creating a Request
by providing everything that is needed (Headers, Fields, decoder, raw io.Reader), passing it to the implementation by invoking the Process
method and handling the Response
or the error
returned by the processor.
The sync package contains only a function definition along with the models needed:
type ProcessorFunc func(context.Context, *Request) (*Response, error)
The Request
model contains the following properties (which are provided when calling the “constructor” NewRequest
)
io.Reader
map[string]string
encoding.Decode
that decodes the raw readerAn exported function exists for decoding the raw io.Reader in the form of
Decode(v interface{}) error
The Response
model contains the following properties (which are provided when calling the “constructor” NewResponse
)
interface{}
// NewFileServer constructor.
func NewFileServer(path string, assetsDir string, fallbackPath string) *RouteBuilder {
// ...
}
The File Server exposes files from the filesystem to be accessed from the service.
It has baked in support for Single Page Applications or 404 pages by providing a fallback path
Routes using the file server has to follow a pattern, by convention this path has to end in *path
.
http.NewFileServer("/some-path/*path", "...", "...")
The path is used to resolve where in the filesystem we should serve the file from. If no file is found we will serve the fallback path.
// NewRawRouteBuilder constructor.
func NewRawRouteBuilder(path string, handler http.HandlerFunc) *RouteBuilder {
// ...
}
The Raw Route Builder allows for lower level processing of the request and response objects.
It’s main difference with the Route Builder is the processing function. Which in this case is the native
go http handler func.
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)
The Raw Route Builder constructor should be used,
if the default behaviour and assumptions of the wrapped Route Builder
do not fit into the routes requirements or use-case.
Middlewares can also run per routes using the processor as Handler.
So using the Route
builder:
// WithMiddlewares adds middlewares.
func (rb *RouteBuilder) WithMiddlewares(mm ...MiddlewareFunc) *RouteBuilder {
if len(mm) == 0 {
rb.errors = append(rb.errors, errors.New("middlewares are empty"))
}
rb.middlewares = mm
return rb
}
Users can implement the Authenticator
interface to provide authentication capabilities for HTTP components and Routes
type Authenticator interface {
Authenticate(req *http.Request) (bool, error)
}
Patron also includes a ready-to-use implementation of an API key authenticator.
One of the main features of patron is the tracing functionality for Routes. Tracing can either be enabled by default from the Buidler.
// WithTrace enables route tracing.
func (rb *RouteBuilder) WithTrace() *RouteBuilder {
rb.trace = true
return rb
}
The caching layer for HTTP routes is specified per Route.
// RouteCache is the builder needed to build a cache for the corresponding route
type RouteCache struct {
// cache is the ttl cache implementation to be used
cache cache.TTLCache
// age specifies the minimum and maximum amount for max-age and min-fresh header values respectively
// regarding the client cache-control requests in seconds
age age
}
func NewRouteCache(ttlCache cache.TTLCache, age Age) *RouteCache
server cache
Warning
header present in the response.Note : When a cache is used, the handler execution might be skipped.
That implies that all generic handler functionalities MUST be delegated to a custom middleware.
i.e. counting number of server client requests etc ...
Usage
NewRouteBuilder("/", handler).
WithRouteCache(cache, http.Age{
Min: 30 * time.Minute,
Max: 1 * time.Hour,
}).
MethodGet()
NewRouteBuilder("/", handler).
WithMiddlewares(NewCachingMiddleware(NewRouteCache(cc, Age{Max: 10 * time.Second}))).
MethodGet()
client cache-control The client can control the cache with the appropriate Headers
max-age=?
returns the cached instance only if the age of the instance is lower than the max-age parameter.
This parameter is bounded from below by the server option minAge
.
This is to avoid chatty clients with no cache control policy (or very aggressive max-age policy) to effectively disable the cache
min-fresh=?
returns the cached instance if the time left for expiration is lower than the provided parameter.
This parameter is bounded from above by the server option maxFresh
.
This is to avoid chatty clients with no cache control policy (or very aggressive min-fresh policy) to effectively disable the cache
no-cache
/ no-store
returns a new response to the client by executing the route processing function.
NOTE : Except for cases where a minAge
or maxFresh
parameter has been specified in the server.
This is again a safety mechanism to avoid ‘aggressive’ clients put unexpected load on the server.
The server is responsible to cap the refresh time, BUT must respond with a Warning
header in such a case.
only-if-cached
expects any response that is found in the cache, otherwise returns an empty response
metrics
The http cache exposes several metrics, used to
By default, we are using prometheus as the pre-defined metrics framework.
additions = misses + evictions
Always , the cache addition operations (objects added to the cache), must be equal to the misses (requests that were not cached) plus the evictions (expired objects). Otherwise, we would expect to notice also an increased amount of errors or having the cache misbehaving in a different manner.
additions ~ misses
If the additions and misses are comparable e.g. misses are almost as many as the additions, it would point to some cleanup of the cache itself. In that case the cache seems to not be able to support the request patterns and control headers.
hits ~ additions
The cache hit count represents how well the cache performs for the access patterns of client requests. If this number is rather low e.g. comparable to the additions, this would signify that probably a cache is not a good option for the access patterns at hand.
eviction age
The age at which the objects are evicted from the cache is a very useful indicator. If the vast amount of evictions is close to the time to live setting, it would indicate a nicely working cache. If we find that many evictions happen before the time to live threshold, clients would be making use cache-control headers.
cache design reference
improvement considerations
Usage
NewGetRouteBuilder("/", getHandler).WithRateLimiting(limit, burst)
NewRouteBuilder("/", handler).
WithMiddlewares(NewRateLimitingMiddleware(rate.NewLimiter(limit, burst))).
MethodGet()