diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs
index 6f0b868a..69ef2ee3 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs
@@ -1,104 +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);
+ /// The metadata key.
+ /// The metadata value.
+ void AddMetadata(string key, object value);
///
- /// Adds metadata
+ /// Sets the default dimensions.
///
- /// Metadata key
- /// Metadata value
- void AddMetadata(string key, object value);
+ /// The default dimensions.
+ void SetDefaultDimensions(Dictionary defaultDimensions);
///
- /// Pushes a single metric with custom namespace, service and dimensions.
+ /// Sets the namespace for the metrics.
///
- /// 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 namespace.
+ void SetNamespace(string nameSpace);
///
- /// Sets the namespace
+ /// Sets the service name for the metrics.
///
- /// Metrics namespace
- void SetNamespace(string nameSpace);
+ /// The service name.
+ void SetService(string service);
+
+ ///
+ /// Sets whether to raise an event on empty metrics.
+ ///
+ /// If set to true, raises an event on empty metrics.
+ void SetRaiseOnEmptyMetrics(bool raiseOnEmptyMetrics);
///
- /// Gets the namespace
+ /// Sets whether to capture cold start metrics.
///
- /// System.String.
- string GetNamespace();
+ /// If set to true, captures cold start metrics.
+ void SetCaptureColdStart(bool captureColdStart);
///
- /// Gets the service
+ /// Pushes a single metric to the collection.
///
- /// System.String.
- string GetService();
+ /// 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);
///
- /// Serializes metrics instance
+ /// Clears the default dimensions.
///
- /// System.String.
- string Serialize();
+ void ClearDefaultDimensions();
///
- /// Flushes metrics to CloudWatch
+ /// Flushes the metrics.
///
- /// if set to true [metrics overflow].
+ /// If set to true, indicates a metrics overflow.
void Flush(bool metricsOverflow = false);
-
+
///
- /// Clears both default dimensions and dimensions lists
+ /// Gets the metrics options.
///
- void ClearDefaultDimensions();
-}
+ /// 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 a72e205e..1006d25c 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
@@ -70,13 +70,12 @@ public void Before(
var trigger = triggers.OfType().First();
- _metricsInstance ??= new Metrics(
- PowertoolsConfigurations.Instance,
- trigger.Namespace,
- trigger.Service,
- trigger.RaiseOnEmptyMetrics,
- trigger.CaptureColdStart
- );
+ _metricsInstance ??= Metrics.Configure(options => {
+ options.Namespace = trigger.Namespace;
+ options.Service = trigger.Service;
+ options.RaiseOnEmptyMetrics = trigger.IsRaiseOnEmptyMetricsSet ? trigger.RaiseOnEmptyMetrics : null;
+ options.CaptureColdStart = trigger.IsCaptureColdStartSet ? trigger.CaptureColdStart : null;
+ });
var eventArgs = new AspectEventArgs
{
@@ -89,31 +88,27 @@ 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 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 ??= new Dictionary();
+ defaultDimensions.Add("FunctionName", context.FunctionName);
+ _metricsInstance.SetDefaultDimensions(defaultDimensions);
}
_metricsInstance.PushSingleMetric(
"ColdStart",
1.0,
MetricUnit.Count,
- nameSpace,
- service,
- dimensions
+ _metricsInstance.Options?.Namespace ?? "",
+ _metricsInstance.Options?.Service ?? "",
+ defaultDimensions
);
}
}
@@ -137,7 +132,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..86823bf0 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
@@ -15,7 +15,9 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
+using System.Threading;
using AWS.Lambda.Powertools.Common;
namespace AWS.Lambda.Powertools.Metrics;
@@ -27,10 +29,30 @@ 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);
+ private set => Current.Value = value;
+ }
+
+ ///
+ 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
@@ -45,18 +67,46 @@ 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
//
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);
+
+ 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);
+
+ return Instance;
+ }
+
///
/// Creates a Metrics object that provides features to send metrics to Amazon Cloudwatch using the Embedded metric
/// format (EMF). See
@@ -70,86 +120,70 @@ 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;
+ _context = new MetricsContext();
_raiseOnEmptyMetrics = raiseOnEmptyMetrics;
_captureColdStartEnabled = captureColdStartEnabled;
- _context = InitializeContext(nameSpace, service, null);
+ 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)
+ ///
+ void IMetrics.AddMetric(string key, double value, MetricUnit unit, MetricResolution resolution)
{
- 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)
+ if (Instance != null)
{
- throw new ArgumentException(
- "'AddMetric' method requires a valid metrics value. Value must be >= 0.", nameof(value));
- }
+ if (string.IsNullOrWhiteSpace(key))
+ throw new ArgumentNullException(
+ nameof(key),
+ "'AddMetric' method requires a valid metrics key. 'Null' or empty values are not allowed.");
- lock (_lockObj)
- {
- var metrics = _context.GetMetrics();
-
- 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, resolution);
+ }
+ }
+ else
+ {
+ Debug.WriteLine(
+ $"##WARNING##: Metrics should be initialized in Handler method before calling {nameof(AddMetric)} method.");
}
}
- ///
- /// Implements interface that sets metrics namespace identifier.
- ///
- /// Metrics Namespace Identifier
+ ///
void IMetrics.SetNamespace(string nameSpace)
{
- _context.SetNamespace(nameSpace);
+ _context.SetNamespace(!string.IsNullOrWhiteSpace(nameSpace)
+ ? nameSpace
+ : GetNamespace() ?? _powertoolsConfigurations.MetricsNamespace);
}
- ///
- /// Implements interface that allows retrieval of namespace identifier.
- ///
- /// Namespace identifier
- string IMetrics.GetNamespace()
- {
- try
- {
- return _context.GetNamespace();
- }
- catch
- {
- return null;
- }
- }
///
/// Implements interface to get service name
///
/// System.String.
- string IMetrics.GetService()
+ private string GetService()
{
try
{
@@ -161,15 +195,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))
@@ -179,15 +205,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))
@@ -197,30 +215,18 @@ 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)
+ ///
+ 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));
}
- ///
- /// 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
@@ -244,52 +250,73 @@ 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
- ///
+
+ ///
void IMetrics.ClearDefaultDimensions()
{
_context.ClearDefaultDimensions();
}
- ///
- /// Serialize global context object
- ///
- /// Serialized global context object
- public string Serialize()
+ ///
+ public void SetService(string service)
{
- return _context.Serialize();
+ // 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", GetService()) }));
+ }
}
- ///
- /// 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)
+ ///
+ public void SetRaiseOnEmptyMetrics(bool raiseOnEmptyMetrics)
+ {
+ _raiseOnEmptyMetrics = raiseOnEmptyMetrics;
+ }
+
+ ///
+ public void SetCaptureColdStart(bool captureColdStart)
+ {
+ _captureColdStartEnabled = captureColdStart;
+ }
+
+ private Dictionary GetDefaultDimensions()
+ {
+ return ListToDictionary(_context.GetDefaultDimensions());
+ }
+
+ ///
+ 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.");
- using var context = InitializeContext(nameSpace, service, defaultDimensions);
- context.AddMetric(metricName, value, unit, metricResolution);
+ var context = new MetricsContext();
+ context.SetNamespace(nameSpace ?? GetNamespace());
+ context.SetService(service ?? _context.GetService());
+
+ if (defaultDimensions != null)
+ {
+ var defaultDimensionsList = DictionaryToList(defaultDimensions);
+ context.SetDefaultDimensions(defaultDimensionsList);
+ }
+
+ context.AddMetric(name, value, unit, resolution);
Flush(context);
}
+
///
/// Implementation of IDisposable interface
///
@@ -308,7 +335,7 @@ protected virtual void Dispose(bool disposing)
// Cleanup
if (disposing)
{
- _instance.Flush();
+ Instance.Flush();
}
}
@@ -318,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);
}
///
@@ -331,16 +358,23 @@ 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);
}
///
/// Retrieves namespace identifier.
///
/// Namespace identifier
- public static string GetNamespace()
+ public string GetNamespace()
{
- return _instance.GetNamespace();
+ try
+ {
+ return _context.GetNamespace() ?? _powertoolsConfigurations.MetricsNamespace;
+ }
+ catch
+ {
+ return null;
+ }
}
///
@@ -350,7 +384,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 +394,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 +403,7 @@ public static void AddMetadata(string key, object value)
/// Default Dimension List
public static void SetDefaultDimensions(Dictionary defaultDimensions)
{
- _instance.SetDefaultDimensions(defaultDimensions);
+ Instance?.SetDefaultDimensions(defaultDimensions);
}
///
@@ -377,7 +411,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.");
+ }
}
///
@@ -396,55 +438,27 @@ 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)
- {
- _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)
+ MetricResolution resolution = MetricResolution.Default)
{
- 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(name, value, unit, nameSpace, service, defaultDimensions,
+ resolution);
+ }
+ else
+ {
+ Debug.WriteLine(
+ $"##WARNING##: Metrics should be initialized in Handler method before calling {nameof(PushSingleMetric)} method.");
}
-
- context.SetDefaultDimensions(defaultDimensionsList);
-
- return context;
}
///
@@ -462,11 +476,36 @@ 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
///
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..ad5b516a
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsBuilder.cs
@@ -0,0 +1,97 @@
+/*
+ * 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;
+
+///
+/// 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 =>
+ {
+ 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
new file mode 100644
index 00000000..67ae87bc
--- /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/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..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);
@@ -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..1028f58c
--- /dev/null
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/DefaultDimensionsHandler.cs
@@ -0,0 +1,57 @@
+using System.Collections.Generic;
+using Amazon.Lambda.Core;
+
+namespace AWS.Lambda.Powertools.Metrics.Tests.Handlers;
+
+public class DefaultDimensionsHandler
+{
+ public DefaultDimensionsHandler()
+ {
+ Metrics.Configure(options =>
+ {
+ options.DefaultDimensions = new Dictionary
+ {
+ { "Environment", "Prod" },
+ { "Another", "One" }
+ };
+ });
+ }
+
+ [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/FunctionHandler.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs
index f00a8c5f..abc41d7f 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" }
});
@@ -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 3469e2e4..c34397f4 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,35 @@ 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 +64,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 +73,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(
- "\"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);
}
-
+
[Fact]
public void When_LambdaContext_And_Parameter_Should_Add_FunctioName_Dimension_CaptureColdStart()
{
@@ -97,38 +96,276 @@ 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(
- "\"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);
}
-
+
[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()
+ {
+ // 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);
+ }
+
+ [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\":[[\"Service\",\"Environment\",\"Another\",\"FunctionName\"]]}]},\"Service\":\"testService\",\"Environment\":\"Prod1\",\"Another\":\"One\",\"FunctionName\":\"My_Function_Name\",\"ColdStart\":1}",
+ metricsOutput);
+ // Assert successful Memory metrics
+ Assert.Contains(
+ "\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"SuccessfulBooking\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"Environment\",\"Another\",\"FunctionName\"]]}]},\"Service\":\"testService\",\"Environment\":\"Prod1\",\"Another\":\"One\",\"FunctionName\":\"My_Function_Name\",\"SuccessfulBooking\":1}",
+ metricsOutput);
+ }
+
+ [Fact]
+ public void Handler_With_Builder_Should_Configure_In_Constructor_Mock()
+ {
+ var metricsMock = Substitute.For();
+
+ metricsMock.Options.Returns(new MetricsOptions
+ {
+ CaptureColdStart = true,
+ Namespace = "dotnet-powertools-test",
+ Service = "testService",
+ DefaultDimensions = new Dictionary
+ {
+ { "Environment", "Prod" },
+ { "Another", "One" }
+ }
+ });
+
+ Metrics.UseMetricsForTests(metricsMock);
+
+ var sut = new MetricsnBuilderHandler(metricsMock);
+
+ // Act
+ sut.Handler(new TestLambdaContext
+ {
+ FunctionName = "My_Function_Name"
+ });
+
+ metricsMock.Received(1).PushSingleMetric("ColdStart", 1, MetricUnit.Count, "dotnet-powertools-test",
+ service: "testService", Arg.Any>());
+ metricsMock.Received(1).AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
+ }
+
+ [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);
+ }
+
+ [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);
+ }
+
+ [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()
{
Metrics.ResetForTest();
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..83cc0e89
--- /dev/null
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/MetricsnBuilderHandler.cs
@@ -0,0 +1,35 @@
+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")
+ .WithRaiseOnEmptyMetrics(true)
+ .WithDefaultDimensions(new Dictionary
+ {
+ { "Environment", "Prod1" },
+ { "Another", "One" }
+ }).Build();
+ }
+
+ [Metrics]
+ public void Handler(ILambdaContext context)
+ {
+ _metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
+ }
+
+ [Metrics]
+ public void HandlerEmpty()
+ {
+ }
+}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs
index 97aa5bf8..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
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",