diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ae6c9fac5..8ced008c3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup .NET 6.0 & 8.0 uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # 4.0.0 with: @@ -32,10 +32,9 @@ jobs: - name: Test & Code Coverage run: dotnet test --collect:"XPlat Code Coverage" --results-directory ./codecov --verbosity normal - name: Codecov - uses: codecov/codecov-action@5ecb98a3c6b747ed38dc09f787459979aebb39be # 4.3.1 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # 4.5.0 with: + token: ${{ secrets.CODECOV_TOKEN }} flags: unittests fail_ci_if_error: false name: codecov-lambda-powertools-dotnet diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 598b85b48..807f77770 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 89dc3a19a..76d1f9692 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest environment: Docs steps: - - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - name: Set up Python @@ -52,7 +52,7 @@ jobs: runs-on: ubuntu-latest environment: Docs steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup .NET 6.0 uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a with: diff --git a/.github/workflows/label_pr_on_title.yml b/.github/workflows/label_pr_on_title.yml index 3815a49e9..25c2c04fe 100644 --- a/.github/workflows/label_pr_on_title.yml +++ b/.github/workflows/label_pr_on_title.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: "Label PR based on title" uses: actions/github-script@v6 env: diff --git a/.github/workflows/on_label_added.yml b/.github/workflows/on_label_added.yml index e9180d801..9e0f1b734 100644 --- a/.github/workflows/on_label_added.yml +++ b/.github/workflows/on_label_added.yml @@ -23,7 +23,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 # Maintenance: Persist state per PR as an artifact to avoid spam on label add - name: "Suggest split large Pull Request" uses: actions/github-script@v6 diff --git a/.github/workflows/on_merged_pr.yml b/.github/workflows/on_merged_pr.yml index cd97e1c30..f1717fe15 100644 --- a/.github/workflows/on_merged_pr.yml +++ b/.github/workflows/on_merged_pr.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest if: needs.get_pr_details.outputs.prIsMerged == 'true' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: "Label PR related issue for release" uses: actions/github-script@v6 env: diff --git a/.github/workflows/on_opened_pr.yml b/.github/workflows/on_opened_pr.yml index 6c5979c8b..3d6a538d4 100644 --- a/.github/workflows/on_opened_pr.yml +++ b/.github/workflows/on_opened_pr.yml @@ -19,7 +19,7 @@ jobs: needs: get_pr_details runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: "Ensure related issue is present" uses: actions/github-script@v6 env: diff --git a/.github/workflows/record_pr.yml b/.github/workflows/record_pr.yml index 44f445a70..d84d4414c 100644 --- a/.github/workflows/record_pr.yml +++ b/.github/workflows/record_pr.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: "Extract PR details" uses: actions/github-script@v6 with: diff --git a/.github/workflows/reusable_export_pr_details.yml b/.github/workflows/reusable_export_pr_details.yml index 84fc398f5..d85e384c6 100644 --- a/.github/workflows/reusable_export_pr_details.yml +++ b/.github/workflows/reusable_export_pr_details.yml @@ -53,7 +53,7 @@ jobs: prIsMerged: ${{ steps.prIsMerged.outputs.prIsMerged }} steps: - name: Checkout repository # in case caller workflow doesn't checkout thus failing with file not found - uses: actions/checkout@v3 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: "Download previously saved PR" uses: actions/github-script@v6 env: diff --git a/.github/workflows/reusable_publish_changelog.yml b/.github/workflows/reusable_publish_changelog.yml index ecfd71553..ac133cb57 100644 --- a/.github/workflows/reusable_publish_changelog.yml +++ b/.github/workflows/reusable_publish_changelog.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository # reusable workflows start clean, so we need to checkout again - uses: actions/checkout@v3 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - name: Git client setup and refresh tip diff --git a/.github/workflows/reusable_publish_docs.yml b/.github/workflows/reusable_publish_docs.yml index faa5b5ef4..168ddd3e7 100644 --- a/.github/workflows/reusable_publish_docs.yml +++ b/.github/workflows/reusable_publish_docs.yml @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest environment: Docs steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - name: Install poetry @@ -86,7 +86,7 @@ jobs: runs-on: macos-latest environment: Docs steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - name: Configure and build api docs generator diff --git a/.github/workflows/secure_workflows.yml b/.github/workflows/secure_workflows.yml index dcc4eb521..c0de2c759 100644 --- a/.github/workflows/secure_workflows.yml +++ b/.github/workflows/secure_workflows.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Ensure 3rd party workflows have SHA pinned uses: zgosalvez/github-actions-ensure-sha-pinned-actions@6ca5574367befbc9efdb2fa25978084159c5902d # v1.3.0 with: diff --git a/docs/core/metrics.md b/docs/core/metrics.md index 272c40b18..65fb5f50e 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 to native code support [AOT](https://docs.aws.amazon.com/lambda/latest/dg/dotnet-native-aot.html) from version 1.7.0
diff --git a/examples/Metrics/src/HelloWorld/HelloWorld.csproj b/examples/Metrics/src/HelloWorld/HelloWorld.csproj index 9116bbec3..ccbcfc12a 100644 --- a/examples/Metrics/src/HelloWorld/HelloWorld.csproj +++ b/examples/Metrics/src/HelloWorld/HelloWorld.csproj @@ -9,7 +9,7 @@ - + diff --git a/examples/ServerlessApi/src/LambdaPowertoolsAPI/LambdaPowertoolsAPI.csproj b/examples/ServerlessApi/src/LambdaPowertoolsAPI/LambdaPowertoolsAPI.csproj index 100f2d2c6..5a3c115ba 100644 --- a/examples/ServerlessApi/src/LambdaPowertoolsAPI/LambdaPowertoolsAPI.csproj +++ b/examples/ServerlessApi/src/LambdaPowertoolsAPI/LambdaPowertoolsAPI.csproj @@ -14,7 +14,7 @@ - + diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Aspects/UniversalWrapperAspect.cs b/libraries/src/AWS.Lambda.Powertools.Common/Aspects/UniversalWrapperAspect.cs index 8061ef7d0..c4b014682 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 98abe1b5e..8a0359843 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 6389b3a03..8f42bda4c 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 77110d096..0e90e99b0 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 @@ -11,8 +11,4 @@ - - - - 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 000000000..abbefc5f6 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs @@ -0,0 +1,147 @@ +/* + * 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; + + /// + /// 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 + /// + /// + /// + /// + /// + /// + /// + /// + [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 ??= new Metrics( + PowertoolsConfigurations.Instance, + trigger.Namespace, + trigger.Service, + trigger.RaiseOnEmptyMetrics, + trigger.CaptureColdStart + ); + + 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() + { + _metricsInstance = null; + _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 6209f97a1..000000000 --- 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 d0025fc4f..761bf7bd0 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. @@ -129,30 +130,4 @@ public class MetricsAttribute : MethodAspectAttribute /// /// true if [raise on empty metrics]; otherwise, false. public bool RaiseOnEmptyMetrics { get; set; } - - /// - /// Gets the metrics instance. - /// - /// The metrics instance. - private IMetrics MetricsInstance => - _metricsInstance ??= new Metrics( - PowertoolsConfigurations.Instance, - Namespace, - Service, - RaiseOnEmptyMetrics, - CaptureColdStart - ); - - /// - /// Creates the handler. - /// - /// IMethodAspectHandler. - protected override IMethodAspectHandler CreateHandler() - { - return new MetricsAspectHandler - ( - MetricsInstance, - CaptureColdStart - ); - } } diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricResolution.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricResolution.cs index 3c6536ff2..2112c1c5d 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 fd4b97602..132b10f38 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 a77838195..496606cd1 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 000000000..e8a421ac8 --- /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 28c0d54b9..242ee90b2 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 0260344d5..3a1d85f2a 100644 --- a/libraries/src/Directory.Build.props +++ b/libraries/src/Directory.Build.props @@ -19,6 +19,14 @@ + + + + true + true + true + + 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 4af6e5933..9293f6e14 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 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 f6fd9f8d8..16a4e0ce6 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/ClearDimensionsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/ClearDimensionsTests.cs index 879fdfac8..0a46d6fd3 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 72fd1a541..cd89ced5b 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 handler = new MetricsAspectHandler( - metrics, - false - ); + var defaultDimensions = new Dictionary + { { "CustomDefaultDimension", "CustomDefaultDimensionValue" } }; - 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(); - // Reset - handler.ResetForTest(); + var metricsOutput = _consoleOut.ToString(); + + // 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) @@ -736,53 +341,14 @@ private List AllIndexesOf(string str, string value) indexes.Add(index); } } - - #endregion - [Fact] - public async Task WhenMetricsAsyncRaceConditionItemSameKeyExists_ValidateLock() - { - // 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); - - 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); - - - handler.OnExit(eventArgs); + #endregion - var metricsOutput = consoleOut.ToString(); - // 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 0a1aad173..cdda3c52d 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() @@ -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] @@ -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/FunctionHandler.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs index 271b2e21d..8ae59b9f0 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandler.cs @@ -14,14 +14,120 @@ */ 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; 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 +138,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) { @@ -55,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 4afba753b..556d6cce8 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs @@ -13,23 +13,35 @@ * permissions and limitations under the License. */ +using System; using System.Threading.Tasks; +using Amazon.Lambda.TestUtilities; +using AWS.Lambda.Powertools.Common; using Xunit; namespace AWS.Lambda.Powertools.Metrics.Tests.Handlers; [Collection("Sequential")] -public class FunctionHandlerTests +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); @@ -38,27 +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 000000000..63fa1d4d0 --- /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 diff --git a/poetry.lock b/poetry.lock index 259e15ae4..a2ed41a1d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "click" @@ -94,13 +94,13 @@ testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packag [[package]] name = "jinja2" -version = "3.1.3" +version = "3.1.4" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, - {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, ] [package.dependencies] diff --git a/version.json b/version.json index 7f8662990..ec7a61487 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": {