Skip to content

Commit d5593b3

Browse files
authored
Merge pull request #785 from hjgraca/feature/metrics-function-name
chore: Metrics set custom FunctionName cold start dimension
2 parents 807bead + 75ee06b commit d5593b3

File tree

12 files changed

+243
-7
lines changed

12 files changed

+243
-7
lines changed

docs/core/metrics-v2.md

+49-1
Original file line numberDiff line numberDiff line change
@@ -663,7 +663,7 @@ By default it will skip all previously defined dimensions including default dime
663663
Metrics.PushSingleMetric("SingleMetric", 1, MetricUnit.Count, dimensions: Metrics.DefaultDimensions );
664664
...
665665
```
666-
=== "Default Dimensions Options / Builder patterns .cs"
666+
=== "Default Dimensions Options / Builder patterns"
667667

668668
```csharp hl_lines="9-13 18"
669669
using AWS.Lambda.Powertools.Metrics;
@@ -688,6 +688,54 @@ By default it will skip all previously defined dimensions including default dime
688688
...
689689
```
690690

691+
### Cold start Function Name dimension
692+
693+
In cases where you want to customize the `FunctionName` dimension in Cold Start metrics.
694+
695+
This is useful where you want to maintain the same name in case of auto generated handler names (cdk, top-level statement functions, etc.)
696+
697+
Example:
698+
699+
=== "In decorator"
700+
701+
```csharp hl_lines="5"
702+
using AWS.Lambda.Powertools.Metrics;
703+
704+
public class Function {
705+
706+
[Metrics(FunctionName = "MyFunctionName", Namespace = "ExampleApplication", Service = "Booking")]
707+
public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
708+
{
709+
Metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
710+
...
711+
}
712+
```
713+
=== "Configure / Builder patterns"
714+
715+
```csharp hl_lines="12"
716+
using AWS.Lambda.Powertools.Metrics;
717+
718+
public class Function {
719+
720+
public Function()
721+
{
722+
Metrics.Configure(options =>
723+
{
724+
options.Namespace = "dotnet-powertools-test";
725+
options.Service = "testService";
726+
options.CaptureColdStart = true;
727+
options.FunctionName = "MyFunctionName";
728+
});
729+
}
730+
731+
[Metrics]
732+
public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
733+
{
734+
Metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
735+
...
736+
}
737+
```
738+
691739
## AspNetCore
692740

693741
### Installation

libraries/AWS.Lambda.Powertools.sln

+5-2
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Metri
101101
EndProject
102102
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}"
103103
EndProject
104+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Metrics", "Metrics", "{A566F2D7-F8FE-466A-8306-85F266B7E656}"
105+
EndProject
104106
Global
105107
GlobalSection(SolutionConfigurationPlatforms) = preSolution
106108
Debug|Any CPU = Debug|Any CPU
@@ -554,7 +556,6 @@ Global
554556
{3BA6251D-DE4E-4547-AAA9-25F4BA04C636} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5}
555557
{1A3AC28C-3AEE-40FE-B229-9E38BB609547} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5}
556558
{B68A0D0A-4785-48CB-864F-29E3A8ACA526} = {1CFF5568-8486-475F-81F6-06105C437528}
557-
{A422C742-2CF9-409D-BDAE-15825AB62113} = {1CFF5568-8486-475F-81F6-06105C437528}
558559
{4EC48E6A-45B5-4E25-ABBD-C23FE2BD6E1E} = {1CFF5568-8486-475F-81F6-06105C437528}
559560
{A040AED5-BBB8-4BFA-B2A5-BBD82817B8A5} = {1CFF5568-8486-475F-81F6-06105C437528}
560561
{1ECB31E8-2EF0-41E2-8C71-CB9876D207F0} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5}
@@ -592,6 +593,8 @@ Global
592593
{E71C48D2-AD56-4177-BBD7-6BB859A40C92} = {FB2C7DA3-6FCE-429D-86F9-5775D0231EC6}
593594
{CC8CFF43-DC72-464C-A42D-55E023DE8500} = {FB2C7DA3-6FCE-429D-86F9-5775D0231EC6}
594595
{A2AD98B1-2BED-4864-B573-77BE7B52FED2} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5}
595-
{F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB} = {1CFF5568-8486-475F-81F6-06105C437528}
596+
{A566F2D7-F8FE-466A-8306-85F266B7E656} = {1CFF5568-8486-475F-81F6-06105C437528}
597+
{F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB} = {A566F2D7-F8FE-466A-8306-85F266B7E656}
598+
{A422C742-2CF9-409D-BDAE-15825AB62113} = {A566F2D7-F8FE-466A-8306-85F266B7E656}
596599
EndGlobalSection
597600
EndGlobal

libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs

+6
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,12 @@ void PushSingleMetric(string name, double value, MetricUnit unit, string nameSpa
107107
/// </summary>
108108
/// <value>The metrics options.</value>
109109
public MetricsOptions Options { get; }
110+
111+
/// <summary>
112+
/// Sets the function name.
113+
/// </summary>
114+
/// <param name="functionName"></param>
115+
void SetFunctionName(string functionName);
110116

111117
/// <summary>
112118
/// Captures the cold start metric.

libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ public void Before(
7575
options.Service = trigger.Service;
7676
options.RaiseOnEmptyMetrics = trigger.IsRaiseOnEmptyMetricsSet ? trigger.RaiseOnEmptyMetrics : null;
7777
options.CaptureColdStart = trigger.IsCaptureColdStartSet ? trigger.CaptureColdStart : null;
78+
options.FunctionName = trigger.FunctionName;
7879
});
7980

8081
var eventArgs = new AspectEventArgs

libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs

+23-4
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ public static IMetrics Instance
6060
Namespace = GetNamespace(),
6161
Service = GetService(),
6262
RaiseOnEmptyMetrics = _raiseOnEmptyMetrics,
63-
DefaultDimensions = GetDefaultDimensions()
63+
DefaultDimensions = GetDefaultDimensions(),
64+
FunctionName = _functionName
6465
};
6566

6667
/// <summary>
@@ -92,6 +93,11 @@ public static IMetrics Instance
9293
/// Shared synchronization object
9394
/// </summary>
9495
private readonly object _lockObj = new();
96+
97+
/// <summary>
98+
/// Function name is used for metric dimension across all metrics.
99+
/// </summary>
100+
private string _functionName;
95101

96102
/// <summary>
97103
/// The options
@@ -127,9 +133,21 @@ public static IMetrics Configure(Action<MetricsOptions> configure)
127133
if (options.DefaultDimensions != null)
128134
SetDefaultDimensions(options.DefaultDimensions);
129135

136+
if (!string.IsNullOrEmpty(options.FunctionName))
137+
Instance.SetFunctionName(options.FunctionName);
138+
130139
return Instance;
131140
}
132141

142+
/// <summary>
143+
/// Sets the function name.
144+
/// </summary>
145+
/// <param name="functionName"></param>
146+
void IMetrics.SetFunctionName(string functionName)
147+
{
148+
_functionName = functionName;
149+
}
150+
133151
/// <summary>
134152
/// Creates a Metrics object that provides features to send metrics to Amazon Cloudwatch using the Embedded metric
135153
/// format (EMF). See
@@ -513,11 +531,12 @@ void IMetrics.CaptureColdStartMetric(ILambdaContext context)
513531

514532
// bring default dimensions if exist
515533
var dimensions = Options?.DefaultDimensions;
516-
517-
if (context is not null)
534+
535+
var functionName = Options?.FunctionName ?? context?.FunctionName ?? "";
536+
if (!string.IsNullOrWhiteSpace(functionName))
518537
{
519538
dimensions ??= new Dictionary<string, string>();
520-
dimensions.Add("FunctionName", context.FunctionName);
539+
dimensions.Add("FunctionName", functionName);
521540
}
522541

523542
PushSingleMetric(

libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs

+7
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,13 @@ public class MetricsAttribute : Attribute
112112
/// <value>The namespace.</value>
113113
public string Namespace { get; set; }
114114

115+
/// <summary>
116+
/// Function name is used for metric dimension across all metrics.
117+
/// This can be also set using the environment variable <c>LAMBDA_FUNCTION_NAME</c>.
118+
/// If not set, the function name will be automatically set to the Lambda function name.
119+
/// </summary>
120+
public string FunctionName { get; set; }
121+
115122
/// <summary>
116123
/// Service name is used for metric dimension across all metrics.
117124
/// This can be also set using the environment variable <c>POWERTOOLS_SERVICE_NAME</c>.

libraries/src/AWS.Lambda.Powertools.Metrics/MetricsBuilder.cs

+12
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,17 @@ public MetricsBuilder WithDefaultDimensions(Dictionary<string, string> defaultDi
7979
return this;
8080
}
8181

82+
/// <summary>
83+
/// Sets the function name for the metrics dimension.
84+
/// </summary>
85+
/// <param name="functionName"></param>
86+
/// <returns></returns>
87+
public MetricsBuilder WithFunctionName(string functionName)
88+
{
89+
_options.FunctionName = !string.IsNullOrWhiteSpace(functionName) ? functionName : null;
90+
return this;
91+
}
92+
8293
/// <summary>
8394
/// Builds and configures the metrics instance.
8495
/// </summary>
@@ -92,6 +103,7 @@ public IMetrics Build()
92103
opt.RaiseOnEmptyMetrics = _options.RaiseOnEmptyMetrics;
93104
opt.CaptureColdStart = _options.CaptureColdStart;
94105
opt.DefaultDimensions = _options.DefaultDimensions;
106+
opt.FunctionName = _options.FunctionName;
95107
});
96108
}
97109
}

libraries/src/AWS.Lambda.Powertools.Metrics/MetricsOptions.cs

+5
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,9 @@ public class MetricsOptions
3131
/// Gets or sets the default dimensions to be added to all metrics.
3232
/// </summary>
3333
public Dictionary<string, string> DefaultDimensions { get; set; }
34+
35+
/// <summary>
36+
/// Gets or sets the function name to be used as a metric dimension.
37+
/// </summary>
38+
public string FunctionName { get; set; }
3439
}

libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs

+12
Original file line numberDiff line numberDiff line change
@@ -240,4 +240,16 @@ public void HandleOnlyDimensionsInColdStart(ILambdaContext context)
240240
{
241241
Metrics.AddMetric("MyMetric", 1);
242242
}
243+
244+
[Metrics(Namespace = "ns", Service = "svc", CaptureColdStart = true, FunctionName = "MyFunction")]
245+
public void HandleFunctionNameWithContext(ILambdaContext context)
246+
{
247+
248+
}
249+
250+
[Metrics(Namespace = "ns", Service = "svc", CaptureColdStart = true, FunctionName = "MyFunction")]
251+
public void HandleFunctionNameNoContext()
252+
{
253+
254+
}
243255
}

libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs

+71
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,77 @@ public void Dimension_Only_Set_In_Cold_Start()
341341
"\"CloudWatchMetrics\":[{\"Namespace\":\"ns\",\"Metrics\":[{\"Name\":\"MyMetric\",\"Unit\":\"None\"}],\"Dimensions\":[[\"Service\"]]}]},\"Service\":\"svc\",\"MyMetric\":1}",
342342
metricsOutput);
343343
}
344+
345+
[Fact]
346+
public void When_Function_Name_Is_Set()
347+
{
348+
// Arrange
349+
var handler = new FunctionHandler();
350+
351+
// Act
352+
handler.HandleFunctionNameWithContext(new TestLambdaContext
353+
{
354+
FunctionName = "This_Will_Be_Overwritten"
355+
});
356+
357+
// Get the output and parse it
358+
var metricsOutput = _consoleOut.ToString();
359+
360+
// Assert cold start function name is set MyFunction
361+
Assert.Contains(
362+
"\"CloudWatchMetrics\":[{\"Namespace\":\"ns\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"FunctionName\"]]}]},\"Service\":\"svc\",\"FunctionName\":\"MyFunction\",\"ColdStart\":1}",
363+
metricsOutput);
364+
}
365+
366+
[Fact]
367+
public void When_Function_Name_Is_Set_No_Context()
368+
{
369+
// Arrange
370+
var handler = new FunctionHandler();
371+
372+
// Act
373+
handler.HandleFunctionNameNoContext();
374+
375+
// Get the output and parse it
376+
var metricsOutput = _consoleOut.ToString();
377+
378+
// Assert cold start function name is set MyFunction
379+
Assert.Contains(
380+
"\"CloudWatchMetrics\":[{\"Namespace\":\"ns\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"FunctionName\"]]}]},\"Service\":\"svc\",\"FunctionName\":\"MyFunction\",\"ColdStart\":1}",
381+
metricsOutput);
382+
}
383+
384+
[Fact]
385+
public void Handler_With_Builder_Should_Configure_FunctionName_In_Constructor_Mock()
386+
{
387+
var metricsMock = Substitute.For<IMetrics>();
388+
389+
metricsMock.Options.Returns(new MetricsOptions
390+
{
391+
CaptureColdStart = true,
392+
Namespace = "dotnet-powertools-test",
393+
Service = "testService",
394+
FunctionName = "My_Function_Custome_Name",
395+
DefaultDimensions = new Dictionary<string, string>
396+
{
397+
{ "Environment", "Prod" },
398+
{ "Another", "One" }
399+
}
400+
});
401+
402+
Metrics.UseMetricsForTests(metricsMock);
403+
404+
var sut = new MetricsnBuilderHandler(metricsMock);
405+
406+
// Act
407+
sut.Handler(new TestLambdaContext
408+
{
409+
FunctionName = "This_Will_Be_Overwritten"
410+
});
411+
412+
metricsMock.Received(1).CaptureColdStartMetric(Arg.Any<ILambdaContext>());
413+
metricsMock.Received(1).AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
414+
}
344415

345416
public void Dispose()
346417
{

libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/MetricsnBuilderHandler.cs

+1
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,5 @@ public void HandlerSingleMetricDimensions()
4242
{
4343
_metrics.PushSingleMetric("SuccessfulBooking", 1, MetricUnit.Count, dimensions: _metrics.Options.DefaultDimensions);
4444
}
45+
4546
}

libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs

+51
Original file line numberDiff line numberDiff line change
@@ -321,4 +321,55 @@ public void Service_Should_Return_Null_When_Not_Set()
321321
// Assert
322322
Assert.Null(result);
323323
}
324+
325+
[Fact]
326+
public void WithFunctionName_Should_Set_FunctionName_In_Options()
327+
{
328+
// Arrange
329+
var builder = new MetricsBuilder();
330+
var expectedFunctionName = "TestFunction";
331+
332+
// Act
333+
var result = builder.WithFunctionName(expectedFunctionName);
334+
var metrics = result.Build();
335+
336+
// Assert
337+
Assert.Equal(expectedFunctionName, metrics.Options.FunctionName);
338+
Assert.Same(builder, result);
339+
}
340+
341+
[Theory]
342+
[InlineData(null)]
343+
[InlineData("")]
344+
[InlineData(" ")]
345+
public void WithFunctionName_Should_Allow_NullOrEmpty_FunctionName(string functionName)
346+
{
347+
// Arrange
348+
var builder = new MetricsBuilder();
349+
350+
// Act
351+
var result = builder.WithFunctionName(functionName);
352+
var metrics = result.Build();
353+
354+
// Assert
355+
// Assert
356+
Assert.Null(metrics.Options.FunctionName); // All invalid values should result in null
357+
Assert.Same(builder, result);
358+
}
359+
360+
[Fact]
361+
public void Build_Should_Preserve_FunctionName_When_Set_Through_Builder()
362+
{
363+
// Arrange
364+
var builder = new MetricsBuilder()
365+
.WithNamespace("TestNamespace")
366+
.WithService("TestService")
367+
.WithFunctionName("TestFunction");
368+
369+
// Act
370+
var metrics = builder.Build();
371+
372+
// Assert
373+
Assert.Equal("TestFunction", metrics.Options.FunctionName);
374+
}
324375
}

0 commit comments

Comments
 (0)