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