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 @@ [![Build](https://github.com/aws-powertools/powertools-lambda-dotnet/actions/workflows/build.yml/badge.svg?branch=develop)](https://github.com/aws-powertools/powertools-lambda-dotnet/actions/workflows/build.yml) [![codecov.io](https://codecov.io/github/aws-powertools/powertools-lambda-dotnet/branch/develop/graphs/badge.svg)](https://app.codecov.io/gh/aws-powertools/powertools-lambda-dotnet) [![dotnet support](https://img.shields.io/static/v1?label=dotnet&message=%20NET6.0|NET8.0&color=blue?style=flat-square&logo=dotnet)](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) -[![NuGet Downloads](https://img.shields.io/nuget/dt/AWS.Lambda.Powertools.Logging.svg)](https://www.nuget.org/packages?q=AWS.Lambda.Powertools) +[![NuGet Downloads](https://img.shields.io/nuget/dt/AWS.Lambda.Powertools.Logging.svg)](https://www.nuget.org/packages?q=AWS.Lambda.Powertools) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/aws-powertools/powertools-lambda-dotnet/badge)](https://scorecard.dev/viewer/?uri=github.com/aws-powertools/powertools-lambda-dotnet) [![Join our Discord](https://dcbadge.vercel.app/api/server/B8zZKbbyET?style=flat-square)](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",