From 1690fefb6974bb08e22ee689b3a79ef23f1c0cb5 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Fri, 21 Feb 2025 19:11:30 +0000
Subject: [PATCH 01/29] feat(metrics): add ASP.NET Core metrics package with
cold start tracking and middleware support for aspnetcore. Docs
---
docs/core/metrics-v2.md | 884 ++++++++++++++++++
docs/core/metrics.md | 2 +-
libraries/AWS.Lambda.Powertools.sln | 30 +
...ambda.Powertools.Metrics.AspNetCore.csproj | 23 +
.../Http/MetricsEndpointExtensions.cs | 37 +
.../Http/MetricsFilter.cs | 57 ++
.../Http/MetricsHelper.cs | 76 ++
.../Http/MetricsMiddlewareExtensions.cs | 41 +
.../InternalsVisibleTo.cs | 18 +
.../README.md | 149 +++
libraries/src/Directory.Packages.props | 2 +
...Powertools.Metrics.AspNetCore.Tests.csproj | 32 +
.../MetricsFilterTests.cs | 110 +++
.../MetricsHelperTests.cs | 84 ++
.../MetricsMiddlewareExtensionsTests.cs | 99 ++
.../Handlers/DefaultDimensionsHandler.cs | 7 +-
.../Handlers/FunctionHandlerTests.cs | 8 +-
mkdocs.yml | 1 +
18 files changed, 1653 insertions(+), 7 deletions(-)
create mode 100644 docs/core/metrics-v2.md
create mode 100644 libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/AWS.Lambda.Powertools.Metrics.AspNetCore.csproj
create mode 100644 libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsEndpointExtensions.cs
create mode 100644 libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsFilter.cs
create mode 100644 libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsHelper.cs
create mode 100644 libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsMiddlewareExtensions.cs
create mode 100644 libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/InternalsVisibleTo.cs
create mode 100644 libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/README.md
create mode 100644 libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj
create mode 100644 libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsFilterTests.cs
create mode 100644 libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsHelperTests.cs
create mode 100644 libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsMiddlewareExtensionsTests.cs
diff --git a/docs/core/metrics-v2.md b/docs/core/metrics-v2.md
new file mode 100644
index 00000000..af5f6859
--- /dev/null
+++ b/docs/core/metrics-v2.md
@@ -0,0 +1,884 @@
+---
+title: Metrics V2
+description: Core utility
+---
+
+Metrics creates custom metrics asynchronously by logging metrics to standard output following [Amazon CloudWatch Embedded Metric Format (EMF)](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format.html).
+
+These metrics can be visualized through [Amazon CloudWatch Console](https://aws.amazon.com/cloudwatch/).
+
+## Key features
+
+* Aggregate up to 100 metrics using a single [CloudWatch EMF](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html){target="_blank"} object (large JSON blob)
+* Validating your metrics against common metric definitions mistakes (for example, metric unit, values, max dimensions, max metrics)
+* Metrics are created asynchronously by the CloudWatch service. You do not need any custom stacks, and there is no impact to Lambda function latency
+* Context manager to create a one off metric with a different dimension
+* Ahead-of-Time compilation to native code support [AOT](https://docs.aws.amazon.com/lambda/latest/dg/dotnet-native-aot.html) from version 1.7.0
+* Support for AspNetCore middleware and filters to capture metrics for HTTP requests
+
+
+
+
+
+ Metrics showcase - Metrics Explorer
+
+
+## Installation
+
+Powertools for AWS Lambda (.NET) are available as NuGet packages. You can install the packages from [NuGet Gallery](https://www.nuget.org/packages?q=AWS+Lambda+Powertools*){target="_blank"} or from Visual Studio editor by searching `AWS.Lambda.Powertools*` to see various utilities available.
+
+* [AWS.Lambda.Powertools.Metrics](https://www.nuget.org/packages?q=AWS.Lambda.Powertools.Metrics):
+
+ `dotnet nuget add AWS.Lambda.Powertools.Metrics`
+
+## Terminologies
+
+If you're new to Amazon CloudWatch, there are two terminologies you must be aware of before using this utility:
+
+* **Namespace**. It's the highest level container that will group multiple metrics from multiple services for a given application, for example `ServerlessEcommerce`.
+* **Dimensions**. Metrics metadata in key-value format. They help you slice and dice metrics visualization, for example `ColdStart` metric by Payment `service`.
+* **Metric**. It's the name of the metric, for example: SuccessfulBooking or UpdatedBooking.
+* **Unit**. It's a value representing the unit of measure for the corresponding metric, for example: Count or Seconds.
+* **Resolution**. It's a value representing the storage resolution for the corresponding metric. Metrics can be either Standard or High resolution. Read more [here](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Resolution_definition).
+
+Visit the AWS documentation for a complete explanation for [Amazon CloudWatch concepts](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html).
+
+
+
+ Metric terminology, visually explained
+
+
+## Getting started
+
+**`Metrics`** is implemented as a Singleton to keep track of your aggregate metrics in memory and make them accessible anywhere in your code. To guarantee that metrics are flushed properly the **`MetricsAttribute`** must be added on the lambda handler.
+
+Metrics has two global settings that will be used across all metrics emitted. Use your application or main service as the metric namespace to easily group all metrics:
+
+Setting | Description | Environment variable | Constructor parameter
+------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------- | -------------------------------------------------
+**Service** | Optionally, sets **service** metric dimension across all metrics e.g. `payment` | `POWERTOOLS_SERVICE_NAME` | `Service`
+**Metric namespace** | Logical container where all metrics will be placed e.g. `MyCompanyEcommerce` | `POWERTOOLS_METRICS_NAMESPACE` | `Namespace`
+
+!!! info "Autocomplete Metric Units"
+ All parameters in **`Metrics Attribute`** are optional. Following rules apply:
+
+ - **Namespace:** **`Empty`** string by default. You can either specify it in code or environment variable. If not present before flushing metrics, a **`SchemaValidationException`** will be thrown.
+ - **Service:** **`service_undefined`** by default. You can either specify it in code or environment variable.
+ - **CaptureColdStart:** **`false`** by default.
+ - **RaiseOnEmptyMetrics:** **`false`** by default.
+
+### Full list of environment variables
+
+| Environment variable | Description | Default |
+| ------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------- |
+| **POWERTOOLS_SERVICE_NAME** | Sets service name used for tracing namespace, metrics dimension and structured logging | `"service_undefined"` |
+| **POWERTOOLS_METRICS_NAMESPACE** | Sets namespace used for metrics | `None` |
+
+### Metrics object
+
+#### Attribute
+
+The **`MetricsAttribute`** is a class-level attribute that can be used to set the namespace and service for all metrics emitted by the lambda handler.
+
+```csharp hl_lines="3"
+using AWS.Lambda.Powertools.Metrics;
+
+[Metrics(Namespace = "ExampleApplication", Service = "Booking")]
+public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
+{
+ ...
+}
+```
+
+#### Methods
+
+The **`Metrics`** class provides methods to add metrics, dimensions, and metadata to the metrics object.
+
+```csharp hl_lines="5-7"
+using AWS.Lambda.Powertools.Metrics;
+
+public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
+{
+ Metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
+ Metrics.AddDimension("Environment", "Prod");
+ Metrics.AddMetadata("BookingId", "683EEB2D-B2F3-4075-96EE-788E6E2EED45");
+ ...
+}
+```
+
+#### Initialization
+
+The **`Metrics`** object is initialized as a Singleton and can be accessed anywhere in your code.
+
+But can also be initialize with `Configure` or `Builder` patterns in your Lambda constructor, this the best option for testing.
+
+Configure:
+
+```csharp
+using AWS.Lambda.Powertools.Metrics;
+
+public Function()
+{
+ Metrics.Configure(options =>
+ {
+ options.Namespace = "dotnet-powertools-test";
+ options.Service = "testService";
+ options.CaptureColdStart = true;
+ options.DefaultDimensions = new Dictionary
+ {
+ { "Environment", "Prod" },
+ { "Another", "One" }
+ };
+ });
+}
+
+[Metrics]
+public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
+{
+ Metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
+ ...
+}
+```
+
+Builder:
+
+```csharp
+using AWS.Lambda.Powertools.Metrics;
+
+private readonly IMetrics _metrics;
+
+public Function()
+{
+ _metrics = new MetricsBuilder()
+ .WithCaptureColdStart(true)
+ .WithService("testService")
+ .WithNamespace("dotnet-powertools-test")
+ .WithDefaultDimensions(new Dictionary
+ {
+ { "Environment", "Prod1" },
+ { "Another", "One" }
+ }).Build();
+}
+
+[Metrics]
+public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
+{
+ _metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
+ ...
+}
+```
+
+
+### Creating metrics
+
+You can create metrics using **`AddMetric`**, and you can create dimensions for all your aggregate metrics using **`AddDimension`** method.
+
+=== "Metrics"
+
+ ```csharp hl_lines="5 8"
+ using AWS.Lambda.Powertools.Metrics;
+
+ public class Function {
+
+ [Metrics(Namespace = "ExampleApplication", Service = "Booking")]
+ public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
+ {
+ Metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
+ }
+ }
+ ```
+=== "Metrics with custom dimensions"
+
+ ```csharp hl_lines="8-9"
+ using AWS.Lambda.Powertools.Metrics;
+
+ public class Function {
+
+ [Metrics(Namespace = "ExampleApplication", Service = "Booking")]
+ public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
+ {
+ Metrics.AddDimension("Environment","Prod");
+ Metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
+ }
+ }
+ ```
+
+!!! tip "Autocomplete Metric Units"
+ `MetricUnit` enum facilitates finding a supported metric unit by CloudWatch.
+
+!!! note "Metrics overflow"
+ CloudWatch EMF supports a max of 100 metrics per batch. Metrics utility will flush all metrics when adding the 100th metric. Subsequent metrics, e.g. 101th, will be aggregated into a new EMF object, for your convenience.
+
+!!! warning "Metric value must be a positive number"
+ Metric values must be a positive number otherwise an `ArgumentException` will be thrown.
+
+!!! warning "Do not create metrics or dimensions outside the handler"
+ Metrics or dimensions added in the global scope will only be added during cold start. Disregard if that's the intended behavior.
+
+### Adding high-resolution metrics
+
+You can create [high-resolution metrics](https://aws.amazon.com/about-aws/whats-new/2023/02/amazon-cloudwatch-high-resolution-metric-extraction-structured-logs/) passing `MetricResolution` as parameter to `AddMetric`.
+
+!!! tip "When is it useful?"
+ High-resolution metrics are data with a granularity of one second and are very useful in several situations such as telemetry, time series, real-time incident management, and others.
+
+=== "Metrics with high resolution"
+
+ ```csharp hl_lines="9 12 15"
+ using AWS.Lambda.Powertools.Metrics;
+
+ public class Function {
+
+ [Metrics(Namespace = "ExampleApplication", Service = "Booking")]
+ public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
+ {
+ // Publish a metric with standard resolution i.e. StorageResolution = 60
+ Metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count, MetricResolution.Standard);
+
+ // Publish a metric with high resolution i.e. StorageResolution = 1
+ Metrics.AddMetric("FailedBooking", 1, MetricUnit.Count, MetricResolution.High);
+
+ // The last parameter (storage resolution) is optional
+ Metrics.AddMetric("SuccessfulUpgrade", 1, MetricUnit.Count);
+ }
+ }
+ ```
+
+!!! tip "Autocomplete Metric Resolutions"
+ Use the `MetricResolution` enum to easily find a supported metric resolution by CloudWatch.
+
+### Adding default dimensions
+
+You can use **`SetDefaultDimensions`** method to persist dimensions across Lambda invocations.
+
+=== "SetDefaultDimensions method"
+
+ ```csharp hl_lines="4 5 6 7 12"
+ using AWS.Lambda.Powertools.Metrics;
+
+ public class Function {
+ private Dictionary _defaultDimensions = new Dictionary{
+ {"Environment", "Prod"},
+ {"Another", "One"}
+ };
+
+ [Metrics(Namespace = "ExampleApplication", Service = "Booking")]
+ public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
+ {
+ Metrics.SetDefaultDimensions(_defaultDimensions);
+ Metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
+ }
+ }
+ ```
+
+### Adding default dimensions with cold start metric
+
+You can use the Builder or Configure patterns in your Lambda class constructor to set default dimensions.
+
+=== "Builder pattern"
+
+ ```csharp hl_lines="12-16"
+ using AWS.Lambda.Powertools.Metrics;
+
+ public class Function {
+ private readonly IMetrics _metrics;
+
+ public Function()
+ {
+ _metrics = new MetricsBuilder()
+ .WithCaptureColdStart(true)
+ .WithService("testService")
+ .WithNamespace("dotnet-powertools-test")
+ .WithDefaultDimensions(new Dictionary
+ {
+ { "Environment", "Prod1" },
+ { "Another", "One" }
+ }).Build();
+ }
+
+ [Metrics]
+ public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
+ {
+ _metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
+ ...
+ }
+ ```
+=== "Configure pattern"
+
+ ```csharp hl_lines="12-16"
+ using AWS.Lambda.Powertools.Metrics;
+
+ public class Function {
+
+ public Function()
+ {
+ Metrics.Configure(options =>
+ {
+ options.Namespace = "dotnet-powertools-test";
+ options.Service = "testService";
+ options.CaptureColdStart = true;
+ options.DefaultDimensions = new Dictionary
+ {
+ { "Environment", "Prod" },
+ { "Another", "One" }
+ };
+ });
+ }
+
+ [Metrics]
+ public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
+ {
+ Metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
+ ...
+ }
+ ```
+### Adding dimensions
+
+You can add dimensions to your metrics using **`AddDimension`** method.
+
+=== "Function.cs"
+
+ ```csharp hl_lines="8"
+ using AWS.Lambda.Powertools.Metrics;
+
+ public class Function {
+
+ [Metrics(Namespace = "ExampleApplication", Service = "Booking")]
+ public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
+ {
+ Metrics.AddDimension("Environment","Prod");
+ Metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
+ }
+ }
+ ```
+=== "Example CloudWatch Logs excerpt"
+
+ ```json hl_lines="11 24"
+ {
+ "SuccessfulBooking": 1.0,
+ "_aws": {
+ "Timestamp": 1592234975665,
+ "CloudWatchMetrics": [
+ {
+ "Namespace": "ExampleApplication",
+ "Dimensions": [
+ [
+ "service",
+ "Environment"
+ ]
+ ],
+ "Metrics": [
+ {
+ "Name": "SuccessfulBooking",
+ "Unit": "Count"
+ }
+ ]
+ }
+ ]
+ },
+ "service": "ExampleService",
+ "Environment": "Prod"
+ }
+ ```
+
+### Flushing metrics
+
+With **`MetricsAttribute`** all your metrics are validated, serialized and flushed to standard output when lambda handler completes execution or when you had the 100th metric to memory.
+
+You can also flush metrics manually by calling **`Flush`** method.
+
+During metrics validation, if no metrics are provided then a warning will be logged, but no exception will be raised.
+
+=== "Function.cs"
+
+ ```csharp hl_lines="9"
+ using AWS.Lambda.Powertools.Metrics;
+
+ public class Function {
+
+ [Metrics(Namespace = "ExampleApplication", Service = "Booking")]
+ public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
+ {
+ Metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
+ Metrics.Flush();
+ }
+ }
+ ```
+=== "Example CloudWatch Logs excerpt"
+
+ ```json hl_lines="2 7 10 15 22"
+ {
+ "BookingConfirmation": 1.0,
+ "_aws": {
+ "Timestamp": 1592234975665,
+ "CloudWatchMetrics": [
+ {
+ "Namespace": "ExampleApplication",
+ "Dimensions": [
+ [
+ "service"
+ ]
+ ],
+ "Metrics": [
+ {
+ "Name": "BookingConfirmation",
+ "Unit": "Count"
+ }
+ ]
+ }
+ ]
+ },
+ "service": "ExampleService"
+ }
+ ```
+
+!!! tip "Metric validation"
+ If metrics are provided, and any of the following criteria are not met, **`SchemaValidationException`** will be raised:
+
+ * Maximum of 9 dimensions
+ * Namespace is set
+ * Metric units must be [supported by CloudWatch](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_MetricDatum.html)
+
+!!! info "We do not emit 0 as a value for ColdStart metric for cost reasons. [Let us know](https://github.com/aws-powertools/powertools-lambda-dotnet/issues/new?assignees=&labels=feature-request%2Ctriage&template=feature_request.yml&title=Feature+request%3A+TITLE) if you'd prefer a flag to override it"
+
+### Raising SchemaValidationException on empty metrics
+
+If you want to ensure that at least one metric is emitted, you can pass **`RaiseOnEmptyMetrics`** to the Metrics attribute:
+
+=== "Function.cs"
+
+ ```python hl_lines="5"
+ using AWS.Lambda.Powertools.Metrics;
+
+ public class Function {
+
+ [Metrics(RaiseOnEmptyMetrics = true)]
+ public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
+ {
+ ...
+ ```
+
+### Capturing cold start metric
+
+You can optionally capture cold start metrics by setting **`CaptureColdStart`** parameter to `true`.
+
+=== "Function.cs"
+
+ ```csharp hl_lines="5"
+ using AWS.Lambda.Powertools.Metrics;
+
+ public class Function {
+
+ [Metrics(CaptureColdStart = true)]
+ public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
+ {
+ ...
+ ```
+=== "Builder pattern"
+
+ ```csharp hl_lines="9"
+ using AWS.Lambda.Powertools.Metrics;
+
+ public class Function {
+ private readonly IMetrics _metrics;
+
+ public Function()
+ {
+ _metrics = new MetricsBuilder()
+ .WithCaptureColdStart(true)
+ .WithService("testService")
+ .WithNamespace("dotnet-powertools-test")
+ }
+
+ [Metrics]
+ public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
+ {
+ _metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
+ ...
+ }
+ ```
+=== "Configure pattern"
+
+ ```csharp hl_lines="11"
+ using AWS.Lambda.Powertools.Metrics;
+
+ public class Function {
+
+ public Function()
+ {
+ Metrics.Configure(options =>
+ {
+ options.Namespace = "dotnet-powertools-test";
+ options.Service = "testService";
+ options.CaptureColdStart = true;
+ });
+ }
+
+ [Metrics]
+ public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
+ {
+ Metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
+ ...
+ }
+ ```
+
+If it's a cold start invocation, this feature will:
+
+* Create a separate EMF blob solely containing a metric named `ColdStart`
+* Add `FunctionName` and `Service` dimensions
+
+This has the advantage of keeping cold start metric separate from your application metrics, where you might have unrelated dimensions.
+
+## Advanced
+
+### Adding metadata
+
+You can add high-cardinality data as part of your Metrics log with `AddMetadata` method. This is useful when you want to search highly contextual information along with your metrics in your logs.
+
+!!! info
+ **This will not be available during metrics visualization** - Use **dimensions** for this purpose
+
+!!! info
+ Adding metadata with a key that is the same as an existing metric will be ignored
+
+=== "Function.cs"
+
+ ```csharp hl_lines="9"
+ using AWS.Lambda.Powertools.Metrics;
+
+ public class Function {
+
+ [Metrics(Namespace = ExampleApplication, Service = "Booking")]
+ public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
+ {
+ Metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
+ Metrics.AddMetadata("BookingId", "683EEB2D-B2F3-4075-96EE-788E6E2EED45");
+ ...
+ ```
+
+=== "Example CloudWatch Logs excerpt"
+
+ ```json hl_lines="23"
+ {
+ "SuccessfulBooking": 1.0,
+ "_aws": {
+ "Timestamp": 1592234975665,
+ "CloudWatchMetrics": [
+ {
+ "Namespace": "ExampleApplication",
+ "Dimensions": [
+ [
+ "service"
+ ]
+ ],
+ "Metrics": [
+ {
+ "Name": "SuccessfulBooking",
+ "Unit": "Count"
+ }
+ ]
+ }
+ ]
+ },
+ "Service": "Booking",
+ "BookingId": "683EEB2D-B2F3-4075-96EE-788E6E2EED45"
+ }
+ ```
+
+### Single metric with a different dimension
+
+CloudWatch EMF uses the same dimensions across all your metrics. Use **`PushSingleMetric`** if you have a metric that should have different dimensions.
+
+!!! info
+ Generally, this would be an edge case since you [pay for unique metric](https://aws.amazon.com/cloudwatch/pricing). Keep the following formula in mind:
+
+ **unique metric = (metric_name + dimension_name + dimension_value)**
+
+=== "Function.cs"
+
+ ```csharp hl_lines="8-17"
+ using AWS.Lambda.Powertools.Metrics;
+
+ public class Function {
+
+ [Metrics(Namespace = ExampleApplication, Service = "Booking")]
+ public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
+ {
+ Metrics.PushSingleMetric(
+ metricName: "ColdStart",
+ value: 1,
+ unit: MetricUnit.Count,
+ nameSpace: "ExampleApplication",
+ service: "Booking",
+ defaultDimensions: new Dictionary
+ {
+ {"FunctionContext", "$LATEST"}
+ });
+ ...
+ ```
+
+## AspNetCore
+
+### Installation
+
+To use the Metrics middleware in an ASP.NET Core application, you need to install the `AWS.Lambda.Powertools.Metrics.AspNetCore` NuGet package.
+
+```bash
+dotnet add package AWS.Lambda.Powertools.Metrics.AspNetCore
+```
+
+### UseMetrics() Middleware
+
+The `UseMetrics` middleware is an extension method for the `IApplicationBuilder` interface.
+
+It adds a metrics middleware to the specified application builder, which captures cold start metrics (if enabled) and flushes metrics on function exit.
+
+#### Example
+
+```csharp hl_lines="21"
+
+using AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Configure metrics
+builder.Services.AddSingleton(_ => new MetricsBuilder()
+ .WithNamespace("MyApi") // Namespace for the metrics
+ .WithService("WeatherService") // Service name for the metrics
+ .WithCaptureColdStart(true) // Capture cold start metrics
+ .WithDefaultDimensions(new Dictionary // Default dimensions for the metrics
+ {
+ {"Environment", "Prod"},
+ {"Another", "One"}
+ })
+ .Build()); // Build the metrics
+
+builder.Services.AddAWSLambdaHosting(LambdaEventSource.RestApi);
+
+var app = builder.Build();
+
+app.UseMetrics(); // Add the metrics middleware
+
+app.MapGet("/powertools", (IMetrics metrics) =>
+ {
+ // add custom metrics
+ metrics.AddMetric("MyCustomMetric", 1, MetricUnit.Count);
+ // flush metrics - this is required to ensure metrics are sent to CloudWatch
+ metrics.Flush();
+ });
+
+app.Run();
+
+```
+
+Here is the highlighted `UseMetrics` method:
+
+```csharp
+///
+/// Adds a metrics middleware to the specified application builder.
+/// This will capture cold start (if CaptureColdStart is enabled) metrics and flush metrics on function exit.
+///
+/// The application builder to add the metrics middleware to.
+/// The application builder with the metrics middleware added.
+public static IApplicationBuilder UseMetrics(this IApplicationBuilder app)
+{
+ app.UseMiddleware();
+ return app;
+}
+```
+
+Explanation:
+
+- The method is defined as an extension method for the `IApplicationBuilder` interface.
+- It adds a `MetricsMiddleware` to the application builder using the `UseMiddleware` method.
+- The `MetricsMiddleware` captures and records metrics for HTTP requests, including cold start metrics if the `CaptureColdStart` option is enabled.
+
+### WithMetrics() filter
+
+The `WithMetrics` method is an extension method for the `RouteHandlerBuilder` class.
+
+It adds a metrics filter to the specified route handler builder, which captures cold start metrics (if enabled) and flushes metrics on function exit.
+
+#### Example
+
+```csharp hl_lines="31"
+
+using AWS.Lambda.Powertools.Metrics;
+using AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Configure metrics
+builder.Services.AddSingleton(_ => new MetricsBuilder()
+ .WithNamespace("MyApi") // Namespace for the metrics
+ .WithService("WeatherService") // Service name for the metrics
+ .WithCaptureColdStart(true) // Capture cold start metrics
+ .WithDefaultDimensions(new Dictionary // Default dimensions for the metrics
+ {
+ {"Environment", "Prod"},
+ {"Another", "One"}
+ })
+ .Build()); // Build the metrics
+
+// Add AWS Lambda support. When the application is run in Lambda, Kestrel is swapped out as the web server with Amazon.Lambda.AspNetCoreServer. This
+// package will act as the web server translating requests and responses between the Lambda event source and ASP.NET Core.
+builder.Services.AddAWSLambdaHosting(LambdaEventSource.RestApi);
+
+var app = builder.Build();
+
+app.MapGet("/powertools", (IMetrics metrics) =>
+ {
+ // add custom metrics
+ metrics.AddMetric("MyCustomMetric", 1, MetricUnit.Count);
+ // flush metrics - this is required to ensure metrics are sent to CloudWatch
+ metrics.Flush();
+ })
+ .WithMetrics();
+
+app.Run();
+
+```
+
+Here is the highlighted `WithMetrics` method:
+
+```csharp
+///
+/// Adds a metrics filter to the specified route handler builder.
+/// This will capture cold start (if CaptureColdStart is enabled) metrics and flush metrics on function exit.
+///
+/// The route handler builder to add the metrics filter to.
+/// The route handler builder with the metrics filter added.
+public static RouteHandlerBuilder WithMetrics(this RouteHandlerBuilder builder)
+{
+ builder.AddEndpointFilter();
+ return builder;
+}
+```
+
+Explanation:
+
+- The method is defined as an extension method for the `RouteHandlerBuilder` class.
+- It adds a `MetricsFilter` to the route handler builder using the `AddEndpointFilter` method.
+- The `MetricsFilter` captures and records metrics for HTTP endpoints, including cold start metrics if the `CaptureColdStart` option is enabled.
+- The method returns the modified `RouteHandlerBuilder` instance with the metrics filter added.
+
+
+## Testing your code
+
+### Unit testing
+
+To test your code that uses the Metrics utility, you can use the `TestLambdaContext` class from the `Amazon.Lambda.TestUtilities` package.
+
+You can also use the `IMetrics` interface to mock the Metrics utility in your tests.
+
+Here is an example of how you can test a Lambda function that uses the Metrics utility:
+
+#### Lambda Function
+
+```csharp
+using System.Collections.Generic;
+using Amazon.Lambda.Core;
+
+public class MetricsnBuilderHandler
+{
+ private readonly IMetrics _metrics;
+
+ // Allow injection of IMetrics for testing
+ public MetricsnBuilderHandler(IMetrics metrics = null)
+ {
+ _metrics = metrics ?? new MetricsBuilder()
+ .WithCaptureColdStart(true)
+ .WithService("testService")
+ .WithNamespace("dotnet-powertools-test")
+ .WithDefaultDimensions(new Dictionary
+ {
+ { "Environment", "Prod1" },
+ { "Another", "One" }
+ }).Build();
+ }
+
+ [Metrics]
+ public void Handler(ILambdaContext context)
+ {
+ _metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
+ }
+}
+
+```
+#### Unit Tests
+
+
+```csharp
+[Fact]
+ public void Handler_With_Builder_Should_Configure_In_Constructor()
+ {
+ // Arrange
+ var handler = new MetricsnBuilderHandler();
+
+ // Act
+ handler.Handler(new TestLambdaContext
+ {
+ FunctionName = "My_Function_Name"
+ });
+
+ // Get the output and parse it
+ var metricsOutput = _consoleOut.ToString();
+
+ // Assert cold start
+ Assert.Contains(
+ "\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"Environment\",\"Another\",\"FunctionName\"]]}]},\"Service\":\"testService\",\"Environment\":\"Prod1\",\"Another\":\"One\",\"FunctionName\":\"My_Function_Name\",\"ColdStart\":1}",
+ metricsOutput);
+ // Assert successful Memory metrics
+ Assert.Contains(
+ "\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"SuccessfulBooking\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"Environment\",\"Another\",\"FunctionName\"]]}]},\"Service\":\"testService\",\"Environment\":\"Prod1\",\"Another\":\"One\",\"FunctionName\":\"My_Function_Name\",\"SuccessfulBooking\":1}",
+ metricsOutput);
+ }
+
+ [Fact]
+ public void Handler_With_Builder_Should_Configure_In_Constructor_Mock()
+ {
+ var metricsMock = Substitute.For();
+
+ metricsMock.Options.Returns(new MetricsOptions
+ {
+ CaptureColdStart = true,
+ Namespace = "dotnet-powertools-test",
+ Service = "testService",
+ DefaultDimensions = new Dictionary
+ {
+ { "Environment", "Prod" },
+ { "Another", "One" }
+ }
+ });
+
+ Metrics.UseMetricsForTests(metricsMock);
+
+ var sut = new MetricsnBuilderHandler(metricsMock);
+
+ // Act
+ sut.Handler(new TestLambdaContext
+ {
+ FunctionName = "My_Function_Name"
+ });
+
+ metricsMock.Received(1).PushSingleMetric("ColdStart", 1, MetricUnit.Count, "dotnet-powertools-test",
+ service: "testService", Arg.Any>());
+ metricsMock.Received(1).AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
+ }
+```
+
+### Environment variables
+
+???+ tip
+ Ignore this section, if:
+
+ * You are explicitly setting namespace/default dimension via `namespace` and `service` parameters
+ * You're not instantiating `Metrics` in the global namespace
+
+ For example, `Metrics(namespace="ExampleApplication", service="booking")`
+
+Make sure to set `POWERTOOLS_METRICS_NAMESPACE` and `POWERTOOLS_SERVICE_NAME` before running your tests to prevent failing on `SchemaValidation` exception. You can set it before you run tests by adding the environment variable.
+
+```csharp title="Injecting Metric Namespace before running tests"
+Environment.SetEnvironmentVariable("POWERTOOLS_METRICS_NAMESPACE","AWSLambdaPowertools");
+```
diff --git a/docs/core/metrics.md b/docs/core/metrics.md
index 65fb5f50..0a766414 100644
--- a/docs/core/metrics.md
+++ b/docs/core/metrics.md
@@ -109,7 +109,7 @@ You can create metrics using **`AddMetric`**, and you can create dimensions for
=== "Metrics"
- ```csharp hl_lines="8"
+ ```csharp hl_lines="5 8"
using AWS.Lambda.Powertools.Metrics;
public class Function {
diff --git a/libraries/AWS.Lambda.Powertools.sln b/libraries/AWS.Lambda.Powertools.sln
index 72aea967..bcc1a2c9 100644
--- a/libraries/AWS.Lambda.Powertools.sln
+++ b/libraries/AWS.Lambda.Powertools.sln
@@ -97,6 +97,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AOT-FunctionHandlerTest", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AOT-FunctionMethodAttributeTest", "tests\e2e\functions\idempotency\AOT-Function\src\AOT-FunctionMethodAttributeTest\AOT-FunctionMethodAttributeTest.csproj", "{CC8CFF43-DC72-464C-A42D-55E023DE8500}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Metrics.AspNetCore", "src\AWS.Lambda.Powertools.Metrics.AspNetCore\AWS.Lambda.Powertools.Metrics.AspNetCore.csproj", "{A2AD98B1-2BED-4864-B573-77BE7B52FED2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Metrics.AspNetCore.Tests", "tests\AWS.Lambda.Powertools.Metrics.AspNetCore.Tests\AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj", "{F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -518,6 +522,30 @@ Global
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Release|x64.Build.0 = Release|Any CPU
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Release|x86.ActiveCfg = Release|Any CPU
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Release|x86.Build.0 = Release|Any CPU
+ {A2AD98B1-2BED-4864-B573-77BE7B52FED2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A2AD98B1-2BED-4864-B573-77BE7B52FED2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A2AD98B1-2BED-4864-B573-77BE7B52FED2}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A2AD98B1-2BED-4864-B573-77BE7B52FED2}.Debug|x64.Build.0 = Debug|Any CPU
+ {A2AD98B1-2BED-4864-B573-77BE7B52FED2}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A2AD98B1-2BED-4864-B573-77BE7B52FED2}.Debug|x86.Build.0 = Debug|Any CPU
+ {A2AD98B1-2BED-4864-B573-77BE7B52FED2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A2AD98B1-2BED-4864-B573-77BE7B52FED2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A2AD98B1-2BED-4864-B573-77BE7B52FED2}.Release|x64.ActiveCfg = Release|Any CPU
+ {A2AD98B1-2BED-4864-B573-77BE7B52FED2}.Release|x64.Build.0 = Release|Any CPU
+ {A2AD98B1-2BED-4864-B573-77BE7B52FED2}.Release|x86.ActiveCfg = Release|Any CPU
+ {A2AD98B1-2BED-4864-B573-77BE7B52FED2}.Release|x86.Build.0 = Release|Any CPU
+ {F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB}.Debug|x64.Build.0 = Debug|Any CPU
+ {F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB}.Debug|x86.Build.0 = Debug|Any CPU
+ {F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB}.Release|x64.ActiveCfg = Release|Any CPU
+ {F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB}.Release|x64.Build.0 = Release|Any CPU
+ {F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB}.Release|x86.ActiveCfg = Release|Any CPU
+ {F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
@@ -563,5 +591,7 @@ Global
{ACA789EA-BD38-490B-A7F8-6A3A86985025} = {FB2C7DA3-6FCE-429D-86F9-5775D0231EC6}
{E71C48D2-AD56-4177-BBD7-6BB859A40C92} = {FB2C7DA3-6FCE-429D-86F9-5775D0231EC6}
{CC8CFF43-DC72-464C-A42D-55E023DE8500} = {FB2C7DA3-6FCE-429D-86F9-5775D0231EC6}
+ {A2AD98B1-2BED-4864-B573-77BE7B52FED2} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5}
+ {F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB} = {1CFF5568-8486-475F-81F6-06105C437528}
EndGlobalSection
EndGlobal
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/AWS.Lambda.Powertools.Metrics.AspNetCore.csproj b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/AWS.Lambda.Powertools.Metrics.AspNetCore.csproj
new file mode 100644
index 00000000..529fd973
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/AWS.Lambda.Powertools.Metrics.AspNetCore.csproj
@@ -0,0 +1,23 @@
+
+
+
+ AWS.Lambda.Powertools.Metrics.AspNetCore
+ Powertools for AWS Lambda (.NET) - Metrics AspNetCore package.
+ AWS.Lambda.Powertools.Metrics.AspNetCore
+ AWS.Lambda.Powertools.Metrics.AspNetCore
+ net8.0;net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsEndpointExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsEndpointExtensions.cs
new file mode 100644
index 00000000..a2101229
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsEndpointExtensions.cs
@@ -0,0 +1,37 @@
+/*
+ * 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.Builder;
+using Microsoft.AspNetCore.Http;
+
+namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
+
+///
+/// Provides extension methods for adding metrics to route handlers.
+///
+public static class MetricsEndpointExtensions
+{
+ ///
+ /// Adds a metrics filter to the specified route handler builder.
+ /// This will capture cold start (if CaptureColdStart is enabled) metrics and flush metrics on function exit.
+ ///
+ /// The route handler builder to add the metrics filter to.
+ /// The route handler builder with the metrics filter added.
+ public static RouteHandlerBuilder WithMetrics(this RouteHandlerBuilder builder)
+ {
+ builder.AddEndpointFilter();
+ return builder;
+ }
+}
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsFilter.cs b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsFilter.cs
new file mode 100644
index 00000000..a2c776f1
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsFilter.cs
@@ -0,0 +1,57 @@
+/*
+ * 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.Http;
+
+namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
+
+///
+/// Represents a filter that captures and records metrics for HTTP endpoints.
+///
+public class MetricsFilter : IEndpointFilter
+{
+ private readonly MetricsHelper _metricsHelper;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The metrics instance to use for recording metrics.
+ public MetricsFilter(IMetrics metrics)
+ {
+ _metricsHelper = new MetricsHelper(metrics);
+ }
+
+ ///
+ /// Invokes the filter asynchronously.
+ ///
+ /// The context for the endpoint filter invocation.
+ /// The delegate to invoke the next filter or endpoint.
+ /// A task that represents the asynchronous operation, containing the result of the endpoint invocation.
+ public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
+ {
+ var result = await next(context);
+
+ try
+ {
+ await _metricsHelper.CaptureColdStartMetrics(context.HttpContext);
+ return result;
+ }
+ catch
+ {
+ // ignored
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsHelper.cs b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsHelper.cs
new file mode 100644
index 00000000..3a4e0357
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsHelper.cs
@@ -0,0 +1,76 @@
+/*
+ * 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.Core;
+using Microsoft.AspNetCore.Http;
+
+namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
+
+
+///
+/// Helper class for capturing and recording metrics in ASP.NET Core applications.
+///
+public class MetricsHelper
+{
+ private readonly IMetrics _metrics;
+ private static bool _isColdStart = true;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The metrics instance to use for recording metrics.
+ public MetricsHelper(IMetrics metrics)
+ {
+ _metrics = metrics;
+ }
+
+ ///
+ /// Captures cold start metrics for the given HTTP context.
+ ///
+ /// The HTTP context.
+ /// A task that represents the asynchronous operation.
+ public Task CaptureColdStartMetrics(HttpContext context)
+ {
+ if (_metrics.Options.CaptureColdStart == null || !_metrics.Options.CaptureColdStart.Value || !_isColdStart)
+ return Task.CompletedTask;
+
+ var defaultDimensions = _metrics.Options.DefaultDimensions;
+ _isColdStart = false;
+
+ if (context.Items["LambdaContext"] is ILambdaContext lambdaContext)
+ {
+ defaultDimensions?.Add("FunctionName", lambdaContext.FunctionName);
+ _metrics.SetDefaultDimensions(defaultDimensions);
+ }
+
+ _metrics.PushSingleMetric(
+ "ColdStart",
+ 1.0,
+ MetricUnit.Count,
+ _metrics.Options.Namespace,
+ _metrics.Options.Service,
+ defaultDimensions
+ );
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Resets the cold start flag for testing purposes.
+ ///
+ internal static void ResetColdStart()
+ {
+ _isColdStart = true;
+ }
+}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsMiddlewareExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsMiddlewareExtensions.cs
new file mode 100644
index 00000000..05e0d3f9
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsMiddlewareExtensions.cs
@@ -0,0 +1,41 @@
+/*
+ * 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.Builder;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
+
+///
+/// Provides extension methods for adding metrics middleware to the application pipeline.
+///
+public static class MetricsMiddlewareExtensions
+{
+ ///
+ /// Adds middleware to capture and record metrics for HTTP requests.
+ ///
+ /// The application builder.
+ /// The application builder with the metrics middleware added.
+ public static IApplicationBuilder UseMetrics(this IApplicationBuilder app)
+ {
+ return app.Use(async (context, next) =>
+ {
+ var metrics = context.RequestServices.GetRequiredService();
+ var metricsHelper = new MetricsHelper(metrics);
+ await metricsHelper.CaptureColdStartMetrics(context);
+ await next();
+ });
+ }
+}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/InternalsVisibleTo.cs b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/InternalsVisibleTo.cs
new file mode 100644
index 00000000..5b9c15a1
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/InternalsVisibleTo.cs
@@ -0,0 +1,18 @@
+/*
+ * 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 System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Metrics.AspNetCore.Tests")]
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/README.md b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/README.md
new file mode 100644
index 00000000..a1ca8fee
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/README.md
@@ -0,0 +1,149 @@
+# AWS Lambda Powertools Metrics for ASP.NET Core
+
+This library provides utilities for capturing and publishing custom metrics from your AWS Lambda functions using ASP.NET Core.
+
+## Getting Started
+
+This library provides utilities for capturing and publishing custom metrics from your AWS Lambda functions using ASP.NET Core.
+
+### Installation
+
+You can install the package via the NuGet package manager just search for `AWS.Lambda.Powertools.Metrics.AspNetCore`.
+
+You can also install via powershell using the following command.
+
+```shell
+dotnet add package AWS.Lambda.Powertools.Metrics.AspNetCore
+```
+
+```csharp
+
+using AWS.Lambda.Powertools.Metrics;
+using AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Configure metrics
+builder.Services.AddSingleton(_ => new MetricsBuilder()
+ .WithNamespace("MyApi") // Namespace for the metrics
+ .WithService("WeatherService") // Service name for the metrics
+ .WithCaptureColdStart(true) // Capture cold start metrics
+ .WithDefaultDimensions(new Dictionary // Default dimensions for the metrics
+ {
+ {"Environment", "Prod"},
+ {"Another", "One"}
+ })
+ .Build()); // Build the metrics
+
+// Add AWS Lambda support. When the application is run in Lambda, Kestrel is swapped out as the web server with Amazon.Lambda.AspNetCoreServer. This
+// package will act as the web server translating requests and responses between the Lambda event source and ASP.NET Core.
+builder.Services.AddAWSLambdaHosting(LambdaEventSource.RestApi);
+
+var app = builder.Build();
+
+app.MapGet("/powertools", (IMetrics metrics) =>
+ {
+ // add custom metrics
+ metrics.AddMetric("MyCustomMetric", 1, MetricUnit.Count);
+ // flush metrics - this is required to ensure metrics are sent to CloudWatch
+ metrics.Flush();
+ })
+ .WithMetrics();
+
+app.Run();
+
+```
+
+### WithMetrics() filter
+
+The `WithMetrics` method is an extension method for the `RouteHandlerBuilder` class.
+
+It adds a metrics filter to the specified route handler builder, which captures cold start metrics (if enabled) and flushes metrics on function exit.
+
+Here is the highlighted `WithMetrics` method:
+
+```csharp
+///
+/// Adds a metrics filter to the specified route handler builder.
+/// This will capture cold start (if CaptureColdStart is enabled) metrics and flush metrics on function exit.
+///
+/// The route handler builder to add the metrics filter to.
+/// The route handler builder with the metrics filter added.
+public static RouteHandlerBuilder WithMetrics(this RouteHandlerBuilder builder)
+{
+ builder.AddEndpointFilter();
+ return builder;
+}
+```
+
+Explanation:
+- The method is defined as an extension method for the `RouteHandlerBuilder` class.
+- It adds a `MetricsFilter` to the route handler builder using the `AddEndpointFilter` method.
+- The `MetricsFilter` captures and records metrics for HTTP endpoints, including cold start metrics if the `CaptureColdStart` option is enabled.
+- The method returns the modified `RouteHandlerBuilder` instance with the metrics filter added.
+
+
+### UseMetrics() Middleware
+
+The `UseMetrics` middleware is an extension method for the `IApplicationBuilder` interface.
+
+It adds a metrics middleware to the specified application builder, which captures cold start metrics (if enabled) and flushes metrics on function exit.
+
+#### Example
+
+```csharp
+
+using AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Configure metrics
+builder.Services.AddSingleton(_ => new MetricsBuilder()
+ .WithNamespace("MyApi") // Namespace for the metrics
+ .WithService("WeatherService") // Service name for the metrics
+ .WithCaptureColdStart(true) // Capture cold start metrics
+ .WithDefaultDimensions(new Dictionary // Default dimensions for the metrics
+ {
+ {"Environment", "Prod"},
+ {"Another", "One"}
+ })
+ .Build()); // Build the metrics
+
+builder.Services.AddAWSLambdaHosting(LambdaEventSource.RestApi);
+
+var app = builder.Build();
+
+app.UseMetrics(); // Add the metrics middleware
+
+app.MapGet("/powertools", (IMetrics metrics) =>
+ {
+ // add custom metrics
+ metrics.AddMetric("MyCustomMetric", 1, MetricUnit.Count);
+ // flush metrics - this is required to ensure metrics are sent to CloudWatch
+ metrics.Flush();
+ });
+
+app.Run();
+
+```
+
+Here is the highlighted `UseMetrics` method:
+
+```csharp
+///
+/// Adds a metrics middleware to the specified application builder.
+/// This will capture cold start (if CaptureColdStart is enabled) metrics and flush metrics on function exit.
+///
+/// The application builder to add the metrics middleware to.
+/// The application builder with the metrics middleware added.
+public static IApplicationBuilder UseMetrics(this IApplicationBuilder app)
+{
+ app.UseMiddleware();
+ return app;
+}
+```
+
+Explanation:
+- The method is defined as an extension method for the `IApplicationBuilder` interface.
+- It adds a `MetricsMiddleware` to the application builder using the `UseMiddleware` method.
+- The `MetricsMiddleware` captures and records metrics for HTTP requests, including cold start metrics if the `CaptureColdStart` option is enabled.
\ No newline at end of file
diff --git a/libraries/src/Directory.Packages.props b/libraries/src/Directory.Packages.props
index 56d0fba9..c5af6311 100644
--- a/libraries/src/Directory.Packages.props
+++ b/libraries/src/Directory.Packages.props
@@ -11,6 +11,8 @@
+
+
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj
new file mode 100644
index 00000000..c336bde1
--- /dev/null
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj
@@ -0,0 +1,32 @@
+
+
+
+ AWS.Lambda.Powertools.Metrics.AspNetCore.Tests
+ AWS.Lambda.Powertools.Metrics.AspNetCore.Tests
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsFilterTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsFilterTests.cs
new file mode 100644
index 00000000..6a0df634
--- /dev/null
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsFilterTests.cs
@@ -0,0 +1,110 @@
+using Amazon.Lambda.Core;
+using AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
+using Microsoft.AspNetCore.Http;
+using NSubstitute;
+using Xunit;
+
+namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Tests;
+
+public class MetricsFilterTests
+{
+ private readonly IMetrics _metrics;
+ private readonly EndpointFilterInvocationContext _context;
+ private readonly ILambdaContext _lambdaContext;
+
+ public MetricsFilterTests()
+ {
+ MetricsHelper.ResetColdStart(); // Reset before each test
+ _metrics = Substitute.For();
+ _context = Substitute.For();
+ _lambdaContext = Substitute.For();
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.Items["LambdaContext"] = _lambdaContext;
+ _context.HttpContext.Returns(httpContext);
+ }
+
+ [Fact]
+ public async Task InvokeAsync_WhenColdStartEnabled_RecordsColdStartMetric()
+ {
+ // Arrange
+ var options = new MetricsOptions
+ {
+ CaptureColdStart = true,
+ Namespace = "TestNamespace",
+ Service = "TestService",
+ DefaultDimensions = new Dictionary()
+ };
+
+ _metrics.Options.Returns(options);
+ _lambdaContext.FunctionName.Returns("TestFunction");
+
+ var filter = new MetricsFilter(_metrics);
+ var next = new EndpointFilterDelegate(_ => ValueTask.FromResult("result"));
+
+ // Act
+ var result = await filter.InvokeAsync(_context, next);
+
+ // Assert
+ _metrics.Received(1).PushSingleMetric(
+ "ColdStart",
+ 1.0,
+ MetricUnit.Count,
+ "TestNamespace",
+ "TestService",
+ Arg.Any>()
+ );
+ Assert.Equal("result", result);
+ }
+
+ [Fact]
+ public async Task InvokeAsync_WhenColdStartDisabled_DoesNotRecordMetric()
+ {
+ // Arrange
+ var options = new MetricsOptions { CaptureColdStart = false };
+ _metrics.Options.Returns(options);
+
+ var filter = new MetricsFilter(_metrics);
+ var next = new EndpointFilterDelegate(_ => ValueTask.FromResult("result"));
+
+ // Act
+ var result = await filter.InvokeAsync(_context, next);
+
+ // Assert
+ _metrics.DidNotReceive().PushSingleMetric(
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any>()
+ );
+ Assert.Equal("result", result);
+ }
+
+ [Fact]
+ public async Task InvokeAsync_ShouldCallNextAndContinue()
+ {
+ // Arrange
+ var metrics = Substitute.For();
+ metrics.Options.Returns(new MetricsOptions { CaptureColdStart = true });
+
+ var httpContext = new DefaultHttpContext();
+ var context = new DefaultEndpointFilterInvocationContext(httpContext);
+ var filter = new MetricsFilter(metrics);
+
+ var called = false;
+ EndpointFilterDelegate next = _ =>
+ {
+ called = true;
+ return ValueTask.FromResult("result");
+ };
+
+ // Act
+ var result = await filter.InvokeAsync(context, next);
+
+ // Assert
+ Assert.True(called);
+ Assert.Equal("result", result);
+ }
+}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsHelperTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsHelperTests.cs
new file mode 100644
index 00000000..37064d24
--- /dev/null
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsHelperTests.cs
@@ -0,0 +1,84 @@
+using System.Reflection;
+using AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Routing.Patterns;
+using NSubstitute;
+using Xunit;
+
+namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Tests;
+
+public class MetricsHelperTests
+{
+ [Fact]
+ public async Task CaptureColdStartMetrics_WhenEnabled_ShouldPushMetric()
+ {
+ // Arrange
+ var metrics = Substitute.For();
+ metrics.Options.Returns(new MetricsOptions
+ {
+ CaptureColdStart = true,
+ Namespace = "TestNamespace",
+ Service = "TestService"
+ });
+
+ var context = new DefaultHttpContext();
+ var helper = new MetricsHelper(metrics);
+
+ // Act
+ await helper.CaptureColdStartMetrics(context);
+
+ // Assert
+ metrics.Received(1).PushSingleMetric(
+ Arg.Is(s => s == "ColdStart"),
+ Arg.Is(d => d == 1.0),
+ Arg.Is(u => u == MetricUnit.Count),
+ Arg.Is(s => s == "TestNamespace"),
+ Arg.Is(s => s == "TestService"),
+ Arg.Any>()
+ );
+ }
+
+ [Fact]
+ public async Task CaptureColdStartMetrics_WhenDisabled_ShouldNotPushMetric()
+ {
+ // Arrange
+ var metrics = Substitute.For();
+ metrics.Options.Returns(new MetricsOptions { CaptureColdStart = false });
+
+ var context = new DefaultHttpContext();
+ var helper = new MetricsHelper(metrics);
+
+ // Act
+ await helper.CaptureColdStartMetrics(context);
+
+ // Assert
+ metrics.DidNotReceive().PushSingleMetric(
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any>()
+ );
+ }
+}
+
+public static class EndpointFilterInvocationContextHelper
+{
+ public static EndpointFilterInvocationContext Create(HttpContext httpContext, object[] arguments)
+ {
+ var endpoint = new RouteEndpoint(
+ c => Task.CompletedTask,
+ RoutePatternFactory.Parse("/"),
+ 0,
+ EndpointMetadataCollection.Empty,
+ "test");
+
+ var constructor = typeof(EndpointFilterInvocationContext)
+ .GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)
+ .First();
+
+ return (EndpointFilterInvocationContext)constructor.Invoke(new object[] { httpContext, endpoint, arguments });
+ }
+}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsMiddlewareExtensionsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsMiddlewareExtensionsTests.cs
new file mode 100644
index 00000000..3d5a9892
--- /dev/null
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsMiddlewareExtensionsTests.cs
@@ -0,0 +1,99 @@
+using AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using NSubstitute;
+using Xunit;
+
+namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Tests;
+
+public class MetricsMiddlewareExtensionsTests : IDisposable
+{
+ public MetricsMiddlewareExtensionsTests()
+ {
+ MetricsHelper.ResetColdStart();
+ }
+
+ public void Dispose()
+ {
+ MetricsHelper.ResetColdStart();
+ }
+
+ [Fact]
+ public async Task UseMetrics_ShouldCaptureColdStart_WhenEnabled()
+ {
+ // Arrange
+ var metrics = Substitute.For();
+ metrics.Options.Returns(new MetricsOptions
+ {
+ CaptureColdStart = true,
+ Namespace = "TestNamespace",
+ Service = "TestService"
+ });
+
+ var services = new ServiceCollection();
+ services.AddSingleton(metrics);
+ var serviceProvider = services.BuildServiceProvider();
+
+ var context = new DefaultHttpContext
+ {
+ RequestServices = serviceProvider
+ };
+
+ var appBuilder = new ApplicationBuilder(serviceProvider);
+ appBuilder.UseMetrics();
+ var app = appBuilder.Build();
+
+ // Act
+ await app.Invoke(context);
+
+ // Assert
+ metrics.Received(1).PushSingleMetric(
+ Arg.Is(s => s == "ColdStart"),
+ Arg.Is(d => d == 1.0),
+ Arg.Is(u => u == MetricUnit.Count),
+ Arg.Is(s => s == "TestNamespace"),
+ Arg.Is(s => s == "TestService"),
+ Arg.Any>()
+ );
+ }
+
+ [Fact]
+ public async Task UseMetrics_ShouldNotCaptureColdStart_WhenDisabled()
+ {
+ // Arrange
+ var metrics = Substitute.For();
+ metrics.Options.Returns(new MetricsOptions
+ {
+ CaptureColdStart = false,
+ Namespace = "TestNamespace",
+ Service = "TestService"
+ });
+
+ var services = new ServiceCollection();
+ services.AddSingleton(metrics);
+ var serviceProvider = services.BuildServiceProvider();
+
+ var context = new DefaultHttpContext
+ {
+ RequestServices = serviceProvider
+ };
+
+ var appBuilder = new ApplicationBuilder(serviceProvider);
+ appBuilder.UseMetrics();
+ var app = appBuilder.Build();
+
+ // Act
+ await app.Invoke(context);
+
+ // Assert
+ metrics.DidNotReceive().PushSingleMetric(
+ Arg.Is(s => s == "ColdStart"),
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any>()
+ );
+ }
+}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/DefaultDimensionsHandler.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/DefaultDimensionsHandler.cs
index 1028f58c..95e6a9f3 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/DefaultDimensionsHandler.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/DefaultDimensionsHandler.cs
@@ -9,6 +9,9 @@ public DefaultDimensionsHandler()
{
Metrics.Configure(options =>
{
+ options.Namespace = "dotnet-powertools-test";
+ options.Service = "testService";
+ options.CaptureColdStart = true;
options.DefaultDimensions = new Dictionary
{
{ "Environment", "Prod" },
@@ -17,14 +20,14 @@ public DefaultDimensionsHandler()
});
}
- [Metrics(Namespace = "dotnet-powertools-test", Service = "testService", CaptureColdStart = true)]
+ [Metrics]
public void Handler()
{
// Default dimensions are already set
Metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
}
- [Metrics(Namespace = "dotnet-powertools-test", Service = "testService", CaptureColdStart = true)]
+ [Metrics]
public void HandlerWithContext(ILambdaContext context)
{
// Default dimensions are already set and adds FunctionName dimension
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
index 36a5818e..dc338d59 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
@@ -145,11 +145,11 @@ public void DefaultDimensions_AreAppliedCorrectly()
// Assert cold start
Assert.Contains(
- "\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Environment\",\"Another\",\"Service\"]]}]},\"Environment\":\"Prod\",\"Another\":\"One\",\"Service\":\"testService\",\"ColdStart\":1}",
+ "\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"Environment\",\"Another\"]]}]},\"Service\":\"testService\",\"Environment\":\"Prod\",\"Another\":\"One\",\"ColdStart\":1}",
metricsOutput);
// Assert successful booking metrics
Assert.Contains(
- "\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"SuccessfulBooking\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Environment\",\"Another\",\"Service\"]]}]},\"Environment\":\"Prod\",\"Another\":\"One\",\"Service\":\"testService\",\"SuccessfulBooking\":1}",
+ "\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"SuccessfulBooking\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"Environment\",\"Another\"]]}]},\"Service\":\"testService\",\"Environment\":\"Prod\",\"Another\":\"One\",\"SuccessfulBooking\":1}",
metricsOutput);
}
@@ -170,11 +170,11 @@ public void DefaultDimensions_AreAppliedCorrectly_WithContext_FunctionName()
// Assert cold start
Assert.Contains(
- "\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Environment\",\"Another\",\"Service\",\"FunctionName\"]]}]},\"Environment\":\"Prod\",\"Another\":\"One\",\"Service\":\"testService\",\"FunctionName\":\"My_Function_Name\",\"ColdStart\":1}",
+ "\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"Environment\",\"Another\",\"FunctionName\"]]}]},\"Service\":\"testService\",\"Environment\":\"Prod\",\"Another\":\"One\",\"FunctionName\":\"My_Function_Name\",\"ColdStart\":1}",
metricsOutput);
// Assert successful Memory metrics
Assert.Contains(
- "\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"Memory\",\"Unit\":\"Megabytes\"}],\"Dimensions\":[[\"Environment\",\"Another\",\"Service\",\"FunctionName\"]]}]},\"Environment\":\"Prod\",\"Another\":\"One\",\"Service\":\"testService\",\"FunctionName\":\"My_Function_Name\",\"Memory\":10}",
+ "\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"Memory\",\"Unit\":\"Megabytes\"}],\"Dimensions\":[[\"Service\",\"Environment\",\"Another\",\"FunctionName\"]]}]},\"Service\":\"testService\",\"Environment\":\"Prod\",\"Another\":\"One\",\"FunctionName\":\"My_Function_Name\",\"Memory\":10}",
metricsOutput);
}
diff --git a/mkdocs.yml b/mkdocs.yml
index c804a353..d1449647 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -15,6 +15,7 @@ nav:
- Core utilities:
- core/logging.md
- core/metrics.md
+ - core/metrics-v2.md
- core/tracing.md
- Utilities:
- utilities/parameters.md
From 1756ce1d47aad605d28cb4b8828532eca3aadb2d Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Sat, 22 Feb 2025 12:00:53 +0000
Subject: [PATCH 02/29] refactor(metrics): standardize parameter names for
metric methods to improve clarity
---
.../AWS.Lambda.Powertools.Metrics/Metrics.cs | 38 +++++++++----------
.../Handlers/FunctionHandler.cs | 8 ++--
.../Function/src/Function/TestHelper.cs | 2 +-
3 files changed, 24 insertions(+), 24 deletions(-)
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
index c6c0e3be..86823bf0 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
@@ -133,7 +133,7 @@ internal Metrics(IPowertoolsConfigurations powertoolsConfigurations, string name
}
///
- void IMetrics.AddMetric(string key, double value, MetricUnit unit, MetricResolution metricResolution)
+ void IMetrics.AddMetric(string key, double value, MetricUnit unit, MetricResolution resolution)
{
if (Instance != null)
{
@@ -160,7 +160,7 @@ void IMetrics.AddMetric(string key, double value, MetricUnit unit, MetricResolut
Instance.Flush(true);
}
- _context.AddMetric(key, value, unit, metricResolution);
+ _context.AddMetric(key, value, unit, resolution);
}
}
else
@@ -216,14 +216,14 @@ void IMetrics.AddMetadata(string key, object value)
}
///
- void IMetrics.SetDefaultDimensions(Dictionary defaultDimension)
+ void IMetrics.SetDefaultDimensions(Dictionary defaultDimensions)
{
- foreach (var item in defaultDimension)
+ foreach (var item in defaultDimensions)
if (string.IsNullOrWhiteSpace(item.Key) || string.IsNullOrWhiteSpace(item.Value))
throw new ArgumentNullException(nameof(item.Key),
"'SetDefaultDimensions' method requires a valid key pair. 'Null' or empty values are not allowed.");
- _context.SetDefaultDimensions(DictionaryToList(defaultDimension));
+ _context.SetDefaultDimensions(DictionaryToList(defaultDimensions));
}
///
@@ -294,11 +294,11 @@ private Dictionary GetDefaultDimensions()
}
///
- void IMetrics.PushSingleMetric(string metricName, double value, MetricUnit unit, string nameSpace,
- string service, Dictionary defaultDimensions, MetricResolution metricResolution)
+ void IMetrics.PushSingleMetric(string name, double value, MetricUnit unit, string nameSpace,
+ string service, Dictionary defaultDimensions, MetricResolution resolution)
{
- if (string.IsNullOrWhiteSpace(metricName))
- throw new ArgumentNullException(nameof(metricName),
+ if (string.IsNullOrWhiteSpace(name))
+ throw new ArgumentNullException(nameof(name),
"'PushSingleMetric' method requires a valid metrics key. 'Null' or empty values are not allowed.");
var context = new MetricsContext();
@@ -311,7 +311,7 @@ void IMetrics.PushSingleMetric(string metricName, double value, MetricUnit unit,
context.SetDefaultDimensions(defaultDimensionsList);
}
- context.AddMetric(metricName, value, unit, metricResolution);
+ context.AddMetric(name, value, unit, resolution);
Flush(context);
}
@@ -345,11 +345,11 @@ protected virtual void Dispose(bool disposing)
/// Metric Key. Must not be null, empty or whitespace
/// Metric Value
/// Metric Unit
- ///
+ ///
public static void AddMetric(string key, double value, MetricUnit unit = MetricUnit.None,
- MetricResolution metricResolution = MetricResolution.Default)
+ MetricResolution resolution = MetricResolution.Default)
{
- Instance.AddMetric(key, value, unit, metricResolution);
+ Instance.AddMetric(key, value, unit, resolution);
}
///
@@ -438,21 +438,21 @@ private void Flush(MetricsContext context)
/// Pushes single metric to CloudWatch using Embedded Metric Format. This can be used to push metrics with a different
/// context.
///
- /// Metric Name. Metric key cannot be null, empty or whitespace
+ /// Metric Name. Metric key cannot be null, empty or whitespace
/// Metric Value
/// Metric Unit
/// Metric Namespace
/// Service Name
/// Default dimensions list
- /// Metrics resolution
- public static void PushSingleMetric(string metricName, double value, MetricUnit unit, string nameSpace = null,
+ /// Metrics resolution
+ public static void PushSingleMetric(string name, double value, MetricUnit unit, string nameSpace = null,
string service = null, Dictionary defaultDimensions = null,
- MetricResolution metricResolution = MetricResolution.Default)
+ MetricResolution resolution = MetricResolution.Default)
{
if (Instance != null)
{
- Instance.PushSingleMetric(metricName, value, unit, nameSpace, service, defaultDimensions,
- metricResolution);
+ Instance.PushSingleMetric(name, value, unit, nameSpace, service, defaultDimensions,
+ resolution);
}
else
{
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs
index f00a8c5f..d860a9f9 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs
@@ -42,12 +42,12 @@ public void AddDimensions()
[Metrics(Namespace = "dotnet-powertools-test", Service = "ServiceName", CaptureColdStart = true)]
public void AddMultipleDimensions()
{
- Metrics.PushSingleMetric("SingleMetric1", 1, MetricUnit.Count, metricResolution: MetricResolution.High,
+ Metrics.PushSingleMetric("SingleMetric1", 1, MetricUnit.Count, resolution: MetricResolution.High,
defaultDimensions: new Dictionary {
{ "Default1", "SingleMetric1" }
});
- Metrics.PushSingleMetric("SingleMetric2", 1, MetricUnit.Count, metricResolution: MetricResolution.High, nameSpace: "ns2",
+ Metrics.PushSingleMetric("SingleMetric2", 1, MetricUnit.Count, resolution: MetricResolution.High, nameSpace: "ns2",
defaultDimensions: new Dictionary {
{ "Default1", "SingleMetric2" },
{ "Default2", "SingleMetric2" }
@@ -59,7 +59,7 @@ public void AddMultipleDimensions()
[Metrics(Namespace = "ExampleApplication")]
public void PushSingleMetricWithNamespace()
{
- Metrics.PushSingleMetric("SingleMetric", 1, MetricUnit.Count, metricResolution: MetricResolution.High,
+ Metrics.PushSingleMetric("SingleMetric", 1, MetricUnit.Count, resolution: MetricResolution.High,
defaultDimensions: new Dictionary {
{ "Default", "SingleMetric" }
});
@@ -68,7 +68,7 @@ public void PushSingleMetricWithNamespace()
[Metrics]
public void PushSingleMetricWithEnvNamespace()
{
- Metrics.PushSingleMetric("SingleMetric", 1, MetricUnit.Count, metricResolution: MetricResolution.High,
+ Metrics.PushSingleMetric("SingleMetric", 1, MetricUnit.Count, resolution: MetricResolution.High,
defaultDimensions: new Dictionary {
{ "Default", "SingleMetric" }
});
diff --git a/libraries/tests/e2e/functions/core/metrics/Function/src/Function/TestHelper.cs b/libraries/tests/e2e/functions/core/metrics/Function/src/Function/TestHelper.cs
index 750c77ab..c3434d28 100644
--- a/libraries/tests/e2e/functions/core/metrics/Function/src/Function/TestHelper.cs
+++ b/libraries/tests/e2e/functions/core/metrics/Function/src/Function/TestHelper.cs
@@ -33,7 +33,7 @@ public static void TestMethod(APIGatewayProxyRequest apigwProxyEvent, ILambdaCon
Metrics.AddMetadata("RequestId", apigwProxyEvent.RequestContext.RequestId);
Metrics.PushSingleMetric(
- metricName: "SingleMetric",
+ name: "SingleMetric",
value: 1,
unit: MetricUnit.Count,
nameSpace: "Test",
From e6f6be62b86bc3c04ccfc21da8d3ef4e69d8474b Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Sat, 22 Feb 2025 12:04:34 +0000
Subject: [PATCH 03/29] fix(metrics): ensure thread safety by locking metrics
during cold start flag reset
---
.../Http/MetricsHelper.cs | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsHelper.cs b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsHelper.cs
index 3a4e0357..250caef8 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsHelper.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsHelper.cs
@@ -47,7 +47,10 @@ public Task CaptureColdStartMetrics(HttpContext context)
return Task.CompletedTask;
var defaultDimensions = _metrics.Options.DefaultDimensions;
- _isColdStart = false;
+ lock (_metrics)
+ {
+ _isColdStart = false;
+ }
if (context.Items["LambdaContext"] is ILambdaContext lambdaContext)
{
From 0ecffb8b192e3c155c3c103767d4f277b1483054 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Sat, 22 Feb 2025 12:26:04 +0000
Subject: [PATCH 04/29] fix(tests): conditionally include project reference for
net8.0 framework
---
.../AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj
index c336bde1..b2f6d2dc 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj
@@ -26,7 +26,8 @@
-
+
From a3e6a31b0face6e4b9f742025a6eb83314665e29 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Sat, 22 Feb 2025 13:01:20 +0000
Subject: [PATCH 05/29] feat(build): enhance CI configuration with
multi-framework support for .NET 6.0 and 8.0
---
.github/workflows/build.yml | 35 +++++++++++++++++++++++++----------
1 file changed, 25 insertions(+), 10 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index cfc6045b..d552fa7c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -20,27 +20,42 @@ permissions:
jobs:
build:
runs-on: ubuntu-latest
-
+ strategy:
+ matrix:
+ dotnet-version: ['6.0.x', '8.0.x']
+ include:
+ - dotnet-version: '6.0.x'
+ framework: 'net6.0'
+ - dotnet-version: '8.0.x'
+ framework: 'net8.0'
+ fail-fast: false
+
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- - name: Setup .NET 6.0 & 8.0
+
+ - name: Setup .NET ${{ dotnet-version }}
uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # 4.3.0
with:
- dotnet-version: |
- 6.0.405
- 8.0.101
+ dotnet-version: ${{ dotnet-version }}
+
+ - name: Install dependencies
+ run: dotnet restore -f ${{ matrix.framework }}
+
- name: Build
- run: dotnet build --configuration Release
+ run: dotnet build --configuration Release --no-restore -f ${{ matrix.framework }}
+
- name: Test Examples
run: dotnet test ../examples/
+
- name: Test & Code Coverage
- run: dotnet test --filter "Category!=E2E" --collect:"XPlat Code Coverage" --results-directory ./codecov --verbosity normal
+ run: dotnet test --no-restore -f ${{ matrix.framework }} --filter "Category!=E2E" --collect:"XPlat Code Coverage" --results-directory ./codecov/${{ matrix.framework }} --verbosity quiet
+
- name: Codecov
uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # 5.3.1
with:
token: ${{ secrets.CODECOV_TOKEN }}
- flags: unittests
+ flags: ${{ matrix.framework }}
fail_ci_if_error: false
- name: codecov-lambda-powertools-dotnet
+ name: codecov-lambda-powertools-dotnet-${{ matrix.framework }}
verbose: true
- directory: ./libraries/codecov
+ directory: ./libraries/codecov/${{ matrix.framework }}
From c1936a515715afa07520b22c1119714e30ddd32c Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Sat, 22 Feb 2025 13:02:56 +0000
Subject: [PATCH 06/29] fix(build): update .NET setup step to use matrix
variable for versioning
---
.github/workflows/build.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index d552fa7c..f70e241c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -33,10 +33,10 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- - name: Setup .NET ${{ dotnet-version }}
+ - name: Setup .NET ${{ matrix.dotnet-version }}
uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # 4.3.0
with:
- dotnet-version: ${{ dotnet-version }}
+ dotnet-version: ${{ matrix.dotnet-version }}
- name: Install dependencies
run: dotnet restore -f ${{ matrix.framework }}
From 910f28667eddf35b5ec1b9eb78e305a3968a3136 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Sat, 22 Feb 2025 13:04:04 +0000
Subject: [PATCH 07/29] fix(build): simplify dependency installation step in CI
configuration
---
.github/workflows/build.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f70e241c..970d3c2b 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -39,7 +39,7 @@ jobs:
dotnet-version: ${{ matrix.dotnet-version }}
- name: Install dependencies
- run: dotnet restore -f ${{ matrix.framework }}
+ run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore -f ${{ matrix.framework }}
From 9f6f46027b2f992b9924207dc3988c1429bc788a Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Sat, 22 Feb 2025 13:13:06 +0000
Subject: [PATCH 08/29] fix(build): pass target framework properties during
restore, build, and test steps
---
.github/workflows/build.yml | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 970d3c2b..3d0350ed 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -26,8 +26,10 @@ jobs:
include:
- dotnet-version: '6.0.x'
framework: 'net6.0'
+ build-props: '/p:TargetFramework=net6.0'
- dotnet-version: '8.0.x'
framework: 'net8.0'
+ build-props: '/p:TargetFramework=net8.0'
fail-fast: false
steps:
@@ -39,16 +41,16 @@ jobs:
dotnet-version: ${{ matrix.dotnet-version }}
- name: Install dependencies
- run: dotnet restore
+ run: dotnet restore ${{ matrix.build-props }}
- name: Build
- run: dotnet build --configuration Release --no-restore -f ${{ matrix.framework }}
+ run: dotnet build --configuration Release --no-restore ${{ matrix.build-props }}
- name: Test Examples
- run: dotnet test ../examples/
+ run: dotnet test ../examples/ ${{ matrix.build-props }} --verbosity quiet
- name: Test & Code Coverage
- run: dotnet test --no-restore -f ${{ matrix.framework }} --filter "Category!=E2E" --collect:"XPlat Code Coverage" --results-directory ./codecov/${{ matrix.framework }} --verbosity quiet
+ run: dotnet test --no-restore ${{ matrix.build-props }} --filter "Category!=E2E" --collect:"XPlat Code Coverage" --results-directory ./codecov/${{ matrix.framework }} --verbosity quiet
- name: Codecov
uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # 5.3.1
From 491338e3d4f1f0acc90beaa1762afcf4926bacd8 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Sat, 22 Feb 2025 13:22:16 +0000
Subject: [PATCH 09/29] fix(build): add SkipInvalidProjects property to build
properties for .NET frameworks
---
.github/workflows/build.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 3d0350ed..a0122d2e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -26,10 +26,10 @@ jobs:
include:
- dotnet-version: '6.0.x'
framework: 'net6.0'
- build-props: '/p:TargetFramework=net6.0'
+ build-props: '/p:TargetFramework=net6.0 /p:SkipInvalidProjects=true'
- dotnet-version: '8.0.x'
framework: 'net8.0'
- build-props: '/p:TargetFramework=net8.0'
+ build-props: '/p:TargetFramework=net8.0 /p:SkipInvalidProjects=true'
fail-fast: false
steps:
From 60d6c64ccc8ba660ae927a162d53c20c8eb7e1c8 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Sat, 22 Feb 2025 16:14:29 +0000
Subject: [PATCH 10/29] revert to single job
---
.github/workflows/build.yml | 42 +++++++++++++------------------------
1 file changed, 14 insertions(+), 28 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a0122d2e..ff2fb598 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -20,44 +20,30 @@ permissions:
jobs:
build:
runs-on: ubuntu-latest
- strategy:
- matrix:
- dotnet-version: ['6.0.x', '8.0.x']
- include:
- - dotnet-version: '6.0.x'
- framework: 'net6.0'
- build-props: '/p:TargetFramework=net6.0 /p:SkipInvalidProjects=true'
- - dotnet-version: '8.0.x'
- framework: 'net8.0'
- build-props: '/p:TargetFramework=net8.0 /p:SkipInvalidProjects=true'
- fail-fast: false
-
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
-
- - name: Setup .NET ${{ matrix.dotnet-version }}
+
+ - name: Setup .NET SDK
uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # 4.3.0
with:
- dotnet-version: ${{ matrix.dotnet-version }}
-
+ dotnet-version: |
+ 6.0.x
+ 8.0.x
+
- name: Install dependencies
- run: dotnet restore ${{ matrix.build-props }}
-
+ run: dotnet restore
+
- name: Build
- run: dotnet build --configuration Release --no-restore ${{ matrix.build-props }}
-
- - name: Test Examples
- run: dotnet test ../examples/ ${{ matrix.build-props }} --verbosity quiet
-
+ run: dotnet build --configuration Release --no-restore
+
- name: Test & Code Coverage
- run: dotnet test --no-restore ${{ matrix.build-props }} --filter "Category!=E2E" --collect:"XPlat Code Coverage" --results-directory ./codecov/${{ matrix.framework }} --verbosity quiet
-
+ run: dotnet test --no-restore --filter "Category!=E2E" --collect:"XPlat Code Coverage" --results-directory ./codecov --verbosity quiet
+
- name: Codecov
uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # 5.3.1
with:
token: ${{ secrets.CODECOV_TOKEN }}
- flags: ${{ matrix.framework }}
fail_ci_if_error: false
- name: codecov-lambda-powertools-dotnet-${{ matrix.framework }}
+ name: codecov-lambda-powertools-dotnet
verbose: true
- directory: ./libraries/codecov/${{ matrix.framework }}
+ directory: ./libraries/codecov
\ No newline at end of file
From 0f7c21d35c09ce11141499f1ec73d127b4785eae Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Sat, 22 Feb 2025 16:23:50 +0000
Subject: [PATCH 11/29] test logger
---
.github/workflows/build.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index ff2fb598..0718756e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -37,7 +37,7 @@ jobs:
run: dotnet build --configuration Release --no-restore
- name: Test & Code Coverage
- run: dotnet test --no-restore --filter "Category!=E2E" --collect:"XPlat Code Coverage" --results-directory ./codecov --verbosity quiet
+ run: dotnet test --no-restore --filter "Category!=E2E" --collect:"XPlat Code Coverage" --results-directory ./codecov --logger "console;verbosity=quiet"
- name: Codecov
uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # 5.3.1
From 001dabfeab8046b80f317e027408adf9c4565217 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Sat, 22 Feb 2025 17:15:21 +0000
Subject: [PATCH 12/29] fix(build): update test commands and project
configurations for .NET frameworks
---
.github/workflows/build.yml | 5 ++++-
.../AWS.Lambda.Powertools.Metrics.AspNetCore.csproj | 6 ++++--
.../AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj | 6 +++---
3 files changed, 11 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 0718756e..b2ecc118 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -37,7 +37,10 @@ jobs:
run: dotnet build --configuration Release --no-restore
- name: Test & Code Coverage
- run: dotnet test --no-restore --filter "Category!=E2E" --collect:"XPlat Code Coverage" --results-directory ./codecov --logger "console;verbosity=quiet"
+ run: dotnet test --no-restore --filter "Category!=E2E" --collect:"XPlat Code Coverage" --results-directory ./codecov --verbosity quiet
+
+ - name: Test Examples
+ run: dotnet test ../examples/ --verbosity quiet
- name: Codecov
uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # 5.3.1
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/AWS.Lambda.Powertools.Metrics.AspNetCore.csproj b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/AWS.Lambda.Powertools.Metrics.AspNetCore.csproj
index 529fd973..976fe8b0 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/AWS.Lambda.Powertools.Metrics.AspNetCore.csproj
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/AWS.Lambda.Powertools.Metrics.AspNetCore.csproj
@@ -1,11 +1,13 @@
-
+
+
AWS.Lambda.Powertools.Metrics.AspNetCore
Powertools for AWS Lambda (.NET) - Metrics AspNetCore package.
AWS.Lambda.Powertools.Metrics.AspNetCore
AWS.Lambda.Powertools.Metrics.AspNetCore
- net8.0;net8.0
+ net8.0
+ false
enable
enable
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj
index b2f6d2dc..d820a783 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj
@@ -1,9 +1,10 @@
+
AWS.Lambda.Powertools.Metrics.AspNetCore.Tests
AWS.Lambda.Powertools.Metrics.AspNetCore.Tests
- net8.0
+ net8.0
enable
enable
@@ -26,8 +27,7 @@
-
+
From d6841ab8286b180dff47710f84bea3aa6f4d3005 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Sat, 22 Feb 2025 17:46:52 +0000
Subject: [PATCH 13/29] fix(build): add /tl option to dotnet build command in
build.yml
---
.github/workflows/build.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index b2ecc118..f0102235 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -34,7 +34,7 @@ jobs:
run: dotnet restore
- name: Build
- run: dotnet build --configuration Release --no-restore
+ run: dotnet build --configuration Release --no-restore /tl
- name: Test & Code Coverage
run: dotnet test --no-restore --filter "Category!=E2E" --collect:"XPlat Code Coverage" --results-directory ./codecov --verbosity quiet
From f8f56cfb9b1f2480d762d1238fb168d8792b436c Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Sat, 22 Feb 2025 18:06:14 +0000
Subject: [PATCH 14/29] fix(metrics): add null checks and unit tests for
MetricsAspect and MetricsAttribute
---
.../Internal/MetricsAspect.cs | 1 +
.../MetricsAttributeTests.cs | 72 +++++++++++
.../MetricsTests.cs | 121 +++++++++++++++++-
3 files changed, 191 insertions(+), 3 deletions(-)
create mode 100644 libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsAttributeTests.cs
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
index 4ebacf14..3a09db42 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
@@ -138,6 +138,7 @@ internal static void ResetForTest()
///
private static ILambdaContext GetContext(AspectEventArgs args)
{
+ if (args == null || args.Method == null) return null;
var index = Array.FindIndex(args.Method.GetParameters(), p => p.ParameterType == typeof(ILambdaContext));
if (index >= 0)
{
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsAttributeTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsAttributeTests.cs
new file mode 100644
index 00000000..04a1f86d
--- /dev/null
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsAttributeTests.cs
@@ -0,0 +1,72 @@
+using Xunit;
+
+namespace AWS.Lambda.Powertools.Metrics.Tests;
+
+[Collection("Sequential")]
+public class MetricsAttributeTests
+{
+ [Fact]
+ public void MetricsAttribute_WhenCaptureColdStartSet_ShouldSetFlag()
+ {
+ // Arrange & Act
+ var attribute = new MetricsAttribute
+ {
+ CaptureColdStart = true
+ };
+
+ // Assert
+ Assert.True(attribute.CaptureColdStart);
+ Assert.True(attribute.IsCaptureColdStartSet);
+ }
+
+ [Fact]
+ public void MetricsAttribute_WhenCaptureColdStartNotSet_ShouldNotSetFlag()
+ {
+ // Arrange & Act
+ var attribute = new MetricsAttribute();
+
+ // Assert
+ Assert.False(attribute.CaptureColdStart);
+ Assert.False(attribute.IsCaptureColdStartSet);
+ }
+
+ [Fact]
+ public void MetricsAttribute_WhenRaiseOnEmptyMetricsSet_ShouldSetFlag()
+ {
+ // Arrange & Act
+ var attribute = new MetricsAttribute
+ {
+ RaiseOnEmptyMetrics = true
+ };
+
+ // Assert
+ Assert.True(attribute.RaiseOnEmptyMetrics);
+ Assert.True(attribute.IsRaiseOnEmptyMetricsSet);
+ }
+
+ [Fact]
+ public void MetricsAttribute_WhenRaiseOnEmptyMetricsNotSet_ShouldNotSetFlag()
+ {
+ // Arrange & Act
+ var attribute = new MetricsAttribute();
+
+ // Assert
+ Assert.False(attribute.RaiseOnEmptyMetrics);
+ Assert.False(attribute.IsRaiseOnEmptyMetricsSet);
+ }
+
+ [Fact]
+ public void MetricsAttribute_ShouldSetNamespaceAndService()
+ {
+ // Arrange & Act
+ var attribute = new MetricsAttribute
+ {
+ Namespace = "TestNamespace",
+ Service = "TestService"
+ };
+
+ // Assert
+ Assert.Equal("TestNamespace", attribute.Namespace);
+ Assert.Equal("TestService", attribute.Service);
+ }
+}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs
index 97aa5bf8..120d1a72 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs
@@ -1,3 +1,7 @@
+using System;
+using System.Collections.Generic;
+using Amazon.Lambda.Core;
+using Amazon.Lambda.TestUtilities;
using AWS.Lambda.Powertools.Common;
using NSubstitute;
using Xunit;
@@ -18,11 +22,11 @@ public void Metrics_Set_Execution_Environment_Context()
var env = Substitute.For();
env.GetAssemblyName(Arg.Any()).Returns(assemblyName);
env.GetAssemblyVersion(Arg.Any()).Returns(assemblyVersion);
-
+
var conf = new PowertoolsConfigurations(new SystemWrapper(env));
-
+
var metrics = new Metrics(conf);
-
+
// Assert
env.Received(1).SetEnvironmentVariable(
"AWS_EXECUTION_ENV", $"{Constants.FeatureContextIdentifier}/Metrics/{assemblyVersion}"
@@ -30,4 +34,115 @@ public void Metrics_Set_Execution_Environment_Context()
env.Received(1).GetEnvironmentVariable("AWS_EXECUTION_ENV");
}
+
+ [Fact]
+ public void Before_With_Null_DefaultDimensions_Should_Not_Throw()
+ {
+ // Arrange
+ MetricsAspect.ResetForTest();
+ var metricsMock = Substitute.For();
+ var optionsMock = new MetricsOptions
+ {
+ CaptureColdStart = true,
+ DefaultDimensions = null
+ };
+ metricsMock.Options.Returns(optionsMock);
+ Metrics.UseMetricsForTests(metricsMock);
+
+ var metricsAspect = new MetricsAspect();
+ var method = typeof(MetricsTests).GetMethod(nameof(TestMethod));
+ var trigger = new MetricsAttribute();
+
+ // Act
+ metricsAspect.Before(
+ this,
+ "TestMethod",
+ new object[] { new TestLambdaContext() },
+ typeof(MetricsTests),
+ method,
+ typeof(void),
+ new Attribute[] { trigger }
+ );
+
+ // Assert
+ metricsMock.Received(1).PushSingleMetric(
+ "ColdStart",
+ 1.0,
+ MetricUnit.Count,
+ Arg.Any(),
+ Arg.Any(),
+ null
+ );
+ }
+
+ [Fact]
+ public void Before_When_CaptureStartNotSet_Should_Not_Push_Metrics()
+ {
+ // Arrange
+ MetricsAspect.ResetForTest();
+ var metricsMock = Substitute.For();
+ var optionsMock = new MetricsOptions
+ {
+ CaptureColdStart = null
+ };
+ metricsMock.Options.Returns(optionsMock);
+ Metrics.UseMetricsForTests(metricsMock);
+
+ var metricsAspect = new MetricsAspect();
+ var method = typeof(MetricsTests).GetMethod(nameof(TestMethod));
+ var trigger = new MetricsAttribute();
+
+ // Act
+ metricsAspect.Before(
+ this,
+ "TestMethod",
+ new object[] { new TestLambdaContext() },
+ typeof(MetricsTests),
+ method,
+ typeof(void),
+ new Attribute[] { trigger }
+ );
+
+ // Assert
+ metricsMock.DidNotReceive().PushSingleMetric(
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any>()
+ );
+ }
+
+ [Fact]
+ public void Before_When_RaiseOnEmptyMetricsNotSet_Should_Configure_Null()
+ {
+ // Arrange
+ MetricsAspect.ResetForTest();
+ var method = typeof(MetricsTests).GetMethod(nameof(TestMethod));
+ var trigger = new MetricsAttribute();
+
+ var metricsAspect = new MetricsAspect();
+
+ // Act
+ metricsAspect.Before(
+ this,
+ "TestMethod",
+ new object[] { new TestLambdaContext() },
+ typeof(MetricsTests),
+ method,
+ typeof(void),
+ new Attribute[] { trigger }
+ );
+
+ // Assert
+ var metrics = Metrics.Instance;
+ Assert.False(trigger.IsRaiseOnEmptyMetricsSet);
+ Assert.False(metrics.Options.RaiseOnEmptyMetrics);
+ }
+
+ // Helper method for the tests
+ internal void TestMethod(ILambdaContext context)
+ {
+ }
}
\ No newline at end of file
From b3a57a8fd8eb7607da04bb725f21f58f3b2245a8 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Mon, 24 Feb 2025 12:25:37 +0000
Subject: [PATCH 15/29] feat(metrics): add support for default dimensions in
metrics handling
---
docs/core/metrics-v2.md | 65 +++++++++++++++++++
.../AWS.Lambda.Powertools.Metrics/Metrics.cs | 18 ++++-
.../EMFValidationTests.cs | 26 ++++++++
.../Handlers/FunctionHandler.cs | 16 +++++
.../Handlers/FunctionHandlerTests.cs | 34 ++++++++++
.../Handlers/MetricsnBuilderHandler.cs | 10 +++
6 files changed, 168 insertions(+), 1 deletion(-)
diff --git a/docs/core/metrics-v2.md b/docs/core/metrics-v2.md
index af5f6859..5502edde 100644
--- a/docs/core/metrics-v2.md
+++ b/docs/core/metrics-v2.md
@@ -596,6 +596,30 @@ CloudWatch EMF uses the same dimensions across all your metrics. Use **`PushSing
=== "Function.cs"
+ ```csharp hl_lines="8-13"
+ using AWS.Lambda.Powertools.Metrics;
+
+ public class Function {
+
+ [Metrics(Namespace = ExampleApplication, Service = "Booking")]
+ public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
+ {
+ Metrics.PushSingleMetric(
+ metricName: "ColdStart",
+ value: 1,
+ unit: MetricUnit.Count,
+ nameSpace: "ExampleApplication",
+ service: "Booking");
+ ...
+ ```
+
+By default it will skip all previously defined dimensions including default dimensions. Use default_dimensions keyword argument if you want to reuse default dimensions or specify custom dimensions from a dictionary.
+
+- `Metrics.DefaultDimensions`: Reuse default dimensions when using static Metrics
+- `Options.DefaultDimensions`: Reuse default dimensions when using Builder or Configure patterns
+
+=== "New Default Dimensions.cs"
+
```csharp hl_lines="8-17"
using AWS.Lambda.Powertools.Metrics;
@@ -616,6 +640,47 @@ CloudWatch EMF uses the same dimensions across all your metrics. Use **`PushSing
});
...
```
+=== "Default Dimensions static.cs"
+
+ ```csharp hl_lines="8-12"
+ using AWS.Lambda.Powertools.Metrics;
+
+ public class Function {
+
+ [Metrics(Namespace = ExampleApplication, Service = "Booking")]
+ public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
+ {
+ Metrics.SetDefaultDimensions(new Dictionary
+ {
+ { "Default", "SingleMetric" }
+ });
+ Metrics.PushSingleMetric("SingleMetric", 1, MetricUnit.Count, defaultDimensions: Metrics.DefaultDimensions );
+ ...
+ ```
+=== "Default Dimensions Options / Builder patterns .cs"
+
+ ```csharp hl_lines="9-13 18"
+ using AWS.Lambda.Powertools.Metrics;
+
+ public MetricsnBuilderHandler(IMetrics metrics = null)
+ {
+ _metrics = metrics ?? new MetricsBuilder()
+ .WithCaptureColdStart(true)
+ .WithService("testService")
+ .WithNamespace("dotnet-powertools-test")
+ .WithDefaultDimensions(new Dictionary
+ {
+ { "Environment", "Prod1" },
+ { "Another", "One" }
+ }).Build();
+ }
+
+ public void HandlerSingleMetricDimensions()
+ {
+ _metrics.PushSingleMetric("SuccessfulBooking", 1, MetricUnit.Count, defaultDimensions: _metrics.Options.DefaultDimensions);
+ }
+ ...
+ ```
## AspNetCore
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
index 86823bf0..8da23030 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
@@ -17,6 +17,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Runtime.CompilerServices;
using System.Threading;
using AWS.Lambda.Powertools.Common;
@@ -32,12 +33,27 @@ public class Metrics : IMetrics, IDisposable
///
/// Gets or sets the instance.
///
- public static IMetrics Instance
+ internal static IMetrics Instance
{
get => Current.Value ?? new Metrics(PowertoolsConfigurations.Instance);
private set => Current.Value = value;
}
+ ///
+ /// Gets DefaultDimensions
+ ///
+ public static Dictionary DefaultDimensions => Instance.Options.DefaultDimensions;
+
+ ///
+ /// Gets Namespace
+ ///
+ public static string Namespace => Instance.Options.Namespace;
+
+ ///
+ /// Gets Service
+ ///
+ public static string Service => Instance.Options.Service;
+
///
public MetricsOptions Options =>
new()
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs
index adce5337..04b86b3c 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs
@@ -176,6 +176,32 @@ public void When_PushSingleMetric_With_Namespace()
Assert.Contains("\"CloudWatchMetrics\":[{\"Namespace\":\"ExampleApplication\",\"Metrics\":[{\"Name\":\"SingleMetric\",\"Unit\":\"Count\",\"StorageResolution\":1}],\"Dimensions\":[[\"Default\"]]}]},\"Default\":\"SingleMetric\",\"SingleMetric\":1}", metricsOutput);
}
+ [Trait("Category", "SchemaValidation")]
+ [Fact]
+ public void When_PushSingleMetric_With_No_DefaultDimensions()
+ {
+ // Act
+ _handler.PushSingleMetricNoDefaultDimensions();
+
+ var metricsOutput = _consoleOut.ToString();
+
+ // Assert
+ Assert.Contains("\"CloudWatchMetrics\":[{\"Namespace\":\"ExampleApplication\",\"Metrics\":[{\"Name\":\"SingleMetric\",\"Unit\":\"Count\"}],\"Dimensions\":[[]]}]},\"SingleMetric\":1}", metricsOutput);
+ }
+
+ [Trait("Category", "SchemaValidation")]
+ [Fact]
+ public void When_PushSingleMetric_With_DefaultDimensions()
+ {
+ // Act
+ _handler.PushSingleMetricDefaultDimensions();
+
+ var metricsOutput = _consoleOut.ToString();
+
+ // Assert
+ Assert.Contains("\"CloudWatchMetrics\":[{\"Namespace\":\"ExampleApplication\",\"Metrics\":[{\"Name\":\"SingleMetric\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Default\"]]}]},\"Default\":\"SingleMetric\",\"SingleMetric\":1}", metricsOutput);
+ }
+
[Trait("Category", "SchemaValidation")]
[Fact]
public void When_PushSingleMetric_With_Env_Namespace()
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs
index d860a9f9..8954134c 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs
@@ -65,6 +65,22 @@ public void PushSingleMetricWithNamespace()
});
}
+ [Metrics(Namespace = "ExampleApplication")]
+ public void PushSingleMetricNoDefaultDimensions()
+ {
+ Metrics.PushSingleMetric("SingleMetric", 1, MetricUnit.Count);
+ }
+
+ [Metrics(Namespace = "ExampleApplication")]
+ public void PushSingleMetricDefaultDimensions()
+ {
+ Metrics.SetDefaultDimensions(new Dictionary
+ {
+ { "Default", "SingleMetric" }
+ });
+ Metrics.PushSingleMetric("SingleMetric", 1, MetricUnit.Count, defaultDimensions: Metrics.DefaultDimensions );
+ }
+
[Metrics]
public void PushSingleMetricWithEnvNamespace()
{
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
index dc338d59..1049c737 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
@@ -266,6 +266,40 @@ public void Handler_With_Builder_Should_Configure_In_Constructor_Mock()
service: "testService", Arg.Any>());
metricsMock.Received(1).AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
}
+
+ [Fact]
+ public void Handler_With_Builder_Push_Single_Metric_No_Dimensions()
+ {
+ // Arrange
+ var handler = new MetricsnBuilderHandler();
+
+ // Act
+ handler.HandlerSingleMetric();
+
+ // Get the output and parse it
+ var metricsOutput = _consoleOut.ToString();
+
+ Assert.Contains(
+ "\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"SuccessfulBooking\",\"Unit\":\"Count\"}],\"Dimensions\":[[]]}]},\"SuccessfulBooking\":1}",
+ metricsOutput);
+ }
+
+ [Fact]
+ public void Handler_With_Builder_Push_Single_Metric_Dimensions()
+ {
+ // Arrange
+ var handler = new MetricsnBuilderHandler();
+
+ // Act
+ handler.HandlerSingleMetricDimensions();
+
+ // Get the output and parse it
+ var metricsOutput = _consoleOut.ToString();
+
+ Assert.Contains(
+ "\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"SuccessfulBooking\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"Environment\",\"Another\"]]}]},\"Service\":\"testService\",\"Environment\":\"Prod1\",\"Another\":\"One\",\"SuccessfulBooking\":1}",
+ metricsOutput);
+ }
public void Dispose()
{
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/MetricsnBuilderHandler.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/MetricsnBuilderHandler.cs
index f9fd329f..82c16b9c 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/MetricsnBuilderHandler.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/MetricsnBuilderHandler.cs
@@ -26,4 +26,14 @@ public void Handler(ILambdaContext context)
{
_metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
}
+
+ public void HandlerSingleMetric()
+ {
+ _metrics.PushSingleMetric("SuccessfulBooking", 1, MetricUnit.Count);
+ }
+
+ public void HandlerSingleMetricDimensions()
+ {
+ _metrics.PushSingleMetric("SuccessfulBooking", 1, MetricUnit.Count, defaultDimensions: _metrics.Options.DefaultDimensions);
+ }
}
\ No newline at end of file
From dbb337795ebce2af9e48aa74750950cd62f55671 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Tue, 25 Feb 2025 09:53:38 +0000
Subject: [PATCH 16/29] fix functionname dimension to only coldstart
---
.../Internal/MetricsAspect.cs | 24 ++++++++++-----
.../Handlers/FunctionHandler.cs | 6 ++++
.../Handlers/FunctionHandlerTests.cs | 30 +++++++++++++++++--
3 files changed, 50 insertions(+), 10 deletions(-)
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
index 3a09db42..dae4c321 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
@@ -14,6 +14,7 @@
*/
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Amazon.Lambda.Core;
@@ -69,11 +70,13 @@ public void Before(
var trigger = triggers.OfType().First();
- _metricsInstance ??= Metrics.Configure(options => {
+ _metricsInstance ??= Metrics.Configure(options =>
+ {
options.Namespace = trigger.Namespace;
options.Service = trigger.Service;
options.RaiseOnEmptyMetrics = trigger.IsRaiseOnEmptyMetricsSet ? trigger.RaiseOnEmptyMetrics : null;
options.CaptureColdStart = trigger.IsCaptureColdStartSet ? trigger.CaptureColdStart : null;
+ options.FunctionName = trigger.FunctionName;
});
var eventArgs = new AspectEventArgs
@@ -87,17 +90,22 @@ public void Before(
Triggers = triggers
};
- if (_metricsInstance.Options.CaptureColdStart != null && _metricsInstance.Options.CaptureColdStart.Value && _isColdStart)
+ if (_metricsInstance.Options.CaptureColdStart != null && _metricsInstance.Options.CaptureColdStart.Value &&
+ _isColdStart)
{
- var defaultDimensions = _metricsInstance.Options?.DefaultDimensions;
_isColdStart = false;
- var context = GetContext(eventArgs);
-
- if (context is not null)
+ var functionName = _metricsInstance.Options?.FunctionName;
+ var defaultDimensions = _metricsInstance.Options?.DefaultDimensions;
+
+ if (string.IsNullOrWhiteSpace(functionName))
+ {
+ functionName = GetContext(eventArgs)?.FunctionName ?? "";
+ }
+
+ if (!string.IsNullOrWhiteSpace(functionName))
{
- defaultDimensions?.Add("FunctionName", context.FunctionName);
- _metricsInstance.SetDefaultDimensions(defaultDimensions);
+ defaultDimensions?.Add("FunctionName", functionName);
}
_metricsInstance.PushSingleMetric(
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs
index 8954134c..4abb6f45 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs
@@ -230,4 +230,10 @@ public void HandleWithParamAndLambdaContext(string input, ILambdaContext context
{
}
+
+ [Metrics(Namespace = "ns", Service = "svc", CaptureColdStart = true)]
+ public void HandleOnlyDimensionsInColdStart(ILambdaContext context)
+ {
+ Metrics.AddMetric("MyMetric", 1);
+ }
}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
index 1049c737..44d4e841 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
@@ -174,7 +174,7 @@ public void DefaultDimensions_AreAppliedCorrectly_WithContext_FunctionName()
metricsOutput);
// Assert successful Memory metrics
Assert.Contains(
- "\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"Memory\",\"Unit\":\"Megabytes\"}],\"Dimensions\":[[\"Service\",\"Environment\",\"Another\",\"FunctionName\"]]}]},\"Service\":\"testService\",\"Environment\":\"Prod\",\"Another\":\"One\",\"FunctionName\":\"My_Function_Name\",\"Memory\":10}",
+ "\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"Memory\",\"Unit\":\"Megabytes\"}],\"Dimensions\":[[\"Service\",\"Environment\",\"Another\"]]}]},\"Service\":\"testService\",\"Environment\":\"Prod\",\"Another\":\"One\",\"Memory\":10}",
metricsOutput);
}
@@ -231,7 +231,7 @@ public void Handler_With_Builder_Should_Configure_In_Constructor()
metricsOutput);
// Assert successful Memory metrics
Assert.Contains(
- "\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"SuccessfulBooking\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"Environment\",\"Another\",\"FunctionName\"]]}]},\"Service\":\"testService\",\"Environment\":\"Prod1\",\"Another\":\"One\",\"FunctionName\":\"My_Function_Name\",\"SuccessfulBooking\":1}",
+ "\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"SuccessfulBooking\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"Environment\",\"Another\"]]}]},\"Service\":\"testService\",\"Environment\":\"Prod1\",\"Another\":\"One\",\"SuccessfulBooking\":1}",
metricsOutput);
}
@@ -300,6 +300,32 @@ public void Handler_With_Builder_Push_Single_Metric_Dimensions()
"\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"SuccessfulBooking\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"Environment\",\"Another\"]]}]},\"Service\":\"testService\",\"Environment\":\"Prod1\",\"Another\":\"One\",\"SuccessfulBooking\":1}",
metricsOutput);
}
+
+ [Fact]
+ public void Dimension_Only_Set_In_Cold_Start()
+ {
+ // Arrange
+ var handler = new FunctionHandler();
+
+ // Act
+ handler.HandleOnlyDimensionsInColdStart(new TestLambdaContext
+ {
+ FunctionName = "My_Function_Name"
+ });
+
+ // Get the output and parse it
+ var metricsOutput = _consoleOut.ToString();
+
+ // Assert cold start
+ Assert.Contains(
+ "\"CloudWatchMetrics\":[{\"Namespace\":\"ns\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"FunctionName\"]]}]},\"Service\":\"svc\",\"FunctionName\":\"My_Function_Name\",\"ColdStart\":1}",
+ metricsOutput);
+
+ // Assert successful add metric without dimensions
+ Assert.Contains(
+ "\"CloudWatchMetrics\":[{\"Namespace\":\"ns\",\"Metrics\":[{\"Name\":\"MyMetric\",\"Unit\":\"None\"}],\"Dimensions\":[[\"Service\"]]}]},\"Service\":\"svc\",\"MyMetric\":1}",
+ metricsOutput);
+ }
public void Dispose()
{
From 9e71bec12e51829d7c57863993860bcd1dcd21a7 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Tue, 25 Feb 2025 10:04:09 +0000
Subject: [PATCH 17/29] remove functionname
---
.../Internal/MetricsAspect.cs | 19 ++++++-------------
1 file changed, 6 insertions(+), 13 deletions(-)
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
index dae4c321..1577f5fc 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
@@ -76,7 +76,6 @@ public void Before(
options.Service = trigger.Service;
options.RaiseOnEmptyMetrics = trigger.IsRaiseOnEmptyMetricsSet ? trigger.RaiseOnEmptyMetrics : null;
options.CaptureColdStart = trigger.IsCaptureColdStartSet ? trigger.CaptureColdStart : null;
- options.FunctionName = trigger.FunctionName;
});
var eventArgs = new AspectEventArgs
@@ -90,22 +89,16 @@ public void Before(
Triggers = triggers
};
- if (_metricsInstance.Options.CaptureColdStart != null && _metricsInstance.Options.CaptureColdStart.Value &&
- _isColdStart)
+ if (_metricsInstance.Options.CaptureColdStart != null && _metricsInstance.Options.CaptureColdStart.Value && _isColdStart)
{
- _isColdStart = false;
-
- var functionName = _metricsInstance.Options?.FunctionName;
var defaultDimensions = _metricsInstance.Options?.DefaultDimensions;
+ _isColdStart = false;
- if (string.IsNullOrWhiteSpace(functionName))
- {
- functionName = GetContext(eventArgs)?.FunctionName ?? "";
- }
-
- if (!string.IsNullOrWhiteSpace(functionName))
+ var context = GetContext(eventArgs);
+
+ if (context is not null)
{
- defaultDimensions?.Add("FunctionName", functionName);
+ defaultDimensions?.Add("FunctionName", context.FunctionName);
}
_metricsInstance.PushSingleMetric(
From 4ea9fa9ec90447b73e548845a1c05c8b7dc31516 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Tue, 25 Feb 2025 16:27:20 +0000
Subject: [PATCH 18/29] fix(metrics): rename variable for default dimensions in
cold start handling
---
.../AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
index 1577f5fc..d5eac029 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
@@ -91,14 +91,14 @@ public void Before(
if (_metricsInstance.Options.CaptureColdStart != null && _metricsInstance.Options.CaptureColdStart.Value && _isColdStart)
{
- var defaultDimensions = _metricsInstance.Options?.DefaultDimensions;
+ var dimensions = _metricsInstance.Options?.DefaultDimensions;
_isColdStart = false;
var context = GetContext(eventArgs);
if (context is not null)
{
- defaultDimensions?.Add("FunctionName", context.FunctionName);
+ dimensions?.Add("FunctionName", context.FunctionName);
}
_metricsInstance.PushSingleMetric(
@@ -107,7 +107,7 @@ public void Before(
MetricUnit.Count,
_metricsInstance.Options?.Namespace ?? "",
_metricsInstance.Options?.Service ?? "",
- defaultDimensions
+ dimensions
);
}
}
From 5bc9af2137b259e13f3fb119978514920ee170cb Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Wed, 26 Feb 2025 10:19:22 +0000
Subject: [PATCH 19/29] refactor(metrics): simplify MetricsTests by removing
unused variables and improving syntax
---
.../MetricsTests.cs | 19 ++++++++-----------
1 file changed, 8 insertions(+), 11 deletions(-)
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs
index ea87558c..6d82cc92 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs
@@ -2,8 +2,6 @@
using System.Collections.Generic;
using Amazon.Lambda.Core;
using Amazon.Lambda.TestUtilities;
-using System;
-using System.Collections.Generic;
using AWS.Lambda.Powertools.Common;
using NSubstitute;
using Xunit;
@@ -27,7 +25,7 @@ public void Metrics_Set_Execution_Environment_Context()
var conf = new PowertoolsConfigurations(new SystemWrapper(env));
- var metrics = new Metrics(conf);
+ _ = new Metrics(conf);
// Assert
env.Received(1).SetEnvironmentVariable(
@@ -59,11 +57,11 @@ public void Before_With_Null_DefaultDimensions_Should_Not_Throw()
metricsAspect.Before(
this,
"TestMethod",
- new object[] { new TestLambdaContext() },
+ [new TestLambdaContext()],
typeof(MetricsTests),
method,
typeof(void),
- new Attribute[] { trigger }
+ [trigger]
);
// Assert
@@ -72,8 +70,7 @@ public void Before_With_Null_DefaultDimensions_Should_Not_Throw()
1.0,
MetricUnit.Count,
Arg.Any(),
- Arg.Any(),
- null
+ Arg.Any()
);
}
@@ -152,7 +149,7 @@ internal void TestMethod(ILambdaContext context)
public void When_Constructor_With_Namespace_And_Service_Should_Set_Both()
{
// Arrange
- var metricsMock = Substitute.For();
+ Substitute.For();
var powertoolsConfigMock = Substitute.For();
// Act
@@ -167,13 +164,13 @@ public void When_Constructor_With_Namespace_And_Service_Should_Set_Both()
public void When_Constructor_With_Null_Namespace_And_Service_Should_Not_Set()
{
// Arrange
- var metricsMock = Substitute.For();
+ Substitute.For();
var powertoolsConfigMock = Substitute.For();
powertoolsConfigMock.MetricsNamespace.Returns((string)null);
powertoolsConfigMock.Service.Returns("service_undefined");
// Act
- var metrics = new Metrics(powertoolsConfigMock, null, null);
+ var metrics = new Metrics(powertoolsConfigMock);
// Assert
Assert.Null(metrics.GetNamespace());
@@ -184,7 +181,7 @@ public void When_Constructor_With_Null_Namespace_And_Service_Should_Not_Set()
public void When_AddMetric_With_EmptyKey_Should_ThrowArgumentNullException()
{
// Arrange
- var metricsMock = Substitute.For();
+ Substitute.For();
var powertoolsConfigMock = Substitute.For();
IMetrics metrics = new Metrics(powertoolsConfigMock);
From 3444d88ee1c1f9fcd1f6422c67c14039d57c3a92 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Wed, 26 Feb 2025 10:28:10 +0000
Subject: [PATCH 20/29] docs(metrics): document breaking changes in metrics
output format and default dimensions
---
docs/core/metrics-v2.md | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/docs/core/metrics-v2.md b/docs/core/metrics-v2.md
index af5f6859..7c31580d 100644
--- a/docs/core/metrics-v2.md
+++ b/docs/core/metrics-v2.md
@@ -16,6 +16,11 @@ These metrics can be visualized through [Amazon CloudWatch Console](https://aws.
* Ahead-of-Time compilation to native code support [AOT](https://docs.aws.amazon.com/lambda/latest/dg/dotnet-native-aot.html) from version 1.7.0
* Support for AspNetCore middleware and filters to capture metrics for HTTP requests
+## Breaking changes from V1
+
+* **`Dimensions`** outputs as an array of arrays instead of an array of objects. Example: `Dimensions: [["service", "Environment"]]` instead of `Dimensions: ["service", "Environment"]`
+* **`FunctionName`** is not added as default dimension and only to cold start metric.
+
From 3b090f76d38e8960c734a9db3dccffc8bbd1e754 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Wed, 26 Feb 2025 15:38:40 +0000
Subject: [PATCH 21/29] feat(metrics): implement IConsoleWrapper for
abstracting console operations and enhance cold start metric capturing
---
.../Core/ConsoleWrapper.cs | 31 ++++
.../Core/IConsoleWrapper.cs | 46 +++++
.../Http/MetricsHelper.cs | 19 +-
.../AWS.Lambda.Powertools.Metrics/IMetrics.cs | 7 +
.../Internal/MetricsAspect.cs | 22 +--
.../InternalsVisibleTo.cs | 4 +-
.../AWS.Lambda.Powertools.Metrics/Metrics.cs | 91 ++++++----
...Powertools.Metrics.AspNetCore.Tests.csproj | 2 +
.../MetricsEndpointExtensionsTests.cs | 164 ++++++++++++++++++
.../MetricsFilterTests.cs | 52 +-----
.../MetricsHelperTests.cs | 84 ---------
.../MetricsMiddlewareExtensionsTests.cs | 99 -----------
.../Handlers/FunctionHandlerTests.cs | 94 +---------
.../MetricsTests.cs | 139 +++++++++------
libraries/tests/Directory.Packages.props | 1 +
15 files changed, 417 insertions(+), 438 deletions(-)
create mode 100644 libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs
create mode 100644 libraries/src/AWS.Lambda.Powertools.Common/Core/IConsoleWrapper.cs
create mode 100644 libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsEndpointExtensionsTests.cs
delete mode 100644 libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsHelperTests.cs
delete mode 100644 libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsMiddlewareExtensionsTests.cs
diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs
new file mode 100644
index 00000000..87321140
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs
@@ -0,0 +1,31 @@
+/*
+ * 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 System;
+
+namespace AWS.Lambda.Powertools.Common;
+
+///
+public class ConsoleWrapper : IConsoleWrapper
+{
+ ///
+ public void WriteLine(string message) => Console.WriteLine(message);
+ ///
+ public void Debug(string message) => System.Diagnostics.Debug.WriteLine(message);
+ ///
+ public void Error(string message) => Console.Error.WriteLine(message);
+ ///
+ public string ReadLine() => Console.ReadLine();
+}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/IConsoleWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/IConsoleWrapper.cs
new file mode 100644
index 00000000..de75020e
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/IConsoleWrapper.cs
@@ -0,0 +1,46 @@
+/*
+ * 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 AWS.Lambda.Powertools.Common;
+
+///
+/// Wrapper for console operations to facilitate testing by abstracting system console interactions.
+///
+public interface IConsoleWrapper
+{
+ ///
+ /// Writes the specified message followed by a line terminator to the standard output stream.
+ ///
+ /// The message to write.
+ void WriteLine(string message);
+
+ ///
+ /// Writes a debug message to the trace listeners in the Debug.Listeners collection.
+ ///
+ /// The debug message to write.
+ void Debug(string message);
+
+ ///
+ /// Writes the specified error message followed by a line terminator to the standard error stream.
+ ///
+ /// The error message to write.
+ void Error(string message);
+
+ ///
+ /// Reads the next line of characters from the standard input stream.
+ ///
+ /// The next line of characters from the input stream, or null if no more lines are available.
+ string ReadLine();
+}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsHelper.cs b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsHelper.cs
index 250caef8..a86ff0b0 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsHelper.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsHelper.cs
@@ -43,29 +43,16 @@ public MetricsHelper(IMetrics metrics)
/// A task that represents the asynchronous operation.
public Task CaptureColdStartMetrics(HttpContext context)
{
- if (_metrics.Options.CaptureColdStart == null || !_metrics.Options.CaptureColdStart.Value || !_isColdStart)
+ if (!_isColdStart)
return Task.CompletedTask;
- var defaultDimensions = _metrics.Options.DefaultDimensions;
lock (_metrics)
{
_isColdStart = false;
}
- if (context.Items["LambdaContext"] is ILambdaContext lambdaContext)
- {
- defaultDimensions?.Add("FunctionName", lambdaContext.FunctionName);
- _metrics.SetDefaultDimensions(defaultDimensions);
- }
-
- _metrics.PushSingleMetric(
- "ColdStart",
- 1.0,
- MetricUnit.Count,
- _metrics.Options.Namespace,
- _metrics.Options.Service,
- defaultDimensions
- );
+ _metrics.CaptureColdStartMetric(context.Items["LambdaContext"] as ILambdaContext);
+
return Task.CompletedTask;
}
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs
index 69ef2ee3..61e15b7b 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs
@@ -14,6 +14,7 @@
*/
using System.Collections.Generic;
+using Amazon.Lambda.Core;
namespace AWS.Lambda.Powertools.Metrics;
@@ -106,4 +107,10 @@ void PushSingleMetric(string name, double value, MetricUnit unit, string nameSpa
///
/// The metrics options.
public MetricsOptions Options { get; }
+
+ ///
+ /// Captures the cold start metric.
+ ///
+ ///
+ void CaptureColdStartMetric(ILambdaContext context);
}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
index 6823b697..aad3617c 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
@@ -88,28 +88,10 @@ public void Before(
Triggers = triggers
};
- if (_metricsInstance.Options.CaptureColdStart != null && _metricsInstance.Options.CaptureColdStart.Value && _isColdStart)
+ if (_isColdStart)
{
- var defaultDimensions = _metricsInstance.Options?.DefaultDimensions;
+ _metricsInstance.CaptureColdStartMetric(GetContext(eventArgs));
_isColdStart = false;
-
- var context = GetContext(eventArgs);
-
- if (context is not null)
- {
- defaultDimensions ??= new Dictionary();
- defaultDimensions.Add("FunctionName", context.FunctionName);
- _metricsInstance.SetDefaultDimensions(defaultDimensions);
- }
-
- _metricsInstance.PushSingleMetric(
- "ColdStart",
- 1.0,
- MetricUnit.Count,
- _metricsInstance.Options?.Namespace ?? "",
- _metricsInstance.Options?.Service ?? "",
- defaultDimensions
- );
}
}
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/InternalsVisibleTo.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/InternalsVisibleTo.cs
index 0e44da1c..a1b53257 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/InternalsVisibleTo.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/InternalsVisibleTo.cs
@@ -15,4 +15,6 @@
using System.Runtime.CompilerServices;
-[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Metrics.Tests")]
\ No newline at end of file
+[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Metrics.Tests")]
+[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Metrics.AspNetCore")]
+[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Metrics.AspNetCore.Tests")]
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
index 86823bf0..06b1dd18 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
@@ -15,9 +15,8 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Linq;
-using System.Threading;
+using Amazon.Lambda.Core;
using AWS.Lambda.Powertools.Common;
namespace AWS.Lambda.Powertools.Metrics;
@@ -34,12 +33,12 @@ public class Metrics : IMetrics, IDisposable
///
public static IMetrics Instance
{
- get => Current.Value ?? new Metrics(PowertoolsConfigurations.Instance);
- private set => Current.Value = value;
+ get => _instance ?? new Metrics(PowertoolsConfigurations.Instance, consoleWrapper: new ConsoleWrapper());
+ private set => _instance = value;
}
///
- public MetricsOptions Options =>
+ public MetricsOptions Options => _options ??
new()
{
CaptureColdStart = _captureColdStartEnabled,
@@ -52,7 +51,7 @@ public static IMetrics Instance
///
/// The instance
///
- private static readonly AsyncLocal Current = new();
+ private static IMetrics _instance;
///
/// The context
@@ -74,11 +73,20 @@ public static IMetrics Instance
///
private bool _captureColdStartEnabled;
- //
- // Shared synchronization object
- //
+ ///
+ /// Shared synchronization object
+ ///
private readonly object _lockObj = new();
+ ///
+ /// The options
+ ///
+ private readonly MetricsOptions _options;
+
+ ///
+ /// The console wrapper for console output
+ ///
+ private readonly IConsoleWrapper _consoleWrapper;
///
/// Initializes a new instance of the class.
@@ -117,13 +125,17 @@ public static IMetrics Configure(Action configure)
/// Metrics Service Name
/// Instructs metrics validation to throw exception if no metrics are provided
/// Instructs metrics capturing the ColdStart is enabled
+ /// For console output
+ /// MetricsOptions
internal Metrics(IPowertoolsConfigurations powertoolsConfigurations, string nameSpace = null, string service = null,
- bool raiseOnEmptyMetrics = false, bool captureColdStartEnabled = false)
+ bool raiseOnEmptyMetrics = false, bool captureColdStartEnabled = false, IConsoleWrapper consoleWrapper = null, MetricsOptions options = null)
{
_powertoolsConfigurations = powertoolsConfigurations;
+ _consoleWrapper = consoleWrapper;
_context = new MetricsContext();
_raiseOnEmptyMetrics = raiseOnEmptyMetrics;
_captureColdStartEnabled = captureColdStartEnabled;
+ _options = options;
Instance = this;
_powertoolsConfigurations.SetExecutionEnvironment(this);
@@ -165,7 +177,7 @@ void IMetrics.AddMetric(string key, double value, MetricUnit unit, MetricResolut
}
else
{
- Debug.WriteLine(
+ _consoleWrapper.Debug(
$"##WARNING##: Metrics should be initialized in Handler method before calling {nameof(AddMetric)} method.");
}
}
@@ -237,7 +249,7 @@ void IMetrics.Flush(bool metricsOverflow)
{
var emfPayload = _context.Serialize();
- Console.WriteLine(emfPayload);
+ _consoleWrapper.WriteLine(emfPayload);
_context.ClearMetrics();
@@ -246,7 +258,7 @@ void IMetrics.Flush(bool metricsOverflow)
else
{
if (!_captureColdStartEnabled)
- Console.WriteLine(
+ _consoleWrapper.WriteLine(
"##User-WARNING## No application metrics to publish. The cold-start metric may be published if enabled. If application metrics should never be empty, consider using 'RaiseOnEmptyMetrics = true'");
}
}
@@ -411,15 +423,7 @@ public static void SetDefaultDimensions(Dictionary defaultDimens
///
public static void ClearDefaultDimensions()
{
- if (Instance != null)
- {
- Instance.ClearDefaultDimensions();
- }
- else
- {
- Debug.WriteLine(
- $"##WARNING##: Metrics should be initialized in Handler method before calling {nameof(ClearDefaultDimensions)} method.");
- }
+ Instance.ClearDefaultDimensions();
}
///
@@ -431,7 +435,7 @@ private void Flush(MetricsContext context)
{
var emfPayload = context.Serialize();
- Console.WriteLine(emfPayload);
+ _consoleWrapper.WriteLine(emfPayload);
}
///
@@ -449,16 +453,8 @@ public static void PushSingleMetric(string name, double value, MetricUnit unit,
string service = null, Dictionary defaultDimensions = null,
MetricResolution resolution = MetricResolution.Default)
{
- if (Instance != null)
- {
- Instance.PushSingleMetric(name, value, unit, nameSpace, service, defaultDimensions,
- resolution);
- }
- else
- {
- Debug.WriteLine(
- $"##WARNING##: Metrics should be initialized in Handler method before calling {nameof(PushSingleMetric)} method.");
- }
+ Instance.PushSingleMetric(name, value, unit, nameSpace, service, defaultDimensions,
+ resolution);
}
///
@@ -487,10 +483,37 @@ private Dictionary ListToDictionary(List dimension
}
catch (Exception e)
{
- Debug.WriteLine("Error converting list to dictionary: " + e.Message);
+ _consoleWrapper.Debug("Error converting list to dictionary: " + e.Message);
return dictionary;
}
}
+
+ ///
+ /// Captures the cold start metric.
+ ///
+ /// The ILambdaContext.
+ void IMetrics.CaptureColdStartMetric(ILambdaContext context)
+ {
+ if (Options.CaptureColdStart == null || !Options.CaptureColdStart.Value) return;
+
+ // bring default dimensions if exist
+ var dimensions = Options?.DefaultDimensions;
+
+ if (context is not null)
+ {
+ dimensions ??= new Dictionary();
+ dimensions.Add("FunctionName", context.FunctionName);
+ }
+
+ PushSingleMetric(
+ "ColdStart",
+ 1.0,
+ MetricUnit.Count,
+ Options?.Namespace ?? "",
+ Options?.Service ?? "",
+ dimensions
+ );
+ }
///
/// Helper method for testing purposes. Clears static instance between test execution
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj
index d820a783..15ac1312 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj
@@ -13,6 +13,8 @@
+
+
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsEndpointExtensionsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsEndpointExtensionsTests.cs
new file mode 100644
index 00000000..155d6e29
--- /dev/null
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsEndpointExtensionsTests.cs
@@ -0,0 +1,164 @@
+using Amazon.Lambda.TestUtilities;
+using AWS.Lambda.Powertools.Common;
+using AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using NSubstitute;
+using Xunit;
+
+namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Tests;
+
+public class MetricsEndpointExtensionsTests : IDisposable
+{
+ [Fact]
+ public async Task When_WithMetrics_Should_Add_ColdStart()
+ {
+ // Arrange
+ var options = new MetricsOptions
+ {
+ CaptureColdStart = true,
+ Namespace = "TestNamespace",
+ Service = "TestService"
+ };
+
+ var conf = Substitute.For();
+ var consoleWrapper = Substitute.For();
+ var metrics = new Metrics(conf, consoleWrapper: consoleWrapper, options: options);
+
+ var builder = WebApplication.CreateBuilder();
+ builder.Services.AddSingleton(metrics);
+ builder.WebHost.UseTestServer();
+
+ var app = builder.Build();
+
+ app.MapGet("/test", () => Results.Ok(new { success = true })).WithMetrics();
+
+ await app.StartAsync();
+ var client = app.GetTestClient();
+
+ // Act
+ var response = await client.GetAsync("/test");
+
+ // Assert
+ Assert.Equal(200, (int)response.StatusCode);
+
+ // Assert metrics calls
+ consoleWrapper.Received(1).WriteLine(
+ Arg.Is(s => s.Contains("CloudWatchMetrics\":[{\"Namespace\":\"TestNamespace\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[]]}]},\"ColdStart\":1}"))
+ );
+
+
+ await app.StopAsync();
+ }
+
+ [Fact]
+ public async Task When_WithMetrics_Should_Add_ColdStart_Dimensions()
+ {
+ // Arrange
+ var options = new MetricsOptions
+ {
+ CaptureColdStart = true,
+ Namespace = "TestNamespace",
+ Service = "TestService"
+ };
+
+ var conf = Substitute.For();
+ var consoleWrapper = Substitute.For();
+ var metrics = new Metrics(conf, consoleWrapper: consoleWrapper, options: options);
+
+ var builder = WebApplication.CreateBuilder();
+ builder.Services.AddSingleton(metrics);
+ builder.WebHost.UseTestServer();
+
+
+ var app = builder.Build();
+ app.Use(async (context, next) =>
+ {
+ var lambdaContext = new TestLambdaContext
+ {
+ FunctionName = "TestFunction"
+ };
+ context.Items["LambdaContext"] = lambdaContext;
+ await next();
+ });
+
+ app.MapGet("/test", () => Results.Ok(new { success = true })).WithMetrics();
+
+ await app.StartAsync();
+ var client = app.GetTestClient();
+
+ // Act
+ var response = await client.GetAsync("/test");
+
+ // Assert
+ Assert.Equal(200, (int)response.StatusCode);
+
+ // Assert metrics calls
+ consoleWrapper.Received(1).WriteLine(
+ Arg.Is(s => s.Contains("CloudWatchMetrics\":[{\"Namespace\":\"TestNamespace\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"FunctionName\"]]}]},\"FunctionName\":\"TestFunction\",\"ColdStart\":1}"))
+ );
+
+ await app.StopAsync();
+ }
+
+ [Fact]
+ public async Task When_WithMetrics_Should_Add_ColdStart_Default_Dimensions()
+ {
+ // Arrange
+ var options = new MetricsOptions
+ {
+ CaptureColdStart = true,
+ Namespace = "TestNamespace",
+ Service = "TestService",
+ DefaultDimensions = new Dictionary
+ {
+ { "Environment", "Prod" }
+ }
+ };
+
+ var conf = Substitute.For();
+ var consoleWrapper = Substitute.For();
+ var metrics = new Metrics(conf, consoleWrapper: consoleWrapper, options: options);
+
+ var builder = WebApplication.CreateBuilder();
+ builder.Services.AddSingleton(metrics);
+ builder.WebHost.UseTestServer();
+
+
+ var app = builder.Build();
+ app.Use(async (context, next) =>
+ {
+ var lambdaContext = new TestLambdaContext
+ {
+ FunctionName = "TestFunction"
+ };
+ context.Items["LambdaContext"] = lambdaContext;
+ await next();
+ });
+
+ app.MapGet("/test", () => Results.Ok(new { success = true })).WithMetrics();
+
+ await app.StartAsync();
+ var client = app.GetTestClient();
+
+ // Act
+ var response = await client.GetAsync("/test");
+
+ // Assert
+ Assert.Equal(200, (int)response.StatusCode);
+
+ // Assert metrics calls
+ consoleWrapper.Received(1).WriteLine(
+ Arg.Is(s => s.Contains("CloudWatchMetrics\":[{\"Namespace\":\"TestNamespace\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Environment\",\"FunctionName\"]]}]},\"Environment\":\"Prod\",\"FunctionName\":\"TestFunction\",\"ColdStart\":1}"))
+ );
+
+ await app.StopAsync();
+ }
+
+ public void Dispose()
+ {
+ MetricsHelper.ResetColdStart();
+ }
+}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsFilterTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsFilterTests.cs
index 6a0df634..d4dad622 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsFilterTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsFilterTests.cs
@@ -6,7 +6,7 @@
namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Tests;
-public class MetricsFilterTests
+public class MetricsFilterTests : IDisposable
{
private readonly IMetrics _metrics;
private readonly EndpointFilterInvocationContext _context;
@@ -25,40 +25,7 @@ public MetricsFilterTests()
}
[Fact]
- public async Task InvokeAsync_WhenColdStartEnabled_RecordsColdStartMetric()
- {
- // Arrange
- var options = new MetricsOptions
- {
- CaptureColdStart = true,
- Namespace = "TestNamespace",
- Service = "TestService",
- DefaultDimensions = new Dictionary()
- };
-
- _metrics.Options.Returns(options);
- _lambdaContext.FunctionName.Returns("TestFunction");
-
- var filter = new MetricsFilter(_metrics);
- var next = new EndpointFilterDelegate(_ => ValueTask.FromResult("result"));
-
- // Act
- var result = await filter.InvokeAsync(_context, next);
-
- // Assert
- _metrics.Received(1).PushSingleMetric(
- "ColdStart",
- 1.0,
- MetricUnit.Count,
- "TestNamespace",
- "TestService",
- Arg.Any>()
- );
- Assert.Equal("result", result);
- }
-
- [Fact]
- public async Task InvokeAsync_WhenColdStartDisabled_DoesNotRecordMetric()
+ public async Task InvokeAsync_Second_Call_DoesNotRecord_ColdStart_Metric()
{
// Arrange
var options = new MetricsOptions { CaptureColdStart = false };
@@ -68,17 +35,11 @@ public async Task InvokeAsync_WhenColdStartDisabled_DoesNotRecordMetric()
var next = new EndpointFilterDelegate(_ => ValueTask.FromResult("result"));
// Act
+ _ = await filter.InvokeAsync(_context, next);
var result = await filter.InvokeAsync(_context, next);
// Assert
- _metrics.DidNotReceive().PushSingleMetric(
- Arg.Any(),
- Arg.Any(),
- Arg.Any(),
- Arg.Any(),
- Arg.Any(),
- Arg.Any>()
- );
+ _metrics.Received(1).CaptureColdStartMetric(Arg.Any() );
Assert.Equal("result", result);
}
@@ -107,4 +68,9 @@ public async Task InvokeAsync_ShouldCallNextAndContinue()
Assert.True(called);
Assert.Equal("result", result);
}
+
+ public void Dispose()
+ {
+ MetricsHelper.ResetColdStart();
+ }
}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsHelperTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsHelperTests.cs
deleted file mode 100644
index 37064d24..00000000
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsHelperTests.cs
+++ /dev/null
@@ -1,84 +0,0 @@
-using System.Reflection;
-using AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Routing;
-using Microsoft.AspNetCore.Routing.Patterns;
-using NSubstitute;
-using Xunit;
-
-namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Tests;
-
-public class MetricsHelperTests
-{
- [Fact]
- public async Task CaptureColdStartMetrics_WhenEnabled_ShouldPushMetric()
- {
- // Arrange
- var metrics = Substitute.For();
- metrics.Options.Returns(new MetricsOptions
- {
- CaptureColdStart = true,
- Namespace = "TestNamespace",
- Service = "TestService"
- });
-
- var context = new DefaultHttpContext();
- var helper = new MetricsHelper(metrics);
-
- // Act
- await helper.CaptureColdStartMetrics(context);
-
- // Assert
- metrics.Received(1).PushSingleMetric(
- Arg.Is(s => s == "ColdStart"),
- Arg.Is(d => d == 1.0),
- Arg.Is(u => u == MetricUnit.Count),
- Arg.Is(s => s == "TestNamespace"),
- Arg.Is(s => s == "TestService"),
- Arg.Any>()
- );
- }
-
- [Fact]
- public async Task CaptureColdStartMetrics_WhenDisabled_ShouldNotPushMetric()
- {
- // Arrange
- var metrics = Substitute.For();
- metrics.Options.Returns(new MetricsOptions { CaptureColdStart = false });
-
- var context = new DefaultHttpContext();
- var helper = new MetricsHelper(metrics);
-
- // Act
- await helper.CaptureColdStartMetrics(context);
-
- // Assert
- metrics.DidNotReceive().PushSingleMetric(
- Arg.Any(),
- Arg.Any(),
- Arg.Any(),
- Arg.Any(),
- Arg.Any(),
- Arg.Any>()
- );
- }
-}
-
-public static class EndpointFilterInvocationContextHelper
-{
- public static EndpointFilterInvocationContext Create(HttpContext httpContext, object[] arguments)
- {
- var endpoint = new RouteEndpoint(
- c => Task.CompletedTask,
- RoutePatternFactory.Parse("/"),
- 0,
- EndpointMetadataCollection.Empty,
- "test");
-
- var constructor = typeof(EndpointFilterInvocationContext)
- .GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)
- .First();
-
- return (EndpointFilterInvocationContext)constructor.Invoke(new object[] { httpContext, endpoint, arguments });
- }
-}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsMiddlewareExtensionsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsMiddlewareExtensionsTests.cs
deleted file mode 100644
index 3d5a9892..00000000
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsMiddlewareExtensionsTests.cs
+++ /dev/null
@@ -1,99 +0,0 @@
-using AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.DependencyInjection;
-using NSubstitute;
-using Xunit;
-
-namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Tests;
-
-public class MetricsMiddlewareExtensionsTests : IDisposable
-{
- public MetricsMiddlewareExtensionsTests()
- {
- MetricsHelper.ResetColdStart();
- }
-
- public void Dispose()
- {
- MetricsHelper.ResetColdStart();
- }
-
- [Fact]
- public async Task UseMetrics_ShouldCaptureColdStart_WhenEnabled()
- {
- // Arrange
- var metrics = Substitute.For();
- metrics.Options.Returns(new MetricsOptions
- {
- CaptureColdStart = true,
- Namespace = "TestNamespace",
- Service = "TestService"
- });
-
- var services = new ServiceCollection();
- services.AddSingleton(metrics);
- var serviceProvider = services.BuildServiceProvider();
-
- var context = new DefaultHttpContext
- {
- RequestServices = serviceProvider
- };
-
- var appBuilder = new ApplicationBuilder(serviceProvider);
- appBuilder.UseMetrics();
- var app = appBuilder.Build();
-
- // Act
- await app.Invoke(context);
-
- // Assert
- metrics.Received(1).PushSingleMetric(
- Arg.Is(s => s == "ColdStart"),
- Arg.Is(d => d == 1.0),
- Arg.Is(u => u == MetricUnit.Count),
- Arg.Is(s => s == "TestNamespace"),
- Arg.Is(s => s == "TestService"),
- Arg.Any>()
- );
- }
-
- [Fact]
- public async Task UseMetrics_ShouldNotCaptureColdStart_WhenDisabled()
- {
- // Arrange
- var metrics = Substitute.For();
- metrics.Options.Returns(new MetricsOptions
- {
- CaptureColdStart = false,
- Namespace = "TestNamespace",
- Service = "TestService"
- });
-
- var services = new ServiceCollection();
- services.AddSingleton(metrics);
- var serviceProvider = services.BuildServiceProvider();
-
- var context = new DefaultHttpContext
- {
- RequestServices = serviceProvider
- };
-
- var appBuilder = new ApplicationBuilder(serviceProvider);
- appBuilder.UseMetrics();
- var app = appBuilder.Build();
-
- // Act
- await app.Invoke(context);
-
- // Assert
- metrics.DidNotReceive().PushSingleMetric(
- Arg.Is(s => s == "ColdStart"),
- Arg.Any(),
- Arg.Any(),
- Arg.Any(),
- Arg.Any(),
- Arg.Any>()
- );
- }
-}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
index f09dc6af..1f89a208 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
@@ -16,6 +16,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
+using Amazon.Lambda.Core;
using Amazon.Lambda.TestUtilities;
using AWS.Lambda.Powertools.Common;
using NSubstitute;
@@ -171,7 +172,7 @@ public void DefaultDimensions_AreAppliedCorrectly_WithContext_FunctionName()
metricsOutput);
// Assert successful Memory metrics
Assert.Contains(
- "\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"Memory\",\"Unit\":\"Megabytes\"}],\"Dimensions\":[[\"Service\",\"Environment\",\"Another\",\"FunctionName\"]]}]},\"Service\":\"testService\",\"Environment\":\"Prod\",\"Another\":\"One\",\"FunctionName\":\"My_Function_Name\",\"Memory\":10}",
+ "\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"Memory\",\"Unit\":\"Megabytes\"}],\"Dimensions\":[[\"Service\",\"Environment\",\"Another\"]]}]},\"Service\":\"testService\",\"Environment\":\"Prod\",\"Another\":\"One\",\"Memory\":10}",
metricsOutput);
}
@@ -202,8 +203,7 @@ public void Handler_WithMockedMetrics_ShouldCallAddMetric()
sut.Handler();
// Assert
- metricsMock.Received(1).PushSingleMetric("ColdStart", 1, MetricUnit.Count, "dotnet-powertools-test",
- service: "testService", Arg.Any>());
+ metricsMock.Received(1).CaptureColdStartMetric(Arg.Any());
metricsMock.Received(1).AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
}
@@ -228,7 +228,7 @@ public void Handler_With_Builder_Should_Configure_In_Constructor()
metricsOutput);
// Assert successful Memory metrics
Assert.Contains(
- "\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"SuccessfulBooking\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"Environment\",\"Another\",\"FunctionName\"]]}]},\"Service\":\"testService\",\"Environment\":\"Prod1\",\"Another\":\"One\",\"FunctionName\":\"My_Function_Name\",\"SuccessfulBooking\":1}",
+ "\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"SuccessfulBooking\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"Environment\",\"Another\"]]}]},\"Service\":\"testService\",\"Environment\":\"Prod1\",\"Another\":\"One\",\"SuccessfulBooking\":1}",
metricsOutput);
}
@@ -259,8 +259,7 @@ public void Handler_With_Builder_Should_Configure_In_Constructor_Mock()
FunctionName = "My_Function_Name"
});
- metricsMock.Received(1).PushSingleMetric("ColdStart", 1, MetricUnit.Count, "dotnet-powertools-test",
- service: "testService", Arg.Any>());
+ metricsMock.Received(1).CaptureColdStartMetric(Arg.Any());
metricsMock.Received(1).AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
}
@@ -282,89 +281,6 @@ public void Handler_With_Builder_Should_Raise_Empty_Metrics()
var exception = Assert.Throws(() => handler.HandlerEmpty());
Assert.Equal("No metrics have been provided.", exception.Message);
}
-
- [Fact]
- public void When_ColdStart_Should_Use_DefaultDimensions_From_Options()
- {
- // Arrange
- var metricsMock = Substitute.For();
- var expectedDimensions = new Dictionary
- {
- { "Environment", "Test" },
- { "Region", "us-east-1" }
- };
-
- metricsMock.Options.Returns(new MetricsOptions
- {
- Namespace = "dotnet-powertools-test",
- Service = "testService",
- CaptureColdStart = true,
- DefaultDimensions = expectedDimensions
- });
-
- Metrics.UseMetricsForTests(metricsMock);
-
- var context = new TestLambdaContext
- {
- FunctionName = "TestFunction"
- };
-
- // Act
- _handler.HandleWithLambdaContext(context);
-
- // Assert
- metricsMock.Received(1).PushSingleMetric(
- "ColdStart",
- 1.0,
- MetricUnit.Count,
- "dotnet-powertools-test",
- "testService",
- Arg.Is>(d =>
- d.ContainsKey("Environment") && d["Environment"] == "Test" &&
- d.ContainsKey("Region") && d["Region"] == "us-east-1" &&
- d.ContainsKey("FunctionName") && d["FunctionName"] == "TestFunction"
- )
- );
- }
-
- [Fact]
- public void When_ColdStart_And_DefaultDimensions_Is_Null_Should_Only_Add_Service_And_FunctionName()
- {
- // Arrange
- var metricsMock = Substitute.For();
-
- metricsMock.Options.Returns(new MetricsOptions
- {
- Namespace = "dotnet-powertools-test",
- Service = "testService",
- CaptureColdStart = true,
- DefaultDimensions = null
- });
-
- Metrics.UseMetricsForTests(metricsMock);
-
- var context = new TestLambdaContext
- {
- FunctionName = "TestFunction"
- };
-
- // Act
- _handler.HandleWithLambdaContext(context);
-
- // Assert
- metricsMock.Received(1).PushSingleMetric(
- "ColdStart",
- 1.0,
- MetricUnit.Count,
- "dotnet-powertools-test",
- "testService",
- Arg.Is>(d =>
- d.Count == 1 &&
- d.ContainsKey("FunctionName") &&
- d["FunctionName"] == "TestFunction"
- )
- );
- }
public void Dispose()
{
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs
index 6d82cc92..a0d7bef5 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs
@@ -35,45 +35,6 @@ public void Metrics_Set_Execution_Environment_Context()
env.Received(1).GetEnvironmentVariable("AWS_EXECUTION_ENV");
}
- [Fact]
- public void Before_With_Null_DefaultDimensions_Should_Not_Throw()
- {
- // Arrange
- MetricsAspect.ResetForTest();
- var metricsMock = Substitute.For();
- var optionsMock = new MetricsOptions
- {
- CaptureColdStart = true,
- DefaultDimensions = null
- };
- metricsMock.Options.Returns(optionsMock);
- Metrics.UseMetricsForTests(metricsMock);
-
- var metricsAspect = new MetricsAspect();
- var method = typeof(MetricsTests).GetMethod(nameof(TestMethod));
- var trigger = new MetricsAttribute();
-
- // Act
- metricsAspect.Before(
- this,
- "TestMethod",
- [new TestLambdaContext()],
- typeof(MetricsTests),
- method,
- typeof(void),
- [trigger]
- );
-
- // Assert
- metricsMock.Received(1).PushSingleMetric(
- "ColdStart",
- 1.0,
- MetricUnit.Count,
- Arg.Any(),
- Arg.Any()
- );
- }
-
[Fact]
public void Before_When_CaptureStartNotSet_Should_Not_Push_Metrics()
{
@@ -144,7 +105,7 @@ public void Before_When_RaiseOnEmptyMetricsNotSet_Should_Configure_Null()
internal void TestMethod(ILambdaContext context)
{
}
-
+
[Fact]
public void When_Constructor_With_Namespace_And_Service_Should_Set_Both()
{
@@ -176,7 +137,7 @@ public void When_Constructor_With_Null_Namespace_And_Service_Should_Not_Set()
Assert.Null(metrics.GetNamespace());
Assert.Null(metrics.Options.Service);
}
-
+
[Fact]
public void When_AddMetric_With_EmptyKey_Should_ThrowArgumentNullException()
{
@@ -188,7 +149,8 @@ public void When_AddMetric_With_EmptyKey_Should_ThrowArgumentNullException()
// Act & Assert
var exception = Assert.Throws(() => metrics.AddMetric("", 1.0));
Assert.Equal("key", exception.ParamName);
- Assert.Contains("'AddMetric' method requires a valid metrics key. 'Null' or empty values are not allowed.", exception.Message);
+ Assert.Contains("'AddMetric' method requires a valid metrics key. 'Null' or empty values are not allowed.",
+ exception.Message);
}
[Theory]
@@ -205,23 +167,24 @@ public void When_AddMetric_With_InvalidKey_Should_ThrowArgumentNullException(str
// Act & Assert
var exception = Assert.Throws(() => metrics.AddMetric(key, 1.0));
Assert.Equal("key", exception.ParamName);
- Assert.Contains("'AddMetric' method requires a valid metrics key. 'Null' or empty values are not allowed.", exception.Message);
+ Assert.Contains("'AddMetric' method requires a valid metrics key. 'Null' or empty values are not allowed.",
+ exception.Message);
}
-
+
[Fact]
public void When_SetDefaultDimensions_With_InvalidKeyOrValue_Should_ThrowArgumentNullException()
{
// Arrange
var powertoolsConfigMock = Substitute.For();
IMetrics metrics = new Metrics(powertoolsConfigMock);
-
+
var invalidDimensions = new Dictionary
{
{ "", "value" }, // empty key
- { "key", "" }, // empty value
+ { "key", "" }, // empty value
{ " ", "value" }, // whitespace key
{ "key1", " " }, // whitespace value
- { "key2", null } // null value
+ { "key2", null } // null value
};
// Act & Assert
@@ -230,10 +193,12 @@ public void When_SetDefaultDimensions_With_InvalidKeyOrValue_Should_ThrowArgumen
var dimensions = new Dictionary { { dimension.Key, dimension.Value } };
var exception = Assert.Throws(() => metrics.SetDefaultDimensions(dimensions));
Assert.Equal("Key", exception.ParamName);
- Assert.Contains("'SetDefaultDimensions' method requires a valid key pair. 'Null' or empty values are not allowed.", exception.Message);
+ Assert.Contains(
+ "'SetDefaultDimensions' method requires a valid key pair. 'Null' or empty values are not allowed.",
+ exception.Message);
}
}
-
+
[Fact]
public void When_PushSingleMetric_With_EmptyName_Should_ThrowArgumentNullException()
{
@@ -244,7 +209,9 @@ public void When_PushSingleMetric_With_EmptyName_Should_ThrowArgumentNullExcepti
// Act & Assert
var exception = Assert.Throws(() => metrics.PushSingleMetric("", 1.0, MetricUnit.Count));
Assert.Equal("name", exception.ParamName);
- Assert.Contains("'PushSingleMetric' method requires a valid metrics key. 'Null' or empty values are not allowed.", exception.Message);
+ Assert.Contains(
+ "'PushSingleMetric' method requires a valid metrics key. 'Null' or empty values are not allowed.",
+ exception.Message);
}
[Theory]
@@ -258,8 +225,76 @@ public void When_PushSingleMetric_With_InvalidName_Should_ThrowArgumentNullExcep
IMetrics metrics = new Metrics(powertoolsConfigMock);
// Act & Assert
- var exception = Assert.Throws(() => metrics.PushSingleMetric(name, 1.0, MetricUnit.Count));
+ var exception =
+ Assert.Throws(() => metrics.PushSingleMetric(name, 1.0, MetricUnit.Count));
Assert.Equal("name", exception.ParamName);
- Assert.Contains("'PushSingleMetric' method requires a valid metrics key. 'Null' or empty values are not allowed.", exception.Message);
+ Assert.Contains(
+ "'PushSingleMetric' method requires a valid metrics key. 'Null' or empty values are not allowed.",
+ exception.Message);
+ }
+
+
+ [Fact]
+ public void When_ColdStart_Should_Use_DefaultDimensions_From_Options()
+ {
+ // Arrange
+ var options = new MetricsOptions
+ {
+ CaptureColdStart = true,
+ Namespace = "dotnet-powertools-test",
+ Service = "testService",
+ DefaultDimensions = new Dictionary
+ {
+ { "Environment", "Test" },
+ { "Region", "us-east-1" }
+ }
+ };
+
+ var conf = Substitute.For();
+ var consoleWrapper = Substitute.For();
+ IMetrics metrics = new Metrics(conf, consoleWrapper: consoleWrapper, options: options);
+
+ var context = new TestLambdaContext
+ {
+ FunctionName = "TestFunction"
+ };
+
+ // Act
+ metrics.CaptureColdStartMetric(context);
+
+ // Assert
+ consoleWrapper.Received(1).WriteLine(
+ Arg.Is(s => s.Contains("\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Environment\",\"Region\",\"FunctionName\"]]}]},\"Environment\":\"Test\",\"Region\":\"us-east-1\",\"FunctionName\":\"TestFunction\",\"ColdStart\":1}"))
+ );
+ }
+
+ [Fact]
+ public void When_ColdStart_And_DefaultDimensions_Is_Null_Should_Only_Add_Service_And_FunctionName()
+ {
+ // Arrange
+ var options = new MetricsOptions
+ {
+ CaptureColdStart = true,
+ Namespace = "dotnet-powertools-test",
+ Service = "testService",
+ DefaultDimensions = null
+ };
+
+ var conf = Substitute.For();
+ var consoleWrapper = Substitute.For();
+ IMetrics metrics = new Metrics(conf, consoleWrapper: consoleWrapper, options: options);
+
+ var context = new TestLambdaContext
+ {
+ FunctionName = "TestFunction"
+ };
+
+ // Act
+ metrics.CaptureColdStartMetric(context);
+
+ // Assert
+ consoleWrapper.Received(1).WriteLine(
+ Arg.Is(s => s.Contains("\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"FunctionName\"]]}]},\"FunctionName\":\"TestFunction\",\"ColdStart\":1}"))
+ );
}
}
\ No newline at end of file
diff --git a/libraries/tests/Directory.Packages.props b/libraries/tests/Directory.Packages.props
index e8c9a16e..516a0e93 100644
--- a/libraries/tests/Directory.Packages.props
+++ b/libraries/tests/Directory.Packages.props
@@ -6,6 +6,7 @@
+
From 3688e74e8f1adec401ec2b03687595177358bfc3 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Wed, 26 Feb 2025 15:50:05 +0000
Subject: [PATCH 22/29] feat(tests): add unit tests for ConsoleWrapper and
Metrics middleware extensions
---
.../ConsoleWrapperTests.cs | 53 +++++++++
.../MetricsMiddlewareExtensionsTests.cs | 104 ++++++++++++++++++
2 files changed, 157 insertions(+)
create mode 100644 libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs
create mode 100644 libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsMiddlewareExtensionsTests.cs
diff --git a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs
new file mode 100644
index 00000000..6395f79a
--- /dev/null
+++ b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs
@@ -0,0 +1,53 @@
+using System;
+using System.IO;
+using Xunit;
+
+namespace AWS.Lambda.Powertools.Common.Tests;
+
+public class ConsoleWrapperTests
+{
+ [Fact]
+ public void WriteLine_Should_Write_To_Console()
+ {
+ // Arrange
+ var consoleWrapper = new ConsoleWrapper();
+ var writer = new StringWriter();
+ Console.SetOut(writer);
+
+ // Act
+ consoleWrapper.WriteLine("test message");
+
+ // Assert
+ Assert.Equal($"test message{Environment.NewLine}", writer.ToString());
+ }
+
+ [Fact]
+ public void Error_Should_Write_To_Error_Console()
+ {
+ // Arrange
+ var consoleWrapper = new ConsoleWrapper();
+ var writer = new StringWriter();
+ Console.SetError(writer);
+
+ // Act
+ consoleWrapper.Error("error message");
+
+ // Assert
+ Assert.Equal($"error message{Environment.NewLine}", writer.ToString());
+ }
+
+ [Fact]
+ public void ReadLine_Should_Read_From_Console()
+ {
+ // Arrange
+ var consoleWrapper = new ConsoleWrapper();
+ var reader = new StringReader("input text");
+ Console.SetIn(reader);
+
+ // Act
+ var result = consoleWrapper.ReadLine();
+
+ // Assert
+ Assert.Equal("input text", result);
+ }
+}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsMiddlewareExtensionsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsMiddlewareExtensionsTests.cs
new file mode 100644
index 00000000..d72bad66
--- /dev/null
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsMiddlewareExtensionsTests.cs
@@ -0,0 +1,104 @@
+using Amazon.Lambda.TestUtilities;
+using AWS.Lambda.Powertools.Common;
+using AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using NSubstitute;
+using Xunit;
+
+namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Tests;
+
+public class MetricsMiddlewareExtensionsTests : IDisposable
+{
+ [Fact]
+ public async Task When_UseMetrics_Should_Add_ColdStart()
+ {
+ // Arrange
+ var options = new MetricsOptions
+ {
+ CaptureColdStart = true,
+ Namespace = "TestNamespace",
+ Service = "TestService"
+ };
+
+ var conf = Substitute.For();
+ var consoleWrapper = Substitute.For();
+ var metrics = new Metrics(conf, consoleWrapper: consoleWrapper, options: options);
+
+ var builder = WebApplication.CreateBuilder();
+ builder.Services.AddSingleton(metrics);
+ builder.WebHost.UseTestServer();
+
+ var app = builder.Build();
+ app.UseMetrics();
+ app.MapGet("/test", () => Results.Ok());
+
+ await app.StartAsync();
+ var client = app.GetTestClient();
+
+ // Act
+ var response = await client.GetAsync("/test");
+
+ // Assert
+ Assert.Equal(200, (int)response.StatusCode);
+ consoleWrapper.Received(1).WriteLine(
+ Arg.Is(s => s.Contains("CloudWatchMetrics\":[{\"Namespace\":\"TestNamespace\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[]]}]},\"ColdStart\":1}"))
+ );
+
+ await app.StopAsync();
+ }
+
+ [Fact]
+ public async Task When_UseMetrics_Should_Add_ColdStart_With_LambdaContext()
+ {
+ // Arrange
+ var options = new MetricsOptions
+ {
+ CaptureColdStart = true,
+ Namespace = "TestNamespace",
+ Service = "TestService"
+ };
+
+ var conf = Substitute.For();
+ var consoleWrapper = Substitute.For();
+ var metrics = new Metrics(conf, consoleWrapper: consoleWrapper, options: options);
+
+ var builder = WebApplication.CreateBuilder();
+ builder.Services.AddSingleton(metrics);
+ builder.WebHost.UseTestServer();
+
+ var app = builder.Build();
+ app.Use(async (context, next) =>
+ {
+ var lambdaContext = new TestLambdaContext
+ {
+ FunctionName = "TestFunction"
+ };
+ context.Items["LambdaContext"] = lambdaContext;
+ await next();
+ });
+ app.UseMetrics();
+ app.MapGet("/test", () => Results.Ok());
+
+ await app.StartAsync();
+ var client = app.GetTestClient();
+
+ // Act
+ var response = await client.GetAsync("/test");
+
+ // Assert
+ Assert.Equal(200, (int)response.StatusCode);
+ consoleWrapper.Received(1).WriteLine(
+ Arg.Is(s => s.Contains("CloudWatchMetrics\":[{\"Namespace\":\"TestNamespace\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"FunctionName\"]]}]},\"FunctionName\":\"TestFunction\",\"ColdStart\":1}"))
+ );
+
+ await app.StopAsync();
+ }
+
+ public void Dispose()
+ {
+ MetricsHelper.ResetColdStart();
+ }
+}
\ No newline at end of file
From 6431e1a945c578f0af6c8384163c08e6f6ba9d66 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Thu, 27 Feb 2025 09:31:39 +0000
Subject: [PATCH 23/29] feat(metrics): add ColdStartTracker for tracking cold
starts in ASP.NET Core applications
---
.../Http/ColdStartTracker.cs | 76 +++++++++++++++++++
.../Http/MetricsFilter.cs | 29 ++++---
.../Http/MetricsHelper.cs | 66 ----------------
.../Http/MetricsMiddlewareExtensions.cs | 19 +++--
.../MetricsEndpointExtensionsTests.cs | 4 +-
.../MetricsFilterTests.cs | 10 +--
.../MetricsMiddlewareExtensionsTests.cs | 5 +-
.../Handlers/FunctionHandler.cs | 2 -
.../MetricsTests.cs | 54 -------------
9 files changed, 120 insertions(+), 145 deletions(-)
create mode 100644 libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/ColdStartTracker.cs
delete mode 100644 libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsHelper.cs
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/ColdStartTracker.cs b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/ColdStartTracker.cs
new file mode 100644
index 00000000..aafaad26
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/ColdStartTracker.cs
@@ -0,0 +1,76 @@
+/*
+ * 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.Core;
+using Microsoft.AspNetCore.Http;
+
+namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
+
+
+///
+/// Tracks and manages cold start metrics for Lambda functions in ASP.NET Core applications.
+///
+///
+/// This class is responsible for detecting and recording the first invocation (cold start) of a Lambda function.
+/// It ensures thread-safe tracking of cold starts and proper metric capture using the provided IMetrics implementation.
+///
+internal class ColdStartTracker : IDisposable
+{
+ private readonly IMetrics _metrics;
+ private static bool _coldStart = true;
+ private static readonly object _lock = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The metrics implementation to use for capturing cold start metrics.
+ public ColdStartTracker(IMetrics metrics)
+ {
+ _metrics = metrics;
+ }
+
+ ///
+ /// Tracks the cold start of the Lambda function.
+ ///
+ /// The current HTTP context.
+ internal void TrackColdStart(HttpContext context)
+ {
+ if (!_coldStart) return;
+
+ lock (_lock)
+ {
+ if (!_coldStart) return;
+ _metrics.CaptureColdStartMetric(context.Items["LambdaContext"] as ILambdaContext);
+ _coldStart = false;
+ }
+ }
+
+ ///
+ /// Resets the cold start tracking state.
+ ///
+ internal static void ResetColdStart()
+ {
+ lock (_lock)
+ {
+ _coldStart = true;
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ ResetColdStart();
+ }
+}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsFilter.cs b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsFilter.cs
index a2c776f1..f89fd94b 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsFilter.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsFilter.cs
@@ -20,17 +20,22 @@ namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
///
/// Represents a filter that captures and records metrics for HTTP endpoints.
///
-public class MetricsFilter : IEndpointFilter
+///
+/// This filter is responsible for tracking cold starts and capturing metrics during HTTP request processing.
+/// It integrates with the ASP.NET Core endpoint routing system to inject metrics collection at the endpoint level.
+///
+///
+///
+public class MetricsFilter : IEndpointFilter, IDisposable
{
- private readonly MetricsHelper _metricsHelper;
+ private readonly ColdStartTracker _coldStartTracker;
///
/// Initializes a new instance of the class.
///
- /// The metrics instance to use for recording metrics.
public MetricsFilter(IMetrics metrics)
{
- _metricsHelper = new MetricsHelper(metrics);
+ _coldStartTracker = new ColdStartTracker(metrics);
}
///
@@ -41,17 +46,23 @@ public MetricsFilter(IMetrics metrics)
/// A task that represents the asynchronous operation, containing the result of the endpoint invocation.
public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
- var result = await next(context);
-
try
{
- await _metricsHelper.CaptureColdStartMetrics(context.HttpContext);
- return result;
+ _coldStartTracker.TrackColdStart(context.HttpContext);
}
catch
{
// ignored
- return result;
}
+
+ return await next(context);
+ }
+
+ ///
+ /// Disposes of the resources used by the filter.
+ ///
+ public void Dispose()
+ {
+ _coldStartTracker.Dispose();
}
}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsHelper.cs b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsHelper.cs
deleted file mode 100644
index a86ff0b0..00000000
--- a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsHelper.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * 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.Core;
-using Microsoft.AspNetCore.Http;
-
-namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
-
-
-///
-/// Helper class for capturing and recording metrics in ASP.NET Core applications.
-///
-public class MetricsHelper
-{
- private readonly IMetrics _metrics;
- private static bool _isColdStart = true;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The metrics instance to use for recording metrics.
- public MetricsHelper(IMetrics metrics)
- {
- _metrics = metrics;
- }
-
- ///
- /// Captures cold start metrics for the given HTTP context.
- ///
- /// The HTTP context.
- /// A task that represents the asynchronous operation.
- public Task CaptureColdStartMetrics(HttpContext context)
- {
- if (!_isColdStart)
- return Task.CompletedTask;
-
- lock (_metrics)
- {
- _isColdStart = false;
- }
-
- _metrics.CaptureColdStartMetric(context.Items["LambdaContext"] as ILambdaContext);
-
- return Task.CompletedTask;
- }
-
- ///
- /// Resets the cold start flag for testing purposes.
- ///
- internal static void ResetColdStart()
- {
- _isColdStart = true;
- }
-}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsMiddlewareExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsMiddlewareExtensions.cs
index 05e0d3f9..7515c1b5 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsMiddlewareExtensions.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsMiddlewareExtensions.cs
@@ -24,18 +24,27 @@ namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
public static class MetricsMiddlewareExtensions
{
///
- /// Adds middleware to capture and record metrics for HTTP requests.
+ /// Adds middleware to capture and record metrics for HTTP requests, including cold start tracking.
///
- /// The application builder.
+ /// The application builder instance used to configure the request pipeline.
/// The application builder with the metrics middleware added.
+ ///
+ /// This middleware tracks cold starts and captures request metrics. To use this middleware, ensure you have registered
+ /// the required services using builder.Services.AddSingleton<IMetrics>()
in your service configuration.
+ ///
+ ///
+ ///
+ /// app.UseMetrics();
+ ///
+ ///
public static IApplicationBuilder UseMetrics(this IApplicationBuilder app)
{
return app.Use(async (context, next) =>
{
var metrics = context.RequestServices.GetRequiredService();
- var metricsHelper = new MetricsHelper(metrics);
- await metricsHelper.CaptureColdStartMetrics(context);
+ using var metricsHelper = new ColdStartTracker(metrics);
+ metricsHelper.TrackColdStart(context);
await next();
});
}
-}
\ No newline at end of file
+}
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsEndpointExtensionsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsEndpointExtensionsTests.cs
index 155d6e29..c5ee7c2c 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsEndpointExtensionsTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsEndpointExtensionsTests.cs
@@ -10,6 +10,7 @@
namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Tests;
+[Collection("Metrics")]
public class MetricsEndpointExtensionsTests : IDisposable
{
[Fact]
@@ -26,7 +27,6 @@ public async Task When_WithMetrics_Should_Add_ColdStart()
var conf = Substitute.For();
var consoleWrapper = Substitute.For();
var metrics = new Metrics(conf, consoleWrapper: consoleWrapper, options: options);
-
var builder = WebApplication.CreateBuilder();
builder.Services.AddSingleton(metrics);
builder.WebHost.UseTestServer();
@@ -159,6 +159,6 @@ public async Task When_WithMetrics_Should_Add_ColdStart_Default_Dimensions()
public void Dispose()
{
- MetricsHelper.ResetColdStart();
+ ColdStartTracker.ResetColdStart();
}
}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsFilterTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsFilterTests.cs
index d4dad622..9951034a 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsFilterTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsFilterTests.cs
@@ -6,21 +6,21 @@
namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Tests;
+[Collection("Metrics")]
public class MetricsFilterTests : IDisposable
{
private readonly IMetrics _metrics;
private readonly EndpointFilterInvocationContext _context;
- private readonly ILambdaContext _lambdaContext;
public MetricsFilterTests()
{
- MetricsHelper.ResetColdStart(); // Reset before each test
+ ColdStartTracker.ResetColdStart(); // Reset before each test
_metrics = Substitute.For();
_context = Substitute.For();
- _lambdaContext = Substitute.For();
+ var lambdaContext = Substitute.For();
var httpContext = new DefaultHttpContext();
- httpContext.Items["LambdaContext"] = _lambdaContext;
+ httpContext.Items["LambdaContext"] = lambdaContext;
_context.HttpContext.Returns(httpContext);
}
@@ -71,6 +71,6 @@ public async Task InvokeAsync_ShouldCallNextAndContinue()
public void Dispose()
{
- MetricsHelper.ResetColdStart();
+ ColdStartTracker.ResetColdStart();
}
}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsMiddlewareExtensionsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsMiddlewareExtensionsTests.cs
index d72bad66..a9510eaa 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsMiddlewareExtensionsTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsMiddlewareExtensionsTests.cs
@@ -10,6 +10,7 @@
namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Tests;
+[Collection("Metrics")]
public class MetricsMiddlewareExtensionsTests : IDisposable
{
[Fact]
@@ -63,7 +64,7 @@ public async Task When_UseMetrics_Should_Add_ColdStart_With_LambdaContext()
var conf = Substitute.For();
var consoleWrapper = Substitute.For();
- var metrics = new Metrics(conf, consoleWrapper: consoleWrapper, options: options);
+ var metrics = new Metrics(conf, consoleWrapper:consoleWrapper, options: options);
var builder = WebApplication.CreateBuilder();
builder.Services.AddSingleton(metrics);
@@ -99,6 +100,6 @@ public async Task When_UseMetrics_Should_Add_ColdStart_With_LambdaContext()
public void Dispose()
{
- MetricsHelper.ResetColdStart();
+ ColdStartTracker.ResetColdStart();
}
}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs
index abc41d7f..80c658f1 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs
@@ -14,13 +14,11 @@
*/
using System;
-using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Amazon.Lambda.Core;
-using Amazon.Lambda.TestUtilities;
namespace AWS.Lambda.Powertools.Metrics.Tests.Handlers;
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs
index a0d7bef5..36039756 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs
@@ -35,45 +35,6 @@ public void Metrics_Set_Execution_Environment_Context()
env.Received(1).GetEnvironmentVariable("AWS_EXECUTION_ENV");
}
- [Fact]
- public void Before_When_CaptureStartNotSet_Should_Not_Push_Metrics()
- {
- // Arrange
- MetricsAspect.ResetForTest();
- var metricsMock = Substitute.For();
- var optionsMock = new MetricsOptions
- {
- CaptureColdStart = null
- };
- metricsMock.Options.Returns(optionsMock);
- Metrics.UseMetricsForTests(metricsMock);
-
- var metricsAspect = new MetricsAspect();
- var method = typeof(MetricsTests).GetMethod(nameof(TestMethod));
- var trigger = new MetricsAttribute();
-
- // Act
- metricsAspect.Before(
- this,
- "TestMethod",
- new object[] { new TestLambdaContext() },
- typeof(MetricsTests),
- method,
- typeof(void),
- new Attribute[] { trigger }
- );
-
- // Assert
- metricsMock.DidNotReceive().PushSingleMetric(
- Arg.Any(),
- Arg.Any(),
- Arg.Any(),
- Arg.Any(),
- Arg.Any(),
- Arg.Any>()
- );
- }
-
[Fact]
public void Before_When_RaiseOnEmptyMetricsNotSet_Should_Configure_Null()
{
@@ -106,21 +67,6 @@ internal void TestMethod(ILambdaContext context)
{
}
- [Fact]
- public void When_Constructor_With_Namespace_And_Service_Should_Set_Both()
- {
- // Arrange
- Substitute.For