Skip to content
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: Create an official utility event_handler for lightweight event handlers #356

Closed
michaelbrewer opened this issue Mar 23, 2021 · 11 comments
Labels
Milestone

Comments

@michaelbrewer
Copy link
Contributor

michaelbrewer commented Mar 23, 2021

Key information

  • RFC PR: (leave this empty)
  • Related issue(s), if known:
  • Area: Utilities
  • Meet tenets: Yes

Summary

Create an official utility event_handler for lightweight event handlers

Motivation

Unofficially we have a very lightweight resolver for AppSync measuring only 50 lines of code, but still very useful. It would be nice to have a good location for these kind of utility and they should just depend on data_classes utility

Proposal

Create a new top level utility for event_handler and then move the AppSync resolver and start to implement other useful ones like API Gateway Proxy Event Handler while ensuring we keeping them lightweight and only depend on existing Powertools libraries

AppSync in python

from typing import Any, Dict
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent
from aws_lambda_powertools.utilities.data_classes.appsync.scalar_types_utils import (
    make_id,
    aws_date,
    aws_datetime,
    aws_time,
    aws_timestamp,
)
from aws_lambda_powertools.utilities.event_handler.appsync import (
    AppSyncResolver
)
from aws_lambda_powertools.utilities.typing import LambdaContext

logger = Logger()
app = AppSyncResolver()


@app.resolver(field_name="listLocations", include_event=True)
@app.resolver(type_name="Merchant", field_name="locations", include_event=True)
def get_locations(event: AppSyncResolverEvent, name: str = None) -> str:
    logger.info(event)
    return f"returning locations for {name}"


@app.resolver(type_name="Merchant", field_name="anotherField")
def another_field_resolver(count: int) -> Dict[str, Any]:
    return {
        "id": make_id(),
        "date": aws_date(),
        "date_time": aws_datetime(),
        "time": aws_time(),
        "ts": aws_timestamp(),
        "message": f"another_field_resolver with parameter count={count}",
    }


@app.resolver(type_name="Query", field_name="noParams")
def no_params() -> str:
    return "no_params has no params"


def handler(event: dict, context: LambdaContext) -> Any:
    return app.resolve(event, context)

API Gateway Proxy Example

from typing import Any, Dict, Tuple
from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.logging import correlation_paths
from aws_lambda_powertools.utilities.data_classes import APIGatewayProxyEvent
from aws_lambda_powertools.utilities.event_handler.appsync import (
    APIGatewayProxyResolver
)
from aws_lambda_powertools.utilities.typing import LambdaContext

logger = Logger()
tracer = Tracer()
app = APIGatewayProxyResolver()


@.get("/locations/<id>", include_event=True)
def get_locations(event: APIGatewayProxyEvent, id: str) -> Tuple[str, str, str]:
    logger.info(event)
    return ("OK", "text/plain", f"returning locations by {id}")


@app.get("/locations/with-query-string")
def another_field_resolver(count: int) -> Tuple[str, str, Dict[str, Any]]:
    return (
        "OK", 
        "application/json",
        {
            "message": f"another_field_resolver with query string parameter /locations/with-query-string?count={count}",
        }
   )


@app.get("/noParams")
def no_params() -> Tuple[str, str, str]:
    return ("OK", "text/plain", "no_params has no params")


@app.post("/test/tests/<id>",  cors=True)
def print_id(id: str, body: Dict[str, Any]) -> Tuple[str, str, str]:
    return ("OK", "plain/text", id)


@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)
@tracer.capture_lambda_handler
def handler(event: dict, context: LambdaContext) -> Dict[str, Any]:
    return app.resolve(event, context)

Drawbacks

Over time Powertools could grow quite a lot as we add more features like this, so we should start to make this more modular (or aim for this as part of V2)

Rationale and alternatives

  • What other designs have been considered? Why not them?
  • What is the impact of not doing this?

Unresolved questions

Optional, stash area for topics that need further development e.g. TBD

@michaelbrewer michaelbrewer added RFC triage Pending triage from maintainers labels Mar 23, 2021
@heitorlessa
Copy link
Contributor

heitorlessa commented Mar 23, 2021

Early stages as this need more details in the Proposal like what does UX look - Questions I have in my mind right now:

  • How non-AppSync example would look like say Kinesis Data Streams?
  • Do we replace the event?
  • Should we support a config object like we do in idempotency?
  • How is the experience today and how can this make it better and easier for customers?

As this will be a core utility, bringing in Java and Typescript folks - @saragerion @pankajagrawal16 @alan-churley @msailes @mndeveci --- Do you agree with this direction? This would mean having implemented in Java and TS too as it'd be a core feature

I'll be happy to edit the RFC and add some more details into it, as it's late here in EMEA :)

@saragerion
Copy link

saragerion commented Mar 23, 2021

This is a great idea!

But I agree with @heitorlessa. Before signing off on it, @michaelbrewer are you able to add more info in this issue detailing:

  • developer experience
  • code snippets and examples
  • benefits gained by the developer
  • whether this should be ported to other languages (without focusing on implementation detail)

@michaelbrewer
Copy link
Contributor Author

I think it should be ported to other languages, and should be easier for ones that can already support decorators or aspect orientated programming.

Benefit for the developer is they can implement a single function to handle multiple request types without doing many if/elses or having to understand how to convert an api gateway proxy event into something more like a http request

@michaelbrewer
Copy link
Contributor Author

Also the only use cases i can think of so far are for ALB, API Gateway V1 and V2 (with the same API) and AppSync.

@michaelbrewer
Copy link
Contributor Author

@heitorlessa @saragerion Initially we would be the plans for the AppSync decorator?

@heitorlessa
Copy link
Contributor

I'm happy with AppSync, just not with API Gateway as I need to think more carefully about it -- It can easily sprawl into a big thing e.g., cookies, body parser, etc.

Feel free to make a PR @michaelbrewer

@michaelbrewer
Copy link
Contributor Author

@heitorlessa

yes, For all of the http proxy based handlers (ALB and API gateway), it can get very bloated.

So I thought we could set the limit of what it can support. Like just request matching, getting the event as a data classes and return helper. All of the extra features we should point to Chalice.

For AppSync, I will start moving it to the new location :)

@michaelbrewer
Copy link
Contributor Author

@heitorlessa ok the first one is there :)

@saragerion
Copy link

saragerion commented Mar 28, 2021

Thanks for adding more information @michaelbrewer! Very useful.
I like and support your idea of a top-level utitility to handle incoming events and make things easier.

One thing I would add on my side here is a consideration of how this would play out in the TypeScript tool and Lambda Node.js runtime.

In TypeScript, utilities related to a particular AWS service will probably be separated into different distinct utilities/packages, thanks to the modularity of the Javascript SDK v3, also to optimize the size of the resulting Lambda. This modularity will likely have an impact on the implementation and the DX, which won't be exactly the same as Python.
Another consideration is that not all functionalities implemented in Python might be needed for other languages. For example in TypeScript, the developer experience is aided by types themselves: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/trigger/appsync-resolver.d.ts

When it comes to porting this to other languages, I don't think we should expect that the implementation will be exactly the same and we probably need to make a case by case judgement. But no need to think about those already now if not necessary.
What's your take @michaelbrewer?

@michaelbrewer
Copy link
Contributor Author

@saragerion

yes, each language does have it's own ecosystem. That being said there are some common things that should be true.

  1. The community should not need to manage the event sources, types and data classes. AWS should be generating this from a single source of truth much like how the AWS SDK is machine generated, so should be the AWS Lambda Event Source Data classes. So we should not have to rely on 3rd party open source projects keeping in sync with AWS.
  2. Data classes are a little more than just typing and docs, they can also include utility methods, like case insensitive lookups in http headers, or unquote plus for S3 Object keys or base64 decoding for Cloudwatch log events etc..
  3. Each language should have very fine grained modular options. Logging, Metrics, Tracing, Data Classes and Event handlers all being separate small modules. And hopefully the AWS SDK is fully modular (bootcore included)
  4. Event handlers are very hopeful as they provide a quick start for common usage pattern of lambdas in combination with a specific event source like http apis (graphql or rest).

Net result for me is that Powertools should both make development time much faster, make it easier for developers implement best practices but also runtime performance should be improved by allowing for fine grained control of what is shipped in the lambda runtime.

@michaelbrewer
Copy link
Contributor Author

FYI, the http proxy event handler is shaping up nicely too:

The idea here is that you should be able to build out a lambda compatible with all http proxy events (ALB, API Gateway and all of its variations)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Development

No branches or pull requests

3 participants