From 97dc991c90db3a0682fbfd48dea668fe08651e12 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Tue, 25 Feb 2025 10:54:50 +0000
Subject: [PATCH] feat(metrics): add function name support for metrics
dimensions
---
libraries/AWS.Lambda.Powertools.sln | 7 +-
.../AWS.Lambda.Powertools.Metrics/IMetrics.cs | 6 ++
.../Internal/MetricsAspect.cs | 19 ++--
.../AWS.Lambda.Powertools.Metrics/Metrics.cs | 40 ++++----
.../MetricsAttribute.cs | 7 ++
.../MetricsBuilder.cs | 12 +++
.../MetricsOptions.cs | 5 +
.../Handlers/FunctionHandler.cs | 12 +++
.../Handlers/FunctionHandlerTests.cs | 91 ++++++++++++++++++-
.../Handlers/MetricsnBuilderHandler.cs | 1 +
10 files changed, 172 insertions(+), 28 deletions(-)
diff --git a/libraries/AWS.Lambda.Powertools.sln b/libraries/AWS.Lambda.Powertools.sln
index bcc1a2c9..c0dc580f 100644
--- a/libraries/AWS.Lambda.Powertools.sln
+++ b/libraries/AWS.Lambda.Powertools.sln
@@ -101,6 +101,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Metri
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Metrics.AspNetCore.Tests", "tests\AWS.Lambda.Powertools.Metrics.AspNetCore.Tests\AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj", "{F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Metrics", "Metrics", "{A566F2D7-F8FE-466A-8306-85F266B7E656}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -554,7 +556,6 @@ Global
{3BA6251D-DE4E-4547-AAA9-25F4BA04C636} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5}
{1A3AC28C-3AEE-40FE-B229-9E38BB609547} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5}
{B68A0D0A-4785-48CB-864F-29E3A8ACA526} = {1CFF5568-8486-475F-81F6-06105C437528}
- {A422C742-2CF9-409D-BDAE-15825AB62113} = {1CFF5568-8486-475F-81F6-06105C437528}
{4EC48E6A-45B5-4E25-ABBD-C23FE2BD6E1E} = {1CFF5568-8486-475F-81F6-06105C437528}
{A040AED5-BBB8-4BFA-B2A5-BBD82817B8A5} = {1CFF5568-8486-475F-81F6-06105C437528}
{1ECB31E8-2EF0-41E2-8C71-CB9876D207F0} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5}
@@ -592,6 +593,8 @@ Global
{E71C48D2-AD56-4177-BBD7-6BB859A40C92} = {FB2C7DA3-6FCE-429D-86F9-5775D0231EC6}
{CC8CFF43-DC72-464C-A42D-55E023DE8500} = {FB2C7DA3-6FCE-429D-86F9-5775D0231EC6}
{A2AD98B1-2BED-4864-B573-77BE7B52FED2} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5}
- {F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB} = {1CFF5568-8486-475F-81F6-06105C437528}
+ {A566F2D7-F8FE-466A-8306-85F266B7E656} = {1CFF5568-8486-475F-81F6-06105C437528}
+ {F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB} = {A566F2D7-F8FE-466A-8306-85F266B7E656}
+ {A422C742-2CF9-409D-BDAE-15825AB62113} = {A566F2D7-F8FE-466A-8306-85F266B7E656}
EndGlobalSection
EndGlobal
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs
index 69ef2ee3..f80e3f9b 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs
@@ -106,4 +106,10 @@ void PushSingleMetric(string name, double value, MetricUnit unit, string nameSpa
///
/// The metrics options.
public MetricsOptions Options { get; }
+
+ ///
+ /// Sets the function name.
+ ///
+ ///
+ void SetFunctionName(string functionName);
}
\ 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 1577f5fc..dae4c321 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
@@ -76,6 +76,7 @@ public void Before(
options.Service = trigger.Service;
options.RaiseOnEmptyMetrics = trigger.IsRaiseOnEmptyMetricsSet ? trigger.RaiseOnEmptyMetrics : null;
options.CaptureColdStart = trigger.IsCaptureColdStartSet ? trigger.CaptureColdStart : null;
+ options.FunctionName = trigger.FunctionName;
});
var eventArgs = new AspectEventArgs
@@ -89,16 +90,22 @@ public void Before(
Triggers = triggers
};
- if (_metricsInstance.Options.CaptureColdStart != null && _metricsInstance.Options.CaptureColdStart.Value && _isColdStart)
+ if (_metricsInstance.Options.CaptureColdStart != null && _metricsInstance.Options.CaptureColdStart.Value &&
+ _isColdStart)
{
- var defaultDimensions = _metricsInstance.Options?.DefaultDimensions;
_isColdStart = false;
- var context = GetContext(eventArgs);
-
- if (context is not null)
+ var functionName = _metricsInstance.Options?.FunctionName;
+ var defaultDimensions = _metricsInstance.Options?.DefaultDimensions;
+
+ if (string.IsNullOrWhiteSpace(functionName))
+ {
+ functionName = GetContext(eventArgs)?.FunctionName ?? "";
+ }
+
+ if (!string.IsNullOrWhiteSpace(functionName))
{
- defaultDimensions?.Add("FunctionName", context.FunctionName);
+ defaultDimensions?.Add("FunctionName", functionName);
}
_metricsInstance.PushSingleMetric(
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
index 8da23030..5d403ad4 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
@@ -17,7 +17,6 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
-using System.Runtime.CompilerServices;
using System.Threading;
using AWS.Lambda.Powertools.Common;
@@ -62,7 +61,8 @@ internal static IMetrics Instance
Namespace = GetNamespace(),
Service = GetService(),
RaiseOnEmptyMetrics = _raiseOnEmptyMetrics,
- DefaultDimensions = GetDefaultDimensions()
+ DefaultDimensions = GetDefaultDimensions(),
+ FunctionName = _functionName
};
///
@@ -94,7 +94,11 @@ internal static IMetrics Instance
// Shared synchronization object
//
private readonly object _lockObj = new();
-
+
+ ///
+ /// Function name is used for metric dimension across all metrics.
+ ///
+ private string _functionName;
///
/// Initializes a new instance of the class.
@@ -120,9 +124,21 @@ public static IMetrics Configure(Action configure)
if (options.DefaultDimensions != null)
SetDefaultDimensions(options.DefaultDimensions);
+ if (!string.IsNullOrEmpty(options.FunctionName))
+ Instance.SetFunctionName(options.FunctionName);
+
return Instance;
}
+ ///
+ /// Sets the function name.
+ ///
+ ///
+ void IMetrics.SetFunctionName(string functionName)
+ {
+ _functionName = functionName;
+ }
+
///
/// Creates a Metrics object that provides features to send metrics to Amazon Cloudwatch using the Embedded metric
/// format (EMF). See
@@ -140,7 +156,7 @@ internal Metrics(IPowertoolsConfigurations powertoolsConfigurations, string name
_context = new MetricsContext();
_raiseOnEmptyMetrics = raiseOnEmptyMetrics;
_captureColdStartEnabled = captureColdStartEnabled;
-
+
Instance = this;
_powertoolsConfigurations.SetExecutionEnvironment(this);
@@ -329,7 +345,9 @@ void IMetrics.PushSingleMetric(string name, double value, MetricUnit unit, strin
context.AddMetric(name, value, unit, resolution);
- Flush(context);
+ var emfPayload = context.Serialize();
+
+ Console.WriteLine(emfPayload);
}
@@ -438,18 +456,6 @@ public static void ClearDefaultDimensions()
}
}
- ///
- /// Flushes metrics in Embedded Metric Format (EMF) to Standard Output. In Lambda, this output is collected
- /// automatically and sent to Cloudwatch.
- ///
- /// If context is provided it is serialized instead of the global context object
- private void Flush(MetricsContext context)
- {
- var emfPayload = context.Serialize();
-
- Console.WriteLine(emfPayload);
- }
-
///
/// Pushes single metric to CloudWatch using Embedded Metric Format. This can be used to push metrics with a different
/// context.
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs
index 6413b597..3e47c1e0 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs
@@ -112,6 +112,13 @@ public class MetricsAttribute : Attribute
/// The namespace.
public string Namespace { get; set; }
+ ///
+ /// Function name is used for metric dimension across all metrics.
+ /// This can be also set using the environment variable LAMBDA_FUNCTION_NAME.
+ /// If not set, the function name will be automatically set to the Lambda function name.
+ ///
+ public string FunctionName { get; set; }
+
///
/// Service name is used for metric dimension across all metrics.
/// This can be also set using the environment variable POWERTOOLS_SERVICE_NAME.
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsBuilder.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsBuilder.cs
index ad5b516a..da16f21e 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsBuilder.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsBuilder.cs
@@ -79,6 +79,17 @@ public MetricsBuilder WithDefaultDimensions(Dictionary defaultDi
return this;
}
+ ///
+ /// Sets the function name for the metrics dimension.
+ ///
+ ///
+ ///
+ public MetricsBuilder WithFunctionName(string functionName)
+ {
+ _options.FunctionName = functionName;
+ return this;
+ }
+
///
/// Builds and configures the metrics instance.
///
@@ -92,6 +103,7 @@ public IMetrics Build()
opt.RaiseOnEmptyMetrics = _options.RaiseOnEmptyMetrics;
opt.CaptureColdStart = _options.CaptureColdStart;
opt.DefaultDimensions = _options.DefaultDimensions;
+ opt.FunctionName = _options.FunctionName;
});
}
}
\ 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 67ae87bc..71adc557 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsOptions.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsOptions.cs
@@ -31,4 +31,9 @@ public class MetricsOptions
/// Gets or sets the default dimensions to be added to all metrics.
///
public Dictionary DefaultDimensions { get; set; }
+
+ ///
+ /// Gets or sets the function name to be used as a metric dimension.
+ ///
+ public string FunctionName { get; set; }
}
\ 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 4abb6f45..1244f39b 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs
@@ -236,4 +236,16 @@ public void HandleOnlyDimensionsInColdStart(ILambdaContext context)
{
Metrics.AddMetric("MyMetric", 1);
}
+
+ [Metrics(Namespace = "ns", Service = "svc", CaptureColdStart = true, FunctionName = "MyFunction")]
+ public void HandleFunctionNameWithContext(ILambdaContext context)
+ {
+
+ }
+
+ [Metrics(Namespace = "ns", Service = "svc", CaptureColdStart = true, FunctionName = "MyFunction")]
+ public void HandleFunctionNameNoContext()
+ {
+
+ }
}
\ 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 44d4e841..4f4c6bd8 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
@@ -198,7 +198,6 @@ public void Handler_WithMockedMetrics_ShouldCallAddMetric()
Metrics.UseMetricsForTests(metricsMock);
-
var sut = new MetricsDependencyInjectionOptionsHandler(metricsMock);
// Act
@@ -206,7 +205,11 @@ public void Handler_WithMockedMetrics_ShouldCallAddMetric()
// Assert
metricsMock.Received(1).PushSingleMetric("ColdStart", 1, MetricUnit.Count, "dotnet-powertools-test",
- service: "testService", Arg.Any>());
+ service: "testService",
+ Arg.Is>(x =>
+ x.ContainsKey("Environment") && x["Environment"] == "Prod"
+ && x.ContainsKey("Another") && x["Another"] == "One"));
+
metricsMock.Received(1).AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
}
@@ -263,7 +266,50 @@ public void Handler_With_Builder_Should_Configure_In_Constructor_Mock()
});
metricsMock.Received(1).PushSingleMetric("ColdStart", 1, MetricUnit.Count, "dotnet-powertools-test",
- service: "testService", Arg.Any>());
+ service: "testService",
+ Arg.Is>(x =>
+ x.ContainsKey("FunctionName") && x["FunctionName"] == "My_Function_Name"
+ && x.ContainsKey("Environment") && x["Environment"] == "Prod"
+ && x.ContainsKey("Another") && x["Another"] == "One"));
+
+ metricsMock.Received(1).AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
+ }
+
+ [Fact]
+ public void Handler_With_Builder_Should_Configure_FunctionName_In_Constructor_Mock()
+ {
+ var metricsMock = Substitute.For();
+
+ metricsMock.Options.Returns(new MetricsOptions
+ {
+ CaptureColdStart = true,
+ Namespace = "dotnet-powertools-test",
+ Service = "testService",
+ FunctionName = "My_Function_Custome_Name",
+ DefaultDimensions = new Dictionary
+ {
+ { "Environment", "Prod" },
+ { "Another", "One" }
+ }
+ });
+
+ Metrics.UseMetricsForTests(metricsMock);
+
+ var sut = new MetricsnBuilderHandler(metricsMock);
+
+ // Act
+ sut.Handler(new TestLambdaContext
+ {
+ FunctionName = "This_Will_Be_Overwritten"
+ });
+
+ metricsMock.Received(1).PushSingleMetric("ColdStart", 1, MetricUnit.Count, "dotnet-powertools-test",
+ service: "testService",
+ Arg.Is>(x =>
+ x.ContainsKey("FunctionName") && x["FunctionName"] == "My_Function_Custome_Name"
+ && x.ContainsKey("Environment") && x["Environment"] == "Prod"
+ && x.ContainsKey("Another") && x["Another"] == "One"));
+
metricsMock.Received(1).AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
}
@@ -326,6 +372,45 @@ public void Dimension_Only_Set_In_Cold_Start()
"\"CloudWatchMetrics\":[{\"Namespace\":\"ns\",\"Metrics\":[{\"Name\":\"MyMetric\",\"Unit\":\"None\"}],\"Dimensions\":[[\"Service\"]]}]},\"Service\":\"svc\",\"MyMetric\":1}",
metricsOutput);
}
+
+ [Fact]
+ public void When_Function_Name_Is_Set()
+ {
+ // Arrange
+ var handler = new FunctionHandler();
+
+ // Act
+ handler.HandleFunctionNameWithContext(new TestLambdaContext
+ {
+ FunctionName = "This_Will_Be_Overwritten"
+ });
+
+ // Get the output and parse it
+ var metricsOutput = _consoleOut.ToString();
+
+ // Assert cold start function name is set MyFunction
+ Assert.Contains(
+ "\"CloudWatchMetrics\":[{\"Namespace\":\"ns\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"FunctionName\"]]}]},\"Service\":\"svc\",\"FunctionName\":\"MyFunction\",\"ColdStart\":1}",
+ metricsOutput);
+ }
+
+ [Fact]
+ public void When_Function_Name_Is_Set_No_Context()
+ {
+ // Arrange
+ var handler = new FunctionHandler();
+
+ // Act
+ handler.HandleFunctionNameNoContext();
+
+ // Get the output and parse it
+ var metricsOutput = _consoleOut.ToString();
+
+ // Assert cold start function name is set MyFunction
+ Assert.Contains(
+ "\"CloudWatchMetrics\":[{\"Namespace\":\"ns\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"FunctionName\"]]}]},\"Service\":\"svc\",\"FunctionName\":\"MyFunction\",\"ColdStart\":1}",
+ metricsOutput);
+ }
public void Dispose()
{
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/MetricsnBuilderHandler.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/MetricsnBuilderHandler.cs
index 82c16b9c..4c71a709 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/MetricsnBuilderHandler.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/MetricsnBuilderHandler.cs
@@ -36,4 +36,5 @@ public void HandlerSingleMetricDimensions()
{
_metrics.PushSingleMetric("SuccessfulBooking", 1, MetricUnit.Count, defaultDimensions: _metrics.Options.DefaultDimensions);
}
+
}
\ No newline at end of file