diff --git a/docs/core/metrics-v2.md b/docs/core/metrics-v2.md index 4854bae5..536d7f38 100644 --- a/docs/core/metrics-v2.md +++ b/docs/core/metrics-v2.md @@ -663,7 +663,7 @@ By default it will skip all previously defined dimensions including default dime Metrics.PushSingleMetric("SingleMetric", 1, MetricUnit.Count, dimensions: Metrics.DefaultDimensions ); ... ``` -=== "Default Dimensions Options / Builder patterns .cs" +=== "Default Dimensions Options / Builder patterns" ```csharp hl_lines="9-13 18" using AWS.Lambda.Powertools.Metrics; @@ -688,6 +688,54 @@ By default it will skip all previously defined dimensions including default dime ... ``` +### Cold start Function Name dimension + +In cases where you want to customize the `FunctionName` dimension in Cold Start metrics. + +This is useful where you want to maintain the same name in case of auto generated handler names (cdk, top-level statement functions, etc.) + +Example: + +=== "In decorator" + + ```csharp hl_lines="5" + using AWS.Lambda.Powertools.Metrics; + + public class Function { + + [Metrics(FunctionName = "MyFunctionName", Namespace = "ExampleApplication", Service = "Booking")] + public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + Metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count); + ... + } + ``` +=== "Configure / Builder patterns" + + ```csharp hl_lines="12" + using AWS.Lambda.Powertools.Metrics; + + public class Function { + + public Function() + { + Metrics.Configure(options => + { + options.Namespace = "dotnet-powertools-test"; + options.Service = "testService"; + options.CaptureColdStart = true; + options.FunctionName = "MyFunctionName"; + }); + } + + [Metrics] + public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + Metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count); + ... + } + ``` + ## AspNetCore ### Installation 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 02164e8a..d49c9b99 100644 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs @@ -107,6 +107,12 @@ 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); /// /// Captures the cold start metric. diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs index aad3617c..177e90a9 100644 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs @@ -75,6 +75,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 diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs index 38e405ae..ea0e0ca3 100644 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs @@ -60,7 +60,8 @@ public static IMetrics Instance Namespace = GetNamespace(), Service = GetService(), RaiseOnEmptyMetrics = _raiseOnEmptyMetrics, - DefaultDimensions = GetDefaultDimensions() + DefaultDimensions = GetDefaultDimensions(), + FunctionName = _functionName }; /// @@ -92,6 +93,11 @@ public static IMetrics Instance /// Shared synchronization object /// private readonly object _lockObj = new(); + + /// + /// Function name is used for metric dimension across all metrics. + /// + private string _functionName; /// /// The options @@ -127,9 +133,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 @@ -513,11 +531,12 @@ void IMetrics.CaptureColdStartMetric(ILambdaContext context) // bring default dimensions if exist var dimensions = Options?.DefaultDimensions; - - if (context is not null) + + var functionName = Options?.FunctionName ?? context?.FunctionName ?? ""; + if (!string.IsNullOrWhiteSpace(functionName)) { dimensions ??= new Dictionary(); - dimensions.Add("FunctionName", context.FunctionName); + dimensions.Add("FunctionName", functionName); } PushSingleMetric( 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..4f6d3c3a 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 = !string.IsNullOrWhiteSpace(functionName) ? functionName : null; + 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 b9f90994..acc66627 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs @@ -240,4 +240,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 2879cac7..254de9e9 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs @@ -341,6 +341,77 @@ 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); + } + + [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).CaptureColdStartMetric(Arg.Any()); + metricsMock.Received(1).AddMetric("SuccessfulBooking", 1, MetricUnit.Count); + } 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 eec4c65b..5aae4cdc 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/MetricsnBuilderHandler.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/MetricsnBuilderHandler.cs @@ -42,4 +42,5 @@ public void HandlerSingleMetricDimensions() { _metrics.PushSingleMetric("SuccessfulBooking", 1, MetricUnit.Count, dimensions: _metrics.Options.DefaultDimensions); } + } \ 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 ecdd94d6..03db82e9 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs @@ -321,4 +321,55 @@ public void Service_Should_Return_Null_When_Not_Set() // Assert Assert.Null(result); } + + [Fact] + public void WithFunctionName_Should_Set_FunctionName_In_Options() + { + // Arrange + var builder = new MetricsBuilder(); + var expectedFunctionName = "TestFunction"; + + // Act + var result = builder.WithFunctionName(expectedFunctionName); + var metrics = result.Build(); + + // Assert + Assert.Equal(expectedFunctionName, metrics.Options.FunctionName); + Assert.Same(builder, result); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void WithFunctionName_Should_Allow_NullOrEmpty_FunctionName(string functionName) + { + // Arrange + var builder = new MetricsBuilder(); + + // Act + var result = builder.WithFunctionName(functionName); + var metrics = result.Build(); + + // Assert + // Assert + Assert.Null(metrics.Options.FunctionName); // All invalid values should result in null + Assert.Same(builder, result); + } + + [Fact] + public void Build_Should_Preserve_FunctionName_When_Set_Through_Builder() + { + // Arrange + var builder = new MetricsBuilder() + .WithNamespace("TestNamespace") + .WithService("TestService") + .WithFunctionName("TestFunction"); + + // Act + var metrics = builder.Build(); + + // Assert + Assert.Equal("TestFunction", metrics.Options.FunctionName); + } } \ No newline at end of file