Handler Dependencies
Handlers need a variety of services and dependencies in order to make life easier. It’s a very big part of why one reaches for a framework. The idea of “dependency injection” is the foundation of many frameworks and Aegis isn’t much different.
Really, routing events to handlers is the foundation of Aegis, but you can’t abstract that away without being able to pass along functionality required by each handler.
Aegis has a few core dependencies that even its internal handler (a handler before your handler) uses and passes along for you to use as well. Also, you can also pass along your own dependencies if you need to.
Log
logger := logrus.New()
// Various configuration options for logrus...
AegisApp = aegis.New(handlers)
// Set your configured logger on Log
AegisApp.Log = logger
AegisApp.Start()
Aegis’ Log dependency provides an adaptable way to log using the popular Go package
logrus.
You’re certainly welcome to use the standard library log
package as well. AWS Lambda will send all
of that stdout to CloudWatch for you automatically.
However, there may be other logging services you wish to use. Rather than re-invent the wheel, Aegis used logrus instead because of its adaptability.
Set the Log
field on the Aegis
interface like any other core dependency to change it. You can
also use the ConfigureLogger(*logrus.Logger)
helper function.
Do note that everything internally will use log
. So anything that Aegis logs out will be found in
CloudWatch. It’s not that Aegis didn’t want to dogfood its own logging dependency, it’s that Aegis
did not want its logging to get in the way of your logging.
For example, if your handler (just one of your Lambdas) wanted to use Log
to send information
to your team Slack channel, you wouldn’t also want internal Aegis logging to be sent as well. Especially
because you couldn’t turn them off. There is currently no sort of verbosity setting in Aegis. If you
were to, say, disable CloudWatch logging for your Lambda altogether then the standard log
calls
would simply go no where.
CloudWatch of course isn’t the prettiest way to view your logs. While logrus has fancy coloring,
CloudWatch is black and white text through your web browser in the AWS Console. So you might end up
configuring Log
in a different way to work with logging in a prettier way.
Another thing worth noting here is that logrus’ 3rd party formatters can be rather powerful. There are formatters for things like fluentd, logstash, and more. So you might just find yourself using it for more than simply basic logging. Also keep in mind that AWS has a hosted version of Elasticsearch with Kibana…See where this is going?? You can implement a rather robust searchable logging solution all within AWS.
Tracer
How to use a different TraceStrategy
AegisApp = aegis.New(handlers)
// For example, disabling tracing
AegisApp.Tracer = &aegis.NoTraceStrategy{}
AegisApp.Start()
How Aegis Router traces internally
// Records data in the tracing strategy. In this case, and by default, X-Ray annotations.
r.Tracer.Record("annotation",
map[string]interface{}{
"RequestPath": req.Path,
"Method": req.HTTPMethod,
},
)
err = r.Tracer.Capture(ctx, "RouteHandler", func(ctx1 context.Context) error {
// Makes the Tracer available inside your handler.
// Capture() also applies the annotations from above in the case of XRayTraceStrategy
d.Tracer = &r.Tracer
return handler.handler(ctx1, d, &req, &res, params)
})
The Tracer
dependency implements a TraceStrategy
interface. By default, this is AWS X-Ray
defined as XRayTraceStrategy
. There is also NoTraceStrategy
available if you’d like to disable
tracing (or for unit tests).
However, you can add your own tracing strategy interfaces and use those instead if you wanted to use something other than X-Ray.
Using a different strategy is as simple as setting the Tracer
field on the Aegis interface.
In the examples, this is usually called AegisApp
. Do keep in mind that this is a pointer.
Aegis’ tracing works a little differently than AWS’ X-Ray. Though you are always free to import
github.com/aws/aws-xray-sdk-go/xray
to use yourself.
Aegis receives all events in one centralized function and then sends them out to the appropriate router which in turn calls the matching handler. This is all internal and while there are a few hooks available, no one wants to use those hooks to run tracing. It’s simply not convenient nor very conventional.
So, Aegis’ XRayTraceStrategy
has some fields, namely; Annotation
, NamespaceAnnotation
, Metadata
,
and Error
. These are maps (except Error
, a singular error). So we are simply adding to them with
Record()
and Capture()
will loop through them and add on to the segment in AWS X-Ray. A different
interface implementing TraceStrategy
may do something completely different. The NoTraceStrategy
that Aegis has will simply do nothing.
Every router should add some default annoatations and metadata automatically for you. Whether your trace strategy handles that data is another story. So if you are using X-Ray, you don’t need to add the API Gateway request path or HTTP method. If you run Aegis with all the defaults, you’re just going to get traced API requests with meaningful annotations.
Segments
BeginSegment()
and BeginSubsegment()
are somewhat universal concepts. AWS X-Ray has the concept
of segments and subsegments, but other tracing services may only deal with segments. Those segments
may even be implicit so you may find yourself not needing to implement these functions in a custom
strategy depending on the service you’re using.
Regardless, all they do is help organize and segment your traces. In the case of X-Ray, Aegis will automatically create an “Aegis” segment for you when running Aegis from the CLI or local web server. When running in AWS Lambda, a segment is created automatically by Lambda for you.
This may not be the case if using a custom trace strategy. You may also want to create subsegments to trace within your handlers. These are the functions to help you out.
CloseSegment()
and CloseSubsegment()
then help you define the end of those segments. In the
case of the default XRayTraceStrategy
, closing a subsegment also sends data off to X-Ray. These
functions just call X-Ray’s Close()
and CloseAndStream()
. They’re aliases more or less.
Recording Data
Regardless of the tracing service you ultimately use, the concept of Record()
is pretty universal.
The idea is that you want to record certain bits of data with your tracing. AWS X-Ray calls these
“metadata,” “annotations,” “namespaced annotations,” and a single “error” that can be sent along with
each trace.
The XRayTraceStrategy
implementation of Record()
will simply set some struct fields with the data
and nothing will actually be “sent” to AWS X-Ray until Capture()
or CaptureAsync()
is called.
The reason for this is because we aren’t “streaming” a bunch of data to AWS X-Ray. We’re “reporting”
on segments of our application. Your trace strategy may differ and there’s no reason Record()
couldn’t
send “real-time” data along. It’s just not how X-Ray works.
Another important thing to note here is that Record()
can be called at any time without context.
In the case of Aegis, it’s called before (and outside of) whatever Capture()
is wrapping. So there
is no context. If we needed the context, we’d call Record()
inside of Capture()
which has context.
Or, the specific context we care about.
By the time Tracer
is given to your handler, you do have the relevant context. So you could leverage
it to pull out whatever data you need.
The idea is that Record()
shouldn’t require a context argument. While AWS X-Ray’s internal methods
for recording annotations and metadata do take a context (for the segment), we don’t know if other trace
services are going to use the context or not. So removing this requirement from the process keeps
TraceStrategy
a lot more flexible as an interface.
Tracing Functions
Capture()
and CaptureAsync()
(for goroutinues) are “wrappers.” They wrap the functions you wish to trace.
In the case of AWS X-Ray, this captures things like execution time and more. Perhaps most important of all here
is that they can capture errors.
What exactly will be traced is going to vary by the trace service (or your own code if you roll your own). In the case of AWS X-Ray, a lot is actually traced for you. How long HTTP connections took, how long it took to marshal things, and more.
One of the biggest things you’re going to see in X-Ray is how long your handlers took to execute and how long your Lambda took to execute. This really starts to give you a good idea about performance and where your bottlenecks are.
An important thing to note about Lambda here is that X-Ray is also going to illustrate for you the difference
between a “cold” and “warm” start in Lambda. You’ll notice in the screenshot above here that the Lambda took
over 800ms to run. Not terrible for API Gateway and all the OAuth work that was going on, but also not stellar.
Subsequent invocations were faster because the Lambda container was “warm” and some data retrieved from the IDP
service (such as the DescribeUserPool
and DescribeUserPoolClient
calls) were already cached. Things like the
well known ket set and so on.
As you learn to leverage caching methods and tune your service configurations, you’ll be able to visually see the performance change over time with X-Ray. It’s an incredibly useful service.
Custom
You can inject your own handler dependencies under the Custom
field. This is great for 3rd party dependencies you
wish to use as well as anything you want to make available to each handler.
These dependencies contain no “configuration” closure (unlike Services) and if your function is simple enough, you
may not even need to pass them through Custom
. Keep in mind the scoping in Go. If you’ve defined something in
your package (“main” perhaps), it’s available to all of the functions within that package…This often will include
your handlers.
So before you reach to shove everything through to your handlers, think about it a bit. Lambda functions should
be easy to follow and your code should read clean. Remember that Custom
here is a map[string]interface{}
so you will likely encounter the need to check for nil and use type assertions.
Aegis isn’t trying to limit you and sometimes you may have no other option, but no one says you have to pass everything through to each handler in this way.
Services
From the Cognito example Aegis app
AegisApp.ConfigureService("cognito", func(ctx context.Context, evt map[string]interface{}) interface{} {
// Automatically get host. You don't need to do this - you typically would have a known domain name to use.
host := ""
stage := "prod"
if headers, ok := evt["headers"]; ok {
headersMap := headers.(map[string]interface{})
if hostValue, ok := headersMap["Host"]; ok {
host = hostValue.(string)
}
}
// Automatically get API stage
if requestContext, ok := evt["requestContext"]; ok {
requestContextMap := requestContext.(map[string]interface{})
if stageValue, ok := requestContextMap["stage"]; ok {
stage = stageValue.(string)
}
}
return &aegis.CognitoAppClientConfig{
// The following three you'll need to fill in or use secrets.
// See README for more info, but set secrets using aegis CLI and update aegis.yaml as needed.
Region: "us-east-1",
PoolID: AegisApp.GetVariable("PoolID"),
ClientID: AegisApp.GetVariable("ClientID"),
// This is just automatic for the example, you would likely replace this too
RedirectURI: "https://" + host + "/" + stage + "/callback",
LogoutRedirectURI: "https://" + host + "/" + stage + "/",
}
})
Handler dependencies contain Services
too. These are primarily AWS services like AWS Cognito for example.
They are a bit different than a simple field on the Aegis
interface because they are “services” that can
carry configurations which often need to be dynamic.
However, services are really just dependencies as well. Your normal dependencies, set on the Aegis
interface can not be configured dynamically. You set the dependency on the interface and that’s it.
Using ConfigureService()
is the important function here. The first argument for this function is the name
of the service you wish to configure. The second argument is a function. That function is given the context
and the event from Lambda. However, this configuration is applied before your event handler is called.
Filters
There are hooks or “filters” available to use here for the handler as well. The Aegis interface has
a field called Filters
for various filters, the ones most helpful to services is likely the
Filters.Handler.BeforeServices
filter.
Custom Services
3rd party services can also be added and configured. They are available under the Custom
field.
TBD - not fully implemented