Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Metrics set custom FunctionName cold start dimension #785

Merged
merged 6 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion docs/core/metrics-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<APIGatewayProxyResponse> 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<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
{
Metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
...
}
```

## AspNetCore

### Installation
Expand Down
7 changes: 5 additions & 2 deletions libraries/AWS.Lambda.Powertools.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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
6 changes: 6 additions & 0 deletions libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ void PushSingleMetric(string name, double value, MetricUnit unit, string nameSpa
/// </summary>
/// <value>The metrics options.</value>
public MetricsOptions Options { get; }

/// <summary>
/// Sets the function name.
/// </summary>
/// <param name="functionName"></param>
void SetFunctionName(string functionName);

/// <summary>
/// Captures the cold start metric.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 23 additions & 4 deletions libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ public static IMetrics Instance
Namespace = GetNamespace(),
Service = GetService(),
RaiseOnEmptyMetrics = _raiseOnEmptyMetrics,
DefaultDimensions = GetDefaultDimensions()
DefaultDimensions = GetDefaultDimensions(),
FunctionName = _functionName
};

/// <summary>
Expand Down Expand Up @@ -92,6 +93,11 @@ public static IMetrics Instance
/// Shared synchronization object
/// </summary>
private readonly object _lockObj = new();

/// <summary>
/// Function name is used for metric dimension across all metrics.
/// </summary>
private string _functionName;

/// <summary>
/// The options
Expand Down Expand Up @@ -127,9 +133,21 @@ public static IMetrics Configure(Action<MetricsOptions> configure)
if (options.DefaultDimensions != null)
SetDefaultDimensions(options.DefaultDimensions);

if (!string.IsNullOrEmpty(options.FunctionName))
Instance.SetFunctionName(options.FunctionName);

return Instance;
}

/// <summary>
/// Sets the function name.
/// </summary>
/// <param name="functionName"></param>
void IMetrics.SetFunctionName(string functionName)
{
_functionName = functionName;
}

/// <summary>
/// Creates a Metrics object that provides features to send metrics to Amazon Cloudwatch using the Embedded metric
/// format (EMF). See
Expand Down Expand Up @@ -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<string, string>();
dimensions.Add("FunctionName", context.FunctionName);
dimensions.Add("FunctionName", functionName);
}

PushSingleMetric(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ public class MetricsAttribute : Attribute
/// <value>The namespace.</value>
public string Namespace { get; set; }

/// <summary>
/// Function name is used for metric dimension across all metrics.
/// This can be also set using the environment variable <c>LAMBDA_FUNCTION_NAME</c>.
/// If not set, the function name will be automatically set to the Lambda function name.
/// </summary>
public string FunctionName { get; set; }

/// <summary>
/// Service name is used for metric dimension across all metrics.
/// This can be also set using the environment variable <c>POWERTOOLS_SERVICE_NAME</c>.
Expand Down
12 changes: 12 additions & 0 deletions libraries/src/AWS.Lambda.Powertools.Metrics/MetricsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ public MetricsBuilder WithDefaultDimensions(Dictionary<string, string> defaultDi
return this;
}

/// <summary>
/// Sets the function name for the metrics dimension.
/// </summary>
/// <param name="functionName"></param>
/// <returns></returns>
public MetricsBuilder WithFunctionName(string functionName)
{
_options.FunctionName = !string.IsNullOrWhiteSpace(functionName) ? functionName : null;
return this;
}

/// <summary>
/// Builds and configures the metrics instance.
/// </summary>
Expand All @@ -92,6 +103,7 @@ public IMetrics Build()
opt.RaiseOnEmptyMetrics = _options.RaiseOnEmptyMetrics;
opt.CaptureColdStart = _options.CaptureColdStart;
opt.DefaultDimensions = _options.DefaultDimensions;
opt.FunctionName = _options.FunctionName;
});
}
}
5 changes: 5 additions & 0 deletions libraries/src/AWS.Lambda.Powertools.Metrics/MetricsOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,9 @@ public class MetricsOptions
/// Gets or sets the default dimensions to be added to all metrics.
/// </summary>
public Dictionary<string, string> DefaultDimensions { get; set; }

/// <summary>
/// Gets or sets the function name to be used as a metric dimension.
/// </summary>
public string FunctionName { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<IMetrics>();

metricsMock.Options.Returns(new MetricsOptions
{
CaptureColdStart = true,
Namespace = "dotnet-powertools-test",
Service = "testService",
FunctionName = "My_Function_Custome_Name",
DefaultDimensions = new Dictionary<string, string>
{
{ "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<ILambdaContext>());
metricsMock.Received(1).AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
}

public void Dispose()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@ public void HandlerSingleMetricDimensions()
{
_metrics.PushSingleMetric("SuccessfulBooking", 1, MetricUnit.Count, dimensions: _metrics.Options.DefaultDimensions);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading