-
Notifications
You must be signed in to change notification settings - Fork 88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Support Micronaut and Spring Cloud Function (and more?) #1701
Comments
Great summary and analysis. I would split this problem into two parts 1) Identifying the handler 2) Identifying the Context.
In general the identification of the handler method has also been a challenge for other tools such as the Lambda SnapStart Spotbugs Plugin which tried to come up with all sorts of different ways one could define a handler in different frameworks such as Function, MicronautRequestHandler or Quarkus Funqy with Funq annotation. I don't think a static approach (mapping Framework to ways it allows to implement handlers) will work in the long run because technically one does not have to follow any interfaces to implement the handler. For example, it is totally valid to do this: public class UnicornRequestIdHandler {
public String handleRequest(Map<String, Object> input, Context context) {
var requestId = context.getAwsRequestId();
return "Received request with id: %s".formatted(requestId);
}
} Working backwards from this simplest way to implement a handler - how are we able to identify this as a handler? Just from pure code analysis or static mapping we can't - however maybe we could introduce an annotation that lets users identify themselves that this is a handler? public class UnicornRequestIdHandler {
@LambdaHandler
public String handleRequest(Map<String, Object> input, Context context) {
var requestId = context.getAwsRequestId();
return "Received request with id: %s".formatted(requestId);
}
} Similar to the Quarkus Funq annoation. Or in the case of Powertools - Couldn't we intrinsically identify methods that I annotate with @logging, @Tracing and @metrics as a handler? as it wouldn't make much sense to annotate anything else with them.
Technically, one could even remove the Context object in the previous example. Starting from this - how would logging or any other module that relies on the context work? Does it make sense to have a fallback mechanisms that works without the context in the worst case? I think you have gathered all relevant ways on how the context can be retrieved (Context Object as seperate input argument to the handler, wrapper of input event with Message<> or injected like Micronaut/Quarkus. Again, would it make sense to automatically catch the most prominent cases (Input argument, message wrapper) but if we can't figure it out provide an explicit Context annotation (Especially for the injected ones)? Spoiler: I'm not deep into the Powertools implementation and therefore not aware if the above thoughts/proposals would be easily achievable but they should rather be a basis for discussion. |
Thanks @maschnetwork for the feedback, it's great to have another vision of the problem, and to actually put things in perspective (for example the fact we don't need to implement any interface). Since the beginning, Powertools was based on this postulate that a function necessarily implements The thing we're trying to achieve with this RFC is to start opening Powertools (other frameworks on top of Lambda first, maybe even more later). Assuming that Also one of our tenet is to simplify developer's life. ➡️ I think we should continue to assume that standard handlers implement |
Good callout that it's not possible to actually infer that Tracing, Logging annotations etc. are always placed on a handler. Then the @LamdaHandler might be more appropriate. That being said and for completeness: It is not given that Spring Cloud Function implementation will always be a class that implements Functional interface and therefore @Bean
public Function<String, String> uppercase() {
return value -> value.toUpperCase();
} This again shows that there is hardly any standard pattern you can derive the handler method from I'm afraid. Regarding context: For completeness afaik you can do also this with Spring Cloud Functions instead of the Message<> wrapper: @Autowired
private Context context; Would it make sense to first try to infer it from the handler methods input arguments and provide a second variant for injected contexts? (Maybe even @LambdaContext)? |
I tried to autowire the context but looks like the bean doesn't exist:
Regarding the |
Key information
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
orRequestStreamHandler
and don't implement thehandleRequest
method. Spring Cloud Function leveragesjava.util.function.Function
and Micronaut providesMicronautRequestHandler
orMicronautRequestStreamHandler
. Thus, users cannot use the@Logging
,@Tracing
and@Metrics
annotations from Powertools which specifically apply on thehandleRequest
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 areplacedOnRequestHandler
orplacedOnStreamHandler
. 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:
isHandlerMethod
is used for log sampling, but we don't really need to be on a handler.isHandlerMethod
is used to limit the annotation on handlers, but we could open it.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 MetricsProposal
Spring Cloud Function
This framework leverages
java.util.function.Function
with theFunctionInvoker
. Users need to create a Function and implement theapply()
method which takes the event as parameter and return the response. Context is not directly available. To get the context, users can use theMessage
class from Spring:➡️ 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
orMicronautRequestStreamHandler
and override theexecute()
method which takes the event as parameter and return the response. Context is not directly accessible but can be injected:➡️ 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 aServiceLoader
to load a service that will implement the following interface: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 thataccept
this kind of Object, and use that one to retrieve the information.Advantages:
Drawbacks:
LambdaHandlerProcessor
...Solution 2: Using ServiceLoader and global process method
In the common module,
LambdaHandlerProcessor
should use aServiceLoader
to load a service that will implement the following interface: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:
Drawbacks:
Solution 3: Use different pointcuts in the aspect
We can add pointcuts for each ways of handling lambda function:
@Around("execution(@Logging * *.apply(..)) && this(java.util.function.Function)")
@Around("execution(@Logging * *.handleRequest(..)) && this(com.amazonaws.services.lambda.runtime.RequestHandler)")
... (we'd need to test the best pointcuts)
Advantages:
Drawbacks:
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:
Advantages:
Drawbacks:
if
in here to add potential other frameworks.The text was updated successfully, but these errors were encountered: