vesper

package module
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Sep 23, 2022 License: MIT Imports: 8 Imported by: 0

README

Vesper - Middleware for AWS Lambda

The Golang Middleware engine for AWS Lambda.

Build Status Coverage Status Go Report Card GoDoc

Introduction

Vesper is a very simple middleware engine for Lamda functions. If you are used to HTTP Web frameworks like Gorilla Mux and Go Kit, then you will be familiar with the concepts adopted in Vesper.

Middleware allows developers to isolate common technical concerns - such as input/output validation, logging and error handling - into functions that decorate the main business logic. This enables you to reuse these focus on writing code that remains clean, readable and easy to test and maintain.

Get started

Create a new serverless project from the Vesper template:

serverless create -u https://github.com/mefellows/vesper/tree/master/template

API

Usage
// MyHandler implements the Lambda Handler interface
func MyHandler(ctx context.Context, u User) (interface{}, error) {
	log.Println("[MyHandler]: handler invoked with user: ", u.Username)

	return u.Username, nil
}

func main() {
  // Create a new vesper instance, passing in all the Middlewares
  v := vesper.New(MyHandler, vesper.WarmupMiddleware)

  // Replace the standard lambda.Start() with Vesper's wrapper
  v.Start()
}
Logging

You can set your own custom logger with vesper.Logger(l LogPrinter).

Auto unmarshalling

The default behavior for Vesper is to automatically JSON unmarshal the payload into the type specificed in the handler parameter. This is consistent with the behaviour of the AWS Go Lambda library. This is useful if your handler accepts an input parameter which can be directly JSON unmarshalled into the parameter type. An example of this is the event types found in github.com/aws/aws-lambda-go/events.

This behaviour can be turned off in which case Vesper will start the middleware chain with the payload in []byte form. If you are using a Parser middleware, you will likely want to turn this off to allow the parser to do the converting instead of Vesper.

Here is an example of how to turn off the auto unmarshalling:

// MyHandler implements the Lambda Handler interface
func MyHandler(ctx context.Context, input []byte) error {
  log.Println("[MyHandler]: handler invoked with payload: ", input)

	return nil
}

func main() {
  // Create a new vesper instance and disable auto unmarshalling
  v := vesper.New(MyHandler).
  			DisableAutoUnmarshal()

  // Replace the standard lambda.Start() with Vespers wrapper
	v.Start()
}

Writing your own Middleware

A middleware is a function that takes a LambdaFunc and returns another LambdaFunc. A LambdaFunc is simple a named type for the AWS Handler signature.

Most middleware's do three things:

  1. Modify or perform some action on the incoming request (such as validating the request)
  2. Call the next middleware in the chain
  3. Modify or perform some action on the outgoing response (such as validating the response)

Example:

var dummyMiddleware = func(next vesper.LambdaFunc) vesper.LambdaFunc {
	// one time scope setup area for middleware - e.g. in-memory FIFO cache

	return func(ctx context.Context, in interface{}) (interface{}, error) {
		log.Println("[dummyMiddleware] START:")
		// (1) Modify the incoming request, or update the context before passing to the
		// next middleware in the chain
		// (2) You must call the next middleware in the chain if the request should proceed
		// and you want other middleware to execute
		res, err := next(ctx, in)
		// (3) Your last chance to modify the response before it is passed to any remaining
		// middleware in the chain
		log.Println("[dummyMiddleware] END:", in)
		return res, err
	}
}

Available Middleware

Warmup

Short circuits a request if the serverless warmup event is detected.

TIP: This middleware should be included early in the chain, before any validation or processing happens

Implements a warmup handler for https://www.npmjs.com/package/serverless-plugin-warmup

Example:

func main() {
	m := vesper.New(MyHandler, vesper.WarmupMiddleware, /* any other middlewares here */)
	m.Start()
}
Parser

Parses the input payload to the type specificed in the handler parameter. It accepts a decoder function so you can decide how it parses the payload.

NOTE: the auto unmarshaling needs to be turned off for this middleware to work correctly. See Auto unmarshalling.

Example of usage:

import (
	"encoding/json"

	"github.com/mefellows/vesper"
)

func main() {
	m := vesper.New(MyHandler).
		DisableAutoUnmarshal().
		Use(vesper.ParserMiddleware(json.Unmarshal))
	m.Start()
}
JSONParser

This is a shorthand for using the Parser middleware - vesper.ParserMiddleware(json.Unmarshal).

Example of usage:

import "github.com/mefellows/vesper"

func main() {
	m := vesper.New(MyHandler).
		DisableAutoUnmarshal().
		Use(vesper.JSONParserMiddleware())
	m.Start()
}
SQSParser

Parses the input payload as an SQS event and then extracts and unmarshals the body into the type specificed in the handler parameter. The handler parameter must be a slice as SQS events are always batched. It accepts a decoder function so you can decide how it parses the SQS record body.

NOTE: the auto unmarshaling needs to be turned off for this middleware to work correctly. See Auto unmarshalling.

Example of usage:

import (
	"encoding/json"

	"github.com/mefellows/vesper"
)

func MyHandler(ctx context.Context, users []users) error {
  log.Println("[MyHandler]: handler invoked with users: ", input)

	return nil
}

func main() {
	m := vesper.New(MyHandler).
		DisableAutoUnmarshal().
		Use(vesper.SQSParserMiddleware(json.Unmarshal))
	m.Start()
}
JSONSQSParser

This is a shorthand for using the SQSParser middleware - vesper.SQSParserMiddleware(json.Unmarshal).

Example of usage:

import "github.com/mefellows/vesper"

func main() {
	m := vesper.New(MyHandler).
				DisableAutoUnmarshal().
  			Use(vesper.JSONParserMiddleware())
	m.Start()
}

How it works

Vesper implements the classic onion-like middleware pattern, with some peculiar details.

Vesper middleware engine diagram

When you attach a new middleware this will wrap the business logic contained in the handler in two separate steps.

When another middleware is attached this will wrap the handler again and it will be wrapped by all the previously added middlewares in order, creating multiple layers for interacting with the request (event) and the response.

This way the request-response cycle flows through all the middlewares, the handler and all the middlewares again, giving the opportunity within every step to modify or enrich the current request, context or the response.

Execution order

Middlewares have two phases: before and after.

The before phase, happens before the handler is executed. In this code the response is not created yet, so you will have access only to the request.

The after phase, happens after the handler is executed. In this code you will have access to both the request and the response.

If you have three middlewares attached as in the image above this is the expected order of execution:

  • middleware1 (before)
  • middleware2 (before)
  • middleware3 (before)
  • handler
  • middleware3 (after)
  • middleware2 (after)
  • middleware1 (after)

Notice that in the after phase, middlewares are executed in reverse order, this way the first handler attached is the one with the highest priority as it will be the first able to change the request and last able to modify the response before it gets sent to the user.

Interrupt middleware execution early

Some middlewares might need to stop the whole execution flow and return a response immediately.

If you want to do this you cansimple omit invoking next middleware and return early.

Note: this will stop the execution of successive middlewares in any phase (before and after) and returns an early response (or an error) directly at the Lambda level. If your middlewares does a specific task on every request like output serialization or error handling, these won't be invoked in this case.

In this example we can use this capability for rejecting an unauthorised request:

var authMiddleware = func(next vesper.LambdaFunc) vesper.LambdaFunc {
	return func(ctx context.Context, in interface{}) (interface{}, error) {
		log.Println("[authMiddleware] START: ", in)
		user := in.(User)
		if user.Username == "fail" {
			error := map[string]string{
				"error": "unauthorised",
			}
			// NOTE: we do not call the "next" middleware, and completely prevent execution of subsequent middlewares
			return error, fmt.Errorf("user %v is unauthorised", in)
		}
		res, err := next(ctx, in)
		log.Printf("[authMiddleware] END: %+v \n", in)
		return res, err
	}
}

TODO

  • Cleanup interface / write tests for Vesper
  • Setup CI
  • Implement HandlerSignatureMiddleware
  • Implement Typed Record Handler Middleware for SQS
  • Implement Typed Record Handler Middleware for Kinesis
  • Implement Typed Record Handler Middleware for SNS
  • Write / Publish documentation
  • Integrate / demo with lambda starter kit (using Message structure proposal)

Developer Documentation

Goals
  1. Vesper is a middleware library - it shall provide a small API for this purpose, along with common middlewares
  2. Compatibility with the AWS Go SDK interface to ensure seamless integration with tools like SAM, Serverless, 1ocal testing and so on, and to reduce cognitive overload for users
  3. Preserve type safety and encourage the use of types throughout the system
  4. Allow user to controls message batch semantics (e.g. ability to control concurrency)
  5. Be comprehensible / avoid magic
  6. Enable/allow use of user-defined messages structures
Contributing

See CONTRIBUTING.

What's in a name?

Golang has a rich history of naming HTTP and middleware type libraries on classy gin-based beverages (think Gin, Martini and Negroni). Vesper is yet another gin-based beverage.

Vesper was also inspired by Middy JS, who's mascot is a Moped.

Vesper is a (not so clever) portmanteau of Vesper, the gin-based martini, and Vespa, a beautiful Italian scooter.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ExtractType

func ExtractType(ctx context.Context, in interface{}) error

ExtractType fetches the original invocation payload (as a []byte) and converts it to the given narrow type This is useful for situations where a function is invoked from multiple contexts (e.g. warmup, http, S3 events) and handlers/middlewares need to be strongly typed

func JSONParserMiddleware

func JSONParserMiddleware() func(LambdaFunc) LambdaFunc

JSONParserMiddleware is a middleware which JSON unmarshals the original payload to the handler input parameter type.

func JSONSQSParserMiddleware

func JSONSQSParserMiddleware() func(LambdaFunc) LambdaFunc

JSONSQSParserMiddleware transforms SQS event records into the handler input parameter type using a JSON unmarshaler. The handler input parameter must be a slice.

func Logger

func Logger(l logPrinter)

Logger sets the log to use

func ParserMiddleware

func ParserMiddleware(unmarshaler encoding.UnmarshalFunc) func(LambdaFunc) LambdaFunc

ParserMiddleware is a middleware which unmarshals the original payload to the handler input parameter type with the given unmarshaler.

func PayloadFromContext

func PayloadFromContext(ctx context.Context) ([]byte, bool)

PayloadFromContext retrieves the original payload with type []byte from a context.

func SQSParserMiddleware

func SQSParserMiddleware(unmarshaler encoding.UnmarshalFunc) func(LambdaFunc) LambdaFunc

SQSParserMiddleware transforms SQS event records into the handler input parameter type using the given unmarshaler. The handler input parameter must be a slice.

func TInFromContext

func TInFromContext(ctx context.Context) (reflect.Type, bool)

TInFromContext retrieves the TIn of the lambda handler with type reflect.Type from a context.

Types

type LambdaFunc

type LambdaFunc func(context.Context, interface{}) (interface{}, error)

LambdaFunc is the long-form of the Lambda handler interface https://github.com/aws/aws-lambda-go/blob/master/lambda/entry.go#L37-L49

func WarmupMiddleware

func WarmupMiddleware(f LambdaFunc) LambdaFunc

WarmupMiddleware detects a warmup invocation event from the plugin "serverless-plugin-warmup", and returns early if found

See https://www.npmjs.com/package/serverless-plugin-warmup for more

type Middleware

type Middleware func(next LambdaFunc) LambdaFunc

Middleware is a definition of what a Middleware is, take in one LambdaFunc and wrap it within another LambdaFunc

type Vesper

type Vesper struct {
	// contains filtered or unexported fields
}

Vesper is a middleware adapter for Lambda Functions

func New

func New(handler interface{}, middlewares ...Middleware) *Vesper

New creates a new Vesper instance given a Handler and set of Middleware Middlewares are evaluated in the order they are provided

func (*Vesper) BuildHandler added in v0.1.2

func (v *Vesper) BuildHandler() lambdaHandler

func (*Vesper) DisableAutoUnmarshal

func (v *Vesper) DisableAutoUnmarshal() *Vesper

DisableAutoUnmarshal disables the default behaviour of JSON unmarshaling the payload into the handler input parameter type. This flag should be set if your handler accepts an input parameter other than context.Context but is not directly JSON unmarshalable from the payload.

An example of this is if your Lambda is triggered by Kinesis events and the handler signature is func(ctx, []MyCustomObject){}. In this case, your Lambda would need to loop through all the records in the event, Base64 decode the body and then unmarshal the body into the type of MyCustomObject.

Situations where you want the default behaviour: - If your handler doesn't accept an input parameter other than context.Context. - If your handler accepts an input parameter other than context.Context that is compatible with an AWS event payload (see github.com/aws/aws-lambda-go/events). - If your Lambda is invoked directly with a JSON payload and your handler accepts an input parameter other than context.Context which is compatible with the payload.

func (*Vesper) Start

func (v *Vesper) Start()

Start is a convenience function run the lambda handler

func (*Vesper) Use

func (v *Vesper) Use(middlewares ...Middleware) *Vesper

Use adds middlewares onto the middleware chain

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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