From 5c4d1995b80904c62766c9bab285b8967ba30d82 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Tue, 25 Feb 2025 13:05:35 +0000
Subject: [PATCH 01/22] refactor(metrics): standardize parameter names for
clarity in metric methods
---
.../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 c185ed19a5a2c23683f2e316c8066416546f710e Mon Sep 17 00:00:00 2001
From: Simon Thulbourn
Date: Tue, 25 Feb 2025 14:34:21 +0100
Subject: [PATCH 02/22] fix(ci): Permissions (#782)
* fix(ci): Permissions
* remove permission
* I don't know
* remove permissions
* update permissions
---------
Co-authored-by: Henrique Graca <999396+hjgraca@users.noreply.github.com>
---
.github/workflows/label_pr_on_title.yml | 7 +++----
.github/workflows/on_label_added.yml | 1 +
.github/workflows/on_opened_pr.yml | 1 +
.github/workflows/reusable_export_pr_details.yml | 1 +
4 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/label_pr_on_title.yml b/.github/workflows/label_pr_on_title.yml
index c5712d75..3fd5d9ca 100644
--- a/.github/workflows/label_pr_on_title.yml
+++ b/.github/workflows/label_pr_on_title.yml
@@ -6,14 +6,12 @@ on:
types:
- completed
-permissions:
- contents: read
-
jobs:
get_pr_details:
permissions:
- id-token: write
contents: read
+ id-token: write
+ pull-requests: read
# Guardrails to only ever run if PR recording workflow was indeed
# run in a PR event and ran successfully
if: ${{ github.event.workflow_run.conclusion == 'success' }}
@@ -27,6 +25,7 @@ jobs:
permissions:
contents: read
id-token: write
+ pull-requests: write
needs: get_pr_details
runs-on: ubuntu-latest
steps:
diff --git a/.github/workflows/on_label_added.yml b/.github/workflows/on_label_added.yml
index f2f407de..4d0613a8 100644
--- a/.github/workflows/on_label_added.yml
+++ b/.github/workflows/on_label_added.yml
@@ -12,6 +12,7 @@ permissions:
jobs:
get_pr_details:
permissions:
+ contents: read
id-token: write
if: ${{ github.event.workflow_run.conclusion == 'success' }}
uses: ./.github/workflows/reusable_export_pr_details.yml
diff --git a/.github/workflows/on_opened_pr.yml b/.github/workflows/on_opened_pr.yml
index b04f6f1a..7f281bad 100644
--- a/.github/workflows/on_opened_pr.yml
+++ b/.github/workflows/on_opened_pr.yml
@@ -13,6 +13,7 @@ jobs:
get_pr_details:
permissions:
id-token: write
+ contents: read
if: ${{ github.event.workflow_run.conclusion == 'success' }}
uses: ./.github/workflows/reusable_export_pr_details.yml
with:
diff --git a/.github/workflows/reusable_export_pr_details.yml b/.github/workflows/reusable_export_pr_details.yml
index 904c7056..83de7718 100644
--- a/.github/workflows/reusable_export_pr_details.yml
+++ b/.github/workflows/reusable_export_pr_details.yml
@@ -43,6 +43,7 @@ jobs:
export_pr_details:
permissions:
id-token: write
+ contents: read
# see https://github.com/aws-powertools/powertools-lambda-python/issues/1349
if: inputs.workflow_origin == 'aws-powertools/powertools-lambda-dotnet'
runs-on: ubuntu-latest
From 735e728a9e518675abadf4ebf8e09d188c46c216 Mon Sep 17 00:00:00 2001
From: Simon Thulbourn
Date: Tue, 25 Feb 2025 13:44:09 +0000
Subject: [PATCH 03/22] fix(ci): Indentation issue
---
.github/workflows/on_label_added.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/on_label_added.yml b/.github/workflows/on_label_added.yml
index 4d0613a8..af8abc5e 100644
--- a/.github/workflows/on_label_added.yml
+++ b/.github/workflows/on_label_added.yml
@@ -12,7 +12,7 @@ permissions:
jobs:
get_pr_details:
permissions:
- contents: read
+ contents: read
id-token: write
if: ${{ github.event.workflow_run.conclusion == 'success' }}
uses: ./.github/workflows/reusable_export_pr_details.yml
From 3c30d9798f2d0a6923b8e16db7fac699e8b597f5 Mon Sep 17 00:00:00 2001
From: Simon Thulbourn
Date: Tue, 25 Feb 2025 13:49:40 +0000
Subject: [PATCH 04/22] fix(ci): Add permissions to read issues and pull
requests
---
.github/workflows/on_merged_pr.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.github/workflows/on_merged_pr.yml b/.github/workflows/on_merged_pr.yml
index cbd6c8b1..8b74ede5 100644
--- a/.github/workflows/on_merged_pr.yml
+++ b/.github/workflows/on_merged_pr.yml
@@ -25,6 +25,8 @@ jobs:
permissions:
contents: read
id-token: write
+ issues: read
+ pull-requests: write
needs: get_pr_details
runs-on: ubuntu-latest
if: needs.get_pr_details.outputs.prIsMerged == 'true'
From 7e8b048bfa8944df8d2599faf57a085977eea3cb Mon Sep 17 00:00:00 2001
From: Simon Thulbourn
Date: Tue, 25 Feb 2025 14:12:49 +0000
Subject: [PATCH 05/22] fix(ci): add write for issues
---
.github/workflows/on_merged_pr.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/on_merged_pr.yml b/.github/workflows/on_merged_pr.yml
index 8b74ede5..1504b975 100644
--- a/.github/workflows/on_merged_pr.yml
+++ b/.github/workflows/on_merged_pr.yml
@@ -25,7 +25,7 @@ jobs:
permissions:
contents: read
id-token: write
- issues: read
+ issues: write
pull-requests: write
needs: get_pr_details
runs-on: ubuntu-latest
From 4b879c923bb455b09b01cc4a6c7d4c1bcb5bf112 Mon Sep 17 00:00:00 2001
From: Henrique Graca <999396+hjgraca@users.noreply.github.com>
Date: Tue, 25 Feb 2025 14:27:40 +0000
Subject: [PATCH 06/22] chore: Add openssf scorecard badge to readme (#790)
* add openssf scorecard badge
Signed-off-by: Henrique Graca <999396+hjgraca@users.noreply.github.com>
* link to ui
Signed-off-by: Henrique Graca <999396+hjgraca@users.noreply.github.com>
---------
Signed-off-by: Henrique Graca <999396+hjgraca@users.noreply.github.com>
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index cb79a9e6..8695f325 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
[](https://github.com/aws-powertools/powertools-lambda-dotnet/actions/workflows/build.yml)
[](https://app.codecov.io/gh/aws-powertools/powertools-lambda-dotnet)
[](https://dotnet.microsoft.com/en-us/download/dotnet/6.0)
-[](https://www.nuget.org/packages?q=AWS.Lambda.Powertools)
+[](https://www.nuget.org/packages?q=AWS.Lambda.Powertools) [](https://scorecard.dev/viewer/?uri=github.com/aws-powertools/powertools-lambda-dotnet)
[](https://discord.gg/B8zZKbbyET)
Powertools for AWS Lambda (.NET) is a developer toolkit to implement Serverless [best practices and increase developer velocity](https://docs.powertools.aws.dev/lambda-dotnet/#features).
From 7f7b1153cfe83363294fe0c47d1a0921ce9c9441 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Tue, 25 Feb 2025 14:56:35 +0000
Subject: [PATCH 07/22] feat(metrics): add HandlerRaiseOnEmptyMetrics method
and corresponding test for empty metrics exception
---
.../Handlers/FunctionHandler.cs | 6 ++++++
.../Handlers/FunctionHandlerTests.cs | 11 ++++++++---
2 files changed, 14 insertions(+), 3 deletions(-)
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..abc41d7f 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs
@@ -214,4 +214,10 @@ public void HandleWithParamAndLambdaContext(string input, ILambdaContext context
{
}
+
+ [Metrics(Namespace = "ns", Service = "svc", RaiseOnEmptyMetrics = true)]
+ public void HandlerRaiseOnEmptyMetrics()
+ {
+
+ }
}
\ 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 36a5818e..c77319f4 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
@@ -39,9 +39,6 @@ public FunctionHandlerTests()
[Fact]
public async Task When_Metrics_Add_Metadata_Same_Key_Should_Ignore_Metadata()
{
- // Arrange
-
-
// Act
var exception = await Record.ExceptionAsync(() => _handler.HandleSameKey("whatever"));
@@ -266,6 +263,14 @@ 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 When_RaiseOnEmptyMetrics_And_NoMetrics_Should_ThrowException()
+ {
+ // Act & Assert
+ var exception = Assert.Throws(() => _handler.HandlerRaiseOnEmptyMetrics());
+ Assert.Equal("No metrics have been provided.", exception.Message);
+ }
public void Dispose()
{
From 3c80a2d6d7e3ee6aaec5a7a53738c80be46b4532 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Tue, 25 Feb 2025 15:26:02 +0000
Subject: [PATCH 08/22] feat(metrics): add HandlerEmpty method and test for
empty metrics exception handling
---
.../Handlers/FunctionHandlerTests.cs | 11 +++++++++++
.../Handlers/MetricsnBuilderHandler.cs | 6 ++++++
2 files changed, 17 insertions(+)
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 c77319f4..dd2fe7e8 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
@@ -271,6 +271,17 @@ public void When_RaiseOnEmptyMetrics_And_NoMetrics_Should_ThrowException()
var exception = Assert.Throws(() => _handler.HandlerRaiseOnEmptyMetrics());
Assert.Equal("No metrics have been provided.", exception.Message);
}
+
+ [Fact]
+ public void Handler_With_Builder_Should_Raise_Empty_Metrics()
+ {
+ // Arrange
+ var handler = new MetricsnBuilderHandler();
+
+ // Act & Assert
+ var exception = Assert.Throws(() => handler.HandlerEmpty());
+ Assert.Equal("No metrics have been provided.", exception.Message);
+ }
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..83cc0e89 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/MetricsnBuilderHandler.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/MetricsnBuilderHandler.cs
@@ -14,6 +14,7 @@ public MetricsnBuilderHandler(IMetrics metrics = null)
.WithCaptureColdStart(true)
.WithService("testService")
.WithNamespace("dotnet-powertools-test")
+ .WithRaiseOnEmptyMetrics(true)
.WithDefaultDimensions(new Dictionary
{
{ "Environment", "Prod1" },
@@ -26,4 +27,9 @@ public void Handler(ILambdaContext context)
{
_metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
}
+
+ [Metrics]
+ public void HandlerEmpty()
+ {
+ }
}
\ No newline at end of file
From b9ba2ce7563c8a0f1e0bb145cf92b3669248c5bd Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Tue, 25 Feb 2025 15:33:35 +0000
Subject: [PATCH 09/22] feat(metrics): enhance cold start handling with default
dimensions and add corresponding tests
---
.../Internal/MetricsAspect.cs | 4 +-
.../Handlers/FunctionHandlerTests.cs | 83 +++++++++++++++++++
2 files changed, 86 insertions(+), 1 deletion(-)
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
index 4ebacf14..1006d25c 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;
@@ -96,7 +97,8 @@ public void Before(
if (context is not null)
{
- defaultDimensions?.Add("FunctionName", context.FunctionName);
+ defaultDimensions ??= new Dictionary();
+ defaultDimensions.Add("FunctionName", context.FunctionName);
_metricsInstance.SetDefaultDimensions(defaultDimensions);
}
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 dd2fe7e8..c34397f4 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
@@ -282,6 +282,89 @@ 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()
{
From 518b87427107d3a741bd69007fac1df1fa4f518b Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Tue, 25 Feb 2025 15:49:27 +0000
Subject: [PATCH 10/22] feat(metrics): add unit tests for Metrics constructor
and validation methods
---
.../MetricsTests.cs | 120 ++++++++++++++++++
1 file changed, 120 insertions(+)
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs
index 97aa5bf8..13afdecd 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs
@@ -1,3 +1,5 @@
+using System;
+using System.Collections.Generic;
using AWS.Lambda.Powertools.Common;
using NSubstitute;
using Xunit;
@@ -30,4 +32,122 @@ public void Metrics_Set_Execution_Environment_Context()
env.Received(1).GetEnvironmentVariable("AWS_EXECUTION_ENV");
}
+
+ [Fact]
+ public void When_Constructor_With_Namespace_And_Service_Should_Set_Both()
+ {
+ // Arrange
+ var metricsMock = Substitute.For();
+ var powertoolsConfigMock = Substitute.For();
+
+ // Act
+ var metrics = new Metrics(powertoolsConfigMock, "TestNamespace", "TestService");
+
+ // Assert
+ Assert.Equal("TestNamespace", metrics.GetNamespace());
+ Assert.Equal("TestService", metrics.Options.Service);
+ }
+
+ [Fact]
+ public void When_Constructor_With_Null_Namespace_And_Service_Should_Not_Set()
+ {
+ // Arrange
+ var metricsMock = Substitute.For();
+ var powertoolsConfigMock = Substitute.For();
+ powertoolsConfigMock.MetricsNamespace.Returns((string)null);
+ powertoolsConfigMock.Service.Returns("service_undefined");
+
+ // Act
+ var metrics = new Metrics(powertoolsConfigMock, null, null);
+
+ // Assert
+ Assert.Null(metrics.GetNamespace());
+ Assert.Null(metrics.Options.Service);
+ }
+
+ [Fact]
+ public void When_AddMetric_With_EmptyKey_Should_ThrowArgumentNullException()
+ {
+ // Arrange
+ var metricsMock = Substitute.For();
+ var powertoolsConfigMock = Substitute.For();
+ IMetrics metrics = new Metrics(powertoolsConfigMock);
+
+ // 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);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void When_AddMetric_With_InvalidKey_Should_ThrowArgumentNullException(string key)
+ {
+ // Arrange
+ // var metricsMock = Substitute.For();
+ var powertoolsConfigMock = Substitute.For();
+ IMetrics metrics = new Metrics(powertoolsConfigMock);
+
+ // 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);
+ }
+
+ [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
+ { " ", "value" }, // whitespace key
+ { "key1", " " }, // whitespace value
+ { "key2", null } // null value
+ };
+
+ // Act & Assert
+ foreach (var dimension in invalidDimensions)
+ {
+ 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);
+ }
+ }
+
+ [Fact]
+ public void When_PushSingleMetric_With_EmptyName_Should_ThrowArgumentNullException()
+ {
+ // Arrange
+ var powertoolsConfigMock = Substitute.For();
+ IMetrics metrics = new Metrics(powertoolsConfigMock);
+
+ // 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);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void When_PushSingleMetric_With_InvalidName_Should_ThrowArgumentNullException(string name)
+ {
+ // Arrange
+ var powertoolsConfigMock = Substitute.For();
+ IMetrics metrics = new Metrics(powertoolsConfigMock);
+
+ // Act & Assert
+ 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);
+ }
}
\ No newline at end of file
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 11/22] 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 12/22] 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 13/22] 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 14/22] 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