diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f0102235..a5a12869 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -37,10 +37,10 @@ jobs:
run: dotnet build --configuration Release --no-restore /tl
- name: Test & Code Coverage
- run: dotnet test --no-restore --filter "Category!=E2E" --collect:"XPlat Code Coverage" --results-directory ./codecov --verbosity quiet
+ run: dotnet test --no-restore --filter "Category!=E2E" --collect:"XPlat Code Coverage" --results-directory ./codecov --verbosity normal
- name: Test Examples
- run: dotnet test ../examples/ --verbosity quiet
+ run: dotnet test ../examples/ --verbosity normal
- name: Codecov
uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # 5.3.1
diff --git a/.github/workflows/label_pr_on_title.yml b/.github/workflows/label_pr_on_title.yml
index c5712d75..3fd5d9ca 100644
--- a/.github/workflows/label_pr_on_title.yml
+++ b/.github/workflows/label_pr_on_title.yml
@@ -6,14 +6,12 @@ on:
types:
- completed
-permissions:
- contents: read
-
jobs:
get_pr_details:
permissions:
- id-token: write
contents: read
+ id-token: write
+ pull-requests: read
# Guardrails to only ever run if PR recording workflow was indeed
# run in a PR event and ran successfully
if: ${{ github.event.workflow_run.conclusion == 'success' }}
@@ -27,6 +25,7 @@ jobs:
permissions:
contents: read
id-token: write
+ pull-requests: write
needs: get_pr_details
runs-on: ubuntu-latest
steps:
diff --git a/.github/workflows/on_label_added.yml b/.github/workflows/on_label_added.yml
index f2f407de..af8abc5e 100644
--- a/.github/workflows/on_label_added.yml
+++ b/.github/workflows/on_label_added.yml
@@ -12,6 +12,7 @@ permissions:
jobs:
get_pr_details:
permissions:
+ contents: read
id-token: write
if: ${{ github.event.workflow_run.conclusion == 'success' }}
uses: ./.github/workflows/reusable_export_pr_details.yml
diff --git a/.github/workflows/on_merged_pr.yml b/.github/workflows/on_merged_pr.yml
index cbd6c8b1..1504b975 100644
--- a/.github/workflows/on_merged_pr.yml
+++ b/.github/workflows/on_merged_pr.yml
@@ -25,6 +25,8 @@ jobs:
permissions:
contents: read
id-token: write
+ issues: write
+ pull-requests: write
needs: get_pr_details
runs-on: ubuntu-latest
if: needs.get_pr_details.outputs.prIsMerged == 'true'
diff --git a/.github/workflows/on_opened_pr.yml b/.github/workflows/on_opened_pr.yml
index b04f6f1a..7f281bad 100644
--- a/.github/workflows/on_opened_pr.yml
+++ b/.github/workflows/on_opened_pr.yml
@@ -13,6 +13,7 @@ jobs:
get_pr_details:
permissions:
id-token: write
+ contents: read
if: ${{ github.event.workflow_run.conclusion == 'success' }}
uses: ./.github/workflows/reusable_export_pr_details.yml
with:
diff --git a/.github/workflows/reusable_export_pr_details.yml b/.github/workflows/reusable_export_pr_details.yml
index 904c7056..83de7718 100644
--- a/.github/workflows/reusable_export_pr_details.yml
+++ b/.github/workflows/reusable_export_pr_details.yml
@@ -43,6 +43,7 @@ jobs:
export_pr_details:
permissions:
id-token: write
+ contents: read
# see https://github.com/aws-powertools/powertools-lambda-python/issues/1349
if: inputs.workflow_origin == 'aws-powertools/powertools-lambda-dotnet'
runs-on: ubuntu-latest
diff --git a/README.md b/README.md
index cb79a9e6..8695f325 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
[](https://github.com/aws-powertools/powertools-lambda-dotnet/actions/workflows/build.yml)
[](https://app.codecov.io/gh/aws-powertools/powertools-lambda-dotnet)
[](https://dotnet.microsoft.com/en-us/download/dotnet/6.0)
-[](https://www.nuget.org/packages?q=AWS.Lambda.Powertools)
+[](https://www.nuget.org/packages?q=AWS.Lambda.Powertools) [](https://scorecard.dev/viewer/?uri=github.com/aws-powertools/powertools-lambda-dotnet)
[](https://discord.gg/B8zZKbbyET)
Powertools for AWS Lambda (.NET) is a developer toolkit to implement Serverless [best practices and increase developer velocity](https://docs.powertools.aws.dev/lambda-dotnet/#features).
diff --git a/docs/core/metrics-v2.md b/docs/core/metrics-v2.md
index 9bd4f481..536d7f38 100644
--- a/docs/core/metrics-v2.md
+++ b/docs/core/metrics-v2.md
@@ -16,6 +16,12 @@ These metrics can be visualized through [Amazon CloudWatch Console](https://aws.
* Ahead-of-Time compilation to native code support [AOT](https://docs.aws.amazon.com/lambda/latest/dg/dotnet-native-aot.html) from version 1.7.0
* Support for AspNetCore middleware and filters to capture metrics for HTTP requests
+## Breaking changes from V1
+
+* **`Dimensions`** outputs as an array of arrays instead of an array of objects. Example: `Dimensions: [["service", "Environment"]]` instead of `Dimensions: ["service", "Environment"]`
+* **`FunctionName`** is not added as default dimension and only to cold start metric.
+* **`Default Dimensions`** can now be included in Cold Start metrics, this is a potential breaking change if you were relying on the absence of default dimensions in Cold Start metrics when searching.
+
@@ -435,7 +441,7 @@ During metrics validation, if no metrics are provided then a warning will be log
!!! tip "Metric validation"
If metrics are provided, and any of the following criteria are not met, **`SchemaValidationException`** will be raised:
- * Maximum of 9 dimensions
+ * Maximum of 30 dimensions
* Namespace is set
* Metric units must be [supported by CloudWatch](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_MetricDatum.html)
@@ -613,7 +619,7 @@ CloudWatch EMF uses the same dimensions across all your metrics. Use **`PushSing
...
```
-By default it will skip all previously defined dimensions including default dimensions. Use default_dimensions keyword argument if you want to reuse default dimensions or specify custom dimensions from a dictionary.
+By default it will skip all previously defined dimensions including default dimensions. Use `dimensions` argument if you want to reuse default dimensions or specify custom dimensions from a dictionary.
- `Metrics.DefaultDimensions`: Reuse default dimensions when using static Metrics
- `Options.DefaultDimensions`: Reuse default dimensions when using Builder or Configure patterns
@@ -634,7 +640,7 @@ By default it will skip all previously defined dimensions including default dime
unit: MetricUnit.Count,
nameSpace: "ExampleApplication",
service: "Booking",
- defaultDimensions: new Dictionary
+ dimensions: new Dictionary
{
{"FunctionContext", "$LATEST"}
});
@@ -654,7 +660,7 @@ By default it will skip all previously defined dimensions including default dime
{
{ "Default", "SingleMetric" }
});
- Metrics.PushSingleMetric("SingleMetric", 1, MetricUnit.Count, defaultDimensions: Metrics.DefaultDimensions );
+ Metrics.PushSingleMetric("SingleMetric", 1, MetricUnit.Count, dimensions: Metrics.DefaultDimensions );
...
```
=== "Default Dimensions Options / Builder patterns"
@@ -677,7 +683,7 @@ By default it will skip all previously defined dimensions including default dime
public void HandlerSingleMetricDimensions()
{
- _metrics.PushSingleMetric("SuccessfulBooking", 1, MetricUnit.Count, defaultDimensions: _metrics.Options.DefaultDimensions);
+ _metrics.PushSingleMetric("SuccessfulBooking", 1, MetricUnit.Count, dimensions: _metrics.Options.DefaultDimensions);
}
...
```
diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs
new file mode 100644
index 00000000..87321140
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs
@@ -0,0 +1,31 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+using System;
+
+namespace AWS.Lambda.Powertools.Common;
+
+///
+public class ConsoleWrapper : IConsoleWrapper
+{
+ ///
+ public void WriteLine(string message) => Console.WriteLine(message);
+ ///
+ public void Debug(string message) => System.Diagnostics.Debug.WriteLine(message);
+ ///
+ public void Error(string message) => Console.Error.WriteLine(message);
+ ///
+ public string ReadLine() => Console.ReadLine();
+}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/IConsoleWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/IConsoleWrapper.cs
new file mode 100644
index 00000000..de75020e
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/IConsoleWrapper.cs
@@ -0,0 +1,46 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+namespace AWS.Lambda.Powertools.Common;
+
+///
+/// Wrapper for console operations to facilitate testing by abstracting system console interactions.
+///
+public interface IConsoleWrapper
+{
+ ///
+ /// Writes the specified message followed by a line terminator to the standard output stream.
+ ///
+ /// The message to write.
+ void WriteLine(string message);
+
+ ///
+ /// Writes a debug message to the trace listeners in the Debug.Listeners collection.
+ ///
+ /// The debug message to write.
+ void Debug(string message);
+
+ ///
+ /// Writes the specified error message followed by a line terminator to the standard error stream.
+ ///
+ /// The error message to write.
+ void Error(string message);
+
+ ///
+ /// Reads the next line of characters from the standard input stream.
+ ///
+ /// The next line of characters from the input stream, or null if no more lines are available.
+ string ReadLine();
+}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/ColdStartTracker.cs b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/ColdStartTracker.cs
new file mode 100644
index 00000000..aafaad26
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/ColdStartTracker.cs
@@ -0,0 +1,76 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+using Amazon.Lambda.Core;
+using Microsoft.AspNetCore.Http;
+
+namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
+
+
+///
+/// Tracks and manages cold start metrics for Lambda functions in ASP.NET Core applications.
+///
+///
+/// This class is responsible for detecting and recording the first invocation (cold start) of a Lambda function.
+/// It ensures thread-safe tracking of cold starts and proper metric capture using the provided IMetrics implementation.
+///
+internal class ColdStartTracker : IDisposable
+{
+ private readonly IMetrics _metrics;
+ private static bool _coldStart = true;
+ private static readonly object _lock = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The metrics implementation to use for capturing cold start metrics.
+ public ColdStartTracker(IMetrics metrics)
+ {
+ _metrics = metrics;
+ }
+
+ ///
+ /// Tracks the cold start of the Lambda function.
+ ///
+ /// The current HTTP context.
+ internal void TrackColdStart(HttpContext context)
+ {
+ if (!_coldStart) return;
+
+ lock (_lock)
+ {
+ if (!_coldStart) return;
+ _metrics.CaptureColdStartMetric(context.Items["LambdaContext"] as ILambdaContext);
+ _coldStart = false;
+ }
+ }
+
+ ///
+ /// Resets the cold start tracking state.
+ ///
+ internal static void ResetColdStart()
+ {
+ lock (_lock)
+ {
+ _coldStart = true;
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ ResetColdStart();
+ }
+}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsFilter.cs b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsFilter.cs
index a2c776f1..f89fd94b 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsFilter.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsFilter.cs
@@ -20,17 +20,22 @@ namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
///
/// Represents a filter that captures and records metrics for HTTP endpoints.
///
-public class MetricsFilter : IEndpointFilter
+///
+/// This filter is responsible for tracking cold starts and capturing metrics during HTTP request processing.
+/// It integrates with the ASP.NET Core endpoint routing system to inject metrics collection at the endpoint level.
+///
+///
+///
+public class MetricsFilter : IEndpointFilter, IDisposable
{
- private readonly MetricsHelper _metricsHelper;
+ private readonly ColdStartTracker _coldStartTracker;
///
/// Initializes a new instance of the class.
///
- /// The metrics instance to use for recording metrics.
public MetricsFilter(IMetrics metrics)
{
- _metricsHelper = new MetricsHelper(metrics);
+ _coldStartTracker = new ColdStartTracker(metrics);
}
///
@@ -41,17 +46,23 @@ public MetricsFilter(IMetrics metrics)
/// A task that represents the asynchronous operation, containing the result of the endpoint invocation.
public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
- var result = await next(context);
-
try
{
- await _metricsHelper.CaptureColdStartMetrics(context.HttpContext);
- return result;
+ _coldStartTracker.TrackColdStart(context.HttpContext);
}
catch
{
// ignored
- return result;
}
+
+ return await next(context);
+ }
+
+ ///
+ /// Disposes of the resources used by the filter.
+ ///
+ public void Dispose()
+ {
+ _coldStartTracker.Dispose();
}
}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsHelper.cs b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsHelper.cs
deleted file mode 100644
index 250caef8..00000000
--- a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsHelper.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed
- * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-using Amazon.Lambda.Core;
-using Microsoft.AspNetCore.Http;
-
-namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
-
-
-///
-/// Helper class for capturing and recording metrics in ASP.NET Core applications.
-///
-public class MetricsHelper
-{
- private readonly IMetrics _metrics;
- private static bool _isColdStart = true;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The metrics instance to use for recording metrics.
- public MetricsHelper(IMetrics metrics)
- {
- _metrics = metrics;
- }
-
- ///
- /// Captures cold start metrics for the given HTTP context.
- ///
- /// The HTTP context.
- /// A task that represents the asynchronous operation.
- public Task CaptureColdStartMetrics(HttpContext context)
- {
- if (_metrics.Options.CaptureColdStart == null || !_metrics.Options.CaptureColdStart.Value || !_isColdStart)
- return Task.CompletedTask;
-
- var defaultDimensions = _metrics.Options.DefaultDimensions;
- lock (_metrics)
- {
- _isColdStart = false;
- }
-
- if (context.Items["LambdaContext"] is ILambdaContext lambdaContext)
- {
- defaultDimensions?.Add("FunctionName", lambdaContext.FunctionName);
- _metrics.SetDefaultDimensions(defaultDimensions);
- }
-
- _metrics.PushSingleMetric(
- "ColdStart",
- 1.0,
- MetricUnit.Count,
- _metrics.Options.Namespace,
- _metrics.Options.Service,
- defaultDimensions
- );
- return Task.CompletedTask;
- }
-
- ///
- /// Resets the cold start flag for testing purposes.
- ///
- internal static void ResetColdStart()
- {
- _isColdStart = true;
- }
-}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsMiddlewareExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsMiddlewareExtensions.cs
index 05e0d3f9..7515c1b5 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsMiddlewareExtensions.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics.AspNetCore/Http/MetricsMiddlewareExtensions.cs
@@ -24,18 +24,27 @@ namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
public static class MetricsMiddlewareExtensions
{
///
- /// Adds middleware to capture and record metrics for HTTP requests.
+ /// Adds middleware to capture and record metrics for HTTP requests, including cold start tracking.
///
- /// The application builder.
+ /// The application builder instance used to configure the request pipeline.
/// The application builder with the metrics middleware added.
+ ///
+ /// This middleware tracks cold starts and captures request metrics. To use this middleware, ensure you have registered
+ /// the required services using builder.Services.AddSingleton<IMetrics>()
in your service configuration.
+ ///
+ ///
+ ///
+ /// app.UseMetrics();
+ ///
+ ///
public static IApplicationBuilder UseMetrics(this IApplicationBuilder app)
{
return app.Use(async (context, next) =>
{
var metrics = context.RequestServices.GetRequiredService();
- var metricsHelper = new MetricsHelper(metrics);
- await metricsHelper.CaptureColdStartMetrics(context);
+ using var metricsHelper = new ColdStartTracker(metrics);
+ metricsHelper.TrackColdStart(context);
await next();
});
}
-}
\ No newline at end of file
+}
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs
index f80e3f9b..d49c9b99 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs
@@ -14,6 +14,7 @@
*/
using System.Collections.Generic;
+using Amazon.Lambda.Core;
namespace AWS.Lambda.Powertools.Metrics;
@@ -85,10 +86,10 @@ void AddMetric(string key, double value, MetricUnit unit = MetricUnit.None,
/// The metric unit.
/// The namespace.
/// The service name.
- /// The default dimensions.
+ /// The default dimensions.
/// The metric resolution.
void PushSingleMetric(string name, double value, MetricUnit unit, string nameSpace = null, string service = null,
- Dictionary defaultDimensions = null, MetricResolution resolution = MetricResolution.Default);
+ Dictionary dimensions = null, MetricResolution resolution = MetricResolution.Default);
///
/// Clears the default dimensions.
@@ -112,4 +113,10 @@ void PushSingleMetric(string name, double value, MetricUnit unit, string nameSpa
///
///
void SetFunctionName(string functionName);
+
+ ///
+ /// Captures the cold start metric.
+ ///
+ ///
+ void CaptureColdStartMetric(ILambdaContext context);
}
\ 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 dae4c321..177e90a9 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs
@@ -70,8 +70,7 @@ public void Before(
var trigger = triggers.OfType().First();
- _metricsInstance ??= Metrics.Configure(options =>
- {
+ _metricsInstance ??= Metrics.Configure(options => {
options.Namespace = trigger.Namespace;
options.Service = trigger.Service;
options.RaiseOnEmptyMetrics = trigger.IsRaiseOnEmptyMetricsSet ? trigger.RaiseOnEmptyMetrics : null;
@@ -90,32 +89,10 @@ public void Before(
Triggers = triggers
};
- if (_metricsInstance.Options.CaptureColdStart != null && _metricsInstance.Options.CaptureColdStart.Value &&
- _isColdStart)
+ if (_isColdStart)
{
+ _metricsInstance.CaptureColdStartMetric(GetContext(eventArgs));
_isColdStart = false;
-
- 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", functionName);
- }
-
- _metricsInstance.PushSingleMetric(
- "ColdStart",
- 1.0,
- MetricUnit.Count,
- _metricsInstance.Options?.Namespace ?? "",
- _metricsInstance.Options?.Service ?? "",
- defaultDimensions
- );
}
}
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/InternalsVisibleTo.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/InternalsVisibleTo.cs
index c13381c0..a1b53257 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/InternalsVisibleTo.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/InternalsVisibleTo.cs
@@ -16,4 +16,5 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Metrics.Tests")]
+[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Metrics.AspNetCore")]
[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Metrics.AspNetCore.Tests")]
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
index 5d403ad4..8eb4c1ce 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
@@ -15,9 +15,8 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Linq;
-using System.Threading;
+using Amazon.Lambda.Core;
using AWS.Lambda.Powertools.Common;
namespace AWS.Lambda.Powertools.Metrics;
@@ -32,12 +31,12 @@ public class Metrics : IMetrics, IDisposable
///
/// Gets or sets the instance.
///
- internal static IMetrics Instance
+ public static IMetrics Instance
{
- get => Current.Value ?? new Metrics(PowertoolsConfigurations.Instance);
- private set => Current.Value = value;
+ get => _instance ?? new Metrics(PowertoolsConfigurations.Instance, consoleWrapper: new ConsoleWrapper());
+ private set => _instance = value;
}
-
+
///
/// Gets DefaultDimensions
///
@@ -54,7 +53,7 @@ internal static IMetrics Instance
public static string Service => Instance.Options.Service;
///
- public MetricsOptions Options =>
+ public MetricsOptions Options => _options ??
new()
{
CaptureColdStart = _captureColdStartEnabled,
@@ -68,7 +67,7 @@ internal static IMetrics Instance
///
/// The instance
///
- private static readonly AsyncLocal Current = new();
+ private static IMetrics _instance;
///
/// The context
@@ -90,9 +89,9 @@ internal static IMetrics Instance
///
private bool _captureColdStartEnabled;
- //
- // Shared synchronization object
- //
+ ///
+ /// Shared synchronization object
+ ///
private readonly object _lockObj = new();
///
@@ -100,6 +99,16 @@ internal static IMetrics Instance
///
private string _functionName;
+ ///
+ /// The options
+ ///
+ private readonly MetricsOptions _options;
+
+ ///
+ /// The console wrapper for console output
+ ///
+ private readonly IConsoleWrapper _consoleWrapper;
+
///
/// Initializes a new instance of the class.
///
@@ -149,14 +158,18 @@ void IMetrics.SetFunctionName(string functionName)
/// Metrics Service Name
/// Instructs metrics validation to throw exception if no metrics are provided
/// Instructs metrics capturing the ColdStart is enabled
+ /// For console output
+ /// MetricsOptions
internal Metrics(IPowertoolsConfigurations powertoolsConfigurations, string nameSpace = null, string service = null,
- bool raiseOnEmptyMetrics = false, bool captureColdStartEnabled = false)
+ bool raiseOnEmptyMetrics = false, bool captureColdStartEnabled = false, IConsoleWrapper consoleWrapper = null, MetricsOptions options = null)
{
_powertoolsConfigurations = powertoolsConfigurations;
+ _consoleWrapper = consoleWrapper;
_context = new MetricsContext();
_raiseOnEmptyMetrics = raiseOnEmptyMetrics;
_captureColdStartEnabled = captureColdStartEnabled;
-
+ _options = options;
+
Instance = this;
_powertoolsConfigurations.SetExecutionEnvironment(this);
@@ -197,7 +210,7 @@ void IMetrics.AddMetric(string key, double value, MetricUnit unit, MetricResolut
}
else
{
- Debug.WriteLine(
+ _consoleWrapper.Debug(
$"##WARNING##: Metrics should be initialized in Handler method before calling {nameof(AddMetric)} method.");
}
}
@@ -269,7 +282,7 @@ void IMetrics.Flush(bool metricsOverflow)
{
var emfPayload = _context.Serialize();
- Console.WriteLine(emfPayload);
+ _consoleWrapper.WriteLine(emfPayload);
_context.ClearMetrics();
@@ -278,7 +291,7 @@ void IMetrics.Flush(bool metricsOverflow)
else
{
if (!_captureColdStartEnabled)
- Console.WriteLine(
+ _consoleWrapper.WriteLine(
"##User-WARNING## No application metrics to publish. The cold-start metric may be published if enabled. If application metrics should never be empty, consider using 'RaiseOnEmptyMetrics = true'");
}
}
@@ -327,7 +340,7 @@ private Dictionary GetDefaultDimensions()
///
void IMetrics.PushSingleMetric(string name, double value, MetricUnit unit, string nameSpace,
- string service, Dictionary defaultDimensions, MetricResolution resolution)
+ string service, Dictionary dimensions, MetricResolution resolution)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentNullException(nameof(name),
@@ -337,17 +350,15 @@ void IMetrics.PushSingleMetric(string name, double value, MetricUnit unit, strin
context.SetNamespace(nameSpace ?? GetNamespace());
context.SetService(service ?? _context.GetService());
- if (defaultDimensions != null)
+ if (dimensions != null)
{
- var defaultDimensionsList = DictionaryToList(defaultDimensions);
- context.SetDefaultDimensions(defaultDimensionsList);
+ var dimensionsList = DictionaryToList(dimensions);
+ context.AddDimensions(dimensionsList);
}
context.AddMetric(name, value, unit, resolution);
- var emfPayload = context.Serialize();
-
- Console.WriteLine(emfPayload);
+ Flush(context);
}
@@ -437,7 +448,7 @@ public static void AddMetadata(string key, object value)
/// Default Dimension List
public static void SetDefaultDimensions(Dictionary defaultDimensions)
{
- Instance?.SetDefaultDimensions(defaultDimensions);
+ Instance.SetDefaultDimensions(defaultDimensions);
}
///
@@ -445,15 +456,19 @@ public static void SetDefaultDimensions(Dictionary defaultDimens
///
public static void ClearDefaultDimensions()
{
- if (Instance != null)
- {
- Instance.ClearDefaultDimensions();
- }
- else
- {
- Debug.WriteLine(
- $"##WARNING##: Metrics should be initialized in Handler method before calling {nameof(ClearDefaultDimensions)} method.");
- }
+ Instance.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();
+
+ _consoleWrapper.WriteLine(emfPayload);
}
///
@@ -465,22 +480,14 @@ public static void ClearDefaultDimensions()
/// Metric Unit
/// Metric Namespace
/// Service Name
- /// Default dimensions list
+ /// Default dimensions list
/// Metrics resolution
public static void PushSingleMetric(string name, double value, MetricUnit unit, string nameSpace = null,
- string service = null, Dictionary defaultDimensions = null,
+ string service = null, Dictionary dimensions = null,
MetricResolution resolution = MetricResolution.Default)
{
- if (Instance != null)
- {
- Instance.PushSingleMetric(name, value, unit, nameSpace, service, defaultDimensions,
- resolution);
- }
- else
- {
- Debug.WriteLine(
- $"##WARNING##: Metrics should be initialized in Handler method before calling {nameof(PushSingleMetric)} method.");
- }
+ Instance.PushSingleMetric(name, value, unit, nameSpace, service, dimensions,
+ resolution);
}
///
@@ -490,12 +497,12 @@ public static void PushSingleMetric(string name, double value, MetricUnit unit,
/// Default dimensions list
private List DictionaryToList(Dictionary defaultDimensions)
{
- var defaultDimensionsList = new List();
+ var dimensionsList = new List();
if (defaultDimensions != null)
foreach (var item in defaultDimensions)
- defaultDimensionsList.Add(new DimensionSet(item.Key, item.Value));
+ dimensionsList.Add(new DimensionSet(item.Key, item.Value));
- return defaultDimensionsList;
+ return dimensionsList;
}
private Dictionary ListToDictionary(List dimensions)
@@ -509,10 +516,37 @@ private Dictionary ListToDictionary(List dimension
}
catch (Exception e)
{
- Debug.WriteLine("Error converting list to dictionary: " + e.Message);
+ _consoleWrapper.Debug("Error converting list to dictionary: " + e.Message);
return dictionary;
}
}
+
+ ///
+ /// Captures the cold start metric.
+ ///
+ /// The ILambdaContext.
+ void IMetrics.CaptureColdStartMetric(ILambdaContext context)
+ {
+ if (Options.CaptureColdStart == null || !Options.CaptureColdStart.Value) return;
+
+ // bring default dimensions if exist
+ var dimensions = Options?.DefaultDimensions;
+
+ if (context is not null)
+ {
+ dimensions ??= new Dictionary();
+ dimensions.Add("FunctionName", context.FunctionName);
+ }
+
+ PushSingleMetric(
+ "ColdStart",
+ 1.0,
+ MetricUnit.Count,
+ Options?.Namespace ?? "",
+ Options?.Service ?? "",
+ dimensions
+ );
+ }
///
/// Helper method for testing purposes. Clears static instance between test execution
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricsContext.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricsContext.cs
index 8e886a90..759cdb9e 100644
--- a/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricsContext.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricsContext.cs
@@ -135,6 +135,18 @@ public void AddDimension(string key, string value)
_rootNode.AWS.AddDimensionSet(new DimensionSet(key, value));
}
+ ///
+ /// Adds new dimensions to memory
+ ///
+ /// List of dimensions
+ public void AddDimensions(List dimensions)
+ {
+ foreach (var dimension in dimensions)
+ {
+ _rootNode.AWS.AddDimensionSet(dimension);
+ }
+ }
+
///
/// Sets default dimensions list
///
diff --git a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs
new file mode 100644
index 00000000..6395f79a
--- /dev/null
+++ b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs
@@ -0,0 +1,53 @@
+using System;
+using System.IO;
+using Xunit;
+
+namespace AWS.Lambda.Powertools.Common.Tests;
+
+public class ConsoleWrapperTests
+{
+ [Fact]
+ public void WriteLine_Should_Write_To_Console()
+ {
+ // Arrange
+ var consoleWrapper = new ConsoleWrapper();
+ var writer = new StringWriter();
+ Console.SetOut(writer);
+
+ // Act
+ consoleWrapper.WriteLine("test message");
+
+ // Assert
+ Assert.Equal($"test message{Environment.NewLine}", writer.ToString());
+ }
+
+ [Fact]
+ public void Error_Should_Write_To_Error_Console()
+ {
+ // Arrange
+ var consoleWrapper = new ConsoleWrapper();
+ var writer = new StringWriter();
+ Console.SetError(writer);
+
+ // Act
+ consoleWrapper.Error("error message");
+
+ // Assert
+ Assert.Equal($"error message{Environment.NewLine}", writer.ToString());
+ }
+
+ [Fact]
+ public void ReadLine_Should_Read_From_Console()
+ {
+ // Arrange
+ var consoleWrapper = new ConsoleWrapper();
+ var reader = new StringReader("input text");
+ Console.SetIn(reader);
+
+ // Act
+ var result = consoleWrapper.ReadLine();
+
+ // Assert
+ Assert.Equal("input text", result);
+ }
+}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj
index d820a783..15ac1312 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests.csproj
@@ -13,6 +13,8 @@
+
+
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsEndpointExtensionsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsEndpointExtensionsTests.cs
new file mode 100644
index 00000000..c5ee7c2c
--- /dev/null
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsEndpointExtensionsTests.cs
@@ -0,0 +1,164 @@
+using Amazon.Lambda.TestUtilities;
+using AWS.Lambda.Powertools.Common;
+using AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using NSubstitute;
+using Xunit;
+
+namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Tests;
+
+[Collection("Metrics")]
+public class MetricsEndpointExtensionsTests : IDisposable
+{
+ [Fact]
+ public async Task When_WithMetrics_Should_Add_ColdStart()
+ {
+ // Arrange
+ var options = new MetricsOptions
+ {
+ CaptureColdStart = true,
+ Namespace = "TestNamespace",
+ Service = "TestService"
+ };
+
+ var conf = Substitute.For();
+ var consoleWrapper = Substitute.For();
+ var metrics = new Metrics(conf, consoleWrapper: consoleWrapper, options: options);
+ var builder = WebApplication.CreateBuilder();
+ builder.Services.AddSingleton(metrics);
+ builder.WebHost.UseTestServer();
+
+ var app = builder.Build();
+
+ app.MapGet("/test", () => Results.Ok(new { success = true })).WithMetrics();
+
+ await app.StartAsync();
+ var client = app.GetTestClient();
+
+ // Act
+ var response = await client.GetAsync("/test");
+
+ // Assert
+ Assert.Equal(200, (int)response.StatusCode);
+
+ // Assert metrics calls
+ consoleWrapper.Received(1).WriteLine(
+ Arg.Is(s => s.Contains("CloudWatchMetrics\":[{\"Namespace\":\"TestNamespace\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[]]}]},\"ColdStart\":1}"))
+ );
+
+
+ await app.StopAsync();
+ }
+
+ [Fact]
+ public async Task When_WithMetrics_Should_Add_ColdStart_Dimensions()
+ {
+ // Arrange
+ var options = new MetricsOptions
+ {
+ CaptureColdStart = true,
+ Namespace = "TestNamespace",
+ Service = "TestService"
+ };
+
+ var conf = Substitute.For();
+ var consoleWrapper = Substitute.For();
+ var metrics = new Metrics(conf, consoleWrapper: consoleWrapper, options: options);
+
+ var builder = WebApplication.CreateBuilder();
+ builder.Services.AddSingleton(metrics);
+ builder.WebHost.UseTestServer();
+
+
+ var app = builder.Build();
+ app.Use(async (context, next) =>
+ {
+ var lambdaContext = new TestLambdaContext
+ {
+ FunctionName = "TestFunction"
+ };
+ context.Items["LambdaContext"] = lambdaContext;
+ await next();
+ });
+
+ app.MapGet("/test", () => Results.Ok(new { success = true })).WithMetrics();
+
+ await app.StartAsync();
+ var client = app.GetTestClient();
+
+ // Act
+ var response = await client.GetAsync("/test");
+
+ // Assert
+ Assert.Equal(200, (int)response.StatusCode);
+
+ // Assert metrics calls
+ consoleWrapper.Received(1).WriteLine(
+ Arg.Is(s => s.Contains("CloudWatchMetrics\":[{\"Namespace\":\"TestNamespace\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"FunctionName\"]]}]},\"FunctionName\":\"TestFunction\",\"ColdStart\":1}"))
+ );
+
+ await app.StopAsync();
+ }
+
+ [Fact]
+ public async Task When_WithMetrics_Should_Add_ColdStart_Default_Dimensions()
+ {
+ // Arrange
+ var options = new MetricsOptions
+ {
+ CaptureColdStart = true,
+ Namespace = "TestNamespace",
+ Service = "TestService",
+ DefaultDimensions = new Dictionary
+ {
+ { "Environment", "Prod" }
+ }
+ };
+
+ var conf = Substitute.For();
+ var consoleWrapper = Substitute.For();
+ var metrics = new Metrics(conf, consoleWrapper: consoleWrapper, options: options);
+
+ var builder = WebApplication.CreateBuilder();
+ builder.Services.AddSingleton(metrics);
+ builder.WebHost.UseTestServer();
+
+
+ var app = builder.Build();
+ app.Use(async (context, next) =>
+ {
+ var lambdaContext = new TestLambdaContext
+ {
+ FunctionName = "TestFunction"
+ };
+ context.Items["LambdaContext"] = lambdaContext;
+ await next();
+ });
+
+ app.MapGet("/test", () => Results.Ok(new { success = true })).WithMetrics();
+
+ await app.StartAsync();
+ var client = app.GetTestClient();
+
+ // Act
+ var response = await client.GetAsync("/test");
+
+ // Assert
+ Assert.Equal(200, (int)response.StatusCode);
+
+ // Assert metrics calls
+ consoleWrapper.Received(1).WriteLine(
+ Arg.Is(s => s.Contains("CloudWatchMetrics\":[{\"Namespace\":\"TestNamespace\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Environment\",\"FunctionName\"]]}]},\"Environment\":\"Prod\",\"FunctionName\":\"TestFunction\",\"ColdStart\":1}"))
+ );
+
+ await app.StopAsync();
+ }
+
+ public void Dispose()
+ {
+ ColdStartTracker.ResetColdStart();
+ }
+}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsFilterTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsFilterTests.cs
index efa36561..9951034a 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsFilterTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsFilterTests.cs
@@ -6,66 +6,26 @@
namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Tests;
-[Collection("Sequential")]
+[Collection("Metrics")]
public class MetricsFilterTests : IDisposable
{
- public void Dispose()
- {
- MetricsHelper.ResetColdStart();
- MetricsAspect.ResetForTest();
- }
-
private readonly IMetrics _metrics;
private readonly EndpointFilterInvocationContext _context;
- private readonly ILambdaContext _lambdaContext;
public MetricsFilterTests()
{
- MetricsHelper.ResetColdStart(); // Reset before each test
+ ColdStartTracker.ResetColdStart(); // Reset before each test
_metrics = Substitute.For();
_context = Substitute.For();
- _lambdaContext = Substitute.For();
+ var lambdaContext = Substitute.For();
var httpContext = new DefaultHttpContext();
- httpContext.Items["LambdaContext"] = _lambdaContext;
+ httpContext.Items["LambdaContext"] = lambdaContext;
_context.HttpContext.Returns(httpContext);
}
[Fact]
- public async Task InvokeAsync_WhenColdStartEnabled_RecordsColdStartMetric()
- {
- // Arrange
- var options = new MetricsOptions
- {
- CaptureColdStart = true,
- Namespace = "TestNamespace",
- Service = "TestService",
- DefaultDimensions = new Dictionary()
- };
-
- _metrics.Options.Returns(options);
- _lambdaContext.FunctionName.Returns("TestFunction");
-
- var filter = new MetricsFilter(_metrics);
- var next = new EndpointFilterDelegate(_ => ValueTask.FromResult("result"));
-
- // Act
- var result = await filter.InvokeAsync(_context, next);
-
- // Assert
- _metrics.Received(1).PushSingleMetric(
- "ColdStart",
- 1.0,
- MetricUnit.Count,
- "TestNamespace",
- "TestService",
- Arg.Any>()
- );
- Assert.Equal("result", result);
- }
-
- [Fact]
- public async Task InvokeAsync_WhenColdStartDisabled_DoesNotRecordMetric()
+ public async Task InvokeAsync_Second_Call_DoesNotRecord_ColdStart_Metric()
{
// Arrange
var options = new MetricsOptions { CaptureColdStart = false };
@@ -75,17 +35,11 @@ public async Task InvokeAsync_WhenColdStartDisabled_DoesNotRecordMetric()
var next = new EndpointFilterDelegate(_ => ValueTask.FromResult("result"));
// Act
+ _ = await filter.InvokeAsync(_context, next);
var result = await filter.InvokeAsync(_context, next);
// Assert
- _metrics.DidNotReceive().PushSingleMetric(
- Arg.Any(),
- Arg.Any(),
- Arg.Any(),
- Arg.Any(),
- Arg.Any(),
- Arg.Any>()
- );
+ _metrics.Received(1).CaptureColdStartMetric(Arg.Any() );
Assert.Equal("result", result);
}
@@ -114,4 +68,9 @@ public async Task InvokeAsync_ShouldCallNextAndContinue()
Assert.True(called);
Assert.Equal("result", result);
}
+
+ public void Dispose()
+ {
+ ColdStartTracker.ResetColdStart();
+ }
}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsHelperTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsHelperTests.cs
deleted file mode 100644
index dfd27e58..00000000
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsHelperTests.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-using System.Reflection;
-using AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Routing;
-using Microsoft.AspNetCore.Routing.Patterns;
-using NSubstitute;
-using Xunit;
-
-namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Tests;
-
-[Collection("Sequential")]
-public class MetricsHelperTests : IDisposable
-{
- public void Dispose()
- {
- MetricsHelper.ResetColdStart();
- MetricsAspect.ResetForTest();
- }
-
- [Fact]
- public async Task CaptureColdStartMetrics_WhenEnabled_ShouldPushMetric()
- {
- // Arrange
- var metrics = Substitute.For();
- metrics.Options.Returns(new MetricsOptions
- {
- CaptureColdStart = true,
- Namespace = "TestNamespace",
- Service = "TestService"
- });
-
- var context = new DefaultHttpContext();
- var helper = new MetricsHelper(metrics);
-
- // Act
- await helper.CaptureColdStartMetrics(context);
-
- // Assert
- metrics.Received(1).PushSingleMetric(
- Arg.Is(s => s == "ColdStart"),
- Arg.Is(d => d == 1.0),
- Arg.Is(u => u == MetricUnit.Count),
- Arg.Is(s => s == "TestNamespace"),
- Arg.Is(s => s == "TestService"),
- Arg.Any>()
- );
- }
-
- [Fact]
- public async Task CaptureColdStartMetrics_WhenDisabled_ShouldNotPushMetric()
- {
- // Arrange
- var metrics = Substitute.For();
- metrics.Options.Returns(new MetricsOptions { CaptureColdStart = false });
-
- var context = new DefaultHttpContext();
- var helper = new MetricsHelper(metrics);
-
- // Act
- await helper.CaptureColdStartMetrics(context);
-
- // Assert
- metrics.DidNotReceive().PushSingleMetric(
- Arg.Any(),
- Arg.Any(),
- Arg.Any(),
- Arg.Any(),
- Arg.Any(),
- Arg.Any>()
- );
- }
-}
-
-public static class EndpointFilterInvocationContextHelper
-{
- public static EndpointFilterInvocationContext Create(HttpContext httpContext, object[] arguments)
- {
- var endpoint = new RouteEndpoint(
- c => Task.CompletedTask,
- RoutePatternFactory.Parse("/"),
- 0,
- EndpointMetadataCollection.Empty,
- "test");
-
- var constructor = typeof(EndpointFilterInvocationContext)
- .GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)
- .First();
-
- return (EndpointFilterInvocationContext)constructor.Invoke(new object[] { httpContext, endpoint, arguments });
- }
-}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsMiddlewareExtensionsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsMiddlewareExtensionsTests.cs
index d972a02f..a9510eaa 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsMiddlewareExtensionsTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.AspNetCore.Tests/MetricsMiddlewareExtensionsTests.cs
@@ -1,102 +1,105 @@
+using Amazon.Lambda.TestUtilities;
+using AWS.Lambda.Powertools.Common;
using AWS.Lambda.Powertools.Metrics.AspNetCore.Http;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using Xunit;
namespace AWS.Lambda.Powertools.Metrics.AspNetCore.Tests;
-[Collection("Sequential")]
+[Collection("Metrics")]
public class MetricsMiddlewareExtensionsTests : IDisposable
{
- public MetricsMiddlewareExtensionsTests()
- {
- MetricsHelper.ResetColdStart();
- MetricsAspect.ResetForTest();
- }
-
- public void Dispose()
- {
- MetricsHelper.ResetColdStart();
- MetricsAspect.ResetForTest();
- }
-
[Fact]
- public async Task UseMetrics_ShouldCaptureColdStart_WhenEnabled()
+ public async Task When_UseMetrics_Should_Add_ColdStart()
{
// Arrange
- var metrics = Substitute.For();
- metrics.Options.Returns(new MetricsOptions
+ var options = new MetricsOptions
{
CaptureColdStart = true,
Namespace = "TestNamespace",
Service = "TestService"
- });
+ };
+
+ var conf = Substitute.For();
+ var consoleWrapper = Substitute.For();
+ var metrics = new Metrics(conf, consoleWrapper: consoleWrapper, options: options);
- var services = new ServiceCollection();
- services.AddSingleton(metrics);
- var serviceProvider = services.BuildServiceProvider();
+ var builder = WebApplication.CreateBuilder();
+ builder.Services.AddSingleton(metrics);
+ builder.WebHost.UseTestServer();
- var context = new DefaultHttpContext
- {
- RequestServices = serviceProvider
- };
+ var app = builder.Build();
+ app.UseMetrics();
+ app.MapGet("/test", () => Results.Ok());
- var appBuilder = new ApplicationBuilder(serviceProvider);
- appBuilder.UseMetrics();
- var app = appBuilder.Build();
+ await app.StartAsync();
+ var client = app.GetTestClient();
// Act
- await app.Invoke(context);
+ var response = await client.GetAsync("/test");
// Assert
- metrics.Received(1).PushSingleMetric(
- Arg.Is(s => s == "ColdStart"),
- Arg.Is(d => d == 1.0),
- Arg.Is(u => u == MetricUnit.Count),
- Arg.Is(s => s == "TestNamespace"),
- Arg.Is(s => s == "TestService"),
- Arg.Any>()
+ Assert.Equal(200, (int)response.StatusCode);
+ consoleWrapper.Received(1).WriteLine(
+ Arg.Is(s => s.Contains("CloudWatchMetrics\":[{\"Namespace\":\"TestNamespace\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[]]}]},\"ColdStart\":1}"))
);
+
+ await app.StopAsync();
}
[Fact]
- public async Task UseMetrics_ShouldNotCaptureColdStart_WhenDisabled()
+ public async Task When_UseMetrics_Should_Add_ColdStart_With_LambdaContext()
{
// Arrange
- var metrics = Substitute.For();
- metrics.Options.Returns(new MetricsOptions
+ var options = new MetricsOptions
{
- CaptureColdStart = false,
+ CaptureColdStart = true,
Namespace = "TestNamespace",
Service = "TestService"
- });
+ };
+
+ var conf = Substitute.For();
+ var consoleWrapper = Substitute.For();
+ var metrics = new Metrics(conf, consoleWrapper:consoleWrapper, options: options);
- var services = new ServiceCollection();
- services.AddSingleton(metrics);
- var serviceProvider = services.BuildServiceProvider();
+ var builder = WebApplication.CreateBuilder();
+ builder.Services.AddSingleton(metrics);
+ builder.WebHost.UseTestServer();
- var context = new DefaultHttpContext
+ var app = builder.Build();
+ app.Use(async (context, next) =>
{
- RequestServices = serviceProvider
- };
+ var lambdaContext = new TestLambdaContext
+ {
+ FunctionName = "TestFunction"
+ };
+ context.Items["LambdaContext"] = lambdaContext;
+ await next();
+ });
+ app.UseMetrics();
+ app.MapGet("/test", () => Results.Ok());
- var appBuilder = new ApplicationBuilder(serviceProvider);
- appBuilder.UseMetrics();
- var app = appBuilder.Build();
+ await app.StartAsync();
+ var client = app.GetTestClient();
// Act
- await app.Invoke(context);
+ var response = await client.GetAsync("/test");
// Assert
- metrics.DidNotReceive().PushSingleMetric(
- Arg.Is(s => s == "ColdStart"),
- Arg.Any(),
- Arg.Any(),
- Arg.Any(),
- Arg.Any(),
- Arg.Any>()
+ Assert.Equal(200, (int)response.StatusCode);
+ consoleWrapper.Received(1).WriteLine(
+ Arg.Is(s => s.Contains("CloudWatchMetrics\":[{\"Namespace\":\"TestNamespace\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"FunctionName\"]]}]},\"FunctionName\":\"TestFunction\",\"ColdStart\":1}"))
);
+
+ await app.StopAsync();
+ }
+
+ public void Dispose()
+ {
+ ColdStartTracker.ResetColdStart();
}
}
\ 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 1244f39b..acc66627 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs
@@ -14,13 +14,11 @@
*/
using System;
-using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Amazon.Lambda.Core;
-using Amazon.Lambda.TestUtilities;
namespace AWS.Lambda.Powertools.Metrics.Tests.Handlers;
@@ -43,12 +41,12 @@ public void AddDimensions()
public void AddMultipleDimensions()
{
Metrics.PushSingleMetric("SingleMetric1", 1, MetricUnit.Count, resolution: MetricResolution.High,
- defaultDimensions: new Dictionary {
+ dimensions: new Dictionary {
{ "Default1", "SingleMetric1" }
});
Metrics.PushSingleMetric("SingleMetric2", 1, MetricUnit.Count, resolution: MetricResolution.High, nameSpace: "ns2",
- defaultDimensions: new Dictionary {
+ dimensions: new Dictionary {
{ "Default1", "SingleMetric2" },
{ "Default2", "SingleMetric2" }
});
@@ -60,7 +58,7 @@ public void AddMultipleDimensions()
public void PushSingleMetricWithNamespace()
{
Metrics.PushSingleMetric("SingleMetric", 1, MetricUnit.Count, resolution: MetricResolution.High,
- defaultDimensions: new Dictionary {
+ dimensions: new Dictionary {
{ "Default", "SingleMetric" }
});
}
@@ -78,14 +76,14 @@ public void PushSingleMetricDefaultDimensions()
{
{ "Default", "SingleMetric" }
});
- Metrics.PushSingleMetric("SingleMetric", 1, MetricUnit.Count, defaultDimensions: Metrics.DefaultDimensions );
+ Metrics.PushSingleMetric("SingleMetric", 1, MetricUnit.Count, dimensions: Metrics.DefaultDimensions );
}
[Metrics]
public void PushSingleMetricWithEnvNamespace()
{
Metrics.PushSingleMetric("SingleMetric", 1, MetricUnit.Count, resolution: MetricResolution.High,
- defaultDimensions: new Dictionary {
+ dimensions: new Dictionary {
{ "Default", "SingleMetric" }
});
}
@@ -231,6 +229,12 @@ public void HandleWithParamAndLambdaContext(string input, ILambdaContext context
}
+ [Metrics(Namespace = "ns", Service = "svc", RaiseOnEmptyMetrics = true)]
+ public void HandlerRaiseOnEmptyMetrics()
+ {
+
+ }
+
[Metrics(Namespace = "ns", Service = "svc", CaptureColdStart = true)]
public void HandleOnlyDimensionsInColdStart(ILambdaContext context)
{
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 4f4c6bd8..87c501dd 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs
@@ -16,6 +16,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
+using Amazon.Lambda.Core;
using Amazon.Lambda.TestUtilities;
using AWS.Lambda.Powertools.Common;
using NSubstitute;
@@ -39,9 +40,6 @@ public FunctionHandlerTests()
[Fact]
public async Task When_Metrics_Add_Metadata_Same_Key_Should_Ignore_Metadata()
{
- // Arrange
-
-
// Act
var exception = await Record.ExceptionAsync(() => _handler.HandleSameKey("whatever"));
@@ -198,18 +196,14 @@ public void Handler_WithMockedMetrics_ShouldCallAddMetric()
Metrics.UseMetricsForTests(metricsMock);
+
var sut = new MetricsDependencyInjectionOptionsHandler(metricsMock);
// Act
sut.Handler();
// Assert
- metricsMock.Received(1).PushSingleMetric("ColdStart", 1, MetricUnit.Count, "dotnet-powertools-test",
- service: "testService",
- Arg.Is>(x =>
- x.ContainsKey("Environment") && x["Environment"] == "Prod"
- && x.ContainsKey("Another") && x["Another"] == "One"));
-
+ metricsMock.Received(1).CaptureColdStartMetric(Arg.Any());
metricsMock.Received(1).AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
}
@@ -265,54 +259,29 @@ public void Handler_With_Builder_Should_Configure_In_Constructor_Mock()
FunctionName = "My_Function_Name"
});
- metricsMock.Received(1).PushSingleMetric("ColdStart", 1, MetricUnit.Count, "dotnet-powertools-test",
- 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).CaptureColdStartMetric(Arg.Any());
metricsMock.Received(1).AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
}
[Fact]
- public void Handler_With_Builder_Should_Configure_FunctionName_In_Constructor_Mock()
+ public void When_RaiseOnEmptyMetrics_And_NoMetrics_Should_ThrowException()
{
- 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 & Assert
+ var exception = Assert.Throws(() => _handler.HandlerRaiseOnEmptyMetrics());
+ Assert.Equal("No metrics have been provided.", exception.Message);
+ }
- // Act
- sut.Handler(new TestLambdaContext
- {
- FunctionName = "This_Will_Be_Overwritten"
- });
+ [Fact]
+ public void Handler_With_Builder_Should_Raise_Empty_Metrics()
+ {
+ // Arrange
+ var handler = new MetricsnBuilderHandler();
- 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);
+ // Act & Assert
+ var exception = Assert.Throws(() => handler.HandlerEmpty());
+ Assert.Equal("No metrics have been provided.", exception.Message);
}
-
+
[Fact]
public void Handler_With_Builder_Push_Single_Metric_No_Dimensions()
{
@@ -411,6 +380,45 @@ public void When_Function_Name_Is_Set_No_Context()
"\"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).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).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 4c71a709..5aae4cdc 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/MetricsnBuilderHandler.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/MetricsnBuilderHandler.cs
@@ -14,6 +14,7 @@ public MetricsnBuilderHandler(IMetrics metrics = null)
.WithCaptureColdStart(true)
.WithService("testService")
.WithNamespace("dotnet-powertools-test")
+ .WithRaiseOnEmptyMetrics(true)
.WithDefaultDimensions(new Dictionary
{
{ "Environment", "Prod1" },
@@ -27,6 +28,11 @@ public void Handler(ILambdaContext context)
_metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count);
}
+ [Metrics]
+ public void HandlerEmpty()
+ {
+ }
+
public void HandlerSingleMetric()
{
_metrics.PushSingleMetric("SuccessfulBooking", 1, MetricUnit.Count);
@@ -34,7 +40,7 @@ public void HandlerSingleMetric()
public void HandlerSingleMetricDimensions()
{
- _metrics.PushSingleMetric("SuccessfulBooking", 1, MetricUnit.Count, defaultDimensions: _metrics.Options.DefaultDimensions);
+ _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 120d1a72..ecdd94d6 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs
@@ -25,7 +25,7 @@ public void Metrics_Set_Execution_Environment_Context()
var conf = new PowertoolsConfigurations(new SystemWrapper(env));
- var metrics = new Metrics(conf);
+ _ = new Metrics(conf);
// Assert
env.Received(1).SetEnvironmentVariable(
@@ -36,23 +36,15 @@ public void Metrics_Set_Execution_Environment_Context()
}
[Fact]
- public void Before_With_Null_DefaultDimensions_Should_Not_Throw()
+ public void Before_When_RaiseOnEmptyMetricsNotSet_Should_Configure_Null()
{
// Arrange
MetricsAspect.ResetForTest();
- var metricsMock = Substitute.For();
- var optionsMock = new MetricsOptions
- {
- CaptureColdStart = true,
- DefaultDimensions = null
- };
- metricsMock.Options.Returns(optionsMock);
- Metrics.UseMetricsForTests(metricsMock);
-
- var metricsAspect = new MetricsAspect();
var method = typeof(MetricsTests).GetMethod(nameof(TestMethod));
var trigger = new MetricsAttribute();
+ var metricsAspect = new MetricsAspect();
+
// Act
metricsAspect.Before(
this,
@@ -65,84 +57,268 @@ public void Before_With_Null_DefaultDimensions_Should_Not_Throw()
);
// Assert
- metricsMock.Received(1).PushSingleMetric(
- "ColdStart",
- 1.0,
- MetricUnit.Count,
- Arg.Any(),
- Arg.Any(),
- null
+ var metrics = Metrics.Instance;
+ Assert.False(trigger.IsRaiseOnEmptyMetricsSet);
+ Assert.False(metrics.Options.RaiseOnEmptyMetrics);
+ }
+
+ // Helper method for the tests
+ internal void TestMethod(ILambdaContext context)
+ {
+ }
+
+ [Fact]
+ public void When_Constructor_With_Null_Namespace_And_Service_Should_Not_Set()
+ {
+ // Arrange
+ Substitute.For();
+ var powertoolsConfigMock = Substitute.For();
+ powertoolsConfigMock.MetricsNamespace.Returns((string)null);
+ powertoolsConfigMock.Service.Returns("service_undefined");
+
+ // Act
+ var metrics = new Metrics(powertoolsConfigMock);
+
+ // Assert
+ Assert.Null(metrics.GetNamespace());
+ Assert.Null(metrics.Options.Service);
+ }
+
+ [Fact]
+ public void When_AddMetric_With_EmptyKey_Should_ThrowArgumentNullException()
+ {
+ // Arrange
+ Substitute.For();
+ var powertoolsConfigMock = Substitute.For();
+ IMetrics metrics = new Metrics(powertoolsConfigMock);
+
+ // Act & Assert
+ var exception = Assert.Throws(() => metrics.AddMetric("", 1.0));
+ Assert.Equal("key", exception.ParamName);
+ Assert.Contains("'AddMetric' method requires a valid metrics key. 'Null' or empty values are not allowed.",
+ exception.Message);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void When_AddMetric_With_InvalidKey_Should_ThrowArgumentNullException(string key)
+ {
+ // Arrange
+ // var metricsMock = Substitute.For();
+ var powertoolsConfigMock = Substitute.For();
+ IMetrics metrics = new Metrics(powertoolsConfigMock);
+
+ // Act & Assert
+ var exception = Assert.Throws(() => metrics.AddMetric(key, 1.0));
+ Assert.Equal("key", exception.ParamName);
+ Assert.Contains("'AddMetric' method requires a valid metrics key. 'Null' or empty values are not allowed.",
+ exception.Message);
+ }
+
+ [Fact]
+ public void When_SetDefaultDimensions_With_InvalidKeyOrValue_Should_ThrowArgumentNullException()
+ {
+ // Arrange
+ var powertoolsConfigMock = Substitute.For();
+ IMetrics metrics = new Metrics(powertoolsConfigMock);
+
+ var invalidDimensions = new Dictionary
+ {
+ { "", "value" }, // empty key
+ { "key", "" }, // empty value
+ { " ", "value" }, // whitespace key
+ { "key1", " " }, // whitespace value
+ { "key2", null } // null value
+ };
+
+ // Act & Assert
+ foreach (var dimension in invalidDimensions)
+ {
+ var dimensions = new Dictionary { { dimension.Key, dimension.Value } };
+ var exception = Assert.Throws(() => metrics.SetDefaultDimensions(dimensions));
+ Assert.Equal("Key", exception.ParamName);
+ Assert.Contains(
+ "'SetDefaultDimensions' method requires a valid key pair. 'Null' or empty values are not allowed.",
+ exception.Message);
+ }
+ }
+
+ [Fact]
+ public void When_PushSingleMetric_With_EmptyName_Should_ThrowArgumentNullException()
+ {
+ // Arrange
+ var powertoolsConfigMock = Substitute.For();
+ IMetrics metrics = new Metrics(powertoolsConfigMock);
+
+ // Act & Assert
+ var exception = Assert.Throws(() => metrics.PushSingleMetric("", 1.0, MetricUnit.Count));
+ Assert.Equal("name", exception.ParamName);
+ Assert.Contains(
+ "'PushSingleMetric' method requires a valid metrics key. 'Null' or empty values are not allowed.",
+ exception.Message);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void When_PushSingleMetric_With_InvalidName_Should_ThrowArgumentNullException(string name)
+ {
+ // Arrange
+ var powertoolsConfigMock = Substitute.For();
+ IMetrics metrics = new Metrics(powertoolsConfigMock);
+
+ // Act & Assert
+ var exception =
+ Assert.Throws(() => metrics.PushSingleMetric(name, 1.0, MetricUnit.Count));
+ Assert.Equal("name", exception.ParamName);
+ Assert.Contains(
+ "'PushSingleMetric' method requires a valid metrics key. 'Null' or empty values are not allowed.",
+ exception.Message);
+ }
+
+
+ [Fact]
+ public void When_ColdStart_Should_Use_DefaultDimensions_From_Options()
+ {
+ // Arrange
+ var options = new MetricsOptions
+ {
+ CaptureColdStart = true,
+ Namespace = "dotnet-powertools-test",
+ Service = "testService",
+ DefaultDimensions = new Dictionary
+ {
+ { "Environment", "Test" },
+ { "Region", "us-east-1" }
+ }
+ };
+
+ var conf = Substitute.For();
+ var consoleWrapper = Substitute.For();
+ IMetrics metrics = new Metrics(conf, consoleWrapper: consoleWrapper, options: options);
+
+ var context = new TestLambdaContext
+ {
+ FunctionName = "TestFunction"
+ };
+
+ // Act
+ metrics.CaptureColdStartMetric(context);
+
+ // Assert
+ consoleWrapper.Received(1).WriteLine(
+ Arg.Is(s => s.Contains("\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Environment\",\"Region\",\"FunctionName\"]]}]},\"Environment\":\"Test\",\"Region\":\"us-east-1\",\"FunctionName\":\"TestFunction\",\"ColdStart\":1}"))
);
}
[Fact]
- public void Before_When_CaptureStartNotSet_Should_Not_Push_Metrics()
+ public void When_ColdStart_And_DefaultDimensions_Is_Null_Should_Only_Add_Service_And_FunctionName()
{
// Arrange
- MetricsAspect.ResetForTest();
+ var options = new MetricsOptions
+ {
+ CaptureColdStart = true,
+ Namespace = "dotnet-powertools-test",
+ Service = "testService",
+ DefaultDimensions = null
+ };
+
+ var conf = Substitute.For();
+ var consoleWrapper = Substitute.For();
+ IMetrics metrics = new Metrics(conf, consoleWrapper: consoleWrapper, options: options);
+
+ var context = new TestLambdaContext
+ {
+ FunctionName = "TestFunction"
+ };
+
+ // Act
+ metrics.CaptureColdStartMetric(context);
+
+ // Assert
+ consoleWrapper.Received(1).WriteLine(
+ Arg.Is(s => s.Contains("\"CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"FunctionName\"]]}]},\"FunctionName\":\"TestFunction\",\"ColdStart\":1}"))
+ );
+ }
+
+ [Fact]
+ public void Namespace_Should_Return_OptionsNamespace()
+ {
+ // Arrange
+ Metrics.ResetForTest();
var metricsMock = Substitute.For();
var optionsMock = new MetricsOptions
{
- CaptureColdStart = null
+ Namespace = "TestNamespace"
};
+
metricsMock.Options.Returns(optionsMock);
Metrics.UseMetricsForTests(metricsMock);
- var metricsAspect = new MetricsAspect();
- var method = typeof(MetricsTests).GetMethod(nameof(TestMethod));
- var trigger = new MetricsAttribute();
-
// Act
- metricsAspect.Before(
- this,
- "TestMethod",
- new object[] { new TestLambdaContext() },
- typeof(MetricsTests),
- method,
- typeof(void),
- new Attribute[] { trigger }
- );
+ var result = Metrics.Namespace;
// Assert
- metricsMock.DidNotReceive().PushSingleMetric(
- Arg.Any(),
- Arg.Any(),
- Arg.Any(),
- Arg.Any(),
- Arg.Any(),
- Arg.Any>()
- );
+ Assert.Equal("TestNamespace", result);
}
[Fact]
- public void Before_When_RaiseOnEmptyMetricsNotSet_Should_Configure_Null()
+ public void Service_Should_Return_OptionsService()
{
// Arrange
- MetricsAspect.ResetForTest();
- var method = typeof(MetricsTests).GetMethod(nameof(TestMethod));
- var trigger = new MetricsAttribute();
+ Metrics.ResetForTest();
+ var metricsMock = Substitute.For();
+ var optionsMock = new MetricsOptions
+ {
+ Service = "TestService"
+ };
+
+ metricsMock.Options.Returns(optionsMock);
+ Metrics.UseMetricsForTests(metricsMock);
- var metricsAspect = new MetricsAspect();
+ // Act
+ var result = Metrics.Service;
+
+ // Assert
+ Assert.Equal("TestService", result);
+ }
+
+ [Fact]
+ public void Namespace_Should_Return_Null_When_Not_Set()
+ {
+ // Arrange
+ Metrics.ResetForTest();
+ var metricsMock = Substitute.For();
+ var optionsMock = new MetricsOptions();
+
+ metricsMock.Options.Returns(optionsMock);
+ Metrics.UseMetricsForTests(metricsMock);
// Act
- metricsAspect.Before(
- this,
- "TestMethod",
- new object[] { new TestLambdaContext() },
- typeof(MetricsTests),
- method,
- typeof(void),
- new Attribute[] { trigger }
- );
+ var result = Metrics.Namespace;
// Assert
- var metrics = Metrics.Instance;
- Assert.False(trigger.IsRaiseOnEmptyMetricsSet);
- Assert.False(metrics.Options.RaiseOnEmptyMetrics);
+ Assert.Null(result);
}
- // Helper method for the tests
- internal void TestMethod(ILambdaContext context)
+ [Fact]
+ public void Service_Should_Return_Null_When_Not_Set()
{
+ // Arrange
+ Metrics.ResetForTest();
+ var metricsMock = Substitute.For();
+ var optionsMock = new MetricsOptions();
+
+ metricsMock.Options.Returns(optionsMock);
+ Metrics.UseMetricsForTests(metricsMock);
+
+ // Act
+ var result = Metrics.Service;
+
+ // Assert
+ Assert.Null(result);
}
}
\ No newline at end of file
diff --git a/libraries/tests/Directory.Packages.props b/libraries/tests/Directory.Packages.props
index e8c9a16e..516a0e93 100644
--- a/libraries/tests/Directory.Packages.props
+++ b/libraries/tests/Directory.Packages.props
@@ -6,6 +6,7 @@
+
diff --git a/libraries/tests/e2e/functions/core/metrics/Function/src/Function/TestHelper.cs b/libraries/tests/e2e/functions/core/metrics/Function/src/Function/TestHelper.cs
index c3434d28..38cb7438 100644
--- a/libraries/tests/e2e/functions/core/metrics/Function/src/Function/TestHelper.cs
+++ b/libraries/tests/e2e/functions/core/metrics/Function/src/Function/TestHelper.cs
@@ -38,7 +38,7 @@ public static void TestMethod(APIGatewayProxyRequest apigwProxyEvent, ILambdaCon
unit: MetricUnit.Count,
nameSpace: "Test",
service: "Test",
- defaultDimensions: new Dictionary
+ dimensions: new Dictionary
{
{"FunctionName", context.FunctionName}
});
diff --git a/mkdocs.yml b/mkdocs.yml
index 9170105e..24f86cf6 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -15,8 +15,9 @@ nav:
- Workshop 🆕: https://s12d.com/powertools-for-aws-lambda-workshop" target="_blank
- Core utilities:
- core/logging.md
- - core/metrics.md
- - core/metrics-v2.md
+ - Metrics:
+ - core/metrics.md
+ - core/metrics-v2.md
- core/tracing.md
- Utilities:
- utilities/parameters.md
diff --git a/version.json b/version.json
index d52ea67c..8ddbf994 100644
--- a/version.json
+++ b/version.json
@@ -1,8 +1,9 @@
{
"Core": {
"Logging": "1.6.4",
- "Metrics": "1.8.0",
- "Tracing": "1.6.1"
+ "Metrics": "2.0.0",
+ "Tracing": "1.6.1",
+ "Metrics.AspNetCore": "0.1.0",
},
"Utilities": {
"Parameters": "1.3.0",