From a1417eaa5fb58636739056aee3604ef6cef23380 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Wed, 19 Feb 2025 19:26:39 +0000
Subject: [PATCH 1/9] feat(metrics): enhance default dimensions handling and
refactor metrics initialization. Adding default dimensions to cold start
metrics
---
.../AWS.Lambda.Powertools.Metrics/IMetrics.cs | 25 +-
.../Internal/MetricsAspect.cs | 29 +--
.../AWS.Lambda.Powertools.Metrics/Metrics.cs | 225 ++++++++++++------
.../Model/Metadata.cs | 8 +
.../Model/MetricsContext.cs | 8 +
.../EMFValidationTests.cs | 1 +
.../Handlers/DefaultDimensionsHandler.cs | 32 +++
.../Handlers/FunctionHandlerTests.cs | 47 +++-
8 files changed, 278 insertions(+), 97 deletions(-)
create mode 100644 libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/DefaultDimensionsHandler.cs
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs
index 6f0b868a..594c9073 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs
@@ -66,7 +66,7 @@ public interface IMetrics
/// Metrics resolution
void PushSingleMetric(string metricName, double value, MetricUnit unit, string nameSpace = null,
string service = null, Dictionary defaultDimensions = null, MetricResolution metricResolution = MetricResolution.Default);
-
+
///
/// Sets the namespace
///
@@ -101,4 +101,27 @@ void PushSingleMetric(string metricName, double value, MetricUnit unit, string n
/// Clears both default dimensions and dimensions lists
///
void ClearDefaultDimensions();
+
+ ///
+ /// Sets the service
+ ///
+ ///
+ void SetService(string triggerService);
+
+ ///
+ /// Sets the raise on empty metrics
+ ///
+ ///
+ void SetRaiseOnEmptyMetrics(bool triggerRaiseOnEmptyMetrics);
+
+ ///
+ /// Sets the capture cold start
+ ///
+ void SetCaptureColdStart(bool captureColdStart);
+
+ ///
+ /// Gets the default dimensions
+ ///
+ Dictionary GetDefaultDimensions();
+
}
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
index a72e205e..4e77cddc 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
@@ -70,13 +70,13 @@ public void Before(
var trigger = triggers.OfType().First();
- _metricsInstance ??= new Metrics(
- PowertoolsConfigurations.Instance,
- trigger.Namespace,
- trigger.Service,
- trigger.RaiseOnEmptyMetrics,
- trigger.CaptureColdStart
- );
+ _metricsInstance ??= Metrics.Instance;
+ _metricsInstance.SetService(trigger.Service);
+ _metricsInstance.SetNamespace(trigger.Namespace);
+ _metricsInstance.SetRaiseOnEmptyMetrics(trigger.RaiseOnEmptyMetrics);
+ _metricsInstance.SetCaptureColdStart(trigger.CaptureColdStart);
+
+ var defaultDimensions = _metricsInstance.GetDefaultDimensions();
var eventArgs = new AspectEventArgs
{
@@ -95,16 +95,13 @@ public void Before(
var nameSpace = _metricsInstance.GetNamespace();
var service = _metricsInstance.GetService();
- Dictionary dimensions = null;
-
+
var context = GetContext(eventArgs);
-
+
if (context is not null)
{
- dimensions = new Dictionary
- {
- { "FunctionName", context.FunctionName }
- };
+ defaultDimensions.Add("FunctionName", context.FunctionName);
+ _metricsInstance.SetDefaultDimensions(defaultDimensions);
}
_metricsInstance.PushSingleMetric(
@@ -113,7 +110,7 @@ public void Before(
MetricUnit.Count,
nameSpace,
service,
- dimensions
+ defaultDimensions
);
}
}
@@ -137,7 +134,7 @@ internal static void ResetForTest()
_isColdStart = true;
Metrics.ResetForTest();
}
-
+
///
/// Gets the Lambda context
///
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
index 2ddbc539..26c91333 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
@@ -15,6 +15,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using AWS.Lambda.Powertools.Common;
@@ -27,6 +28,13 @@ namespace AWS.Lambda.Powertools.Metrics;
///
public class Metrics : IMetrics, IDisposable
{
+ static Metrics()
+ {
+ _instance = new Metrics(PowertoolsConfigurations.Instance);
+ }
+
+ internal static IMetrics Instance => _instance ?? new Metrics(PowertoolsConfigurations.Instance);
+
///
/// The instance
///
@@ -45,12 +53,12 @@ public class Metrics : IMetrics, IDisposable
///
/// If true, Powertools for AWS Lambda (.NET) will throw an exception on empty metrics when trying to flush
///
- private readonly bool _raiseOnEmptyMetrics;
+ private bool _raiseOnEmptyMetrics;
///
/// The capture cold start enabled
///
- private readonly bool _captureColdStartEnabled;
+ private bool _captureColdStartEnabled;
//
// Shared synchronization object
@@ -70,14 +78,16 @@ public class Metrics : IMetrics, IDisposable
internal Metrics(IPowertoolsConfigurations powertoolsConfigurations, string nameSpace = null, string service = null,
bool raiseOnEmptyMetrics = false, bool captureColdStartEnabled = false)
{
- _instance ??= this;
-
- _powertoolsConfigurations = powertoolsConfigurations;
- _raiseOnEmptyMetrics = raiseOnEmptyMetrics;
- _captureColdStartEnabled = captureColdStartEnabled;
- _context = InitializeContext(nameSpace, service, null);
+ if (_instance == null)
+ {
+ _instance = this;
+ _powertoolsConfigurations = powertoolsConfigurations;
+ _raiseOnEmptyMetrics = raiseOnEmptyMetrics;
+ _captureColdStartEnabled = captureColdStartEnabled;
+ _context = new MetricsContext();
- _powertoolsConfigurations.SetExecutionEnvironment(this);
+ _powertoolsConfigurations.SetExecutionEnvironment(this);
+ }
}
///
@@ -93,30 +103,38 @@ internal Metrics(IPowertoolsConfigurations powertoolsConfigurations, string name
///
void IMetrics.AddMetric(string key, double value, MetricUnit unit, MetricResolution metricResolution)
{
- if (string.IsNullOrWhiteSpace(key))
- throw new ArgumentNullException(
- nameof(key),
- "'AddMetric' method requires a valid metrics key. 'Null' or empty values are not allowed.");
-
- if (value < 0)
- {
- throw new ArgumentException(
- "'AddMetric' method requires a valid metrics value. Value must be >= 0.", nameof(value));
- }
-
- lock (_lockObj)
+ if (Instance != null)
{
- var metrics = _context.GetMetrics();
+ if (string.IsNullOrWhiteSpace(key))
+ throw new ArgumentNullException(
+ nameof(key),
+ "'AddMetric' method requires a valid metrics key. 'Null' or empty values are not allowed.");
- if (metrics.Count > 0 &&
- (metrics.Count == PowertoolsConfigurations.MaxMetrics ||
- metrics.FirstOrDefault(x => x.Name == key)
- ?.Values.Count == PowertoolsConfigurations.MaxMetrics))
+ if (value < 0)
{
- _instance.Flush(true);
+ throw new ArgumentException(
+ "'AddMetric' method requires a valid metrics value. Value must be >= 0.", nameof(value));
}
- _context.AddMetric(key, value, unit, metricResolution);
+ lock (_lockObj)
+ {
+ var metrics = _context.GetMetrics();
+
+ if (metrics.Count > 0 &&
+ (metrics.Count == PowertoolsConfigurations.MaxMetrics ||
+ metrics.FirstOrDefault(x => x.Name == key)
+ ?.Values.Count == PowertoolsConfigurations.MaxMetrics))
+ {
+ Instance.Flush(true);
+ }
+
+ _context.AddMetric(key, value, unit, metricResolution);
+ }
+ }
+ else
+ {
+ Debug.WriteLine(
+ $"##WARNING##: Metrics should be initialized in Handler method before calling {nameof(AddMetric)} method.");
}
}
@@ -126,7 +144,9 @@ void IMetrics.AddMetric(string key, double value, MetricUnit unit, MetricResolut
/// Metrics Namespace Identifier
void IMetrics.SetNamespace(string nameSpace)
{
- _context.SetNamespace(nameSpace);
+ _context.SetNamespace(!string.IsNullOrWhiteSpace(nameSpace)
+ ? nameSpace
+ : Instance.GetNamespace() ?? _powertoolsConfigurations.MetricsNamespace);
}
///
@@ -244,7 +264,7 @@ void IMetrics.Flush(bool metricsOverflow)
"##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'");
}
}
-
+
///
/// Clears both default dimensions and dimensions lists
///
@@ -253,6 +273,43 @@ void IMetrics.ClearDefaultDimensions()
_context.ClearDefaultDimensions();
}
+ ///
+ public void SetService(string service)
+ {
+ // this needs to check if service is set through code or env variables
+ // the default value service_undefined has to be ignored and return null so it is not added as default
+ var parsedService = !string.IsNullOrWhiteSpace(service)
+ ? service
+ : _powertoolsConfigurations.Service == "service_undefined"
+ ? null
+ : _powertoolsConfigurations.Service;
+
+ if (parsedService != null)
+ {
+ _context.SetService(parsedService);
+ _context.SetDefaultDimensions(new List(new[]
+ { new DimensionSet("Service", _context.GetService()) }));
+ }
+ }
+
+ ///
+ public void SetRaiseOnEmptyMetrics(bool raiseOnEmptyMetrics)
+ {
+ _raiseOnEmptyMetrics = raiseOnEmptyMetrics;
+ }
+
+ ///
+ public void SetCaptureColdStart(bool captureColdStart)
+ {
+ _captureColdStartEnabled = captureColdStart;
+ }
+
+ ///
+ public Dictionary GetDefaultDimensions()
+ {
+ return ListToDictionary(_context.GetDefaultDimensions());
+ }
+
///
/// Serialize global context object
///
@@ -277,19 +334,29 @@ public string Serialize()
/// 'PushSingleMetric' method requires a valid metrics key. 'Null' or empty
/// values are not allowed.
///
- void IMetrics.PushSingleMetric(string metricName, double value, MetricUnit unit, string nameSpace, string service,
- Dictionary defaultDimensions, MetricResolution metricResolution)
+ void IMetrics.PushSingleMetric(string metricName, double value, MetricUnit unit, string nameSpace,
+ string service, Dictionary defaultDimensions, MetricResolution metricResolution)
{
if (string.IsNullOrWhiteSpace(metricName))
throw new ArgumentNullException(nameof(metricName),
"'PushSingleMetric' method requires a valid metrics key. 'Null' or empty values are not allowed.");
- using var context = InitializeContext(nameSpace, service, defaultDimensions);
+ var context = new MetricsContext();
+ context.SetNamespace(nameSpace ?? _context.GetNamespace());
+ context.SetService(service ?? _context.GetService());
+
+ if (defaultDimensions != null)
+ {
+ var defaultDimensionsList = DictionaryToList(defaultDimensions);
+ context.SetDefaultDimensions(defaultDimensionsList);
+ }
+
context.AddMetric(metricName, value, unit, metricResolution);
Flush(context);
}
+
///
/// Implementation of IDisposable interface
///
@@ -308,7 +375,7 @@ protected virtual void Dispose(bool disposing)
// Cleanup
if (disposing)
{
- _instance.Flush();
+ Instance.Flush();
}
}
@@ -322,7 +389,7 @@ protected virtual void Dispose(bool disposing)
public static void AddMetric(string key, double value, MetricUnit unit = MetricUnit.None,
MetricResolution metricResolution = MetricResolution.Default)
{
- _instance.AddMetric(key, value, unit, metricResolution);
+ Instance.AddMetric(key, value, unit, metricResolution);
}
///
@@ -331,7 +398,7 @@ public static void AddMetric(string key, double value, MetricUnit unit = MetricU
/// Metrics Namespace Identifier
public static void SetNamespace(string nameSpace)
{
- _instance.SetNamespace(nameSpace);
+ Instance.SetNamespace(nameSpace);
}
///
@@ -340,7 +407,7 @@ public static void SetNamespace(string nameSpace)
/// Namespace identifier
public static string GetNamespace()
{
- return _instance.GetNamespace();
+ return Instance.GetNamespace();
}
///
@@ -350,7 +417,7 @@ public static string GetNamespace()
/// Dimension value
public static void AddDimension(string key, string value)
{
- _instance.AddDimension(key, value);
+ Instance.AddDimension(key, value);
}
///
@@ -360,7 +427,7 @@ public static void AddDimension(string key, string value)
/// Metadata value
public static void AddMetadata(string key, object value)
{
- _instance.AddMetadata(key, value);
+ Instance.AddMetadata(key, value);
}
///
@@ -369,7 +436,15 @@ public static void AddMetadata(string key, object value)
/// Default Dimension List
public static void SetDefaultDimensions(Dictionary defaultDimensions)
{
- _instance.SetDefaultDimensions(defaultDimensions);
+ if (Instance != null)
+ {
+ Instance.SetDefaultDimensions(defaultDimensions);
+ // MetricsDefaults.SetDefaultDimensions(defaultDimensions);
+ }
+ else
+ {
+ // MetricsDefaults.SetDefaultDimensions(defaultDimensions);
+ }
}
///
@@ -377,7 +452,15 @@ public static void SetDefaultDimensions(Dictionary defaultDimens
///
public static void ClearDefaultDimensions()
{
- _instance.ClearDefaultDimensions();
+ if (Instance != null)
+ {
+ Instance.ClearDefaultDimensions();
+ }
+ else
+ {
+ Debug.WriteLine(
+ $"##WARNING##: Metrics should be initialized in Handler method before calling {nameof(ClearDefaultDimensions)} method.");
+ }
}
///
@@ -407,44 +490,16 @@ public static void PushSingleMetric(string metricName, double value, MetricUnit
string service = null, Dictionary defaultDimensions = null,
MetricResolution metricResolution = MetricResolution.Default)
{
- _instance.PushSingleMetric(metricName, value, unit, nameSpace, service, defaultDimensions, metricResolution);
- }
-
- ///
- /// Sets global namespace, service name and default dimensions list.
- ///
- /// Metrics namespace
- /// Service Name
- /// Default Dimensions List
- /// MetricsContext.
- private MetricsContext InitializeContext(string nameSpace, string service,
- Dictionary defaultDimensions)
- {
- var context = new MetricsContext();
- var defaultDimensionsList = DictionaryToList(defaultDimensions);
-
- context.SetNamespace(!string.IsNullOrWhiteSpace(nameSpace)
- ? nameSpace
- : _instance.GetNamespace() ?? _powertoolsConfigurations.MetricsNamespace);
-
- // this needs to check if service is set through code or env variables
- // the default value service_undefined has to be ignored and return null so it is not added as default
- // TODO: Check if there is a way to get the default dimensions and if it makes sense
- var parsedService = !string.IsNullOrWhiteSpace(service)
- ? service
- : _powertoolsConfigurations.Service == "service_undefined"
- ? null
- : _powertoolsConfigurations.Service;
-
- if (parsedService != null)
+ if (Instance != null)
{
- context.SetService(parsedService);
- defaultDimensionsList.Add(new DimensionSet("Service", context.GetService()));
+ Instance.PushSingleMetric(metricName, value, unit, nameSpace, service, defaultDimensions,
+ metricResolution);
+ }
+ else
+ {
+ Debug.WriteLine(
+ $"##WARNING##: Metrics should be initialized in Handler method before calling {nameof(PushSingleMetric)} method.");
}
-
- context.SetDefaultDimensions(defaultDimensionsList);
-
- return context;
}
///
@@ -462,6 +517,22 @@ private List DictionaryToList(Dictionary defaultDi
return defaultDimensionsList;
}
+ private Dictionary ListToDictionary(List dimensions)
+ {
+ var dictionary = new Dictionary();
+ try
+ {
+ return dimensions != null
+ ? new Dictionary(dimensions.SelectMany(x => x.Dimensions))
+ : dictionary;
+ }
+ catch (Exception e)
+ {
+ Debug.WriteLine("Error converting list to dictionary: " + e.Message);
+ return dictionary;
+ }
+ }
+
///
/// Helper method for testing purposes. Clears static instance between test execution
///
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Model/Metadata.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Model/Metadata.cs
index ee3d0605..2119dd93 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/Model/Metadata.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Model/Metadata.cs
@@ -184,4 +184,12 @@ internal void ClearDefaultDimensions()
{
_metricDirective.ClearDefaultDimensions();
}
+
+ ///
+ /// Retrieves default dimensions list
+ ///
+ internal List GetDefaultDimensions()
+ {
+ return _metricDirective.DefaultDimensions;
+ }
}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricsContext.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricsContext.cs
index ba77d0ed..8e886a90 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricsContext.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricsContext.cs
@@ -170,4 +170,12 @@ public void ClearDefaultDimensions()
{
_rootNode.AWS.ClearDefaultDimensions();
}
+
+ ///
+ /// Retrieves default dimensions list
+ ///
+ internal List GetDefaultDimensions()
+ {
+ return _rootNode.AWS.GetDefaultDimensions();
+ }
}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs
index 0be19d3b..d4394a2b 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs
@@ -393,6 +393,7 @@ private List AllIndexesOf(string str, string value)
public void Dispose()
{
// need to reset instance after each test
+ Metrics.ResetForTest();
MetricsAspect.ResetForTest();
Environment.SetEnvironmentVariable("POWERTOOLS_METRICS_NAMESPACE", null);
}
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/DefaultDimensionsHandler.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/DefaultDimensionsHandler.cs
new file mode 100644
index 00000000..7450f8cd
--- /dev/null
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/DefaultDimensionsHandler.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using Amazon.Lambda.Core;
+
+namespace AWS.Lambda.Powertools.Metrics.Tests.Handlers;
+
+public class DefaultDimensionsHandler
+{
+ public DefaultDimensionsHandler()
+ {
+ Metrics.SetDefaultDimensions(new Dictionary
+ {
+ {"Environment", "Prod"},
+ {"Another", "One"}
+ });
+ // Metrics.SetNamespace("dotnet-powertools-test");
+ // Metrics.PushSingleMetric("SingleMetric", 1, MetricUnit.Count);
+ }
+
+ [Metrics(Namespace = "dotnet-powertools-test", Service = "testService", CaptureColdStart = true)]
+ public void Handler()
+ {
+ // Default dimensions are already set
+ Metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
+ }
+
+ [Metrics(Namespace = "dotnet-powertools-test", Service = "testService", CaptureColdStart = true)]
+ public void HandlerWithContext(ILambdaContext context)
+ {
+ // Default dimensions are already set and adds FunctionName dimension
+ Metrics.AddMetric("Memory", 10, MetricUnit.Megabytes);
+ }
+}
\ 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 3469e2e4..5b6ad783 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
@@ -85,7 +85,7 @@ public void When_LambdaContext_Should_Add_FunctioName_Dimension_CaptureColdStart
metricsOutput);
Assert.Contains(
- "\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"FunctionName\",\"Service\"]]}]}",
+ "\"CloudWatchMetrics\":[{\"Namespace\":\"ns\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"FunctionName\"]]}]},\"Service\":\"svc\",\"FunctionName\":\"My Function with context\",\"ColdStart\":1}",
metricsOutput);
}
@@ -108,7 +108,7 @@ public void When_LambdaContext_And_Parameter_Should_Add_FunctioName_Dimension_Ca
metricsOutput);
Assert.Contains(
- "\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"FunctionName\",\"Service\"]]}]}",
+ "\"CloudWatchMetrics\":[{\"Namespace\":\"ns\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"FunctionName\"]]}]},\"Service\":\"svc\",\"FunctionName\":\"My Function with context\",\"ColdStart\":1}",
metricsOutput);
}
@@ -128,10 +128,51 @@ public void When_No_LambdaContext_Should_Not_Add_FunctioName_Dimension_CaptureCo
"\"Metrics\":[{\"Name\":\"MyMetric\",\"Unit\":\"None\"}],\"Dimensions\":[[\"Service\"]]}]},\"Service\":\"svc\",\"MyMetric\":1}",
metricsOutput);
}
+
+ [Fact]
+ public void DefaultDimensions_AreAppliedCorrectly()
+ {
+ // Arrange
+ var handler = new DefaultDimensionsHandler();
+
+ // Act
+ handler.Handler();
+
+ // 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\":[[\"Environment\",\"Another\",\"Service\"]]}]},\"Environment\":\"Prod\",\"Another\":\"One\",\"Service\":\"testService\",\"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}", metricsOutput);
+ }
+
+ [Fact]
+ public void DefaultDimensions_AreAppliedCorrectly_WithContext_FunctionName()
+ {
+ // Arrange
+ var handler = new DefaultDimensionsHandler();
+
+ // Act
+ handler.HandlerWithContext(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\":[[\"Environment\",\"Another\",\"Service\",\"FunctionName\"]]}]},\"Environment\":\"Prod\",\"Another\":\"One\",\"Service\":\"testService\",\"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}", metricsOutput);
+ }
public void Dispose()
{
Metrics.ResetForTest();
MetricsAspect.ResetForTest();
}
-}
\ No newline at end of file
+}
+
+
From 450a2da95934a90d1bfa8fc7d4cde87a1342dc2b Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Wed, 19 Feb 2025 20:24:15 +0000
Subject: [PATCH 2/9] feat(metrics): introduce MetricsOptions for configurable
metrics setup and refactor initialization logic
---
.../Internal/MetricsAspect.cs | 19 +++++------
.../AWS.Lambda.Powertools.Metrics/Metrics.cs | 30 ++++++++++++++--
.../MetricsOptions.cs | 34 +++++++++++++++++++
.../Handlers/DefaultDimensionsHandler.cs | 21 +++++++++---
4 files changed, 87 insertions(+), 17 deletions(-)
create mode 100644 libraries/src/AWS.Lambda.Powertools.Metrics/MetricsOptions.cs
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
index 4e77cddc..8531d784 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
@@ -14,7 +14,6 @@
*/
using System;
-using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Amazon.Lambda.Core;
@@ -70,11 +69,12 @@ public void Before(
var trigger = triggers.OfType().First();
- _metricsInstance ??= Metrics.Instance;
- _metricsInstance.SetService(trigger.Service);
- _metricsInstance.SetNamespace(trigger.Namespace);
- _metricsInstance.SetRaiseOnEmptyMetrics(trigger.RaiseOnEmptyMetrics);
- _metricsInstance.SetCaptureColdStart(trigger.CaptureColdStart);
+ _metricsInstance ??= Metrics.Configure(options => {
+ options.Namespace = trigger.Namespace;
+ options.Service = trigger.Service;
+ options.RaiseOnEmptyMetrics = trigger.RaiseOnEmptyMetrics;
+ options.CaptureColdStart = trigger.CaptureColdStart;
+ });
var defaultDimensions = _metricsInstance.GetDefaultDimensions();
@@ -93,9 +93,6 @@ public void Before(
{
_isColdStart = false;
- var nameSpace = _metricsInstance.GetNamespace();
- var service = _metricsInstance.GetService();
-
var context = GetContext(eventArgs);
if (context is not null)
@@ -108,8 +105,8 @@ public void Before(
"ColdStart",
1.0,
MetricUnit.Count,
- nameSpace,
- service,
+ _metricsInstance.GetNamespace(),
+ _metricsInstance.GetService(),
defaultDimensions
);
}
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
index 26c91333..9d45c761 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
@@ -65,6 +65,32 @@ static Metrics()
//
private readonly object _lockObj = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ ///
+ public static IMetrics Configure(Action configure)
+ {
+ var options = new MetricsOptions();
+ configure(options);
+
+ if (!string.IsNullOrEmpty(options.Namespace))
+ SetNamespace(options.Namespace);
+
+ if (!string.IsNullOrEmpty(options.Service))
+ Instance.SetService(options.Service);
+
+ Instance.SetRaiseOnEmptyMetrics(options.RaiseOnEmptyMetrics);
+ Instance.SetCaptureColdStart(options.CaptureColdStart);
+
+ if (options.DefaultDimensions != null)
+ SetDefaultDimensions(options.DefaultDimensions);
+
+ return Instance;
+ }
+
///
/// Creates a Metrics object that provides features to send metrics to Amazon Cloudwatch using the Embedded metric
/// format (EMF). See
@@ -157,7 +183,7 @@ string IMetrics.GetNamespace()
{
try
{
- return _context.GetNamespace();
+ return _context.GetNamespace() ?? _powertoolsConfigurations.MetricsNamespace;
}
catch
{
@@ -342,7 +368,7 @@ void IMetrics.PushSingleMetric(string metricName, double value, MetricUnit unit,
"'PushSingleMetric' method requires a valid metrics key. 'Null' or empty values are not allowed.");
var context = new MetricsContext();
- context.SetNamespace(nameSpace ?? _context.GetNamespace());
+ context.SetNamespace(nameSpace ?? GetNamespace());
context.SetService(service ?? _context.GetService());
if (defaultDimensions != null)
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsOptions.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsOptions.cs
new file mode 100644
index 00000000..52292b1d
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsOptions.cs
@@ -0,0 +1,34 @@
+using System.Collections.Generic;
+
+namespace AWS.Lambda.Powertools.Metrics;
+
+///
+/// Configuration options for AWS Lambda Powertools Metrics.
+///
+public class MetricsOptions
+{
+ ///
+ /// Gets or sets the CloudWatch metrics namespace.
+ ///
+ public string Namespace { get; set; }
+
+ ///
+ /// Gets or sets the service name to be used as a metric dimension.
+ ///
+ public string Service { get; set; }
+
+ ///
+ /// Gets or sets whether to throw an exception when no metrics are emitted.
+ ///
+ public bool RaiseOnEmptyMetrics { get; set; }
+
+ ///
+ /// Gets or sets whether to capture cold start metrics.
+ ///
+ public bool CaptureColdStart { get; set; }
+
+ ///
+ /// Gets or sets the default dimensions to be added to all metrics.
+ ///
+ public Dictionary DefaultDimensions { get; set; }
+}
\ 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 7450f8cd..1baca918 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/DefaultDimensionsHandler.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/DefaultDimensionsHandler.cs
@@ -7,11 +7,24 @@ public class DefaultDimensionsHandler
{
public DefaultDimensionsHandler()
{
- Metrics.SetDefaultDimensions(new Dictionary
- {
- {"Environment", "Prod"},
- {"Another", "One"}
+
+ Metrics.Configure(options => {
+ // options.Namespace = "my-namespace";
+ // options.Service = "my-service";
+ // options.CaptureColdStart = true;
+ // options.RaiseOnEmptyMetrics = true;
+ options.DefaultDimensions = new Dictionary
+ {
+ {"Environment", "Prod"},
+ {"Another", "One"}
+ };
});
+
+ // Metrics.SetDefaultDimensions(new Dictionary
+ // {
+ // {"Environment", "Prod"},
+ // {"Another", "One"}
+ // });
// Metrics.SetNamespace("dotnet-powertools-test");
// Metrics.PushSingleMetric("SingleMetric", 1, MetricUnit.Count);
}
From df9d4fa418668ddd1bd015c9d80378b2e28787a8 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Thu, 20 Feb 2025 16:34:53 +0000
Subject: [PATCH 3/9] feat(metrics): add MetricsBuilder for fluent
configuration of metrics options and enhance default dimensions handling
---
.../AWS.Lambda.Powertools.Metrics/IMetrics.cs | 122 +++++-------
.../Internal/MetricsAspect.cs | 17 +-
.../AWS.Lambda.Powertools.Metrics/Metrics.cs | 181 ++++++------------
.../MetricsAttribute.cs | 30 ++-
.../MetricsBuilder.cs | 65 +++++++
.../MetricsOptions.cs | 4 +-
.../EMFValidationTests.cs | 2 +-
.../Handlers/DefaultDimensionsHandler.cs | 38 +++-
.../Handlers/FunctionHandlerTests.cs | 165 ++++++++++++----
.../Handlers/MetricsnBuilderHandler.cs | 29 +++
10 files changed, 407 insertions(+), 246 deletions(-)
create mode 100644 libraries/src/AWS.Lambda.Powertools.Metrics/MetricsBuilder.cs
create mode 100644 libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/MetricsnBuilderHandler.cs
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs
index 594c9073..69ef2ee3 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs
@@ -1,127 +1,109 @@
/*
* 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;
using System.Collections.Generic;
namespace AWS.Lambda.Powertools.Metrics;
///
-/// Interface IMetrics
-/// Implements the
+/// Interface for metrics operations.
///
///
-public interface IMetrics
+public interface IMetrics
{
///
- /// Adds metric
+ /// Adds a metric to the collection.
///
- /// Metric key
- /// Metric value
- /// Metric unit
- ///
- void AddMetric(string key, double value, MetricUnit unit, MetricResolution metricResolution);
+ /// The metric key.
+ /// The metric value.
+ /// The metric unit.
+ /// The metric resolution.
+ void AddMetric(string key, double value, MetricUnit unit = MetricUnit.None,
+ MetricResolution resolution = MetricResolution.Default);
///
- /// Adds a dimension
+ /// Adds a dimension to the collection.
///
- /// Dimension key
- /// Dimension value
+ /// The dimension key.
+ /// The dimension value.
void AddDimension(string key, string value);
///
- /// Sets the default dimensions
+ /// Adds metadata to the collection.
///
- /// Default dimensions
- void SetDefaultDimensions(Dictionary defaultDimension);
-
- ///
- /// Adds metadata
- ///
- /// Metadata key
- /// Metadata value
+ /// The metadata key.
+ /// The metadata value.
void AddMetadata(string key, object value);
///
- /// Pushes a single metric with custom namespace, service and dimensions.
+ /// Sets the default dimensions.
///
- /// Name of the metric
- /// Metric value
- /// Metric unit
- /// Metric namespace
- /// Metric service
- /// Metric default dimensions
- /// Metrics resolution
- void PushSingleMetric(string metricName, double value, MetricUnit unit, string nameSpace = null,
- string service = null, Dictionary defaultDimensions = null, MetricResolution metricResolution = MetricResolution.Default);
-
+ /// The default dimensions.
+ void SetDefaultDimensions(Dictionary defaultDimensions);
+
///
- /// Sets the namespace
+ /// Sets the namespace for the metrics.
///
- /// Metrics namespace
+ /// The namespace.
void SetNamespace(string nameSpace);
///
- /// Gets the namespace
+ /// Sets the service name for the metrics.
///
- /// System.String.
- string GetNamespace();
+ /// The service name.
+ void SetService(string service);
///
- /// Gets the service
+ /// Sets whether to raise an event on empty metrics.
///
- /// System.String.
- string GetService();
+ /// If set to true, raises an event on empty metrics.
+ void SetRaiseOnEmptyMetrics(bool raiseOnEmptyMetrics);
///
- /// Serializes metrics instance
+ /// Sets whether to capture cold start metrics.
///
- /// System.String.
- string Serialize();
+ /// If set to true, captures cold start metrics.
+ void SetCaptureColdStart(bool captureColdStart);
///
- /// Flushes metrics to CloudWatch
+ /// Pushes a single metric to the collection.
///
- /// if set to true [metrics overflow].
- void Flush(bool metricsOverflow = false);
-
+ /// The metric name.
+ /// The metric value.
+ /// The metric unit.
+ /// The namespace.
+ /// The service name.
+ /// The default dimensions.
+ /// The metric resolution.
+ void PushSingleMetric(string name, double value, MetricUnit unit, string nameSpace = null, string service = null,
+ Dictionary defaultDimensions = null, MetricResolution resolution = MetricResolution.Default);
+
///
- /// Clears both default dimensions and dimensions lists
+ /// Clears the default dimensions.
///
void ClearDefaultDimensions();
///
- /// Sets the service
+ /// Flushes the metrics.
///
- ///
- void SetService(string triggerService);
-
- ///
- /// Sets the raise on empty metrics
- ///
- ///
- void SetRaiseOnEmptyMetrics(bool triggerRaiseOnEmptyMetrics);
-
- ///
- /// Sets the capture cold start
- ///
- void SetCaptureColdStart(bool captureColdStart);
+ /// If set to true, indicates a metrics overflow.
+ void Flush(bool metricsOverflow = false);
///
- /// Gets the default dimensions
+ /// Gets the metrics options.
///
- Dictionary GetDefaultDimensions();
-
-}
+ /// The metrics options.
+ public MetricsOptions Options { get; }
+}
\ 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 8531d784..4ebacf14 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
@@ -72,12 +72,10 @@ public void Before(
_metricsInstance ??= Metrics.Configure(options => {
options.Namespace = trigger.Namespace;
options.Service = trigger.Service;
- options.RaiseOnEmptyMetrics = trigger.RaiseOnEmptyMetrics;
- options.CaptureColdStart = trigger.CaptureColdStart;
+ options.RaiseOnEmptyMetrics = trigger.IsRaiseOnEmptyMetricsSet ? trigger.RaiseOnEmptyMetrics : null;
+ options.CaptureColdStart = trigger.IsCaptureColdStartSet ? trigger.CaptureColdStart : null;
});
- var defaultDimensions = _metricsInstance.GetDefaultDimensions();
-
var eventArgs = new AspectEventArgs
{
Instance = instance,
@@ -89,15 +87,16 @@ public void Before(
Triggers = triggers
};
- if (trigger.CaptureColdStart && _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)
{
- defaultDimensions.Add("FunctionName", context.FunctionName);
+ defaultDimensions?.Add("FunctionName", context.FunctionName);
_metricsInstance.SetDefaultDimensions(defaultDimensions);
}
@@ -105,8 +104,8 @@ public void Before(
"ColdStart",
1.0,
MetricUnit.Count,
- _metricsInstance.GetNamespace(),
- _metricsInstance.GetService(),
+ _metricsInstance.Options?.Namespace ?? "",
+ _metricsInstance.Options?.Service ?? "",
defaultDimensions
);
}
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
index 9d45c761..38bf94fc 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.Threading;
using AWS.Lambda.Powertools.Common;
namespace AWS.Lambda.Powertools.Metrics;
@@ -28,17 +29,27 @@ namespace AWS.Lambda.Powertools.Metrics;
///
public class Metrics : IMetrics, IDisposable
{
- static Metrics()
+ public static IMetrics Instance
{
- _instance = new Metrics(PowertoolsConfigurations.Instance);
+ get => Current.Value ?? new Metrics(PowertoolsConfigurations.Instance);
+ private set => Current.Value = value;
}
- internal static IMetrics Instance => _instance ?? new Metrics(PowertoolsConfigurations.Instance);
+ ///
+ public MetricsOptions Options =>
+ new()
+ {
+ CaptureColdStart = _captureColdStartEnabled,
+ Namespace = GetNamespace(),
+ Service = GetService(),
+ RaiseOnEmptyMetrics = _raiseOnEmptyMetrics,
+ DefaultDimensions = GetDefaultDimensions()
+ };
///
/// The instance
///
- private static IMetrics _instance;
+ private static readonly AsyncLocal Current = new();
///
/// The context
@@ -82,8 +93,10 @@ public static IMetrics Configure(Action configure)
if (!string.IsNullOrEmpty(options.Service))
Instance.SetService(options.Service);
- Instance.SetRaiseOnEmptyMetrics(options.RaiseOnEmptyMetrics);
- Instance.SetCaptureColdStart(options.CaptureColdStart);
+ if (options.RaiseOnEmptyMetrics.HasValue)
+ Instance.SetRaiseOnEmptyMetrics(options.RaiseOnEmptyMetrics.Value);
+ if (options.CaptureColdStart.HasValue)
+ Instance.SetCaptureColdStart(options.CaptureColdStart.Value);
if (options.DefaultDimensions != null)
SetDefaultDimensions(options.DefaultDimensions);
@@ -104,29 +117,19 @@ public static IMetrics Configure(Action configure)
internal Metrics(IPowertoolsConfigurations powertoolsConfigurations, string nameSpace = null, string service = null,
bool raiseOnEmptyMetrics = false, bool captureColdStartEnabled = false)
{
- if (_instance == null)
- {
- _instance = this;
- _powertoolsConfigurations = powertoolsConfigurations;
- _raiseOnEmptyMetrics = raiseOnEmptyMetrics;
- _captureColdStartEnabled = captureColdStartEnabled;
- _context = new MetricsContext();
+ _powertoolsConfigurations = powertoolsConfigurations;
+ _context = new MetricsContext();
+ _raiseOnEmptyMetrics = raiseOnEmptyMetrics;
+ _captureColdStartEnabled = captureColdStartEnabled;
- _powertoolsConfigurations.SetExecutionEnvironment(this);
- }
+ Instance = this;
+ _powertoolsConfigurations.SetExecutionEnvironment(this);
+
+ if (!string.IsNullOrEmpty(nameSpace)) SetNamespace(nameSpace);
+ if (!string.IsNullOrEmpty(service)) SetService(service);
}
- ///
- /// Implements interface that adds new metric to memory.
- ///
- /// Metric Key
- /// Metric Value
- /// Metric Unit
- /// Metric resolution
- ///
- /// 'AddMetric' method requires a valid metrics key. 'Null' or empty values
- /// are not allowed.
- ///
+ ///
void IMetrics.AddMetric(string key, double value, MetricUnit unit, MetricResolution metricResolution)
{
if (Instance != null)
@@ -164,38 +167,20 @@ void IMetrics.AddMetric(string key, double value, MetricUnit unit, MetricResolut
}
}
- ///
- /// Implements interface that sets metrics namespace identifier.
- ///
- /// Metrics Namespace Identifier
+ ///
void IMetrics.SetNamespace(string nameSpace)
{
_context.SetNamespace(!string.IsNullOrWhiteSpace(nameSpace)
? nameSpace
- : Instance.GetNamespace() ?? _powertoolsConfigurations.MetricsNamespace);
+ : GetNamespace() ?? _powertoolsConfigurations.MetricsNamespace);
}
- ///
- /// Implements interface that allows retrieval of namespace identifier.
- ///
- /// Namespace identifier
- string IMetrics.GetNamespace()
- {
- try
- {
- return _context.GetNamespace() ?? _powertoolsConfigurations.MetricsNamespace;
- }
- catch
- {
- return null;
- }
- }
///
/// Implements interface to get service name
///
/// System.String.
- string IMetrics.GetService()
+ private string GetService()
{
try
{
@@ -207,15 +192,7 @@ string IMetrics.GetService()
}
}
- ///
- /// Implements interface that adds a dimension.
- ///
- /// Dimension key. Must not be null, empty or whitespace
- /// Dimension value
- ///
- /// 'AddDimension' method requires a valid dimension key. 'Null' or empty
- /// values are not allowed.
- ///
+ ///
void IMetrics.AddDimension(string key, string value)
{
if (string.IsNullOrWhiteSpace(key))
@@ -225,15 +202,7 @@ void IMetrics.AddDimension(string key, string value)
_context.AddDimension(key, value);
}
- ///
- /// Implements interface that adds metadata.
- ///
- /// Metadata key. Must not be null, empty or whitespace
- /// Metadata value
- ///
- /// 'AddMetadata' method requires a valid metadata key. 'Null' or empty
- /// values are not allowed.
- ///
+ ///
void IMetrics.AddMetadata(string key, object value)
{
if (string.IsNullOrWhiteSpace(key))
@@ -243,14 +212,7 @@ void IMetrics.AddMetadata(string key, object value)
_context.AddMetadata(key, value);
}
- ///
- /// Implements interface that sets default dimension list
- ///
- /// Default Dimension List
- ///
- /// 'SetDefaultDimensions' method requires a valid key pair. 'Null' or empty
- /// values are not allowed.
- ///
+ ///
void IMetrics.SetDefaultDimensions(Dictionary defaultDimension)
{
foreach (var item in defaultDimension)
@@ -261,12 +223,7 @@ void IMetrics.SetDefaultDimensions(Dictionary defaultDimension)
_context.SetDefaultDimensions(DictionaryToList(defaultDimension));
}
- ///
- /// Flushes metrics in Embedded Metric Format (EMF) to Standard Output. In Lambda, this output is collected
- /// automatically and sent to Cloudwatch.
- ///
- /// If enabled, non-default dimensions are cleared after flushing metrics
- /// true
+ ///
void IMetrics.Flush(bool metricsOverflow)
{
if (_context.GetMetrics().Count == 0
@@ -291,9 +248,7 @@ void IMetrics.Flush(bool metricsOverflow)
}
}
- ///
- /// Clears both default dimensions and dimensions lists
- ///
+ ///
void IMetrics.ClearDefaultDimensions()
{
_context.ClearDefaultDimensions();
@@ -314,7 +269,7 @@ public void SetService(string service)
{
_context.SetService(parsedService);
_context.SetDefaultDimensions(new List(new[]
- { new DimensionSet("Service", _context.GetService()) }));
+ { new DimensionSet("Service", GetService()) }));
}
}
@@ -330,36 +285,12 @@ public void SetCaptureColdStart(bool captureColdStart)
_captureColdStartEnabled = captureColdStart;
}
- ///
- public Dictionary GetDefaultDimensions()
+ private Dictionary GetDefaultDimensions()
{
return ListToDictionary(_context.GetDefaultDimensions());
}
- ///
- /// Serialize global context object
- ///
- /// Serialized global context object
- public string Serialize()
- {
- return _context.Serialize();
- }
-
- ///
- /// Implements the interface that 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 Value
- /// Metric Unit
- /// Metric Namespace
- /// Service Name
- /// Default dimensions list
- /// Metrics resolution
- ///
- /// 'PushSingleMetric' method requires a valid metrics key. 'Null' or empty
- /// values are not allowed.
- ///
+ ///
void IMetrics.PushSingleMetric(string metricName, double value, MetricUnit unit, string nameSpace,
string service, Dictionary defaultDimensions, MetricResolution metricResolution)
{
@@ -431,9 +362,16 @@ public static void SetNamespace(string nameSpace)
/// Retrieves namespace identifier.
///
/// Namespace identifier
- public static string GetNamespace()
+ public string GetNamespace()
{
- return Instance.GetNamespace();
+ try
+ {
+ return _context.GetNamespace() ?? _powertoolsConfigurations.MetricsNamespace;
+ }
+ catch
+ {
+ return null;
+ }
}
///
@@ -462,15 +400,7 @@ public static void AddMetadata(string key, object value)
/// Default Dimension List
public static void SetDefaultDimensions(Dictionary defaultDimensions)
{
- if (Instance != null)
- {
- Instance.SetDefaultDimensions(defaultDimensions);
- // MetricsDefaults.SetDefaultDimensions(defaultDimensions);
- }
- else
- {
- // MetricsDefaults.SetDefaultDimensions(defaultDimensions);
- }
+ Instance?.SetDefaultDimensions(defaultDimensions);
}
///
@@ -564,6 +494,15 @@ private Dictionary ListToDictionary(List dimension
///
internal static void ResetForTest()
{
- _instance = null;
+ Instance = null;
+ }
+
+ ///
+ /// For testing purposes, resets the Instance to the provided metrics instance.
+ ///
+ ///
+ public static void UseMetricsForTests(IMetrics metricsInstance)
+ {
+ Instance = metricsInstance;
}
}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs
index 0033f560..6413b597 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs
@@ -119,15 +119,41 @@ public class MetricsAttribute : Attribute
/// The service.
public string Service { get; set; }
+ private bool _captureColdStartSet;
+ private bool _captureColdStart;
+
///
/// Captures cold start during Lambda execution
///
/// true if [capture cold start]; otherwise, false.
- public bool CaptureColdStart { get; set; }
+ public bool CaptureColdStart
+ {
+ get => _captureColdStart;
+ set
+ {
+ _captureColdStart = value;
+ _captureColdStartSet = true;
+ }
+ }
+
+ internal bool IsCaptureColdStartSet => _captureColdStartSet;
+
+ private bool _raiseOnEmptyMetricsSet;
+ private bool _raiseOnEmptyMetrics;
///
/// Instructs metrics validation to throw exception if no metrics are provided.
///
/// true if [raise on empty metrics]; otherwise, false.
- public bool RaiseOnEmptyMetrics { get; set; }
+ public bool RaiseOnEmptyMetrics
+ {
+ get => _raiseOnEmptyMetrics;
+ set
+ {
+ _raiseOnEmptyMetrics = value;
+ _raiseOnEmptyMetricsSet = true;
+ }
+ }
+
+ internal bool IsRaiseOnEmptyMetricsSet => _raiseOnEmptyMetricsSet;
}
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsBuilder.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsBuilder.cs
new file mode 100644
index 00000000..f2d030e7
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsBuilder.cs
@@ -0,0 +1,65 @@
+/*
+ * 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.Collections.Generic;
+
+namespace AWS.Lambda.Powertools.Metrics;
+
+public class MetricsBuilder
+{
+ private readonly MetricsOptions _options = new();
+
+ public MetricsBuilder WithNamespace(string nameSpace)
+ {
+ _options.Namespace = nameSpace;
+ return this;
+ }
+
+ public MetricsBuilder WithService(string service)
+ {
+ _options.Service = service;
+ return this;
+ }
+
+ public MetricsBuilder WithRaiseOnEmptyMetrics(bool raiseOnEmptyMetrics)
+ {
+ _options.RaiseOnEmptyMetrics = raiseOnEmptyMetrics;
+ return this;
+ }
+
+ public MetricsBuilder WithCaptureColdStart(bool captureColdStart)
+ {
+ _options.CaptureColdStart = captureColdStart;
+ return this;
+ }
+
+ public MetricsBuilder WithDefaultDimensions(Dictionary defaultDimensions)
+ {
+ _options.DefaultDimensions = defaultDimensions;
+ return this;
+ }
+
+ public IMetrics Build()
+ {
+ return Metrics.Configure(opt =>
+ {
+ opt.Namespace = _options.Namespace;
+ opt.Service = _options.Service;
+ opt.RaiseOnEmptyMetrics = _options.RaiseOnEmptyMetrics;
+ opt.CaptureColdStart = _options.CaptureColdStart;
+ opt.DefaultDimensions = _options.DefaultDimensions;
+ });
+ }
+}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsOptions.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsOptions.cs
index 52292b1d..67ae87bc 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsOptions.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsOptions.cs
@@ -20,12 +20,12 @@ public class MetricsOptions
///
/// Gets or sets whether to throw an exception when no metrics are emitted.
///
- public bool RaiseOnEmptyMetrics { get; set; }
+ public bool? RaiseOnEmptyMetrics { get; set; }
///
/// Gets or sets whether to capture cold start metrics.
///
- public bool CaptureColdStart { get; set; }
+ public bool? CaptureColdStart { get; set; }
///
/// Gets or sets the default dimensions to be added to all metrics.
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs
index d4394a2b..adce5337 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs
@@ -201,7 +201,7 @@ public void WhenNamespaceIsDefined_AbleToRetrieveNamespace()
var metricsOutput = _consoleOut.ToString();
- var result = Metrics.GetNamespace();
+ var result = Metrics.Instance.Options.Namespace;
// Assert
Assert.Equal("dotnet-powertools-test", result);
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 1baca918..426f4147 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/DefaultDimensionsHandler.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/DefaultDimensionsHandler.cs
@@ -7,19 +7,19 @@ public class DefaultDimensionsHandler
{
public DefaultDimensionsHandler()
{
-
- Metrics.Configure(options => {
+ Metrics.Configure(options =>
+ {
// options.Namespace = "my-namespace";
// options.Service = "my-service";
// options.CaptureColdStart = true;
// options.RaiseOnEmptyMetrics = true;
options.DefaultDimensions = new Dictionary
{
- {"Environment", "Prod"},
- {"Another", "One"}
+ { "Environment", "Prod" },
+ { "Another", "One" }
};
});
-
+
// Metrics.SetDefaultDimensions(new Dictionary
// {
// {"Environment", "Prod"},
@@ -28,18 +28,42 @@ public DefaultDimensionsHandler()
// Metrics.SetNamespace("dotnet-powertools-test");
// Metrics.PushSingleMetric("SingleMetric", 1, MetricUnit.Count);
}
-
+
[Metrics(Namespace = "dotnet-powertools-test", Service = "testService", CaptureColdStart = true)]
public void Handler()
{
// Default dimensions are already set
Metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
}
-
+
[Metrics(Namespace = "dotnet-powertools-test", Service = "testService", CaptureColdStart = true)]
public void HandlerWithContext(ILambdaContext context)
{
// Default dimensions are already set and adds FunctionName dimension
Metrics.AddMetric("Memory", 10, MetricUnit.Megabytes);
}
+}
+
+public class MetricsDependencyInjectionOptionsHandler
+{
+ private readonly IMetrics _metrics;
+
+ // Allow injection of IMetrics for testing
+ public MetricsDependencyInjectionOptionsHandler(IMetrics metrics = null)
+ {
+ _metrics = metrics ?? Metrics.Configure(options =>
+ {
+ options.DefaultDimensions = new Dictionary
+ {
+ { "Environment", "Prod" },
+ { "Another", "One" }
+ };
+ });
+ }
+
+ [Metrics(Namespace = "dotnet-powertools-test", Service = "testService", CaptureColdStart = true)]
+ public void Handler()
+ {
+ _metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
+ }
}
\ 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 5b6ad783..36a5818e 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
@@ -14,9 +14,11 @@
*/
using System;
+using System.Collections.Generic;
using System.Threading.Tasks;
using Amazon.Lambda.TestUtilities;
using AWS.Lambda.Powertools.Common;
+using NSubstitute;
using Xunit;
namespace AWS.Lambda.Powertools.Metrics.Tests.Handlers;
@@ -26,38 +28,38 @@ public class FunctionHandlerTests : IDisposable
{
private readonly FunctionHandler _handler;
private readonly CustomConsoleWriter _consoleOut;
-
+
public FunctionHandlerTests()
{
_handler = new FunctionHandler();
_consoleOut = new CustomConsoleWriter();
SystemWrapper.Instance.SetOut(_consoleOut);
}
-
+
[Fact]
public async Task When_Metrics_Add_Metadata_Same_Key_Should_Ignore_Metadata()
{
// Arrange
-
-
+
+
// Act
- var exception = await Record.ExceptionAsync( () => _handler.HandleSameKey("whatever"));
-
+ var exception = await Record.ExceptionAsync(() => _handler.HandleSameKey("whatever"));
+
// Assert
Assert.Null(exception);
}
-
+
[Fact]
public async Task When_Metrics_Add_Metadata_Second_Invocation_Should_Not_Throw_Exception()
{
// Act
- var exception = await Record.ExceptionAsync( () => _handler.HandleTestSecondCall("whatever"));
+ var exception = await Record.ExceptionAsync(() => _handler.HandleTestSecondCall("whatever"));
Assert.Null(exception);
-
- exception = await Record.ExceptionAsync( () => _handler.HandleTestSecondCall("whatever"));
+
+ exception = await Record.ExceptionAsync(() => _handler.HandleTestSecondCall("whatever"));
Assert.Null(exception);
}
-
+
[Fact]
public async Task When_Metrics_Add_Metadata_FromMultipleThread_Should_Not_Throw_Exception()
{
@@ -65,7 +67,7 @@ public async Task When_Metrics_Add_Metadata_FromMultipleThread_Should_Not_Throw_
var exception = await Record.ExceptionAsync(() => _handler.HandleMultipleThreads("whatever"));
Assert.Null(exception);
}
-
+
[Fact]
public void When_LambdaContext_Should_Add_FunctioName_Dimension_CaptureColdStart()
{
@@ -74,21 +76,21 @@ public void When_LambdaContext_Should_Add_FunctioName_Dimension_CaptureColdStart
{
FunctionName = "My Function with context"
};
-
+
// Act
_handler.HandleWithLambdaContext(context);
var metricsOutput = _consoleOut.ToString();
-
+
// Assert
Assert.Contains(
"\"FunctionName\":\"My Function with context\"",
metricsOutput);
-
+
Assert.Contains(
"\"CloudWatchMetrics\":[{\"Namespace\":\"ns\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"FunctionName\"]]}]},\"Service\":\"svc\",\"FunctionName\":\"My Function with context\",\"ColdStart\":1}",
metricsOutput);
}
-
+
[Fact]
public void When_LambdaContext_And_Parameter_Should_Add_FunctioName_Dimension_CaptureColdStart()
{
@@ -97,38 +99,38 @@ public void When_LambdaContext_And_Parameter_Should_Add_FunctioName_Dimension_Ca
{
FunctionName = "My Function with context"
};
-
+
// Act
- _handler.HandleWithParamAndLambdaContext("Hello",context);
+ _handler.HandleWithParamAndLambdaContext("Hello", context);
var metricsOutput = _consoleOut.ToString();
-
+
// Assert
Assert.Contains(
"\"FunctionName\":\"My Function with context\"",
metricsOutput);
-
+
Assert.Contains(
"\"CloudWatchMetrics\":[{\"Namespace\":\"ns\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"FunctionName\"]]}]},\"Service\":\"svc\",\"FunctionName\":\"My Function with context\",\"ColdStart\":1}",
metricsOutput);
}
-
+
[Fact]
public void When_No_LambdaContext_Should_Not_Add_FunctioName_Dimension_CaptureColdStart()
{
// Act
_handler.HandleColdStartNoContext();
var metricsOutput = _consoleOut.ToString();
-
+
// Assert
Assert.DoesNotContain(
"\"FunctionName\"",
metricsOutput);
-
+
Assert.Contains(
"\"Metrics\":[{\"Name\":\"MyMetric\",\"Unit\":\"None\"}],\"Dimensions\":[[\"Service\"]]}]},\"Service\":\"svc\",\"MyMetric\":1}",
metricsOutput);
}
-
+
[Fact]
public void DefaultDimensions_AreAppliedCorrectly()
{
@@ -140,13 +142,17 @@ public void DefaultDimensions_AreAppliedCorrectly()
// 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\":[[\"Environment\",\"Another\",\"Service\"]]}]},\"Environment\":\"Prod\",\"Another\":\"One\",\"Service\":\"testService\",\"ColdStart\":1}", metricsOutput);
+ Assert.Contains(
+ "\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Environment\",\"Another\",\"Service\"]]}]},\"Environment\":\"Prod\",\"Another\":\"One\",\"Service\":\"testService\",\"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}", metricsOutput);
+ Assert.Contains(
+ "\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"SuccessfulBooking\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Environment\",\"Another\",\"Service\"]]}]},\"Environment\":\"Prod\",\"Another\":\"One\",\"Service\":\"testService\",\"SuccessfulBooking\":1}",
+ metricsOutput);
}
-
+
[Fact]
public void DefaultDimensions_AreAppliedCorrectly_WithContext_FunctionName()
{
@@ -161,11 +167,104 @@ public void DefaultDimensions_AreAppliedCorrectly_WithContext_FunctionName()
// 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\":[[\"Environment\",\"Another\",\"Service\",\"FunctionName\"]]}]},\"Environment\":\"Prod\",\"Another\":\"One\",\"Service\":\"testService\",\"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}",
+ metricsOutput);
+ }
+
+ [Fact]
+ public void Handler_WithMockedMetrics_ShouldCallAddMetric()
+ {
+ // Arrange
+ 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 MetricsDependencyInjectionOptionsHandler(metricsMock);
+
+ // Act
+ sut.Handler();
+
+ // Assert
+ metricsMock.Received(1).PushSingleMetric("ColdStart", 1, MetricUnit.Count, "dotnet-powertools-test",
+ service: "testService", Arg.Any>());
+ metricsMock.Received(1).AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
+ }
+
+ [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\":[[\"Environment\",\"Another\",\"Service\",\"FunctionName\"]]}]},\"Environment\":\"Prod\",\"Another\":\"One\",\"Service\":\"testService\",\"FunctionName\":\"My_Function_Name\",\"ColdStart\":1}", metricsOutput);
+ 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\":\"Memory\",\"Unit\":\"Megabytes\"}],\"Dimensions\":[[\"Environment\",\"Another\",\"Service\",\"FunctionName\"]]}]},\"Environment\":\"Prod\",\"Another\":\"One\",\"Service\":\"testService\",\"FunctionName\":\"My_Function_Name\",\"Memory\":10}", metricsOutput);
+ 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);
}
public void Dispose()
@@ -173,6 +272,4 @@ public void Dispose()
Metrics.ResetForTest();
MetricsAspect.ResetForTest();
}
-}
-
-
+}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/MetricsnBuilderHandler.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/MetricsnBuilderHandler.cs
new file mode 100644
index 00000000..f9fd329f
--- /dev/null
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/MetricsnBuilderHandler.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using Amazon.Lambda.Core;
+
+namespace AWS.Lambda.Powertools.Metrics.Tests.Handlers;
+
+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);
+ }
+}
\ No newline at end of file
From e8ef8dd12454a145ac3cd9747b47abaa82d6a20a Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Thu, 20 Feb 2025 16:47:01 +0000
Subject: [PATCH 4/9] feat(metrics): enhance MetricsBuilder with detailed
configuration options and improve documentation
---
.../AWS.Lambda.Powertools.Metrics/Metrics.cs | 3 ++
.../MetricsBuilder.cs | 32 +++++++++++++++++++
.../Handlers/DefaultDimensionsHandler.cs | 12 -------
3 files changed, 35 insertions(+), 12 deletions(-)
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
index 38bf94fc..c6c0e3be 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
@@ -29,6 +29,9 @@ namespace AWS.Lambda.Powertools.Metrics;
///
public class Metrics : IMetrics, IDisposable
{
+ ///
+ /// Gets or sets the instance.
+ ///
public static IMetrics Instance
{
get => Current.Value ?? new Metrics(PowertoolsConfigurations.Instance);
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsBuilder.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsBuilder.cs
index f2d030e7..ad5b516a 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsBuilder.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsBuilder.cs
@@ -17,40 +17,72 @@
namespace AWS.Lambda.Powertools.Metrics;
+///
+/// Provides a builder for configuring metrics.
+///
public class MetricsBuilder
{
private readonly MetricsOptions _options = new();
+ ///
+ /// Sets the namespace for the metrics.
+ ///
+ /// The namespace identifier.
+ /// The current instance of .
public MetricsBuilder WithNamespace(string nameSpace)
{
_options.Namespace = nameSpace;
return this;
}
+ ///
+ /// Sets the service name for the metrics.
+ ///
+ /// The service name.
+ /// The current instance of .
public MetricsBuilder WithService(string service)
{
_options.Service = service;
return this;
}
+ ///
+ /// Sets whether to raise an exception if no metrics are captured.
+ ///
+ /// If true, raises an exception when no metrics are captured.
+ /// The current instance of .
public MetricsBuilder WithRaiseOnEmptyMetrics(bool raiseOnEmptyMetrics)
{
_options.RaiseOnEmptyMetrics = raiseOnEmptyMetrics;
return this;
}
+ ///
+ /// Sets whether to capture cold start metrics.
+ ///
+ /// If true, captures cold start metrics.
+ /// The current instance of .
public MetricsBuilder WithCaptureColdStart(bool captureColdStart)
{
_options.CaptureColdStart = captureColdStart;
return this;
}
+ ///
+ /// Sets the default dimensions for the metrics.
+ ///
+ /// A dictionary of default dimensions.
+ /// The current instance of .
public MetricsBuilder WithDefaultDimensions(Dictionary defaultDimensions)
{
_options.DefaultDimensions = defaultDimensions;
return this;
}
+ ///
+ /// Builds and configures the metrics instance.
+ ///
+ /// An instance of .
public IMetrics Build()
{
return Metrics.Configure(opt =>
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 426f4147..1028f58c 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/DefaultDimensionsHandler.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/DefaultDimensionsHandler.cs
@@ -9,24 +9,12 @@ public DefaultDimensionsHandler()
{
Metrics.Configure(options =>
{
- // options.Namespace = "my-namespace";
- // options.Service = "my-service";
- // options.CaptureColdStart = true;
- // options.RaiseOnEmptyMetrics = true;
options.DefaultDimensions = new Dictionary
{
{ "Environment", "Prod" },
{ "Another", "One" }
};
});
-
- // Metrics.SetDefaultDimensions(new Dictionary
- // {
- // {"Environment", "Prod"},
- // {"Another", "One"}
- // });
- // Metrics.SetNamespace("dotnet-powertools-test");
- // Metrics.PushSingleMetric("SingleMetric", 1, MetricUnit.Count);
}
[Metrics(Namespace = "dotnet-powertools-test", Service = "testService", CaptureColdStart = true)]
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 5/9] 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 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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