From a59fe9349a1abfdd3e27a4f4e0d2ea4c298ad61e Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Wed, 21 Feb 2024 16:17:34 +0000 Subject: [PATCH 1/9] Metrics AOT ready, New SerializationContext --- .../AWS.Lambda.Powertools.Metrics.csproj | 6 +-- .../Model/MetricResolution.cs | 1 - .../Model/MetricUnit.cs | 4 ++ .../Model/RootNode.cs | 5 +++ .../Serializer/MetricsSerializationContext.cs | 40 +++++++++++++++++++ .../Serializer/StringEnumConverter.cs | 6 ++- libraries/src/Directory.Build.props | 8 ++++ 7 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.Metrics/Serializer/MetricsSerializationContext.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/AWS.Lambda.Powertools.Metrics.csproj b/libraries/src/AWS.Lambda.Powertools.Metrics/AWS.Lambda.Powertools.Metrics.csproj index 77110d09..128169c8 100644 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/AWS.Lambda.Powertools.Metrics.csproj +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/AWS.Lambda.Powertools.Metrics.csproj @@ -5,14 +5,12 @@ Powertools for AWS Lambda (.NET) - Metrics package. AWS.Lambda.Powertools.Metrics AWS.Lambda.Powertools.Metrics + 1.9.3 - - - - + diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricResolution.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricResolution.cs index 3c6536ff..2112c1c5 100644 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricResolution.cs +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricResolution.cs @@ -6,7 +6,6 @@ namespace AWS.Lambda.Powertools.Metrics; /// /// Enum MetricResolution /// -// [JsonConverter(typeof(StringEnumConverter))] public enum MetricResolution { /// diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricUnit.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricUnit.cs index fd4b9760..132b10f3 100644 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricUnit.cs +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricUnit.cs @@ -21,7 +21,11 @@ namespace AWS.Lambda.Powertools.Metrics; /// /// Enum MetricUnit /// +#if NET8_0_OR_GREATER +[JsonConverter(typeof(JsonStringEnumConverter))] +#else [JsonConverter(typeof(StringEnumConverter))] +#endif public enum MetricUnit { /// diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Model/RootNode.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Model/RootNode.cs index a7783819..496606cd 100644 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/Model/RootNode.cs +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Model/RootNode.cs @@ -65,6 +65,11 @@ public string Serialize() { if (string.IsNullOrWhiteSpace(AWS.GetNamespace())) throw new SchemaValidationException("namespace"); +#if NET8_0_OR_GREATER + + return JsonSerializer.Serialize(this, typeof(RootNode), MetricsSerializationContext.Default); +#else return JsonSerializer.Serialize(this); +#endif } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Serializer/MetricsSerializationContext.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Serializer/MetricsSerializationContext.cs new file mode 100644 index 00000000..e8a421ac --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Serializer/MetricsSerializationContext.cs @@ -0,0 +1,40 @@ +/* + * 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.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AWS.Lambda.Powertools.Metrics; + +#if NET8_0_OR_GREATER +/// +/// Source generator for Metrics types +/// +[JsonSerializable(typeof(string))] +[JsonSerializable(typeof(double))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(MetricUnit))] +[JsonSerializable(typeof(MetricDefinition))] +[JsonSerializable(typeof(DimensionSet))] +[JsonSerializable(typeof(Metadata))] +[JsonSerializable(typeof(MetricDirective))] +[JsonSerializable(typeof(MetricResolution))] +[JsonSerializable(typeof(MetricsContext))] +[JsonSerializable(typeof(RootNode))] +public partial class MetricsSerializationContext : JsonSerializerContext +{ + +} +#endif \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Serializer/StringEnumConverter.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Serializer/StringEnumConverter.cs index 28c0d54b..242ee90b 100644 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/Serializer/StringEnumConverter.cs +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Serializer/StringEnumConverter.cs @@ -22,9 +22,12 @@ namespace AWS.Lambda.Powertools.Metrics; +#if NET6_0 + /// /// Class StringEnumConverter. /// Implements the +/// .NET 6 only /// /// public class StringEnumConverter : JsonConverterFactory @@ -95,4 +98,5 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer _allowIntegerValues).CreateConverter(typeToConvert, options) : _baseConverter.CreateConverter(typeToConvert, options); } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/libraries/src/Directory.Build.props b/libraries/src/Directory.Build.props index 0260344d..3a1d85f2 100644 --- a/libraries/src/Directory.Build.props +++ b/libraries/src/Directory.Build.props @@ -19,6 +19,14 @@ + + + + true + true + true + + From 96239ea817ed12d3f3856f66cb9c9ab60640a712 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 27 Jun 2024 00:34:16 +0100 Subject: [PATCH 2/9] Metrics AOT Support. REmove dependency from UniversalWrapperAspect.cs. Refactor EMFValidationTests.cs no more mocks --- .../Aspects/UniversalWrapperAspect.cs | 40 +- .../Core/ISystemWrapper.cs | 13 + .../Core/SystemWrapper.cs | 6 + .../AWS.Lambda.Powertools.Metrics.csproj | 2 - .../Internal/MetricsAspect.cs | 136 ++++ .../Internal/MetricsAspectHandler.cs | 145 ---- .../MetricsAttribute.cs | 42 +- .../ClearDimensionsTests.cs | 33 +- .../EMFValidationTests.cs | 637 +++--------------- .../Handlers/ExceptionFunctionHandlerTests.cs | 2 +- .../Handlers/FunctionHandler.cs | 110 ++- 11 files changed, 418 insertions(+), 748 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs delete mode 100644 libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspectHandler.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Aspects/UniversalWrapperAspect.cs b/libraries/src/AWS.Lambda.Powertools.Common/Aspects/UniversalWrapperAspect.cs index 8061ef7d..c4b01468 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Aspects/UniversalWrapperAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Aspects/UniversalWrapperAspect.cs @@ -1,12 +1,12 @@ /* * 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 @@ -15,6 +15,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -32,19 +33,19 @@ public class UniversalWrapperAspect /// /// The delegate cache /// - private static readonly Dictionary _delegateCache = new(); + private static readonly Dictionary DelegateCache = new(); /// /// The asynchronous generic handler /// - private static readonly MethodInfo _asyncGenericHandler = + private static readonly MethodInfo AsyncGenericHandler = typeof(UniversalWrapperAttribute).GetMethod(nameof(UniversalWrapperAttribute.WrapAsync), BindingFlags.NonPublic | BindingFlags.Instance); /// /// The synchronize generic handler /// - private static readonly MethodInfo _syncGenericHandler = + private static readonly MethodInfo SyncGenericHandler = typeof(UniversalWrapperAttribute).GetMethod(nameof(UniversalWrapperAttribute.WrapSync), BindingFlags.NonPublic | BindingFlags.Instance); @@ -94,6 +95,7 @@ public object Handle( /// Type of the return. /// The wrappers. /// Handler. + [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] private static Handler CreateMethodHandler(Type returnType, IEnumerable wrappers) { var targetParam = Expression.Parameter(typeof(Func), "orig"); @@ -107,13 +109,13 @@ private static Handler CreateMethodHandler(Type returnType, IEnumerable).MakeGenericType(taskType); - wrapperMethod = _asyncGenericHandler.MakeGenericMethod(taskType); + wrapperMethod = AsyncGenericHandler.MakeGenericMethod(taskType); } else { if (returnType == typeof(void)) returnType = typeof(object); - wrapperMethod = _syncGenericHandler.MakeGenericMethod(returnType); + wrapperMethod = SyncGenericHandler.MakeGenericMethod(returnType); } var converArgs = Expression.Parameter(typeof(object[]), "args"); @@ -128,9 +130,9 @@ private static Handler CreateMethodHandler(Type returnType, IEnumerable(Expression.Convert(Expression.Invoke(next, orig_args), typeof(object)), - targetParam, orig_args, eventArgsParam); + var origArgs = Expression.Parameter(typeof(object[]), "orig_args"); + var handler = Expression.Lambda(Expression.Convert(Expression.Invoke(next, origArgs), typeof(object)), + targetParam, origArgs, eventArgsParam); var handlerCompiled = handler.Compile(); @@ -147,14 +149,14 @@ private static Handler CreateMethodHandler(Type returnType, IEnumerable wrappers) { - if (!_delegateCache.TryGetValue(method, out var handler)) - lock (method) - { - if (!_delegateCache.TryGetValue(method, out handler)) - _delegateCache[method] = handler = CreateMethodHandler(returnType, wrappers); - } - - return handler; + lock (method) + { + if (!DelegateCache.TryGetValue(method, out var handler)) + if (!DelegateCache.TryGetValue(method, out handler)) + DelegateCache[method] = handler = CreateMethodHandler(returnType, wrappers); + + return handler; + } } /// diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/ISystemWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/ISystemWrapper.cs index 98abe1b5..8a035984 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/ISystemWrapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/ISystemWrapper.cs @@ -13,6 +13,8 @@ * permissions and limitations under the License. */ +using System.IO; + namespace AWS.Lambda.Powertools.Common; /// @@ -57,4 +59,15 @@ public interface ISystemWrapper /// /// void SetExecutionEnvironment(T type); + + /// + /// Sets console output + /// Useful for testing and checking the console output + /// + /// var consoleOut = new StringWriter(); + /// SystemWrapper.Instance.SetOut(consoleOut); + /// + /// + /// The TextWriter instance where to write to + void SetOut(TextWriter writeTo); } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs index 6389b3a0..8f42bda4 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs @@ -126,6 +126,12 @@ public void SetExecutionEnvironment(T type) SetEnvironmentVariable(envName, envValue.ToString()); } + /// + public void SetOut(TextWriter writeTo) + { + Console.SetOut(writeTo); + } + /// /// Parsing the name to conform with the required naming convention for the UserAgent header (PTFeature/Name/Version) /// Fallback to Assembly Name on exception diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/AWS.Lambda.Powertools.Metrics.csproj b/libraries/src/AWS.Lambda.Powertools.Metrics/AWS.Lambda.Powertools.Metrics.csproj index 128169c8..0e90e99b 100644 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/AWS.Lambda.Powertools.Metrics.csproj +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/AWS.Lambda.Powertools.Metrics.csproj @@ -5,12 +5,10 @@ Powertools for AWS Lambda (.NET) - Metrics package. AWS.Lambda.Powertools.Metrics AWS.Lambda.Powertools.Metrics - 1.9.3 - diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs new file mode 100644 index 00000000..1e60af6b --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs @@ -0,0 +1,136 @@ +/* + * 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; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using AspectInjector.Broker; +using AWS.Lambda.Powertools.Common; + +namespace AWS.Lambda.Powertools.Metrics; + +/// +/// MetricsAspect class is responsible for capturing ColdStart metric and flushing metrics on function exit. +/// Scope.Global - means aspect will operate as singleton. +/// +[Aspect(Scope.Global)] +public class MetricsAspect +{ + /// + /// The is cold start + /// + private static bool _isColdStart; + + /// + /// Specify to clear Lambda Context on exit + /// + private bool _clearLambdaContext; + + private IMetrics _metricsInstance; + + static MetricsAspect() + { + _isColdStart = true; + } + + /// + /// Runs before the execution of the method marked with the Metrics Attribute + /// + /// + /// + /// + /// + /// + /// + /// + [Advice(Kind.Before)] + public void Before( + [Argument(Source.Instance)] object instance, + [Argument(Source.Name)] string name, + [Argument(Source.Arguments)] object[] args, + [Argument(Source.Type)] Type hostType, + [Argument(Source.Metadata)] MethodBase method, + [Argument(Source.ReturnType)] Type returnType, + [Argument(Source.Triggers)] Attribute[] triggers) + { + + // Before running Function + + var trigger = triggers.OfType().First(); + + _metricsInstance = trigger.MetricsInstance; + + var eventArgs = new AspectEventArgs + { + Instance = instance, + Type = hostType, + Method = method, + Name = name, + Args = args, + ReturnType = returnType, + Triggers = triggers + }; + + if (trigger.CaptureColdStart && _isColdStart) + { + _isColdStart = false; + + var nameSpace = _metricsInstance.GetNamespace(); + var service = _metricsInstance.GetService(); + Dictionary dimensions = null; + + _clearLambdaContext = PowertoolsLambdaContext.Extract(eventArgs); + + if (PowertoolsLambdaContext.Instance is not null) + { + dimensions = new Dictionary + { + { "FunctionName", PowertoolsLambdaContext.Instance.FunctionName } + }; + } + + _metricsInstance.PushSingleMetric( + "ColdStart", + 1.0, + MetricUnit.Count, + nameSpace, + service, + dimensions + ); + } + } + + /// + /// OnExit runs after the execution of the method marked with the Metrics Attribute + /// + [Advice(Kind.After)] + public void Exit() { + _metricsInstance.Flush(); + if (_clearLambdaContext) + PowertoolsLambdaContext.Clear(); + } + + + /// + /// Reset the aspect for testing purposes. + /// + internal static void ResetForTest() + { + _isColdStart = true; + Metrics.ResetForTest(); + PowertoolsLambdaContext.Clear(); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspectHandler.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspectHandler.cs deleted file mode 100644 index 6209f97a..00000000 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspectHandler.cs +++ /dev/null @@ -1,145 +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 System; -using System.Collections.Generic; -using System.Runtime.ExceptionServices; -using AWS.Lambda.Powertools.Common; - -namespace AWS.Lambda.Powertools.Metrics; - -/// -/// Class MetricsAspectHandler. -/// Implements the -/// -/// -internal class MetricsAspectHandler : IMethodAspectHandler -{ - /// - /// The is cold start - /// - private static bool _isColdStart = true; - - /// - /// The capture cold start enabled - /// - private readonly bool _captureColdStartEnabled; - - /// - /// The metrics - /// - private readonly IMetrics _metrics; - - /// - /// Specify to clear Lambda Context on exit - /// - private bool _clearLambdaContext; - - /// - /// Creates MetricsAspectHandler to supports running code before and after marked methods. - /// - /// AWS.Lambda.Powertools.Metrics Instance - /// If 'true', captures cold start during Lambda execution - internal MetricsAspectHandler - ( - IMetrics metricsInstance, - bool captureColdStartEnabled - ) - { - _metrics = metricsInstance; - _captureColdStartEnabled = captureColdStartEnabled; - } - - /// - /// OnExit runs after the execution of the method marked with the Metrics Attribute - /// - /// Aspect Arguments - public void OnExit(AspectEventArgs eventArgs) - { - _metrics.Flush(); - if (_clearLambdaContext) - PowertoolsLambdaContext.Clear(); - } - - /// - /// OnEntry runs before the execution of the method marked with the Metrics Attribute - /// - /// Aspect Arguments - public void OnEntry(AspectEventArgs eventArgs) - { - if (!_isColdStart) - return; - - _isColdStart = false; - - if (!_captureColdStartEnabled) - return; - - var nameSpace = _metrics.GetNamespace(); - var service = _metrics.GetService(); - Dictionary dimensions = null; - - _clearLambdaContext = PowertoolsLambdaContext.Extract(eventArgs); - - if (PowertoolsLambdaContext.Instance is not null) - { - dimensions = new Dictionary - { - {"FunctionName", PowertoolsLambdaContext.Instance.FunctionName} - }; - } - - _metrics.PushSingleMetric( - "ColdStart", - 1.0, - MetricUnit.Count, - nameSpace, - service, - dimensions - ); - } - - /// - /// OnSuccess run after successful execution of the method marked with the Metrics Attribute - /// - /// Aspect Arguments - /// Object returned by the method marked with Metrics Attribute - public void OnSuccess(AspectEventArgs eventArgs, object result) - { - } - - /// - /// OnException runs when an unhandled exception occurs inside the method marked with the Metrics Attribute - /// - /// Aspect Arguments - /// Exception thrown by the method marked with Metrics Attribute - /// Generic unhandled exception - public void OnException(AspectEventArgs eventArgs, Exception exception) - { - // The purpose of ExceptionDispatchInfo.Capture is to capture a potentially mutating exception's StackTrace at a point in time: - // https://learn.microsoft.com/en-us/dotnet/standard/exceptions/best-practices-for-exceptions#capture-exceptions-to-rethrow-later - ExceptionDispatchInfo.Capture(exception).Throw(); - } - - /// - /// Helper method for testing purposes. Clears static instance between test execution - /// - internal void ResetForTest() - { - _isColdStart = true; - Metrics.ResetForTest(); - PowertoolsLambdaContext.Clear(); - } -} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs index d0025fc4..644167d7 100644 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs @@ -14,6 +14,7 @@ */ using System; +using AspectInjector.Broker; using AWS.Lambda.Powertools.Common; namespace AWS.Lambda.Powertools.Metrics; @@ -96,13 +97,13 @@ namespace AWS.Lambda.Powertools.Metrics; /// } /// /// -[AttributeUsage(AttributeTargets.Method)] -public class MetricsAttribute : MethodAspectAttribute +[Injection(typeof(MetricsAspect))] +public class MetricsAttribute : Attribute { - /// - /// The metrics instance - /// - private IMetrics _metricsInstance; + // /// + // /// The metrics instance + // /// + // private IMetrics _metricsInstance; /// /// Set namespace. @@ -134,25 +135,24 @@ public class MetricsAttribute : MethodAspectAttribute /// Gets the metrics instance. /// /// The metrics instance. - private IMetrics MetricsInstance => - _metricsInstance ??= new Metrics( + internal IMetrics MetricsInstance => new Metrics( PowertoolsConfigurations.Instance, Namespace, Service, RaiseOnEmptyMetrics, CaptureColdStart ); - - /// - /// Creates the handler. - /// - /// IMethodAspectHandler. - protected override IMethodAspectHandler CreateHandler() - { - return new MetricsAspectHandler - ( - MetricsInstance, - CaptureColdStart - ); - } + // + // /// + // /// Creates the handler. + // /// + // /// IMethodAspectHandler. + // protected override IMethodAspectHandler CreateHandler() + // { + // return new MetricsAspectHandler + // ( + // MetricsInstance, + // CaptureColdStart + // ); + // } } diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/ClearDimensionsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/ClearDimensionsTests.cs index 879fdfac..0a46d6fd 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/ClearDimensionsTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/ClearDimensionsTests.cs @@ -1,7 +1,6 @@ -using System; using System.IO; using AWS.Lambda.Powertools.Common; -using NSubstitute; +using AWS.Lambda.Powertools.Metrics.Tests.Handlers; using Xunit; namespace AWS.Lambda.Powertools.Metrics.Tests; @@ -13,39 +12,19 @@ public class ClearDimensionsTests public void WhenClearAllDimensions_NoDimensionsInOutput() { // Arrange - var methodName = Guid.NewGuid().ToString(); var consoleOut = new StringWriter(); - Console.SetOut(consoleOut); + SystemWrapper.Instance.SetOut(consoleOut); - var configurations = Substitute.For(); - - var metrics = new Metrics( - configurations, - nameSpace: "dotnet-powertools-test", - service: "testService" - ); - - var handler = new MetricsAspectHandler( - metrics, - false - ); - - var eventArgs = new AspectEventArgs { Name = methodName }; - // Act - handler.OnEntry(eventArgs); - - Metrics.ClearDefaultDimensions(); - Metrics.AddMetric($"Metric Name", 1, MetricUnit.Count); - - handler.OnExit(eventArgs); + var handler = new FunctionHandler(); + handler.ClearDimensions(); var metricsOutput = consoleOut.ToString(); // Assert Assert.Contains("{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"Metric Name\",\"Unit\":\"Count\"}],\"Dimensions\":[[]]", metricsOutput); - + // Reset - handler.ResetForTest(); + MetricsAspect.ResetForTest(); } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs index 72fd1a54..bcf2336d 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs @@ -1,12 +1,12 @@ /* * 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 @@ -18,7 +18,7 @@ using System.IO; using System.Threading.Tasks; using AWS.Lambda.Powertools.Common; -using NSubstitute; +using AWS.Lambda.Powertools.Metrics.Tests.Handlers; using Xunit; [assembly: CollectionBehavior(DisableTestParallelization = true)] @@ -26,372 +26,154 @@ namespace AWS.Lambda.Powertools.Metrics.Tests { [Collection("Sequential")] - public class EmfValidationTests + public class EmfValidationTests : IDisposable { + private readonly CustomConsoleWriter _consoleOut; + private readonly FunctionHandler _handler; + + public EmfValidationTests() + { + _handler = new FunctionHandler(); + _consoleOut = new CustomConsoleWriter(); + SystemWrapper.Instance.SetOut(_consoleOut); + } + [Trait("Category", value: "SchemaValidation")] [Fact] public void WhenCaptureColdStart_CreateSeparateBlob() { - // Arrange - var methodName = Guid.NewGuid().ToString(); - var consoleOut = new StringWriter(); - const bool captureColdStartEnabled = true; - Console.SetOut(consoleOut); - - var configurations = Substitute.For(); - - var metrics = new Metrics( - configurations, - nameSpace: "dotnet-powertools-test", - service: "testService", - captureColdStartEnabled: captureColdStartEnabled - ); - - var handler = new MetricsAspectHandler( - metrics, - captureColdStartEnabled - ); - - var eventArgs = new AspectEventArgs { Name = methodName }; - // Act - handler.OnEntry(eventArgs); - Metrics.AddMetric("TestMetric", 1, MetricUnit.Count); - handler.OnExit(eventArgs); + _handler.AddMetric(); - var metricsOutput = consoleOut.ToString(); + var metricsOutput = _consoleOut.ToString(); // Assert var metricBlobs = AllIndexesOf(metricsOutput, "_aws"); - Assert.Equal(2, metricBlobs.Count); - - // Reset - handler.ResetForTest(); } [Trait("Category", "SchemaValidation")] [Fact] public void WhenCaptureColdStartEnabled_ValidateExists() { - // Arrange - var methodName = Guid.NewGuid().ToString(); - var consoleOut = new StringWriter(); - const bool captureColdStartEnabled = true; - Console.SetOut(consoleOut); - - var configurations = Substitute.For(); - - var logger = new Metrics( - configurations, - nameSpace: "dotnet-powertools-test", - service: "testService", - captureColdStartEnabled: captureColdStartEnabled - ); - - var handler = new MetricsAspectHandler( - logger, - captureColdStartEnabled - ); - - var eventArgs = new AspectEventArgs { Name = methodName }; - // Act - handler.OnEntry(eventArgs); - Metrics.AddMetric("TestMetric", 1, MetricUnit.Count); - handler.OnExit(eventArgs); + _handler.AddMetric(); - var result = consoleOut.ToString(); + var result = _consoleOut.ToString(); // Assert Assert.Contains("\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}]", result); Assert.Contains("\"ColdStart\":1", result); - - handler.ResetForTest(); } [Trait("Category", "EMFLimits")] [Fact] public void WhenMaxMetricsAreAdded_FlushAutomatically() { - // Arrange - var methodName = Guid.NewGuid().ToString(); - var consoleOut = new StringWriter(); - Console.SetOut(consoleOut); - - var configurations = Substitute.For(); - - var metrics = new Metrics( - configurations, - nameSpace: "dotnet-powertools-test", - service: "testService" - ); - - var handler = new MetricsAspectHandler( - metrics, - false - ); - - var eventArgs = new AspectEventArgs { Name = methodName }; - // Act - handler.OnEntry(eventArgs); + _handler.MaxMetrics(PowertoolsConfigurations.MaxMetrics); - for (var i = 0; i <= PowertoolsConfigurations.MaxMetrics; i++) - { - Metrics.AddMetric($"Metric Name {i + 1}", i, MetricUnit.Count); + var metricsOutput = _consoleOut.OutputValues; - if (i == PowertoolsConfigurations.MaxMetrics) - { - // flush when it reaches MaxMetrics - Assert.Contains("{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"Metric Name 1\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 2\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 3\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 4\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 5\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 6\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 7\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 8\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 9\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 10\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 11\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 12\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 13\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 14\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 15\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 16\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 17\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 18\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 19\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 20\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 21\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 22\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 23\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 24\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 25\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 26\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 27\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 28\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 29\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 30\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 31\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 32\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 33\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 34\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 35\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 36\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 37\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 38\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 39\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 40\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 41\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 42\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 43\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 44\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 45\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 46\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 47\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 48\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 49\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 50\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 51\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 52\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 53\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 54\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 55\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 56\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 57\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 58\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 59\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 60\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 61\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 62\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 63\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 64\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 65\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 66\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 67\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 68\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 69\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 70\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 71\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 72\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 73\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 74\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 75\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 76\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 77\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 78\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 79\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 80\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 81\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 82\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 83\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 84\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 85\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 86\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 87\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 88\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 89\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 90\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 91\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 92\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 93\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 94\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 95\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 96\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 97\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 98\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 99\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 100\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\"]]", consoleOut.ToString()); - } - } - handler.OnExit(eventArgs); + // Assert - var metricsOutput = consoleOut.ToString(); + // flush when it reaches MaxMetrics + Assert.Contains( + "{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"Metric Name 1\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 2\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 3\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 4\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 5\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 6\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 7\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 8\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 9\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 10\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 11\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 12\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 13\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 14\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 15\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 16\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 17\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 18\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 19\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 20\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 21\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 22\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 23\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 24\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 25\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 26\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 27\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 28\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 29\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 30\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 31\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 32\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 33\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 34\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 35\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 36\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 37\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 38\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 39\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 40\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 41\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 42\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 43\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 44\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 45\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 46\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 47\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 48\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 49\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 50\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 51\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 52\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 53\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 54\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 55\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 56\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 57\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 58\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 59\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 60\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 61\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 62\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 63\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 64\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 65\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 66\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 67\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 68\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 69\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 70\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 71\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 72\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 73\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 74\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 75\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 76\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 77\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 78\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 79\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 80\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 81\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 82\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 83\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 84\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 85\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 86\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 87\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 88\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 89\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 90\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 91\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 92\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 93\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 94\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 95\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 96\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 97\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 98\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 99\",\"Unit\":\"Count\"},{\"Name\":\"Metric Name 100\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\"]]", + metricsOutput[0]); - // Assert // flush the (MaxMetrics + 1) item only - Assert.Contains("{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"Metric Name 101\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\"]", metricsOutput); - - // Reset - handler.ResetForTest(); + Assert.Contains( + "{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"Metric Name 101\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\"]", + metricsOutput[1]); } [Trait("Category", "EMFLimits")] [Fact] public void WhenMaxDataPointsAreAddedToTheSameMetric_FlushAutomatically() { - // Arrange - var methodName = Guid.NewGuid().ToString(); - var consoleOut = new StringWriter(); - Console.SetOut(consoleOut); - - var configurations = Substitute.For(); - - var metrics = new Metrics( - configurations, - nameSpace: "dotnet-powertools-test", - service: "testService" - ); - - var handler = new MetricsAspectHandler( - metrics, - false - ); - - var eventArgs = new AspectEventArgs { Name = methodName }; - // Act - handler.OnEntry(eventArgs); + _handler.MaxMetricsSameName(PowertoolsConfigurations.MaxMetrics); - for (var i = 0; i <= PowertoolsConfigurations.MaxMetrics; i++) - { - Metrics.AddMetric($"Metric Name", i, MetricUnit.Count); - if(i == PowertoolsConfigurations.MaxMetrics) - { - // flush when it reaches MaxMetrics - Assert.Contains( - "\"Service\":\"testService\",\"Metric Name\":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99]}", - consoleOut.ToString()); - } - } + var metricsOutput = _consoleOut.OutputValues; - handler.OnExit(eventArgs); + // Assert - var metricsOutput = consoleOut.ToString(); + // flush when it reaches MaxMetrics + Assert.Contains( + "\"Service\":\"testService\",\"Metric Name\":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99]}", + metricsOutput[0]); - // Assert // flush the (MaxMetrics + 1) item only - Assert.Contains("[[\"Service\"]]}]},\"Service\":\"testService\",\"Metric Name\":100}", metricsOutput); - - // Reset - handler.ResetForTest(); + Assert.Contains("[[\"Service\"]]}]},\"Service\":\"testService\",\"Metric Name\":100}", metricsOutput[1]); } [Trait("Category", "EMFLimits")] [Fact] public void WhenMoreThan9DimensionsAdded_ThrowArgumentOutOfRangeException() { - // Arrange - var methodName = Guid.NewGuid().ToString(); - var configurations = Substitute.For(); - - var metrics = new Metrics( - configurations, - nameSpace: "dotnet-powertools-test", - service: "testService" - ); - - var handler = new MetricsAspectHandler( - metrics, - false - ); - - var eventArgs = new AspectEventArgs { Name = methodName }; - // Act - handler.OnEntry(eventArgs); - - var act = () => - { - for (var i = 0; i <= 9; i++) - { - Metrics.AddDimension($"Dimension Name {i + 1}", $"Dimension Value {i + 1}"); - } - }; - - handler.OnExit(eventArgs); + var act = () => { _handler.MaxDimensions(9); }; // Assert Assert.Throws(act); - - // Reset - handler.ResetForTest(); } [Trait("Category", "SchemaValidation")] [Fact] public void WhenNamespaceNotDefined_ThrowSchemaValidationException() { - // Arrange - var methodName = Guid.NewGuid().ToString(); - var configurations = Substitute.For(); - - var metrics = new Metrics( - configurations - ); - - var handler = new MetricsAspectHandler( - metrics, - false - ); - - var eventArgs = new AspectEventArgs { Name = methodName }; - // Act - var act = () => - { - handler.OnEntry(eventArgs); - Metrics.AddMetric("TestMetric", 1, MetricUnit.Count); - handler.OnExit(eventArgs); - }; + var act = () => { _handler.NoNamespace(); }; // Assert var exception = Assert.Throws(act); Assert.Equal("EMF schema is invalid. 'namespace' is mandatory and not specified.", exception.Message); - - // RESET - handler.ResetForTest(); } [Trait("Category", "SchemaValidation")] [Fact] public void WhenDimensionsAreAdded_MustExistAsMembers() { - // Arrange - var methodName = Guid.NewGuid().ToString(); - var consoleOut = new StringWriter(); - Console.SetOut(consoleOut); - - var configurations = Substitute.For(); - - var metrics = new Metrics( - configurations, - nameSpace: "dotnet-powertools-test", - service: "testService" - ); - - var handler = new MetricsAspectHandler( - metrics, - false - ); - - var eventArgs = new AspectEventArgs { Name = methodName }; - // Act - handler.OnEntry(eventArgs); - Metrics.AddDimension("functionVersion", "$LATEST"); - Metrics.AddMetric("TestMetric", 1, MetricUnit.Count); - handler.OnExit(eventArgs); + _handler.AddDimensions(); - var result = consoleOut.ToString(); + var metricsOutput = _consoleOut.ToString(); // Assert Assert.Contains("\"Dimensions\":[[\"Service\"],[\"functionVersion\"]]" - , result); + , metricsOutput); Assert.Contains("\"Service\":\"testService\",\"functionVersion\":\"$LATEST\"" - , result); - - // Reset - handler.ResetForTest(); + , metricsOutput); } [Trait("Category", "MetricsImplementation")] [Fact] public void WhenNamespaceIsDefined_AbleToRetrieveNamespace() { - // Arrange - var methodName = Guid.NewGuid().ToString(); - var configurations = Substitute.For(); - var metrics = new Metrics(configurations); - - var handler = new MetricsAspectHandler( - metrics, - false - ); - - var eventArgs = new AspectEventArgs { Name = methodName }; - // Act - handler.OnEntry(eventArgs); - Metrics.SetNamespace("dotnet-powertools-test"); + _handler.AddMetric(); + + var metricsOutput = _consoleOut.ToString(); var result = Metrics.GetNamespace(); // Assert Assert.Equal("dotnet-powertools-test", result); - - // Reset - handler.ResetForTest(); + Assert.Contains("\"Namespace\":\"dotnet-powertools-test\"", metricsOutput); } [Trait("Category", "MetricsImplementation")] [Fact] public void WhenMetricsDefined_AbleToAddMetadata() { - // Arrange - var methodName = Guid.NewGuid().ToString(); - var consoleOut = new StringWriter(); - Console.SetOut(consoleOut); - - var configurations = Substitute.For(); - var metrics = new Metrics( - configurations, - nameSpace: "dotnet-powertools-test", - service: "testService" - ); - - var handler = new MetricsAspectHandler( - metrics, - false - ); - - var eventArgs = new AspectEventArgs { Name = methodName }; - // Act - handler.OnEntry(eventArgs); - Metrics.AddMetadata("test_metadata", "test_value"); - handler.OnExit(eventArgs); + _handler.AddMetadata(); - var result = consoleOut.ToString(); + var result = _consoleOut.ToString(); // Assert Assert.Contains("\"test_metadata\":\"test_value\"", result); - - // Reset - handler.ResetForTest(); } [Trait("Category", "MetricsImplementation")] @@ -399,78 +181,36 @@ public void WhenMetricsDefined_AbleToAddMetadata() public void WhenDefaultDimensionsSet_ValidInitialization() { // Arrange - var methodName = Guid.NewGuid().ToString(); - var consoleOut = new StringWriter(); - Console.SetOut(consoleOut); - - var defaultDimensions = new Dictionary { { "CustomDefaultDimension", "CustomDefaultDimensionValue" } }; - var configurations = Substitute.For(); - - var metrics = new Metrics( - configurations, - nameSpace: "dotnet-powertools-test", - service: "testService" - ); + var key = "CustomDefaultDimension"; + var value = "CustomDefaultDimensionValue"; - var handler = new MetricsAspectHandler( - metrics, - false - ); - - var eventArgs = new AspectEventArgs { Name = methodName }; + var defaultDimensions = new Dictionary { { key, value } }; // Act - handler.OnEntry(eventArgs); - Metrics.SetDefaultDimensions(defaultDimensions); - Metrics.AddMetric("TestMetric", 1, MetricUnit.Count); - handler.OnExit(eventArgs); + _handler.AddDefaultDimensions(defaultDimensions); - var result = consoleOut.ToString(); + var result = _consoleOut.ToString(); // Assert - Assert.Contains("\"Dimensions\":[[\"Service\"],[\"CustomDefaultDimension\"]", result); - Assert.Contains("\"CustomDefaultDimension\":\"CustomDefaultDimensionValue\"", result); - - // Reset - handler.ResetForTest(); + Assert.Contains($"\"Dimensions\":[[\"Service\"],[\"{key}\"]", result); + Assert.Contains($"\"CustomDefaultDimension\":\"{value}\"", result); } [Trait("Category", "MetricsImplementation")] [Fact] public void WhenMetricIsNegativeValue_ThrowException() { - // Arrange - var methodName = Guid.NewGuid().ToString(); - var configurations = Substitute.For(); - - var metrics = new Metrics( - configurations, - nameSpace: "dotnet-powertools-test", - service: "testService" - ); - - var handler = new MetricsAspectHandler( - metrics, - false - ); - - var eventArgs = new AspectEventArgs { Name = methodName }; - // Act var act = () => { const int metricValue = -1; - handler.OnEntry(eventArgs); - Metrics.AddMetric("TestMetric", metricValue, MetricUnit.Count); - handler.OnExit(eventArgs); + _handler.AddMetric("TestMetric", metricValue); }; // Assert var exception = Assert.Throws(act); - Assert.Equal("'AddMetric' method requires a valid metrics value. Value must be >= 0. (Parameter 'value')", exception.Message); - - // RESET - handler.ResetForTest(); + Assert.Equal("'AddMetric' method requires a valid metrics value. Value must be >= 0. (Parameter 'value')", + exception.Message); } [Trait("Category", "SchemaValidation")] @@ -478,248 +218,113 @@ public void WhenMetricIsNegativeValue_ThrowException() public void WhenDefaultDimensionSet_IgnoreDuplicates() { // Arrange - var methodName = Guid.NewGuid().ToString(); - var consoleOut = new StringWriter(); - Console.SetOut(consoleOut); - var configurations = Substitute.For(); - var defaultDimensions = new Dictionary { { "CustomDefaultDimension", "CustomDefaultDimensionValue" } }; - var metrics = new Metrics( - configurations, - nameSpace: "dotnet-powertools-test", - service: "testService" - ); + var defaultDimensions = new Dictionary + { { "CustomDefaultDimension", "CustomDefaultDimensionValue" } }; - var handler = new MetricsAspectHandler( - metrics, - false - ); - - var eventArgs = new AspectEventArgs { Name = methodName }; // Act - handler.OnEntry(eventArgs); - Metrics.SetDefaultDimensions(defaultDimensions); - Metrics.SetDefaultDimensions(defaultDimensions); - Metrics.AddMetric("TestMetric", 1, MetricUnit.Count); - handler.OnExit(eventArgs); + _handler.AddDefaultDimensionsTwice(defaultDimensions); - var result = consoleOut.ToString(); + var result = _consoleOut.ToString(); // Assert Assert.Contains("\"Dimensions\":[[\"Service\"],[\"CustomDefaultDimension\"]", result); Assert.Contains("\"CustomDefaultDimension\":\"CustomDefaultDimensionValue\"", result); - - // Reset - handler.ResetForTest(); } [Fact] public void WhenMetricsAndMetadataAdded_ValidateOutput() { - // Arrange - var methodName = Guid.NewGuid().ToString(); - var consoleOut = new StringWriter(); - Console.SetOut(consoleOut); - var configurations = Substitute.For(); - - var metrics = new Metrics( - configurations, - nameSpace: "dotnet-powertools-test", - service: "testService" - ); - - var handler = new MetricsAspectHandler( - metrics, - false - ); - - var eventArgs = new AspectEventArgs { Name = methodName }; - // Act - handler.OnEntry(eventArgs); - Metrics.AddDimension("functionVersion", "$LATEST"); - Metrics.AddMetric("Time", 100.7, MetricUnit.Milliseconds); - Metrics.AddMetadata("env", "dev"); - handler.OnExit(eventArgs); + _handler.AddDimensionMetricMetadata("Time", "env"); - var result = consoleOut.ToString(); + var result = _consoleOut.ToString(); // Assert - Assert.Contains("CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"Time\",\"Unit\":\"Milliseconds\"}],\"Dimensions\":[[\"Service\"],[\"functionVersion\"]]}]},\"Service\":\"testService\",\"functionVersion\":\"$LATEST\",\"Time\":100.7,\"env\":\"dev\"}" + Assert.Contains( + "CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"Time\",\"Unit\":\"Milliseconds\"}],\"Dimensions\":[[\"Service\"],[\"functionVersion\"]]}]},\"Service\":\"testService\",\"functionVersion\":\"$LATEST\",\"Time\":100.7,\"env\":\"dev\"}" , result); - - // Reset - handler.ResetForTest(); } - + [Fact] public void When_Metrics_And_Metadata_Added_With_Same_Key_Should_Only_Write_Metrics() { - // Arrange - var methodName = Guid.NewGuid().ToString(); - var consoleOut = new StringWriter(); - Console.SetOut(consoleOut); - var configurations = Substitute.For(); - - var metrics = new Metrics( - configurations, - nameSpace: "dotnet-powertools-test", - service: "testService" - ); - - var handler = new MetricsAspectHandler( - metrics, - false - ); - - var eventArgs = new AspectEventArgs { Name = methodName }; - // Act - handler.OnEntry(eventArgs); - Metrics.AddDimension("functionVersion", "$LATEST"); - Metrics.AddMetric("Time", 100.7, MetricUnit.Milliseconds); - Metrics.AddMetadata("Time", "dev"); - handler.OnExit(eventArgs); + _handler.AddDimensionMetricMetadata("Time", "Time"); - var result = consoleOut.ToString(); + var result = _consoleOut.ToString(); // Assert // No Metadata key was added - Assert.Contains("CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"Time\",\"Unit\":\"Milliseconds\"}],\"Dimensions\":[[\"Service\"],[\"functionVersion\"]]}]},\"Service\":\"testService\",\"functionVersion\":\"$LATEST\",\"Time\":100.7}" + Assert.Contains( + "CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"Time\",\"Unit\":\"Milliseconds\"}],\"Dimensions\":[[\"Service\"],[\"functionVersion\"]]}]},\"Service\":\"testService\",\"functionVersion\":\"$LATEST\",\"Time\":100.7}" , result); - - // Reset - handler.ResetForTest(); } [Trait("Category", "MetricsImplementation")] [Fact] public void WhenMetricsWithSameNameAdded_ValidateMetricArray() { - // Arrange - var methodName = Guid.NewGuid().ToString(); - var consoleOut = new StringWriter(); - Console.SetOut(consoleOut); - - var configurations = Substitute.For(); - - var metrics = new Metrics( - configurations, - nameSpace: "dotnet-powertools-test", - service: "testService" - ); - - var handler = new MetricsAspectHandler( - metrics, - false - ); - - var eventArgs = new AspectEventArgs { Name = methodName }; - // Act - handler.OnEntry(eventArgs); - Metrics.AddDimension("functionVersion", "$LATEST"); - Metrics.AddMetric("Time", 100.5, MetricUnit.Milliseconds); - Metrics.AddMetric("Time", 200, MetricUnit.Milliseconds); - handler.OnExit(eventArgs); + _handler.AddMetricSameName(); - var result = consoleOut.ToString(); + var result = _consoleOut.ToString(); // Assert Assert.Contains("\"Metrics\":[{\"Name\":\"Time\",\"Unit\":\"Milliseconds\"}]" , result); Assert.Contains("\"Time\":[100.5,200]" , result); - - // Reset - handler.ResetForTest(); } - + [Trait("Category", "MetricsImplementation")] [Fact] public void WhenMetricsWithStandardResolutionAdded_ValidateMetricArray() { - // Arrange - var methodName = Guid.NewGuid().ToString(); - var consoleOut = new StringWriter(); - Console.SetOut(consoleOut); - - var configurations = Substitute.For(); - - var metrics = new Metrics( - configurations, - nameSpace: "dotnet-powertools-test", - service: "testService" - ); - - var handler = new MetricsAspectHandler( - metrics, - false - ); - - var eventArgs = new AspectEventArgs { Name = methodName }; - // Act - handler.OnEntry(eventArgs); - Metrics.AddDimension("functionVersion", "$LATEST"); - Metrics.AddMetric("Time", 100.5, MetricUnit.Milliseconds, MetricResolution.Standard); - handler.OnExit(eventArgs); + _handler.AddMetric("Time", 100.5, MetricUnit.Milliseconds, MetricResolution.Standard); - var result = consoleOut.ToString(); + var result = _consoleOut.ToString(); // Assert Assert.Contains("\"Metrics\":[{\"Name\":\"Time\",\"Unit\":\"Milliseconds\",\"StorageResolution\":60}]" , result); Assert.Contains("\"Time\":100.5" , result); - - // Reset - handler.ResetForTest(); } - + [Trait("Category", "MetricsImplementation")] [Fact] public void WhenMetricsWithHighResolutionAdded_ValidateMetricArray() { - // Arrange - var methodName = Guid.NewGuid().ToString(); - var consoleOut = new StringWriter(); - Console.SetOut(consoleOut); - - var configurations = Substitute.For(); - - var metrics = new Metrics( - configurations, - nameSpace: "dotnet-powertools-test", - service: "testService" - ); - - var handler = new MetricsAspectHandler( - metrics, - false - ); - - var eventArgs = new AspectEventArgs { Name = methodName }; - // Act - handler.OnEntry(eventArgs); - Metrics.AddDimension("functionVersion", "$LATEST"); - Metrics.AddMetric("Time", 100.5, MetricUnit.Milliseconds, MetricResolution.High); - handler.OnExit(eventArgs); + _handler.AddMetric("Time", 100.5, MetricUnit.Milliseconds, MetricResolution.High); - var result = consoleOut.ToString(); + var result = _consoleOut.ToString(); // Assert Assert.Contains("\"Metrics\":[{\"Name\":\"Time\",\"Unit\":\"Milliseconds\",\"StorageResolution\":1}]" , result); Assert.Contains("\"Time\":100.5" , result); + } + + [Fact] + public async Task WhenMetricsAsyncRaceConditionItemSameKeyExists_ValidateLock() + { + // Act + await _handler.RaceConditon(); + + var metricsOutput = _consoleOut.ToString(); - // Reset - handler.ResetForTest(); + // Assert + Assert.Contains( + "{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"Metric Name\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\"]]", + metricsOutput); } + #region Helpers private List AllIndexesOf(string str, string value) @@ -728,7 +333,7 @@ private List AllIndexesOf(string str, string value) if (string.IsNullOrEmpty(value)) return indexes; - for (var index = 0; ; index += value.Length) + for (var index = 0;; index += value.Length) { index = str.IndexOf(value, index, StringComparison.Ordinal); if (index == -1) @@ -737,52 +342,24 @@ private List AllIndexesOf(string str, string value) } } - #endregion - - [Fact] - public async Task WhenMetricsAsyncRaceConditionItemSameKeyExists_ValidateLock() + private class CustomConsoleWriter : StringWriter { - // Arrange - var methodName = Guid.NewGuid().ToString(); - var consoleOut = new StringWriter(); - Console.SetOut(consoleOut); + public readonly List OutputValues = new(); - var configurations = Substitute.For(); - - var metrics = new Metrics(configurations, - nameSpace: "dotnet-powertools-test", - service: "testService"); - - var handler = new MetricsAspectHandler(metrics, - false); - - var eventArgs = new AspectEventArgs { Name = methodName }; - - // Act - handler.OnEntry(eventArgs); - - var tasks = new List(); - for (var i = 0; i < 100; i++) + public override void WriteLine(string value) { - tasks.Add(Task.Run(() => - { - Metrics.AddMetric($"Metric Name", 0, MetricUnit.Count); - })); + OutputValues.Add(value); + base.WriteLine(value); } + } - await Task.WhenAll(tasks); - - - handler.OnExit(eventArgs); - - var metricsOutput = consoleOut.ToString(); + #endregion - // Assert - Assert.Contains("{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"Metric Name\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\"]]", - metricsOutput); - // Reset - handler.ResetForTest(); + public void Dispose() + { + // need to reset instance after each test + MetricsAspect.ResetForTest(); } } -} +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/ExceptionFunctionHandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/ExceptionFunctionHandlerTests.cs index 0a1aad17..be036e16 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/ExceptionFunctionHandlerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/ExceptionFunctionHandlerTests.cs @@ -34,7 +34,7 @@ public async Task Stack_Trace_Included_When_Decorator_Present_In_Method() // Assert var tracedException = await Assert.ThrowsAsync(Handle); - Assert.StartsWith("at AWS.Lambda.Powertools.Metrics.Tests.Handlers.ExceptionFunctionHandler.__a$_around_ThisThrows", tracedException.StackTrace?.TrimStart()); + Assert.StartsWith("at AWS.Lambda.Powertools.Metrics.Tests.Handlers.ExceptionFunctionHandler.ThisThrowsDecorated()", tracedException.StackTrace?.TrimStart()); } [Fact] 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 271b2e21..be9f5e9f 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs @@ -14,6 +14,8 @@ */ using System; +using System.Collections; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; @@ -22,6 +24,108 @@ namespace AWS.Lambda.Powertools.Metrics.Tests.Handlers; public class FunctionHandler { + [Metrics(Namespace = "dotnet-powertools-test", Service = "testService", CaptureColdStart = true)] + public void AddMetric(string name = "TestMetric", double value =1, MetricUnit unit = MetricUnit.Count,MetricResolution resolution = MetricResolution.Default) + { + Metrics.AddMetric(name, value, unit, resolution); + } + + [Metrics(Namespace = "dotnet-powertools-test", Service = "testService")] + public void AddDimensions() + { + Metrics.AddDimension("functionVersion", "$LATEST"); + Metrics.AddMetric("TestMetric", 1, MetricUnit.Count); + } + + [Metrics(Namespace = "dotnet-powertools-test", Service = "testService")] + public void ClearDimensions() + { + Metrics.ClearDefaultDimensions(); + Metrics.AddMetric("Metric Name", 1, MetricUnit.Count); + } + + [Metrics(Namespace = "dotnet-powertools-test", Service = "testService")] + public void MaxMetrics(int maxMetrics) + { + for (var i = 0; i <= maxMetrics; i++) + { + Metrics.AddMetric($"Metric Name {i + 1}", i, MetricUnit.Count); + } + } + + [Metrics(Namespace = "dotnet-powertools-test", Service = "testService")] + public void MaxMetricsSameName(int maxMetrics) + { + for (var i = 0; i <= maxMetrics; i++) + { + Metrics.AddMetric("Metric Name", i, MetricUnit.Count); + } + } + + [Metrics(Namespace = "dotnet-powertools-test", Service = "testService")] + public void MaxDimensions(int maxDimensions) + { + for (var i = 0; i <= maxDimensions; i++) + { + Metrics.AddDimension($"Dimension Name {i + 1}", $"Dimension Value {i + 1}"); + } + } + + [Metrics(Service = "testService")] + public void NoNamespace() + { + Metrics.AddMetric("TestMetric", 1, MetricUnit.Count); + } + + [Metrics(Namespace = "dotnet-powertools-test", Service = "testService")] + public void AddMetadata() + { + Metrics.AddMetadata("test_metadata", "test_value"); + } + + [Metrics(Namespace = "dotnet-powertools-test", Service = "testService")] + public void AddDefaultDimensions(Dictionary defaultDimensions) + { + Metrics.SetDefaultDimensions(defaultDimensions); + Metrics.AddMetric("TestMetric", 1, MetricUnit.Count); + } + + [Metrics(Namespace = "dotnet-powertools-test", Service = "testService")] + public void AddDefaultDimensionsTwice(Dictionary defaultDimensions) + { + Metrics.SetDefaultDimensions(defaultDimensions); + Metrics.SetDefaultDimensions(defaultDimensions); + Metrics.AddMetric("TestMetric", 1, MetricUnit.Count); + } + + [Metrics(Namespace = "dotnet-powertools-test", Service = "testService")] + public void AddDimensionMetricMetadata(string metricKey, string metadataKey) + { + Metrics.AddDimension("functionVersion", "$LATEST"); + Metrics.AddMetric(metricKey, 100.7, MetricUnit.Milliseconds); + Metrics.AddMetadata(metadataKey, "dev"); + } + + [Metrics(Namespace = "dotnet-powertools-test", Service = "testService")] + public void AddMetricSameName() + { + Metrics.AddDimension("functionVersion", "$LATEST"); + Metrics.AddMetric("Time", 100.5, MetricUnit.Milliseconds); + Metrics.AddMetric("Time", 200, MetricUnit.Milliseconds); + } + + [Metrics(Namespace = "dotnet-powertools-test", Service = "testService")] + public async Task RaceConditon() + { + var tasks = new List(); + for (var i = 0; i < 100; i++) + { + tasks.Add(Task.Run(() => { Metrics.AddMetric($"Metric Name", 0, MetricUnit.Count); })); + } + + await Task.WhenAll(tasks); + } + [Metrics(Namespace = "ns", Service = "svc")] public async Task HandleSameKey(string input) { @@ -32,18 +136,18 @@ public async Task HandleSameKey(string input) return input.ToUpper(CultureInfo.InvariantCulture); } - + [Metrics(Namespace = "ns", Service = "svc")] public async Task HandleTestSecondCall(string input) { Metrics.AddMetric("MyMetric", 1); Metrics.AddMetadata("MyMetadata", "meta"); - + await Task.Delay(1); return input.ToUpper(CultureInfo.InvariantCulture); } - + [Metrics(Namespace = "ns", Service = "svc")] public async Task HandleMultipleThreads(string input) { From f1fd08d047884fcb15d549af74eb107fd83d6338 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 27 Jun 2024 00:44:31 +0100 Subject: [PATCH 3/9] missing mock implementation of SetOut --- .../Utilities/SystemWrapperMock.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/SystemWrapperMock.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/SystemWrapperMock.cs index 4af6e593..9293f6e1 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/SystemWrapperMock.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/SystemWrapperMock.cs @@ -13,6 +13,7 @@ * permissions and limitations under the License. */ +using System.IO; using AWS.Lambda.Powertools.Common; namespace AWS.Lambda.Powertools.Logging.Tests.Utilities; @@ -59,4 +60,9 @@ public void SetEnvironmentVariable(string variable, string value) public void SetExecutionEnvironment(T type) { } + + public void SetOut(TextWriter writeTo) + { + + } } \ No newline at end of file From 9d8f93af31ddf4a95d0569262c28a5d2d93380aa Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:16:49 +0100 Subject: [PATCH 4/9] add AOT support text to docs --- docs/core/metrics.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/core/metrics.md b/docs/core/metrics.md index 272c40b1..610d9147 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -13,6 +13,7 @@ These metrics can be visualized through [Amazon CloudWatch Console](https://aws. * Validating your metrics against common metric definitions mistakes (for example, metric unit, values, max dimensions, max metrics) * Metrics are created asynchronously by the CloudWatch service. You do not need any custom stacks, and there is no impact to Lambda function latency * Context manager to create a one off metric with a different dimension +* Ahead of Time compilation support [AOT](https://docs.aws.amazon.com/lambda/latest/dg/dotnet-native-aot.html) from version 1.7.0
From a91c91202746925371a160ea0b822d41b38280f7 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:34:08 +0100 Subject: [PATCH 5/9] update doc --- docs/core/metrics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/metrics.md b/docs/core/metrics.md index 610d9147..65fb5f50 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -13,7 +13,7 @@ These metrics can be visualized through [Amazon CloudWatch Console](https://aws. * Validating your metrics against common metric definitions mistakes (for example, metric unit, values, max dimensions, max metrics) * Metrics are created asynchronously by the CloudWatch service. You do not need any custom stacks, and there is no impact to Lambda function latency * Context manager to create a one off metric with a different dimension -* Ahead of Time compilation support [AOT](https://docs.aws.amazon.com/lambda/latest/dg/dotnet-native-aot.html) from version 1.7.0 +* 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
From ae64408caf80d7c3795ef950d61e92a26bc7c585 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:18:13 +0100 Subject: [PATCH 6/9] remove comments --- .../MetricsAttribute.cs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs index 644167d7..fc507a03 100644 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs @@ -142,17 +142,4 @@ public class MetricsAttribute : Attribute RaiseOnEmptyMetrics, CaptureColdStart ); - // - // /// - // /// Creates the handler. - // /// - // /// IMethodAspectHandler. - // protected override IMethodAspectHandler CreateHandler() - // { - // return new MetricsAspectHandler - // ( - // MetricsInstance, - // CaptureColdStart - // ); - // } } From e46b90f8efde72bfa942ae43aeb799d041cae408 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 2 Jul 2024 15:11:30 +0100 Subject: [PATCH 7/9] move metrics instanciation to aspect. reset test after run --- .../Internal/MetricsAspect.cs | 45 ++++++++++++------- .../MetricsAttribute.cs | 12 ----- .../Handlers/ExceptionFunctionHandlerTests.cs | 7 ++- .../Handlers/FunctionHandlerTests.cs | 8 +++- 4 files changed, 41 insertions(+), 31 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs index 1e60af6b..abbefc5f 100644 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs @@ -39,13 +39,17 @@ public class MetricsAspect ///
private bool _clearLambdaContext; - private IMetrics _metricsInstance; + /// + /// Gets the metrics instance. + /// + /// The metrics instance. + private static IMetrics _metricsInstance; static MetricsAspect() { _isColdStart = true; } - + /// /// Runs before the execution of the method marked with the Metrics Attribute /// @@ -66,13 +70,18 @@ public void Before( [Argument(Source.ReturnType)] Type returnType, [Argument(Source.Triggers)] Attribute[] triggers) { - // Before running Function - + var trigger = triggers.OfType().First(); - - _metricsInstance = trigger.MetricsInstance; - + + _metricsInstance ??= new Metrics( + PowertoolsConfigurations.Instance, + trigger.Namespace, + trigger.Service, + trigger.RaiseOnEmptyMetrics, + trigger.CaptureColdStart + ); + var eventArgs = new AspectEventArgs { Instance = instance, @@ -112,25 +121,27 @@ public void Before( ); } } - + /// /// OnExit runs after the execution of the method marked with the Metrics Attribute /// [Advice(Kind.After)] - public void Exit() { + public void Exit() + { _metricsInstance.Flush(); if (_clearLambdaContext) PowertoolsLambdaContext.Clear(); } - - + + /// /// Reset the aspect for testing purposes. /// - internal static void ResetForTest() - { - _isColdStart = true; - Metrics.ResetForTest(); - PowertoolsLambdaContext.Clear(); - } + internal static void ResetForTest() + { + _metricsInstance = null; + _isColdStart = true; + Metrics.ResetForTest(); + PowertoolsLambdaContext.Clear(); + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs index fc507a03..761bf7bd 100644 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs @@ -130,16 +130,4 @@ public class MetricsAttribute : Attribute ///
/// true if [raise on empty metrics]; otherwise, false. public bool RaiseOnEmptyMetrics { get; set; } - - /// - /// Gets the metrics instance. - /// - /// The metrics instance. - internal IMetrics MetricsInstance => new Metrics( - PowertoolsConfigurations.Instance, - Namespace, - Service, - RaiseOnEmptyMetrics, - CaptureColdStart - ); } diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/ExceptionFunctionHandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/ExceptionFunctionHandlerTests.cs index be036e16..cdda3c52 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/ExceptionFunctionHandlerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/ExceptionFunctionHandlerTests.cs @@ -5,7 +5,7 @@ namespace AWS.Lambda.Powertools.Metrics.Tests.Handlers; [Collection("Sequential")] -public sealed class ExceptionFunctionHandlerTests +public sealed class ExceptionFunctionHandlerTests : IDisposable { [Fact] public async Task Stack_Trace_Included_When_Decorator_Present() @@ -51,4 +51,9 @@ public async Task Decorator_In_Non_Handler_Method_Does_Not_Throw_Exception() var tracedException = await Record.ExceptionAsync(Handle); Assert.Null(tracedException); } + + public void Dispose() + { + MetricsAspect.ResetForTest(); + } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs index 4afba753..a51fc0c2 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs @@ -13,13 +13,14 @@ * permissions and limitations under the License. */ +using System; using System.Threading.Tasks; using Xunit; namespace AWS.Lambda.Powertools.Metrics.Tests.Handlers; [Collection("Sequential")] -public class FunctionHandlerTests +public class FunctionHandlerTests : IDisposable { [Fact] public async Task When_Metrics_Add_Metadata_Same_Key_Should_Ignore_Metadata() @@ -61,4 +62,9 @@ public async Task When_Metrics_Add_Metadata_FromMultipleThread_Should_Not_Throw_ var exception = await Record.ExceptionAsync(() => handler.HandleMultipleThreads("whatever")); Assert.Null(exception); } + + public void Dispose() + { + MetricsAspect.ResetForTest(); + } } \ No newline at end of file From 4b392da278269d47041207603bbf1c7c4fed27c7 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Wed, 3 Jul 2024 12:36:33 +0100 Subject: [PATCH 8/9] Update version.json --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 7f866299..ec7a6148 100644 --- a/version.json +++ b/version.json @@ -1,7 +1,7 @@ { "Core": { "Logging": "1.5.1", - "Metrics": "1.6.2", + "Metrics": "1.7.0", "Tracing": "1.4.2" }, "Utilities": { From d666ab22edfc835cb2313baa719d67c762ad10b0 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:05:43 +0100 Subject: [PATCH 9/9] add test with lambdacontext, refactor --- ...AWS.Lambda.Powertools.Metrics.Tests.csproj | 2 + .../EMFValidationTests.cs | 13 +---- .../Handlers/FunctionHandler.cs | 14 +++++ .../Handlers/FunctionHandlerTests.cs | 55 ++++++++++++++----- .../Utils.cs | 30 ++++++++++ 5 files changed, 88 insertions(+), 26 deletions(-) create mode 100644 libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Utils.cs diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/AWS.Lambda.Powertools.Metrics.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/AWS.Lambda.Powertools.Metrics.Tests.csproj index f6fd9f8d..16a4e0ce 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/AWS.Lambda.Powertools.Metrics.Tests.csproj +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/AWS.Lambda.Powertools.Metrics.Tests.csproj @@ -10,6 +10,8 @@ + + diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs index bcf2336d..cd89ced5 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs @@ -341,18 +341,7 @@ private List AllIndexesOf(string str, string value) indexes.Add(index); } } - - private class CustomConsoleWriter : StringWriter - { - public readonly List OutputValues = new(); - - public override void WriteLine(string value) - { - OutputValues.Add(value); - base.WriteLine(value); - } - } - + #endregion 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 be9f5e9f..8ae59b9f 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs @@ -19,6 +19,8 @@ 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; @@ -159,4 +161,16 @@ await Parallel.ForEachAsync(Enumerable.Range(0, Environment.ProcessorCount * 2), return input.ToUpper(CultureInfo.InvariantCulture); } + + [Metrics(Namespace = "ns", Service = "svc", CaptureColdStart = true)] + public void HandleWithLambdaContext(ILambdaContext context) + { + + } + + [Metrics(Namespace = "ns", Service = "svc")] + public void HandleWithLambdaContextAndMetrics(TestLambdaContext context) + { + Metrics.AddMetric("MyMetric", 1); + } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs index a51fc0c2..556d6cce 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs @@ -15,6 +15,8 @@ using System; using System.Threading.Tasks; +using Amazon.Lambda.TestUtilities; +using AWS.Lambda.Powertools.Common; using Xunit; namespace AWS.Lambda.Powertools.Metrics.Tests.Handlers; @@ -22,15 +24,24 @@ namespace AWS.Lambda.Powertools.Metrics.Tests.Handlers; [Collection("Sequential")] public class FunctionHandlerTests : IDisposable { + private readonly FunctionHandler _handler; + private readonly CustomConsoleWriter _consoleOut; + + public FunctionHandlerTests() + { + _handler = new FunctionHandler(); + _consoleOut = new CustomConsoleWriter(); + SystemWrapper.Instance.SetOut(_consoleOut); + } + [Fact] public async Task When_Metrics_Add_Metadata_Same_Key_Should_Ignore_Metadata() { // Arrange - Metrics.ResetForTest(); - var handler = new FunctionHandler(); + // Act - var exception = await Record.ExceptionAsync( () => handler.HandleSameKey("whatever")); + var exception = await Record.ExceptionAsync( () => _handler.HandleSameKey("whatever")); // Assert Assert.Null(exception); @@ -39,32 +50,48 @@ public async Task When_Metrics_Add_Metadata_Same_Key_Should_Ignore_Metadata() [Fact] public async Task When_Metrics_Add_Metadata_Second_Invocation_Should_Not_Throw_Exception() { - // Arrange - Metrics.ResetForTest(); - var handler = new FunctionHandler(); - // Act - var exception = await Record.ExceptionAsync( () => handler.HandleTestSecondCall("whatever")); + var exception = await Record.ExceptionAsync( () => _handler.HandleTestSecondCall("whatever")); Assert.Null(exception); - exception = await Record.ExceptionAsync( () => handler.HandleTestSecondCall("whatever")); + exception = await Record.ExceptionAsync( () => _handler.HandleTestSecondCall("whatever")); Assert.Null(exception); } [Fact] public async Task When_Metrics_Add_Metadata_FromMultipleThread_Should_Not_Throw_Exception() { - // Arrange - Metrics.ResetForTest(); - var handler = new FunctionHandler(); - // Act - var exception = await Record.ExceptionAsync(() => handler.HandleMultipleThreads("whatever")); + var exception = await Record.ExceptionAsync(() => _handler.HandleMultipleThreads("whatever")); Assert.Null(exception); } + + [Fact] + public void When_LambdaContext_Should_Add_FunctioName_Dimension_CaptureColdStart() + { + // Arrange + var context = new TestLambdaContext + { + FunctionName = "My Function with context" + }; + + // Act + _handler.HandleWithLambdaContext(context); + var metricsOutput = _consoleOut.ToString(); + + // Assert + Assert.Contains( + "\"FunctionName\":\"My Function with context\"", + metricsOutput); + + Assert.Contains( + "\"Metrics\":[{\"Name\":\"ColdStart\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"FunctionName\"],[\"Service\"]]}]}", + metricsOutput); + } public void Dispose() { + Metrics.ResetForTest(); MetricsAspect.ResetForTest(); } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Utils.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Utils.cs new file mode 100644 index 00000000..63fa1d4d --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Utils.cs @@ -0,0 +1,30 @@ +/* + * 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.Collections.Generic; +using System.IO; + +namespace AWS.Lambda.Powertools.Metrics.Tests; + +public class CustomConsoleWriter : StringWriter +{ + public readonly List OutputValues = new(); + + public override void WriteLine(string value) + { + OutputValues.Add(value); + base.WriteLine(value); + } +} \ No newline at end of file