Description
Key information
- RFC PR: (leave this empty)
- Related issue(s), if known:
- Area: Logger, Tracer, Metrics
- Meet tenets: Yes
Summary
Allow Spring Cloud Function and Micronaut users to leverage Powertools, as they don't use the standard lambda programming model (RequestHandler
/ handleRequest()
)
Motivation
When using Spring Cloud Function and Micronaut, users don't implement RequestHandler
or RequestStreamHandler
and don't implement the handleRequest
method. Spring Cloud Function leverages java.util.function.Function
and Micronaut provides MicronautRequestHandler
or MicronautRequestStreamHandler
. Thus, users cannot use the @Logging
, @Tracing
and @Metrics
annotations from Powertools which specifically apply on the handleRequest
method.
We want to allow users to leverage Powertools when using one of these (common) frameworks, thus we need to adapt Powertools.
Current state
LambdaHandlerProcessor
from the common module is used to verify if annotations are placedOnRequestHandler
or placedOnStreamHandler
. We test these to retrieve information from event (event itself, correlation id) in logging.
Concretely, we don't really need to be on a handler method to perform any of the powertools core features:
- In Logging,
isHandlerMethod
is used for log sampling, but we don't really need to be on a handler. - In Metrics,
isHandlerMethod
is used to limit the annotation on handlers, but we could open it. - In Tracing,
isHandlerMethod
is used to add some annotations, that's ok if we are not in handler, we don't add them.
We leverage the extractContext()
method to get Lambda context in Logging and Metrics
Proposal
- We can continue to check if we are in a handler to retrieve the event or context, but we should not use it to block a feature (like in Metrics).
- We need to be able to extract event & context when using Spring Cloud Function and Micronaut:
Spring Cloud Function
This framework leverages java.util.function.Function
with the FunctionInvoker
. Users need to create a Function and implement the apply()
method which takes the event as parameter and return the response. Context is not directly available. To get the context, users can use the Message
class from Spring:
public class MyFunction implements Function<Message<APIGatewayProxyRequestEvent>, APIGatewayProxyResponseEvent> {
@Override
public APIGatewayProxyResponseEvent apply(Message<APIGatewayProxyRequestEvent> message) {
Context context = message.getHeaders().get(AWSLambdaUtils.AWS_CONTEXT, Context.class);
APIGatewayProxyRequestEvent event = message.getPayload();
// ...
}
}
➡️ we can get the event with message payload
➡️ we can get the context with message headers. Requires the developer to actually use Message
...
Micronaut
With Micronaut, users need to extend MicronautRequestHandler
or MicronautRequestStreamHandler
and override the execute()
method which takes the event as parameter and return the response. Context is not directly accessible but can be injected:
public class MyHandler extends MicronautRequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
@Any
BeanProvider<Context> context;
@Override
public APIGatewayProxyResponseEvent execute(APIGatewayProxyRequestEvent requestEvent) {
}
@NonNull
@Override
protected ApplicationContextBuilder newApplicationContextBuilder() {
return new LambdaApplicationContextBuilder();
}
}
➡️ we can get the event with the execute()
method's 1st parameter
➡️ context is a bit harder to get as it's not in the execute method but as a field in the class. Requires the developer to actually inject it...
There are other options I didn't analyse yet:
Potential solutions
Solution 1: Using ServiceLoader and granular methods
In the common module, LambdaHandlerProcessor
should use a ServiceLoader
to load a service that will implement the following interface:
public interface HandlerProcessor {
/**
* Determine if this handler can process the given join point.
* @param pjp
* @return true if this handler can process the given join point, false otherwise.
*/
boolean accept(final ProceedingJoinPoint pjp);
/**
* Determine if this join point is a handler method:
* - lambda <code>handleRequest</code>,
* - Spring cloud <code>apply</code> function,
* - micronaut <code>execute</code> function,
* - ...
* This method is called only if {@link #accept(ProceedingJoinPoint)} returns true.
* @param pjp
* @return true if this join point is a handler method (or equivalent), false otherwise.
*/
boolean isHandlerMethod(final ProceedingJoinPoint pjp);
/**
* Extract the Lambda context from the given join point.
* This method is called only if {@link #accept(ProceedingJoinPoint)} returns true.
* @param pjp
* @return the Lambda context
*/
Context extractContext(final ProceedingJoinPoint pjp);
/**
* Extract the Lambda event from the given join point.
* This method is called only if {@link #accept(ProceedingJoinPoint)} returns true.
* @param pjp
* @return the Lambda event
*/
Object extractEvent(final ProceedingJoinPoint pjp);
}
We'll need to add small libraries for Spring / Micronaut users to add their implementation.
The LambdaHandlerProcessor
will iterate on available implementations to find one that accept
this kind of Object, and use that one to retrieve the information.
Advantages:
- With this option we keep the logic in only one class (the aspect), and we just delegate the "lambda stuff" to the service.
- It's extensible, we can easily add other frameworks by adding a new service/implementation of this interface.
Drawbacks:
- ServiceLoader can add a bit of latency at initialisation of the function (cold start).
- We add significant amount of code for frameworks that people might not use.
- We might need to restructure the code to avoid looping against available services for each method in the
LambdaHandlerProcessor
...
Solution 2: Using ServiceLoader and global process method
In the common module, LambdaHandlerProcessor
should use a ServiceLoader
to load a service that will implement the following interface:
public interface HandlerProcessor {
/**
* Determine if this handler can process the given join point.
* @param pjp
* @return true if this handler can process the given join point, false otherwise.
*/
boolean accept(final ProceedingJoinPoint pjp);
/**
* Process the handler adding the required feature (logging, tracing, metrics)
* @param pjp
* @return true if this handler can process the given join point, false otherwise.
*/
Object process(final ProceedingJoinPoint pjp) throws Throwable;
}
We'll need to add small libraries for Spring / Micronaut users to add their implementation.
With this structure, the majority of the code in the aspect will move to the implementation of this processor in the process method.
Advantages:
- It's extensible, we can easily add other frameworks by adding a new service/implementation of this interface.
- It's flexible, it permits to potentially operate differently when in a framework vs Lambda standard.
- Looks simpler than the solution 1.
Drawbacks:
- Potentially duplicate code between the diverse implementations
- ServiceLoader can add a bit of latency at initialisation of the function (cold start).
- We add significant amount of code for frameworks that people might not use.
Solution 3: Use different pointcuts in the aspect
We can add pointcuts for each ways of handling lambda function:
- Spring Cloud Function:
@Around("execution(@Logging * *.apply(..)) && this(java.util.function.Function)")
- Lambda standard handler:
@Around("execution(@Logging * *.handleRequest(..)) && this(com.amazonaws.services.lambda.runtime.RequestHandler)")
... (we'd need to test the best pointcuts)
Advantages:
- We don't increase the latency too much, just adding a bit of code, no ServiceLoader
Drawbacks:
- The Aspects will grow quite a lot
- It does not fully solve the initial problem, we still need some code to retrieve context and event.
- Less flexible/extensible, we need to add pointcuts and code in the aspect if we want to add other frameworks
Rationale and alternatives
To reduce some of the drawbacks of these 3 solutions, we can probably use a factory and simply check for class presence to get the right implementation:
public class LambdaHandlerFactory {
public static LambdaHandlerProcessor getProcessor(Object handler) {
if (handler instanceof RequestHandler || handler instanceof RequestStreamHandler) {
return new StandardLambdaProcessor();
}
if (isClassPresent("org.springframework.cloud.function.adapter.aws.SpringBootRequestHandler")
&& handler instanceof Function) {
return new SpringCloudFunctionProcessor();
}
if (isClassPresent("io.micronaut.function.aws.MicronautRequestHandler")
&& handler.getClass().getSuperclass().getSimpleName().startsWith("MicronautRequest")) {
return new MicronautProcessor();
}
throw new IllegalArgumentException("No processor found for handler: " + handler.getClass());
}
private static boolean isClassPresent(String className) {
try {
Class.forName(className, false, this.getClass().getClassLoader());
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
}
Advantages:
- This can solve the ServiceLoader problem.
- It can also help extracting code from the Aspect and moving it to the processors.
Drawbacks:
- It does not solve the extensibility problem, we'll need to add a
if
in here to add potential other frameworks.
Metadata
Metadata
Assignees
Type
Projects
Status