diff --git a/.gitignore b/.gitignore index db876828..3e593c9a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ deploy/** .idea .vscode .vs/ +.aws-sam examples/SimpleLambda/.aws-sam examples/SimpleLambda/samconfig.toml @@ -20,4 +21,5 @@ AWS.Lambda.Powertools.sln.DotSettings.user .DS_Store dist/ -site/ \ No newline at end of file +site/ +samconfig.toml \ No newline at end of file diff --git a/README.md b/README.md index 668e8e05..1977bd2f 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,8 @@ We have provided examples focused specifically on each of the utilities. Each so * **[Logging example](examples/Logging/)** * **[Metrics example](examples/Metrics/)** * **[Tracing example](examples/Tracing/)** +* **[Serverless API example](examples/ServerlessApi/)** + * **[Parameters example](examples/Parameters/)** * **[Idempotency example](examples/Idempotency)** diff --git a/docs/index.md b/docs/index.md index 9870eaf4..fd343df7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -104,6 +104,7 @@ We have provided a few examples that should you how to use the each of the core * [Tracing](https://github.com/aws-powertools/powertools-lambda-dotnet/tree/main/examples/Tracing){target="_blank"} * [Logging](https://github.com/aws-powertools/powertools-lambda-dotnet/tree/main/examples/Logging/){target="_blank"} * [Metrics](https://github.com/aws-powertools/powertools-lambda-dotnet/tree/main/examples/Metrics/){target="_blank"} +* [Serverless API example](https://github.com/aws-powertools/powertools-lambda-dotnet/tree/main/examples/ServerlessApi/){target="_blank"} * [Parameters](https://github.com/aws-powertools/powertools-lambda-dotnet/tree/main/examples/Parameters/){target="_blank"} * [Idempotency](https://github.com/aws-powertools/powertools-lambda-dotnet/tree/main/examples/Idempotency/){target="_blank"} diff --git a/examples/ServerlessApi/PowertoolsServerlessApiExample.sln b/examples/ServerlessApi/PowertoolsServerlessApiExample.sln new file mode 100644 index 00000000..bf7f1df1 --- /dev/null +++ b/examples/ServerlessApi/PowertoolsServerlessApiExample.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{97038683-80A8-4DEC-B494-E774D5D7CA8E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LambdaPowertoolsAPI", "src\LambdaPowertoolsAPI\LambdaPowertoolsAPI.csproj", "{07541883-22A3-49AB-9A7B-3E2C8CC347F5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{744F4880-67A3-4BC9-B4E7-D879EC646492}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LambdaPowertoolsAPI.Tests", "test\LambdaPowertoolsAPI.Tests\LambdaPowertoolsAPI.Tests.csproj", "{151BBDCE-FB35-4025-A925-BEDD50DDCD9F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {07541883-22A3-49AB-9A7B-3E2C8CC347F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07541883-22A3-49AB-9A7B-3E2C8CC347F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07541883-22A3-49AB-9A7B-3E2C8CC347F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07541883-22A3-49AB-9A7B-3E2C8CC347F5}.Release|Any CPU.Build.0 = Release|Any CPU + {151BBDCE-FB35-4025-A925-BEDD50DDCD9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {151BBDCE-FB35-4025-A925-BEDD50DDCD9F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {151BBDCE-FB35-4025-A925-BEDD50DDCD9F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {151BBDCE-FB35-4025-A925-BEDD50DDCD9F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {07541883-22A3-49AB-9A7B-3E2C8CC347F5} = {97038683-80A8-4DEC-B494-E774D5D7CA8E} + {151BBDCE-FB35-4025-A925-BEDD50DDCD9F} = {744F4880-67A3-4BC9-B4E7-D879EC646492} + EndGlobalSection +EndGlobal diff --git a/examples/ServerlessApi/Readme.md b/examples/ServerlessApi/Readme.md new file mode 100644 index 00000000..0da85b50 --- /dev/null +++ b/examples/ServerlessApi/Readme.md @@ -0,0 +1,243 @@ +# AWS Lambda Powertools for .NET - Web Api Example + +This project contains the source code and supporting files for a web api hosted on AWS Lambda that you can deploy with the AWS Serverless Application Model Command Line Interface (AWS SAM CLI). It includes the following files and folders. + +* src - Code for the application's Lambda function and Project Dockerfile. +* src/LambdaPowertoolsAPI/serverless.template - A template that defines the application's AWS resources. +* events- Invocation events that you can use to invoke the function. +* test - Unit tests for the application code. + + +The application uses several AWS resources, including Lambda functions and an API Gateway API. The project has been created using `Lambda ASP.NET Core Web API` [.NET Core project templates](https://github.com/aws/aws-lambda-dotnet/tree/master#amazonlambdaaspnetcoreserver). The `events` folder contains an exampleAPI Gateway proxy Lambda event and is not offered by the project template. + +If you prefer to use an integrated development environment (IDE) to build and test your application, you can use the AWS Toolkit. The AWS Toolkit is an open source plug-in for popular IDEs that uses the AWS SAM CLI to build and deploy serverless applications on AWS. The AWS Toolkit also adds a simplified step-through debugging experience for Lambda function code. See the following links to get started. + +* [Visual Studio Code](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/welcome.html) +* [Visual Studio](https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/welcome.html) +* [Rider](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) + + +## Deploy the sample application + +The AWS SAM CLI is an extension of the AWS Command Line Interface (AWS CLI) that adds functionality for building and testing Lambda applications. It uses Docker to run your functions in an Amazon Linux environment that matches Lambda. It can also emulate your application's build environment and API. + +To use the AWS SAM CLI, you need the following tools. + +* AWS SAM CLI - [Install the AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) +* Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) + +You will need the following for local testing. +* .NET 6.0 - [Install .NET 6.0](https://www.microsoft.com/net/download) + +To build and deploy your application for the first time, run the following in your shell. Make sure the `serverless.template` file is in your current directory: + +```bash +sam build -t ./serverless.template +sam deploy --guided +``` + +The first command will build a docker image from a Dockerfile and then copy the source of your application inside the Docker image. The second command will package and deploy your application to AWS, with a series of prompts: + +* **Stack Name**: The name of the stack to deploy to CloudFormation. This should be unique to your account and region, and a good starting point would be something matching your project name. A rest of the document will assume it is `aws-lambda-powertools-web-api-sample` +* **AWS Region**: The AWS region you want to deploy your app to. The rest of the document will assume that stack name is `eu-central-1` +* **Confirm changes before deploy**: If set to yes, any change sets will be shown to you before execution for manual review. If set to no, the AWS SAM CLI will automatically deploy application changes. +* **Allow SAM CLI IAM role creation**: Many AWS SAM templates, including this example, create AWS IAM roles required for the AWS Lambda function(s) included to access AWS services. By default, these are scoped down to minimum required permissions. To deploy an AWS CloudFormation stack which creates or modifies IAM roles, the `CAPABILITY_IAM` value for `capabilities` must be provided. If permission isn't provided through this prompt, to deploy this example you must explicitly pass `--capabilities CAPABILITY_IAM` to the `sam deploy` command. +* **Save arguments to samconfig.toml**: If set to yes, your choices will be saved to a configuration file inside the project, so that in the future you can just re-run `sam deploy` without parameters to deploy changes to your application. + +You can find your API Gateway Endpoint URL in the output values displayed after deployment. Remember that an attribute on `ValuesController` makes the resources available under `/api/values` so you need to at the end of the URL. + +## Code Walkthrough + +Once we have deployed the application on an AWS account we can execute below command to check if everything works. + +```bash +curl -i api/values +``` + +where `` is an output value from the `sam deploy` command. The result should be something like + +``` +HTTP/2 200 +content-type: application/json; charset=utf-8 +content-length: 19 +date: Sat, 27 May 2023 20:18:52 GMT +x-amzn-requestid: 1f6ec544-90ea-419f-b7a7-eed798bd5977 +x-amz-apigw-id: FmTSnEH6FiAFnkQ= +x-amzn-trace-id: Root=1-647265aa-75513ad44dcd2e0f40ba1468;Sampled=1;lineage=383de489:0 +x-cache: Miss from cloudfront +via: 1.1 d5bd9c82cbbad6f05501bb737b3688dc.cloudfront.net (CloudFront) +x-amz-cf-pop: WAW51-P3 +x-amz-cf-id: X3PY9DHBUaduHJqjpOzk1KhrYVr6SoivVghabML4X_Rcq3AcX7uutA== + +["value1","value2"] +``` + +If something went wrong you can use logs and tracing commands, described in one of the below sections, to debug. + +The Lambda hosted API is a default one created by `Lambda ASP.NET Core Web API` [.NET Core project templates](https://github.com/aws/aws-lambda-dotnet/tree/master#amazonlambdaaspnetcoreserver). To enhance the API with AWS Lambda Powertools, we didn't need to change the Kestrel start up code nor the logic of an API. We added standard logging commands, metrics emission, and tracing instrumentation code. + +### Logging +Logging uses the context data available in an AWS Lambda Event. This is the reason why to have information about function name, its memory size etc `[Logging]` attribute needs to be placed on a method with the proper input parameters. The handler function seams like a good choice. [`LambdaEntryPoint`](src/LambdaPowertoolsAPI/LambdaEntryPoint.cs) overrides the handler method (`FunctionHandlerAsync`) from the abstract class `Amazon.Lambda.AspNetCoreServer.FunctionHandlerAsync`. The method is pointed as the handler in the function definition in the SAM template. + +```yaml + AspNetCoreFunction: + Type: AWS::Serverless::Function + Properties: + Handler: LambdaPowertoolsAPI::LambdaPowertoolsAPI.LambdaEntryPoint::FunctionHandlerAsync +``` + +The template defines a log level and service names through environment variables in the [SAM Template](src/LambdaPowertoolsAPI/serverless.template) + +```yaml +POWERTOOLS_SERVICE_NAME: aws-lambda-powertools-web-api-sample +POWERTOOLS_LOG_LEVEL: Debug +``` + +In this example we point to the correlation ID and instruct PowerTools to log an incoming event. + +```c# +[Logging(CorrelationIdPath = CorrelationIdPaths.ApiGatewayRest, LogEvent = true)] // we are enabling logging, it needs to be added on method which have Lambda event +``` + +Later on in the `Get` method of the [`ValuesController`](src/LambdaPowertoolsAPI/Controllers/ValuesController.cs) logs as it would normally do using a `Logger`. The log output contains all additional information injected by the AWS Lambda Powertools: + +```json +{"cold_start":true,"correlation_id":"4749a5a8-93ea-464e-8778-3bffc5f9a35d","function_name":"AspNetCoreFunction","function_version":"$LATEST","function_memory_size":256,"function_arn":"arn:aws:lambda:us-east-1:012345678912:function:AspNetCoreFunction","function_request_id":"150991e0-20ec-4776-acd8-0f9290f1e968","timestamp":"2023-06-05T10:18:02.2434792Z","level":"Information","service":"aws-lambda-powertools-web-api-sample","name":"AWS.Lambda.Powertools.Logging.Logger","message":"Log entry information only about getting values? Or maybe something more "} +``` + +### Metrics + +Metrics does not use any context data in the background. The Metrics initialization can occur on any method. The good practice would be to define [Namespace, service and some additional default dimensions](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html) which will be used for every Metric emit call in the single place. The entry point, once again, sounds as the best choice.(_defaultDimensions);`. +```c# + // We are defining some default dimensions. + private Dictionary _defaultDimensions = new Dictionary{ + {"Environment", Environment.GetEnvironmentVariable("ENVIRONMENT") ?? "Unknown"}, + {"Runtime",Environment.Version.ToString()} + }; + + [LambdaSerializer(typeof(DefaultLambdaJsonSerializer))] + [Logging(CorrelationIdPath = CorrelationIdPaths.ApiGatewayRest, LogEvent = true)] // we are enabling logging, it needs to be added on method which have Lambda event + [Tracing] // Adding a tracing attribute here we will see additional function call which might be important in terms of debugging + [Metrics] // Metrics need to be initialized. The best place is the entry point opposite on adding attributes on each controller. + public override Task FunctionHandlerAsync(APIGatewayProxyRequest request, ILambdaContext lambdaContext) + { + if (!_defaultDimensions.ContainsKey("Version")) + _defaultDimensions.Add("Version", lambdaContext.FunctionVersion ?? "Unknown"); + + // Setting the default dimensions. They will be added to every emitted metric. + Metrics.SetDefaultDimensions(_defaultDimensions); + +``` +The `_defaultDimensions` is a variable which will store the default dimensions. They are set as the defaults using`Metrics.SetDefaultDimensions + +In this example the Namespace and service are set in the [SAM template](src/LambdaPowertoolsAPI/serverless.template) and they will be added to each Metric automatically. Thus when a resource `GetById` will be called and a `SuccessfulRetrieval` metric will be emitted it will have all default dimensions, namespace and service. It can be observed in the logs or via AWS Console. + +```log +2023/05/27/[$LATEST]7e90626ab88447328171eef7c4c3286f 2023-05-27T20:46:42.883000 2023-05-27T20:46:42.883Z 6408f2f1-ecdc-4f02-9480-1da3ed0a0936 info {"_aws":{"Timestamp":1685220402624,"CloudWatchMetrics":[{"Namespace":"AWSLambdaPowertools","Metrics":[{"Name":"SuccessfulRetrieval","Unit":"Count"}],"Dimensions":[["Service"],["Environment"],["Runtime"],["Version"]]}]},"Service":"aws-lambda-powertools-web-api-sample","Environment":"Unknown","Runtime":"6.0.15","Version":"$LATEST","SuccessfulRetrieval":1} +``` + +### Tracing + +Tracing is enabled by setting the `Tracing` property in the [SAM Template](src/LambdaPowertoolsAPI/serverless.template) to `true`. To see more insides in what are execution times of each endpoint `GET /api/values` and `GET /api/values/{id}` are annotated with the `[TracingAttribute]`. The segments has been named to different them in traces. On the `Get` method, `SegmentName` has been set using property of `Tracing` attribute. + +```c# + [Tracing(SegmentName = "Values::GetById")] + public string Get(int id) +``` + +When analyzing a trace for execution of a resource `GetById` it can be observed `Values::GetById` which is equal to `SegmentName`. With the segment name definition API does not need to be verified to understand which `method` was executed + +```log +XRay Event at (2023-05-27T22:46:39.922000) with id (1-64726c2f-3244a3317c5e1f4f738e532c) and duration (3.160s) + - 3.103s - aws-lambda-powertools-web-api-s-AspNetCoreFunction-ottogomkubjL [HTTP: 200] + - 2.517s - aws-lambda-powertools-web-api-s-AspNetCoreFunction-ottogomkubjL + - 0.489s - Initialization + - 2.459s - Invocation + - 0.961s - ## FunctionHandlerAsync + - 0.038s - Values::GetById + - 0.057s - Overhead` +``` + +## Use the AWS SAM CLI to build and test locally + +Build your application with the `sam build` command. + +```bash +sam build +``` + +The AWS SAM CLI builds a docker image from a Dockerfile and then installs dependencies defined in `src/LambdaPowertoolsAPI.csproj` inside the docker image. The processed template file is saved in the `.aws-sam/build` folder. + +Test a single function by invoking it directly with a test event. An event is a JSON document that represents the input that the function receives from the event source. Test events are included in the `events` folder in this project. + +Run functions locally and invoke them with the `sam local invoke` command. You should be in the same folder as `serverless.template` + +```bash +sam local invoke -e ../../events/event.json +``` + +The AWS SAM CLI can also emulate your application's API. Use the `sam local start-api` to run the API locally on port 3000. + +```bash +sam local start-api +curl http://localhost:3000/api/values +``` +The AWS SAM CLI reads the application template to determine the API's routes and the functions that they invoke. The `Events` property on each function's definition includes the route and method for each path. + +```yaml + Events: + ProxyResource: + Type: Api + Properties: + Path: "/{proxy+}" + Method: ANY + RootResource: + Type: Api + Properties: + Path: "/" + Method: ANY +``` + + +## Fetch, tail, and filter Lambda function logs + +To simplify troubleshooting, AWS SAM CLI has a command `sam logs`. `sam logs` lets you fetch logs generated by your deployed Lambda function from the command line. In addition to printing the logs on the terminal, this command has several nifty features to help you quickly find the bug. + +`NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM. + +```bash +sam logs -n AspNetCoreFunction --stack-name aws-lambda-powertools-web-api-sample --tail +``` + + +## Lambda function tracing + +To simplify troubleshooting, AWS SAM CLI has a command `sam traces`. `sam traces` lets you fetch traces generated by your deployed Api Gateway and Lambda and Api function from the command line. To see the trace for the given In addition to printing the logs on the terminal, this command has several nifty features to help you quickly find the bug. + +`NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM. + +```bash +sam traces --trace-id +``` + +where `` should be replaced with value of a `x-amzn-trace-id` response header. To see response headers you should pass `-i` to the `curl` command. + +## Add a resource to your application + +The application template uses AWS Serverless Application Model (AWS SAM) to define application resources. AWS SAM is an extension of AWS CloudFormation with a simpler syntax for configuring common serverless application resources such as functions, triggers, and APIs. For resources not included in [the AWS SAM specification](https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md), you can use standard [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) resource types. + + +## Cleanup + +To delete the sample application that you created, use the AWS SAM CLI. Assuming you used your project name for the stack name, you can run the following: + +```bash +sam delete +``` + +## Resources + +See the [AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) for an introduction to SAM specification, the AWS SAM CLI, and serverless application concepts. + +Next, you can use AWS Serverless Application Repository to deploy ready to use Apps that go beyond hello world samples and learn how authors developed their applications: [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/) + diff --git a/examples/ServerlessApi/events/event.json b/examples/ServerlessApi/events/event.json new file mode 100644 index 00000000..e2025f0c --- /dev/null +++ b/examples/ServerlessApi/events/event.json @@ -0,0 +1,10 @@ +{ + "resource": "/{proxy+}", + "path": "api/values", + "httpMethod": "GET", + "body": "{\r\n\t\"message\": \"Hello\"\r\n}", + "isBase64Encoded": false, + "requestContext": { + "requestId": "4749a5a8-93ea-464e-8778-3bffc5f9a35d" + } +} \ No newline at end of file diff --git a/examples/ServerlessApi/events/event_getById.json b/examples/ServerlessApi/events/event_getById.json new file mode 100644 index 00000000..1f10e1ee --- /dev/null +++ b/examples/ServerlessApi/events/event_getById.json @@ -0,0 +1,10 @@ +{ + "resource": "/{proxy+}", + "path": "api/values/5", + "httpMethod": "GET", + "body": "{\r\n\t\"message\": \"Hello\"\r\n}", + "isBase64Encoded": false, + "requestContext": { + "requestId": "4749a5a8-93ea-464e-8778-3bffc5f9a35d" + } +} \ No newline at end of file diff --git a/examples/ServerlessApi/src/LambdaPowertoolsAPI/Controllers/ValuesController.cs b/examples/ServerlessApi/src/LambdaPowertoolsAPI/Controllers/ValuesController.cs new file mode 100644 index 00000000..8a9cf483 --- /dev/null +++ b/examples/ServerlessApi/src/LambdaPowertoolsAPI/Controllers/ValuesController.cs @@ -0,0 +1,71 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using Microsoft.AspNetCore.Mvc; +using System; +using AWS.Lambda.Powertools.Logging; +using AWS.Lambda.Powertools.Tracing; +using AWS.Lambda.Powertools.Metrics; + +namespace LambdaPowertoolsAPI.Controllers; + +[Route("api/[controller]")] +public class ValuesController : ControllerBase +{ + // GET api/values + [HttpGet] + [Tracing(SegmentName = "Values::Get")] + public IEnumerable Get() + { + Logger.LogInformation("Log entry information only about getting values? Or maybe something more "); + + return new string[] { "value1", "value2" }; + } + + // GET api/values/5 + [HttpGet("{id}")] + [Tracing(SegmentName = "Values::GetById")] + public string Get(int id) + { + + try + { + Metrics.AddMetric("SuccessfulRetrieval", 1, MetricUnit.Count); + } + catch (Exception e) + { + Logger.LogError("Failed to add metric", e); + } + return "value"; + } + + // POST api/values + [HttpPost] + public void Post([FromBody] string value) + { + } + + // PUT api/values/5 + [HttpPut("{id}")] + public void Put(int id, [FromBody] string value) + { + } + + // DELETE api/values/5 + [HttpDelete("{id}")] + public void Delete(int id) + { + } +} \ No newline at end of file diff --git a/examples/ServerlessApi/src/LambdaPowertoolsAPI/LambdaEntryPoint.cs b/examples/ServerlessApi/src/LambdaPowertoolsAPI/LambdaEntryPoint.cs new file mode 100644 index 00000000..a899a0f0 --- /dev/null +++ b/examples/ServerlessApi/src/LambdaPowertoolsAPI/LambdaEntryPoint.cs @@ -0,0 +1,81 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.Core; +using Amazon.Lambda.Serialization.SystemTextJson; +using AWS.Lambda.Powertools.Logging; // We are adding logging +using AWS.Lambda.Powertools.Tracing; // We are adding tracing +using AWS.Lambda.Powertools.Metrics; // We are adding metrics + +namespace LambdaPowertoolsAPI; + +/// +/// This class extends from APIGatewayProxyFunction which contains the method FunctionHandlerAsync which is the +/// actual Lambda function entry point. The Lambda handler field should be set to +/// +/// LambdaPowertoolsAPI::LambdaPowertoolsAPI.LambdaEntryPoint::FunctionHandlerAsync +/// +public class LambdaEntryPoint : + + // The base class must be set to match the AWS service invoking the Lambda function. If not Amazon.Lambda.AspNetCoreServer + // will fail to convert the incoming request correctly into a valid ASP.NET Core request. + // + // API Gateway REST API -> Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction + // API Gateway HTTP API payload version 1.0 -> Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction + // API Gateway HTTP API payload version 2.0 -> Amazon.Lambda.AspNetCoreServer.APIGatewayHttpApiV2ProxyFunction + // Application Load Balancer -> Amazon.Lambda.AspNetCoreServer.ApplicationLoadBalancerFunction + // + // Note: When using the AWS::Serverless::Function resource with an event type of "HttpApi" then payload version 2.0 + // will be the default and you must make Amazon.Lambda.AspNetCoreServer.APIGatewayHttpApiV2ProxyFunction the base class. + + Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction +{ + /// + /// The builder has configuration, logging and Amazon API Gateway already configured. The startup class + /// needs to be configured in this method using the UseStartup<>() method. + /// + /// + protected override void Init(IWebHostBuilder builder) + { + builder + .UseStartup(); + + + Console.WriteLine("Startup done"); + } + + + // We are defining some default dimensions. + private Dictionary _defaultDimensions = new Dictionary{ + {"Environment", Environment.GetEnvironmentVariable("ENVIRONMENT") ?? "Unknown"}, + {"Runtime",Environment.Version.ToString()} + }; + + [LambdaSerializer(typeof(DefaultLambdaJsonSerializer))] + [Logging(CorrelationIdPath = CorrelationIdPaths.ApiGatewayRest, LogEvent = true)] // we are enabling logging, it needs to be added on method which have Lambda event + [Tracing] // Adding a tracing attribute here we will see additional function call which might be important in terms of debugging + [Metrics] // Metrics need to be initialized the best place is entry point in opposite on adding attribute on each controller. + public override Task FunctionHandlerAsync(APIGatewayProxyRequest request, ILambdaContext lambdaContext) + { + + if (!_defaultDimensions.ContainsKey("Version")) + _defaultDimensions.Add("Version", lambdaContext.FunctionVersion ?? "Unknown"); + + // Setting the default dimensions. They will be added to every emitted metric. + Metrics.SetDefaultDimensions(_defaultDimensions); + return base.FunctionHandlerAsync(request, lambdaContext); + } +} \ No newline at end of file diff --git a/examples/ServerlessApi/src/LambdaPowertoolsAPI/LambdaPowertoolsAPI.csproj b/examples/ServerlessApi/src/LambdaPowertoolsAPI/LambdaPowertoolsAPI.csproj new file mode 100644 index 00000000..aa726e98 --- /dev/null +++ b/examples/ServerlessApi/src/LambdaPowertoolsAPI/LambdaPowertoolsAPI.csproj @@ -0,0 +1,20 @@ + + + Exe + net6.0 + enable + enable + true + Lambda + + true + + true + + + + + + + + \ No newline at end of file diff --git a/examples/ServerlessApi/src/LambdaPowertoolsAPI/LocalEntryPoint.cs b/examples/ServerlessApi/src/LambdaPowertoolsAPI/LocalEntryPoint.cs new file mode 100644 index 00000000..7ddda8fc --- /dev/null +++ b/examples/ServerlessApi/src/LambdaPowertoolsAPI/LocalEntryPoint.cs @@ -0,0 +1,34 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +namespace LambdaPowertoolsAPI; + +/// +/// The Main function can be used to run the ASP.NET Core application locally using the Kestrel webserver. +/// +public class LocalEntryPoint +{ + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); +} \ No newline at end of file diff --git a/examples/ServerlessApi/src/LambdaPowertoolsAPI/Startup.cs b/examples/ServerlessApi/src/LambdaPowertoolsAPI/Startup.cs new file mode 100644 index 00000000..9fd36222 --- /dev/null +++ b/examples/ServerlessApi/src/LambdaPowertoolsAPI/Startup.cs @@ -0,0 +1,51 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +namespace LambdaPowertoolsAPI; + +public class Startup +{ + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + + } +} \ No newline at end of file diff --git a/examples/ServerlessApi/src/LambdaPowertoolsAPI/appsettings.Development.json b/examples/ServerlessApi/src/LambdaPowertoolsAPI/appsettings.Development.json new file mode 100644 index 00000000..11931d0c --- /dev/null +++ b/examples/ServerlessApi/src/LambdaPowertoolsAPI/appsettings.Development.json @@ -0,0 +1,5 @@ +{ + "AWS": { + "Region": "" + } +} \ No newline at end of file diff --git a/examples/ServerlessApi/src/LambdaPowertoolsAPI/appsettings.json b/examples/ServerlessApi/src/LambdaPowertoolsAPI/appsettings.json new file mode 100644 index 00000000..8e3bea15 --- /dev/null +++ b/examples/ServerlessApi/src/LambdaPowertoolsAPI/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Warning" + } + } +} \ No newline at end of file diff --git a/examples/ServerlessApi/src/LambdaPowertoolsAPI/aws-lambda-tools-defaults.json b/examples/ServerlessApi/src/LambdaPowertoolsAPI/aws-lambda-tools-defaults.json new file mode 100644 index 00000000..036a86d0 --- /dev/null +++ b/examples/ServerlessApi/src/LambdaPowertoolsAPI/aws-lambda-tools-defaults.json @@ -0,0 +1,14 @@ +{ + "Information": [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help", + "All the command line options for the Lambda command can be specified in this file." + ], + "profile": "", + "region": "", + "configuration": "Release", + "s3-prefix": "LambdaPowertoolsAPI/", + "template": "serverless.template", + "template-parameters": "" +} \ No newline at end of file diff --git a/examples/ServerlessApi/src/LambdaPowertoolsAPI/samconfig.toml b/examples/ServerlessApi/src/LambdaPowertoolsAPI/samconfig.toml new file mode 100644 index 00000000..00b90f55 --- /dev/null +++ b/examples/ServerlessApi/src/LambdaPowertoolsAPI/samconfig.toml @@ -0,0 +1,10 @@ +version = 0.1 +[default.deploy.parameters] +stack_name = "aws-lambda-powertools-web-api-sample" +resolve_s3 = true +s3_prefix = "aws-lambda-powertools-web-api-sample" +region = "eu-central-1" +confirm_changeset = true +capabilities = "CAPABILITY_IAM" +disable_rollback = true +image_repositories = [] diff --git a/examples/ServerlessApi/src/LambdaPowertoolsAPI/template.yaml b/examples/ServerlessApi/src/LambdaPowertoolsAPI/template.yaml new file mode 100644 index 00000000..600bcc84 --- /dev/null +++ b/examples/ServerlessApi/src/LambdaPowertoolsAPI/template.yaml @@ -0,0 +1,45 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +--- +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: An AWS Serverless Application that uses the ASP.NET Core framework running + in Amazon Lambda. +Parameters: {} +Conditions: {} +Resources: + AspNetCoreFunction: + Type: AWS::Serverless::Function + Properties: + Handler: LambdaPowertoolsAPI::LambdaPowertoolsAPI.LambdaEntryPoint::FunctionHandlerAsync + Runtime: dotnet6 + CodeUri: '.' + MemorySize: 256 + Timeout: 30 + Role: + Tracing: Active + Architectures: + - arm64 + Policies: + - AWSLambda_FullAccess + Environment: + Variables: + POWERTOOLS_SERVICE_NAME: aws-lambda-powertools-web-api-sample # This can also be set using the Metrics decorator on your handler [Metrics(Service = "aws-lambda-powertools-web-api-sample"] + POWERTOOLS_LOG_LEVEL: Debug + POWERTOOLS_METRICS_NAMESPACE: AWSLambdaPowertools # This can also be set using the Metrics decorator on your handler [Metrics(Namespace = "AWSLambdaPowertools"] + Events: + ProxyResource: + Type: Api + Properties: + Path: "/{proxy+}" + Method: ANY + RootResource: + Type: Api + Properties: + Path: "/" + Method: ANY +Outputs: + ApiURL: + Description: API endpoint URL for Prod environment + Value: + Fn::Sub: https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/ diff --git a/examples/ServerlessApi/test/LambdaPowertoolsAPI.Tests/LambdaPowertoolsAPI.Tests.csproj b/examples/ServerlessApi/test/LambdaPowertoolsAPI.Tests/LambdaPowertoolsAPI.Tests.csproj new file mode 100644 index 00000000..25b28382 --- /dev/null +++ b/examples/ServerlessApi/test/LambdaPowertoolsAPI.Tests/LambdaPowertoolsAPI.Tests.csproj @@ -0,0 +1,30 @@ + + + net6.0 + enable + enable + False + + + + PreserveNewest + + + + + PreserveNewest + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/ServerlessApi/test/LambdaPowertoolsAPI.Tests/SampleRequests/ValuesController-Get.json b/examples/ServerlessApi/test/LambdaPowertoolsAPI.Tests/SampleRequests/ValuesController-Get.json new file mode 100644 index 00000000..4d91804b --- /dev/null +++ b/examples/ServerlessApi/test/LambdaPowertoolsAPI.Tests/SampleRequests/ValuesController-Get.json @@ -0,0 +1,34 @@ +{ + "resource": "/{proxy+}", + "path": "/api/values", + "httpMethod": "GET", + "headers": null, + "queryStringParameters": null, + "pathParameters": { + "proxy": "api/values" + }, + "stageVariables": null, + "requestContext": { + "accountId": "AAAAAAAAAAAA", + "resourceId": "5agfss", + "stage": "test-invoke-stage", + "requestId": "test-invoke-request", + "identity": { + "cognitoIdentityPoolId": null, + "accountId": "AAAAAAAAAAAA", + "cognitoIdentityId": null, + "caller": "BBBBBBBBBBBB", + "apiKey": "test-invoke-api-key", + "sourceIp": "test-invoke-source-ip", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": "arn:aws:iam::AAAAAAAAAAAA:root", + "userAgent": "Apache-HttpClient/4.5.x (Java/1.8.0_102)", + "user": "AAAAAAAAAAAA" + }, + "resourcePath": "/{proxy+}", + "httpMethod": "GET", + "apiId": "t2yh6sjnmk" + }, + "body": null +} \ No newline at end of file diff --git a/examples/ServerlessApi/test/LambdaPowertoolsAPI.Tests/ValuesControllerTests.cs b/examples/ServerlessApi/test/LambdaPowertoolsAPI.Tests/ValuesControllerTests.cs new file mode 100644 index 00000000..442ed4fe --- /dev/null +++ b/examples/ServerlessApi/test/LambdaPowertoolsAPI.Tests/ValuesControllerTests.cs @@ -0,0 +1,52 @@ +using System.Text.Json; +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using Xunit; +using Amazon.Lambda.Core; +using Amazon.Lambda.TestUtilities; +using Amazon.Lambda.APIGatewayEvents; + +namespace LambdaPowertoolsAPI.Tests; + +public class ValuesControllerTests +{ + + + [Fact] + public async Task TestGet() + { + var lambdaFunction = new LambdaEntryPoint(); + + var requestStr = File.ReadAllText("./SampleRequests/ValuesController-Get.json"); + var request = JsonSerializer.Deserialize(requestStr, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + var context = new TestLambdaContext + { + FunctionVersion = Guid.NewGuid().ToString() + }; + + var response = await lambdaFunction.FunctionHandlerAsync(request, context); + + Assert.Equal(200, response.StatusCode); + Assert.Equal("[\"value1\",\"value2\"]", response.Body); + Assert.True(response.MultiValueHeaders.ContainsKey("Content-Type")); + Assert.Equal("application/json; charset=utf-8", response.MultiValueHeaders["Content-Type"][0]); + } + + +} \ No newline at end of file diff --git a/examples/ServerlessApi/test/LambdaPowertoolsAPI.Tests/appsettings.json b/examples/ServerlessApi/test/LambdaPowertoolsAPI.Tests/appsettings.json new file mode 100644 index 00000000..6ba3a30b --- /dev/null +++ b/examples/ServerlessApi/test/LambdaPowertoolsAPI.Tests/appsettings.json @@ -0,0 +1,14 @@ +{ + "Lambda.Logging": { + "IncludeCategory": false, + "IncludeLogLevel": false, + "IncludeNewline": true, + "LogLevel": { + "Default": "Debug", + "Microsoft": "Information" + } + }, + "AWS": { + "Region": "" + } +} \ No newline at end of file