From 577d1e933b8aa085f4f5b2b509273572550a30ae Mon Sep 17 00:00:00 2001 From: jamesuk Date: Tue, 4 Jul 2023 17:59:00 +0100 Subject: [PATCH 1/4] #212 Add native AOT support --- .../IPowerToolsSerializer.cs | 25 + .../SourceGeneratedSerializer.cs | 96 ++ .../SystemTextJsonSerializer.cs | 20 + .../Internal/Converters/ExceptionConverter.cs | 11 +- .../Internal/LoggingAspectHandler.cs | 5 +- .../Internal/PowertoolsLogger.cs | 11 +- .../AWS.Lambda.Powertools.Logging/Logger.cs | 17 + .../LoggerConfiguration.cs | 2 + .../AWS.Lambda.Powertools.Metrics/Metrics.cs | 3 + .../MetricsAttribute.cs | 2 + .../Model/MetricsContext.cs | 13 +- .../Model/RootNode.cs | 9 +- .../LoggingAttributeTest.cs | 5 + .../PowertoolsLoggerTest.cs | 6 + ...wertoolsLoggerWithSourceGenerationTests.cs | 1269 +++++++++++++++++ .../EMFValidationTests.cs | 5 +- .../EMFValidationWithSourceGenerationTests.cs | 773 ++++++++++ 17 files changed, 2264 insertions(+), 8 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.Common/JsonSerialization/IPowerToolsSerializer.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Common/JsonSerialization/SourceGeneratedSerializer.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Common/JsonSerialization/SystemTextJsonSerializer.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerWithSourceGenerationTests.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationWithSourceGenerationTests.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Common/JsonSerialization/IPowerToolsSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Common/JsonSerialization/IPowerToolsSerializer.cs new file mode 100644 index 00000000..792a721e --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Common/JsonSerialization/IPowerToolsSerializer.cs @@ -0,0 +1,25 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +namespace AWS.Lambda.Powertools.Common; + +using System.Text.Json; + +public interface IPowerToolsSerializer +{ + void InternalSerialize(Utf8JsonWriter writer, T response, JsonSerializerOptions options = null); + + string InternalSerializeAsString(T response, JsonSerializerOptions options = null); +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/JsonSerialization/SourceGeneratedSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Common/JsonSerialization/SourceGeneratedSerializer.cs new file mode 100644 index 00000000..521af102 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Common/JsonSerialization/SourceGeneratedSerializer.cs @@ -0,0 +1,96 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +namespace AWS.Lambda.Powertools.Common; + +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +public class SourceGeneratedSerializer<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TSGContext> : IPowerToolsSerializer where TSGContext : JsonSerializerContext +{ + TSGContext _jsonSerializerContext; + + /// + public void InternalSerialize(Utf8JsonWriter writer, T response, JsonSerializerOptions options) + { + try + { + if (this._jsonSerializerContext == null) + { + var constructor = typeof(TSGContext).GetConstructor(new Type[] { typeof(JsonSerializerOptions) }); + if(constructor == null) + { + throw new ApplicationException($"The serializer {typeof(TSGContext).FullName} is missing a constructor that takes in JsonSerializerOptions object"); + } + + _jsonSerializerContext = constructor.Invoke(new object[] { options }) as TSGContext; + } + + var jsonTypeInfo = _jsonSerializerContext.GetTypeInfo(typeof(T)) as JsonTypeInfo; + if (jsonTypeInfo == null) + { + throw new Exception($"No JsonTypeInfo registered in {_jsonSerializerContext.GetType().FullName} for type {typeof(T).FullName}."); + } + + JsonSerializer.Serialize(writer, response, jsonTypeInfo); + } + catch (Exception) + { + writer.WriteRawValue("{}"); + } + } + + /// + public string InternalSerializeAsString(T response, JsonSerializerOptions options = null) + { + try + { + if (this._jsonSerializerContext == null) + { + var constructor = typeof(TSGContext).GetConstructor(new Type[] { typeof(JsonSerializerOptions) }); + if(constructor == null) + { + throw new ApplicationException($"The serializer {typeof(TSGContext).FullName} is missing a constructor that takes in JsonSerializerOptions object"); + } + + _jsonSerializerContext = constructor.Invoke(new object[] { options }) as TSGContext; + } + + var jsonTypeInfo = _jsonSerializerContext.GetTypeInfo(typeof(T)) as JsonTypeInfo; + + if (jsonTypeInfo == null) + { + throw new Exception($"No JsonTypeInfo registered in {_jsonSerializerContext.GetType().FullName} for type {typeof(T).FullName}."); + } + + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream); + + JsonSerializer.Serialize(writer, response, jsonTypeInfo); + + return Encoding.UTF8.GetString(stream.ToArray()); + } + catch (Exception) + { + return ""; + } + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/JsonSerialization/SystemTextJsonSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Common/JsonSerialization/SystemTextJsonSerializer.cs new file mode 100644 index 00000000..e26ecdfc --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Common/JsonSerialization/SystemTextJsonSerializer.cs @@ -0,0 +1,20 @@ +namespace AWS.Lambda.Powertools.Common; + +using System.Text.Json; + +public class SystemTextJsonSerializer: IPowerToolsSerializer +{ + /// + public void InternalSerialize(Utf8JsonWriter writer, T response, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, response, options); + } + + /// + public string InternalSerializeAsString(T response, JsonSerializerOptions options = null) + { + return JsonSerializer.Serialize( + response, + options); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionConverter.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionConverter.cs index 7a85abf7..a8cb6cd2 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionConverter.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionConverter.cs @@ -20,6 +20,8 @@ namespace AWS.Lambda.Powertools.Logging.Internal.Converters; +using Microsoft.Extensions.Logging; + /// /// Converts an exception to JSON. /// @@ -84,9 +86,16 @@ public override void Write(Utf8JsonWriter writer, Exception value, JsonSerialize case Type propType: writer.WriteString(ApplyPropertyNamingPolicy(prop.Name, options), propType.FullName); break; + case String stringType: + writer.WritePropertyName(ApplyPropertyNamingPolicy(prop.Name, options)); + + Logger.PowerToolsSerializer.InternalSerialize(writer, stringType, options); + break; default: writer.WritePropertyName(ApplyPropertyNamingPolicy(prop.Name, options)); - JsonSerializer.Serialize(writer, prop.Value, options); + + Logger.PowerToolsSerializer.InternalSerialize(writer, prop.Value, options); + break; } } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs index a73235c8..b9e1f967 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs @@ -24,6 +24,8 @@ namespace AWS.Lambda.Powertools.Logging.Internal; +using System.Text; + /// /// Class LoggingAspectHandler. /// Implements the @@ -316,7 +318,8 @@ private void CaptureCorrelationId(object eventArg) try { var correlationId = string.Empty; - var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(eventArg, JsonSerializerOptions)); + + var jsonDoc = JsonDocument.Parse(Logger.PowerToolsSerializer.InternalSerializeAsString(eventArg, JsonSerializerOptions)); var element = jsonDoc.RootElement; for (var i = 0; i < correlationIdPaths.Length; i++) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs index 68561a2c..9a321663 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -24,6 +24,9 @@ namespace AWS.Lambda.Powertools.Logging.Internal; +using System.IO; +using System.Text; + /// /// Class PowertoolsLogger. This class cannot be inherited. /// Implements the @@ -68,11 +71,13 @@ internal sealed class PowertoolsLogger : ILogger /// The Powertools for AWS Lambda (.NET) configurations. /// The system wrapper. /// The get current configuration. + /// A serializer to use when using source generated serialization. public PowertoolsLogger( string name, IPowertoolsConfigurations powertoolsConfigurations, ISystemWrapper systemWrapper, - Func getCurrentConfig) + Func getCurrentConfig, + IPowerToolsSerializer serializer = null) { (_name, _powertoolsConfigurations, _systemWrapper, _getCurrentConfig) = (name, powertoolsConfigurations, systemWrapper, getCurrentConfig); @@ -245,7 +250,9 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except if (exception != null) message.TryAdd(LoggingConstants.KeyException, exception); - _systemWrapper.LogLine(JsonSerializer.Serialize(message, JsonSerializerOptions)); + _systemWrapper.LogLine(Logger.PowerToolsSerializer.InternalSerializeAsString( + message, + JsonSerializerOptions)); } /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs index 0989abac..8de36d4a 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs @@ -21,6 +21,8 @@ namespace AWS.Lambda.Powertools.Logging; +using AWS.Lambda.Powertools.Common; + /// /// Class Logger. /// @@ -31,6 +33,11 @@ public class Logger /// private static ILogger _loggerInstance; + /// + /// The logger instance + /// + internal static IPowerToolsSerializer PowerToolsSerializer = new SystemTextJsonSerializer(); + /// /// Gets the logger instance. /// @@ -65,6 +72,16 @@ public static ILogger Create(string categoryName) return LoggerProvider.CreateLogger(categoryName); } + /// + /// Set a custom serializer to use. + /// + /// An implementation of . + public static void SetSerializer(IPowerToolsSerializer powerToolsSerializer) + { + PowerToolsSerializer = powerToolsSerializer; + + } + /// /// Creates a new instance. /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs index 4d8ec0c8..f5f83fb7 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs @@ -18,6 +18,8 @@ namespace AWS.Lambda.Powertools.Logging; +using AWS.Lambda.Powertools.Common; + /// /// Class LoggerConfiguration. /// Implements the diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs index 9d55b06a..5168ca6f 100644 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs @@ -20,6 +20,8 @@ namespace AWS.Lambda.Powertools.Metrics; +using System.Text.Json.Serialization; + /// /// Class Metrics. /// Implements the @@ -62,6 +64,7 @@ public class Metrics : IMetrics /// Metrics Service Name /// Instructs metrics validation to throw exception if no metrics are provided /// Instructs metrics capturing the ColdStart is enabled + /// Custom serialization context name. internal Metrics(IPowertoolsConfigurations powertoolsConfigurations, string nameSpace = null, string service = null, bool raiseOnEmptyMetrics = false, bool captureColdStartEnabled = false) { diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs index d0025fc4..10dfba86 100644 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/MetricsAttribute.cs @@ -18,6 +18,8 @@ namespace AWS.Lambda.Powertools.Metrics; +using System.Text.Json.Serialization; + /// /// Creates custom metrics asynchronously by logging metrics to /// standard output following Amazon CloudWatch Embedded Metric Format (EMF).
diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricsContext.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricsContext.cs index ba77d0ed..6f4c8348 100644 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricsContext.cs +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricsContext.cs @@ -18,6 +18,10 @@ namespace AWS.Lambda.Powertools.Metrics; +using System.Text.Json.Serialization; + +using AWS.Lambda.Powertools.Common; + /// /// Class MetricsContext. /// Implements the @@ -30,6 +34,8 @@ public class MetricsContext : IDisposable /// private RootNode _rootNode; + private static IPowerToolsSerializer _serializationContext = new SystemTextJsonSerializer(); + /// /// Creates empty MetricsContext object /// @@ -160,7 +166,7 @@ public void AddMetadata(string key, object value) /// String object representing all metrics in memory public string Serialize() { - return _rootNode.Serialize(); + return _rootNode.Serialize(_serializationContext); } /// @@ -170,4 +176,9 @@ public void ClearDefaultDimensions() { _rootNode.AWS.ClearDefaultDimensions(); } + + public static void SetJsonSerializationContext(IPowerToolsSerializer serializer) + { + _serializationContext = serializer; + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Model/RootNode.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Model/RootNode.cs index f386f32a..22184a8a 100644 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/Model/RootNode.cs +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Model/RootNode.cs @@ -19,6 +19,11 @@ namespace AWS.Lambda.Powertools.Metrics; +using System.IO; +using System.Text; + +using AWS.Lambda.Powertools.Common; + /// /// Class RootNode. /// @@ -61,10 +66,10 @@ public Dictionary MetricData /// /// JSON EMF payload in string format /// namespace - public string Serialize() + public string Serialize(IPowerToolsSerializer serializer = null) { if (string.IsNullOrWhiteSpace(AWS.GetNamespace())) throw new SchemaValidationException("namespace"); - return JsonSerializer.Serialize(this); + return serializer.InternalSerializeAsString(this); } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LoggingAttributeTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LoggingAttributeTest.cs index 55b0134b..ffc6ab49 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LoggingAttributeTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LoggingAttributeTest.cs @@ -31,6 +31,11 @@ namespace AWS.Lambda.Powertools.Logging.Tests [Collection("Sequential")] public class LoggingAttributeTestWithoutLambdaContext { + public LoggingAttributeTestWithoutLambdaContext() + { + Logger.SetSerializer(new SystemTextJsonSerializer()); + } + [Fact] public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContext() { diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs index f0ec6d01..b6ddcfdc 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs @@ -27,8 +27,14 @@ namespace AWS.Lambda.Powertools.Logging.Tests { + [Collection("Sequential")] public class PowertoolsLoggerTest { + public PowertoolsLoggerTest() + { + Logger.SetSerializer(new SystemTextJsonSerializer()); + } + private static void Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel logLevel, LogLevel minimumLevel) { // Arrange diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerWithSourceGenerationTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerWithSourceGenerationTests.cs new file mode 100644 index 00000000..4e2aa89d --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerWithSourceGenerationTests.cs @@ -0,0 +1,1269 @@ +/* + * 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.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; + +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging; +using AWS.Lambda.Powertools.Logging.Internal; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace AWS.Lambda.Powertools.Logging.Tests +{ + [Collection("Sequential")] + public class PowertoolsLoggerWithSourceGenerationTests + { + public PowertoolsLoggerWithSourceGenerationTests() + { + Logger.SetSerializer(new SourceGeneratedSerializer()); + } + + private static void Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel logLevel, LogLevel minimumLevel) + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + + var configurations = new Mock(); + var systemWrapper = new Mock(); + + var logger = new PowertoolsLogger(loggerName,configurations.Object, systemWrapper.Object, () => + new LoggerConfiguration + { + Service = service, + MinimumLevel = minimumLevel + }); + + switch (logLevel) + { + // Act + case LogLevel.Critical: + logger.LogCritical("Test"); + break; + case LogLevel.Debug: + logger.LogDebug("Test"); + break; + case LogLevel.Error: + logger.LogError("Test"); + break; + case LogLevel.Information: + logger.LogInformation("Test"); + break; + case LogLevel.Trace: + logger.LogTrace("Test"); + break; + case LogLevel.Warning: + logger.LogWarning("Test"); + break; + case LogLevel.None: + break; + default: + throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null); + } + + // Assert + systemWrapper.Verify(v => + v.LogLine( + It.Is(s=> s.Contains(service)) + ), Times.Once); + + } + + private static void Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel logLevel, LogLevel minimumLevel) + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + + var configurations = new Mock(); + var systemWrapper = new Mock(); + + var logger = new PowertoolsLogger(loggerName,configurations.Object, systemWrapper.Object, () => + new LoggerConfiguration + { + Service = service, + MinimumLevel = minimumLevel + }); + + switch (logLevel) + { + // Act + case LogLevel.Critical: + logger.LogCritical("Test"); + break; + case LogLevel.Debug: + logger.LogDebug("Test"); + break; + case LogLevel.Error: + logger.LogError("Test"); + break; + case LogLevel.Information: + logger.LogInformation("Test"); + break; + case LogLevel.Trace: + logger.LogTrace("Test"); + break; + case LogLevel.Warning: + logger.LogWarning("Test"); + break; + case LogLevel.None: + break; + default: + throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null); + } + + // Assert + systemWrapper.Verify(v => + v.LogLine( + It.IsAny() + ), Times.Never); + + } + + [Theory] + [InlineData(LogLevel.Trace)] + public void LogTrace_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLevel) + { + Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel.Trace, minimumLevel); + } + + [Theory] + [InlineData(LogLevel.Trace)] + [InlineData(LogLevel.Debug)] + public void LogDebug_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLevel) + { + Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel.Debug, minimumLevel); + } + + [Theory] + [InlineData(LogLevel.Trace)] + [InlineData(LogLevel.Debug)] + [InlineData(LogLevel.Information)] + public void LogInformation_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLevel) + { + Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel.Information, minimumLevel); + } + + [Theory] + [InlineData(LogLevel.Trace)] + [InlineData(LogLevel.Debug)] + [InlineData(LogLevel.Information)] + [InlineData(LogLevel.Warning)] + public void LogWarning_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLevel) + { + Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel.Warning, minimumLevel); + } + + [Theory] + [InlineData(LogLevel.Trace)] + [InlineData(LogLevel.Debug)] + [InlineData(LogLevel.Information)] + [InlineData(LogLevel.Warning)] + [InlineData(LogLevel.Error)] + public void LogError_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLevel) + { + Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel.Error, minimumLevel); + } + + [Theory] + [InlineData(LogLevel.Trace)] + [InlineData(LogLevel.Debug)] + [InlineData(LogLevel.Information)] + [InlineData(LogLevel.Warning)] + [InlineData(LogLevel.Error)] + [InlineData(LogLevel.Critical)] + public void LogCritical_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLevel) + { + Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel.Critical, minimumLevel); + } + + + [Theory] + [InlineData(LogLevel.Debug)] + [InlineData(LogLevel.Information)] + [InlineData(LogLevel.Warning)] + [InlineData(LogLevel.Error)] + [InlineData(LogLevel.Critical)] + public void LogTrace_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel minimumLevel) + { + Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel.Trace, minimumLevel); + } + + + [Theory] + [InlineData(LogLevel.Information)] + [InlineData(LogLevel.Warning)] + [InlineData(LogLevel.Error)] + [InlineData(LogLevel.Critical)] + public void LogDebug_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel minimumLevel) + { + Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel.Debug, minimumLevel); + } + + [Theory] + [InlineData(LogLevel.Warning)] + [InlineData(LogLevel.Error)] + [InlineData(LogLevel.Critical)] + public void LogInformation_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel minimumLevel) + { + Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel.Information, minimumLevel); + } + + [Theory] + [InlineData(LogLevel.Error)] + [InlineData(LogLevel.Critical)] + public void LogWarning_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel minimumLevel) + { + Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel.Warning, minimumLevel); + } + + [Theory] + [InlineData(LogLevel.Critical)] + public void LogError_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel minimumLevel) + { + Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel.Error, minimumLevel); + } + + [Theory] + [InlineData(LogLevel.Trace)] + [InlineData(LogLevel.Debug)] + [InlineData(LogLevel.Information)] + [InlineData(LogLevel.Warning)] + [InlineData(LogLevel.Error)] + [InlineData(LogLevel.Critical)] + public void LogNone_WithAnyMinimumLevel_DoesNotLog(LogLevel minimumLevel) + { + Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel.None, minimumLevel); + } + + [Fact] + public void Log_ConfigurationIsNotProvided_ReadsFromEnvironmentVariables() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var logLevel = LogLevel.Trace; + var loggerSampleRate = 0.7; + var randomSampleRate = 0.5; + + var configurations = new Mock(); + configurations.Setup(c => c.Service).Returns(service); + configurations.Setup(c => c.LogLevel).Returns(logLevel.ToString); + configurations.Setup(c => c.LoggerSampleRate).Returns(loggerSampleRate); + + var systemWrapper = new Mock(); + systemWrapper.Setup(c => c.GetRandom()).Returns(randomSampleRate); + + var logger = new PowertoolsLogger(loggerName,configurations.Object, systemWrapper.Object, () => + new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }); + + logger.LogInformation("Test"); + + // Assert + systemWrapper.Verify(v => + v.LogLine( + It.Is + (s=> + s.Contains(service) && + s.Contains(loggerSampleRate.ToString(CultureInfo.InvariantCulture)) + ) + ), Times.Once); + } + + [Fact] + public void Log_SamplingRateGreaterThanRandom_ChangedLogLevelToDebug() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var logLevel = LogLevel.Trace; + var loggerSampleRate = 0.7; + var randomSampleRate = 0.5; + + var configurations = new Mock(); + configurations.Setup(c => c.Service).Returns(service); + configurations.Setup(c => c.LogLevel).Returns(logLevel.ToString); + configurations.Setup(c => c.LoggerSampleRate).Returns(loggerSampleRate); + + var systemWrapper = new Mock(); + systemWrapper.Setup(c => c.GetRandom()).Returns(randomSampleRate); + + var logger = new PowertoolsLogger(loggerName,configurations.Object, systemWrapper.Object, () => + new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }); + + logger.LogInformation("Test"); + + // Assert + systemWrapper.Verify(v => + v.LogLine( + It.Is + (s=> + s == $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {loggerSampleRate}, Sampler Value: {randomSampleRate}." + ) + ), Times.Once); + + } + + [Fact] + public void Log_SamplingRateGreaterThanOne_SkipsSamplingRateConfiguration() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var logLevel = LogLevel.Trace; + var loggerSampleRate = 2; + + var configurations = new Mock(); + configurations.Setup(c => c.Service).Returns(service); + configurations.Setup(c => c.LogLevel).Returns(logLevel.ToString); + configurations.Setup(c => c.LoggerSampleRate).Returns(loggerSampleRate); + + var systemWrapper = new Mock(); + + var logger = new PowertoolsLogger(loggerName,configurations.Object, systemWrapper.Object, () => + new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }); + + logger.LogInformation("Test"); + + // Assert + systemWrapper.Verify(v => + v.LogLine( + It.Is + (s=> + s == $"Skipping sampling rate configuration because of invalid value. Sampling rate: {loggerSampleRate}" + ) + ), Times.Once); + + } + + [Fact] + public void Log_EnvVarSetsCaseToCamelCase_OutputsCamelCaseLog() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var logLevel = LogLevel.Information; + var randomSampleRate = 0.5; + + var configurations = new Mock(); + configurations.Setup(c => c.Service).Returns(service); + configurations.Setup(c => c.LogLevel).Returns(logLevel.ToString); + configurations.Setup(c => c.LoggerOutputCase).Returns(LoggerOutputCase.CamelCase.ToString); + + var systemWrapper = new Mock(); + systemWrapper.Setup(c => c.GetRandom()).Returns(randomSampleRate); + + var logger = new PowertoolsLogger(loggerName, configurations.Object, systemWrapper.Object, () => + new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }); + + var message = new CustomMessage( + "Value 1", + "Value 2"); + + logger.LogInformation(message); + + // Assert + systemWrapper.Verify(v => + v.LogLine( + It.Is + (s => + s.Contains("\"message\":{\"propOne\":\"Value 1\",\"propTwo\":\"Value 2\"}") + ) + ), Times.Once); + } + + [Fact] + public void Log_AttributeSetsCaseToCamelCase_OutputsCamelCaseLog() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var logLevel = LogLevel.Information; + var randomSampleRate = 0.5; + + var configurations = new Mock(); + configurations.Setup(c => c.Service).Returns(service); + configurations.Setup(c => c.LogLevel).Returns(logLevel.ToString); + + var systemWrapper = new Mock(); + systemWrapper.Setup(c => c.GetRandom()).Returns(randomSampleRate); + + var logger = new PowertoolsLogger(loggerName, configurations.Object, systemWrapper.Object, () => + new LoggerConfiguration + { + Service = null, + MinimumLevel = null, + LoggerOutputCase = LoggerOutputCase.CamelCase + }); + + var message = new CustomMessage( + "Value 1", + "Value 2"); + + logger.LogInformation(message); + + // Assert + systemWrapper.Verify(v => + v.LogLine( + It.Is + (s => + s.Contains("\"message\":{\"propOne\":\"Value 1\",\"propTwo\":\"Value 2\"}") + ) + ), Times.Once); + } + + [Fact] + public void Log_EnvVarSetsCaseToPascalCase_OutputsPascalCaseLog() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var logLevel = LogLevel.Information; + var randomSampleRate = 0.5; + + var configurations = new Mock(); + configurations.Setup(c => c.Service).Returns(service); + configurations.Setup(c => c.LogLevel).Returns(logLevel.ToString); + configurations.Setup(c => c.LoggerOutputCase).Returns(LoggerOutputCase.PascalCase.ToString); + + var systemWrapper = new Mock(); + systemWrapper.Setup(c => c.GetRandom()).Returns(randomSampleRate); + + var logger = new PowertoolsLogger(loggerName, configurations.Object, systemWrapper.Object, () => + new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }); + + var message = new CustomMessage( + "Value 1", + "Value 2"); + + logger.LogInformation(message); + + // Assert + systemWrapper.Verify(v => + v.LogLine( + It.Is + (s => + s.Contains("\"Message\":{\"PropOne\":\"Value 1\",\"PropTwo\":\"Value 2\"}") + ) + ), Times.Once); + } + + [Fact] + public void Log_AttributeSetsCaseToPascalCase_OutputsPascalCaseLog() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var logLevel = LogLevel.Information; + var randomSampleRate = 0.5; + + var configurations = new Mock(); + configurations.Setup(c => c.Service).Returns(service); + configurations.Setup(c => c.LogLevel).Returns(logLevel.ToString); + + var systemWrapper = new Mock(); + systemWrapper.Setup(c => c.GetRandom()).Returns(randomSampleRate); + + var logger = new PowertoolsLogger(loggerName, configurations.Object, systemWrapper.Object, () => + new LoggerConfiguration + { + Service = null, + MinimumLevel = null, + LoggerOutputCase = LoggerOutputCase.PascalCase + }); + + var message = new CustomMessage( + "Value 1", + "Value 2"); + + logger.LogInformation(message); + + // Assert + systemWrapper.Verify(v => + v.LogLine( + It.Is + (s => + s.Contains("\"Message\":{\"PropOne\":\"Value 1\",\"PropTwo\":\"Value 2\"}") + ) + ), Times.Once); + } + + [Fact] + public void Log_EnvVarSetsCaseToSnakeCase_OutputsSnakeCaseLog() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var logLevel = LogLevel.Information; + var randomSampleRate = 0.5; + + var configurations = new Mock(); + configurations.Setup(c => c.Service).Returns(service); + configurations.Setup(c => c.LogLevel).Returns(logLevel.ToString); + configurations.Setup(c => c.LoggerOutputCase).Returns(LoggerOutputCase.SnakeCase.ToString); + + var systemWrapper = new Mock(); + systemWrapper.Setup(c => c.GetRandom()).Returns(randomSampleRate); + + var logger = new PowertoolsLogger(loggerName, configurations.Object, systemWrapper.Object, () => + new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }); + + var message = new CustomMessage( + "Value 1", + "Value 2"); + + logger.LogInformation(message); + + // Assert + systemWrapper.Verify(v => + v.LogLine( + It.Is + (s => + s.Contains("\"message\":{\"prop_one\":\"Value 1\",\"prop_two\":\"Value 2\"}") + ) + ), Times.Once); + } + + [Fact] + public void Log_AttributeSetsCaseToSnakeCase_OutputsSnakeCaseLog() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var logLevel = LogLevel.Information; + var randomSampleRate = 0.5; + + var configurations = new Mock(); + configurations.Setup(c => c.Service).Returns(service); + configurations.Setup(c => c.LogLevel).Returns(logLevel.ToString); + + var systemWrapper = new Mock(); + systemWrapper.Setup(c => c.GetRandom()).Returns(randomSampleRate); + + var logger = new PowertoolsLogger(loggerName, configurations.Object, systemWrapper.Object, () => + new LoggerConfiguration + { + Service = null, + MinimumLevel = null, + LoggerOutputCase = LoggerOutputCase.SnakeCase + }); + + var message = new CustomMessage( + "Value 1", + "Value 2"); + + logger.LogInformation(message); + + // Assert + systemWrapper.Verify(v => + v.LogLine( + It.Is + (s => + s.Contains("\"message\":{\"prop_one\":\"Value 1\",\"prop_two\":\"Value 2\"}") + ) + ), Times.Once); + } + + [Fact] + public void Log_NoOutputCaseSet_OutputDefaultsToSnakeCaseLog() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var logLevel = LogLevel.Information; + var randomSampleRate = 0.5; + + var configurations = new Mock(); + configurations.Setup(c => c.Service).Returns(service); + configurations.Setup(c => c.LogLevel).Returns(logLevel.ToString); + + var systemWrapper = new Mock(); + systemWrapper.Setup(c => c.GetRandom()).Returns(randomSampleRate); + + var logger = new PowertoolsLogger(loggerName, configurations.Object, systemWrapper.Object, () => + new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }); + + var message = new CustomMessage( + "Value 1", + "Value 2"); + + logger.LogInformation(message); + + // Assert + systemWrapper.Verify(v => + v.LogLine( + It.Is + (s => + s.Contains("\"message\":{\"prop_one\":\"Value 1\",\"prop_two\":\"Value 2\"}") + ) + ), Times.Once); + } + + [Fact] + public void BeginScope_WhenScopeIsObject_ExtractScopeKeys() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var logLevel = LogLevel.Information; + + var configurations = new Mock(); + configurations.Setup(c => c.Service).Returns(service); + configurations.Setup(c => c.LogLevel).Returns(logLevel.ToString); + var systemWrapper = new Mock(); + + var logger = new PowertoolsLogger(loggerName,configurations.Object, systemWrapper.Object, () => + new LoggerConfiguration + { + Service = service, + MinimumLevel = logLevel + }); + + var scopeKeys = new CustomMessage( + "Value 1", + "Value 2"); + + using (var loggerScope = logger.BeginScope(scopeKeys) as PowertoolsLoggerScope) + { + Assert.NotNull(loggerScope); + Assert.NotNull(loggerScope.ExtraKeys); + Assert.True(loggerScope.ExtraKeys.Count == 2); + Assert.True(loggerScope.ExtraKeys.ContainsKey("PropOne")); + Assert.True((string)loggerScope.ExtraKeys["PropOne"] == scopeKeys.PropOne); + Assert.True(loggerScope.ExtraKeys.ContainsKey("PropTwo")); + Assert.True((string)loggerScope.ExtraKeys["PropTwo"] == scopeKeys.PropTwo); + } + Assert.Null(logger.CurrentScope?.ExtraKeys); + } + + [Fact] + public void BeginScope_WhenScopeIsObjectDictionary_ExtractScopeKeys() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var logLevel = LogLevel.Information; + + var configurations = new Mock(); + configurations.Setup(c => c.Service).Returns(service); + configurations.Setup(c => c.LogLevel).Returns(logLevel.ToString); + var systemWrapper = new Mock(); + + var logger = new PowertoolsLogger(loggerName,configurations.Object, systemWrapper.Object, () => + new LoggerConfiguration + { + Service = service, + MinimumLevel = logLevel + }); + + var scopeKeys = new Dictionary + { + { "PropOne", "Value 1" }, + { "PropTwo", "Value 2" } + }; + + using (var loggerScope = logger.BeginScope(scopeKeys) as PowertoolsLoggerScope) + { + Assert.NotNull(loggerScope); + Assert.NotNull(loggerScope.ExtraKeys); + Assert.True(loggerScope.ExtraKeys.Count == 2); + Assert.True(loggerScope.ExtraKeys.ContainsKey("PropOne")); + Assert.True(loggerScope.ExtraKeys["PropOne"] == scopeKeys["PropOne"]); + Assert.True(loggerScope.ExtraKeys.ContainsKey("PropTwo")); + Assert.True(loggerScope.ExtraKeys["PropTwo"] == scopeKeys["PropTwo"]); + } + Assert.Null(logger.CurrentScope?.ExtraKeys); + } + + [Fact] + public void BeginScope_WhenScopeIsStringDictionary_ExtractScopeKeys() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var logLevel = LogLevel.Information; + + var configurations = new Mock(); + configurations.Setup(c => c.Service).Returns(service); + configurations.Setup(c => c.LogLevel).Returns(logLevel.ToString); + var systemWrapper = new Mock(); + + var logger = new PowertoolsLogger(loggerName,configurations.Object, systemWrapper.Object, () => + new LoggerConfiguration + { + Service = service, + MinimumLevel = logLevel + }); + + var scopeKeys = new Dictionary + { + { "PropOne", "Value 1" }, + { "PropTwo", "Value 2" } + }; + + using (var loggerScope = logger.BeginScope(scopeKeys) as PowertoolsLoggerScope) + { + Assert.NotNull(loggerScope); + Assert.NotNull(loggerScope.ExtraKeys); + Assert.True(loggerScope.ExtraKeys.Count == 2); + Assert.True(loggerScope.ExtraKeys.ContainsKey("PropOne")); + Assert.True((string)loggerScope.ExtraKeys["PropOne"] == scopeKeys["PropOne"]); + Assert.True(loggerScope.ExtraKeys.ContainsKey("PropTwo")); + Assert.True((string)loggerScope.ExtraKeys["PropTwo"] == scopeKeys["PropTwo"]); + } + Assert.Null(logger.CurrentScope?.ExtraKeys); + } + + [Theory] + [InlineData(LogLevel.Trace, true)] + [InlineData(LogLevel.Debug, true)] + [InlineData(LogLevel.Information, true)] + [InlineData(LogLevel.Warning, true)] + [InlineData(LogLevel.Error, true)] + [InlineData(LogLevel.Critical, true)] + [InlineData(LogLevel.Trace, false)] + [InlineData(LogLevel.Debug, false)] + [InlineData(LogLevel.Information, false)] + [InlineData(LogLevel.Warning, false)] + [InlineData(LogLevel.Error, false)] + [InlineData(LogLevel.Critical, false)] + public void Log_WhenExtraKeysIsObjectDictionary_AppendExtraKeys(LogLevel logLevel, bool logMethod) + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var message = Guid.NewGuid().ToString(); + + var configurations = new Mock(); + configurations.Setup(c => c.Service).Returns(service); + configurations.Setup(c => c.LogLevel).Returns(logLevel.ToString); + configurations.Setup(c => c.LoggerOutputCase).Returns(LoggerOutputCase.PascalCase.ToString); + var systemWrapper = new Mock(); + + var logger = new PowertoolsLogger(loggerName,configurations.Object, systemWrapper.Object, () => + new LoggerConfiguration + { + Service = service, + MinimumLevel = LogLevel.Trace, + }); + + var scopeKeys = new Dictionary + { + { "PropOne", "Value 1" }, + { "PropTwo", "Value 2" } + }; + + if(logMethod) + logger.Log(logLevel, scopeKeys, message); + else switch (logLevel) + { + case LogLevel.Trace: + logger.LogTrace(scopeKeys, message); + break; + case LogLevel.Debug: + logger.LogDebug(scopeKeys, message); + break; + case LogLevel.Information: + logger.LogInformation(scopeKeys, message); + break; + case LogLevel.Warning: + logger.LogWarning(scopeKeys, message); + break; + case LogLevel.Error: + logger.LogError(scopeKeys, message); + break; + case LogLevel.Critical: + logger.LogCritical(scopeKeys, message); + break; + case LogLevel.None: + break; + default: + throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null); + } + + systemWrapper.Verify(v => + v.LogLine( + It.Is + (s=> + s.Contains(scopeKeys.Keys.First()) && + s.Contains(scopeKeys.Keys.Last()) && + s.Contains(scopeKeys.Values.First().ToString()) && + s.Contains(scopeKeys.Values.Last().ToString()) + ) + ), Times.Once); + + Assert.Null(logger.CurrentScope?.ExtraKeys); + } + + [Theory] + [InlineData(LogLevel.Trace, true)] + [InlineData(LogLevel.Debug, true)] + [InlineData(LogLevel.Information, true)] + [InlineData(LogLevel.Warning, true)] + [InlineData(LogLevel.Error, true)] + [InlineData(LogLevel.Critical, true)] + [InlineData(LogLevel.Trace, false)] + [InlineData(LogLevel.Debug, false)] + [InlineData(LogLevel.Information, false)] + [InlineData(LogLevel.Warning, false)] + [InlineData(LogLevel.Error, false)] + [InlineData(LogLevel.Critical, false)] + public void Log_WhenExtraKeysIsStringDictionary_AppendExtraKeys(LogLevel logLevel, bool logMethod) + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var message = Guid.NewGuid().ToString(); + + var configurations = new Mock(); + configurations.Setup(c => c.Service).Returns(service); + configurations.Setup(c => c.LogLevel).Returns(logLevel.ToString); + configurations.Setup(c => c.LoggerOutputCase).Returns(LoggerOutputCase.PascalCase.ToString); + var systemWrapper = new Mock(); + + var logger = new PowertoolsLogger(loggerName,configurations.Object, systemWrapper.Object, () => + new LoggerConfiguration + { + Service = service, + MinimumLevel = LogLevel.Trace, + }); + + var scopeKeys = new Dictionary + { + { "PropOne", "Value 1" }, + { "PropTwo", "Value 2" } + }; + + if(logMethod) + logger.Log(logLevel, scopeKeys, message); + else switch (logLevel) + { + case LogLevel.Trace: + logger.LogTrace(scopeKeys, message); + break; + case LogLevel.Debug: + logger.LogDebug(scopeKeys, message); + break; + case LogLevel.Information: + logger.LogInformation(scopeKeys, message); + break; + case LogLevel.Warning: + logger.LogWarning(scopeKeys, message); + break; + case LogLevel.Error: + logger.LogError(scopeKeys, message); + break; + case LogLevel.Critical: + logger.LogCritical(scopeKeys, message); + break; + case LogLevel.None: + break; + default: + throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null); + } + + systemWrapper.Verify(v => + v.LogLine( + It.Is + (s=> + s.Contains(scopeKeys.Keys.First()) && + s.Contains(scopeKeys.Keys.Last()) && + s.Contains(scopeKeys.Values.First()) && + s.Contains(scopeKeys.Values.Last()) + ) + ), Times.Once); + + Assert.Null(logger.CurrentScope?.ExtraKeys); + } + + [Theory] + [InlineData(LogLevel.Trace, true)] + [InlineData(LogLevel.Debug, true)] + [InlineData(LogLevel.Information, true)] + [InlineData(LogLevel.Warning, true)] + [InlineData(LogLevel.Error, true)] + [InlineData(LogLevel.Critical, true)] + [InlineData(LogLevel.Trace, false)] + [InlineData(LogLevel.Debug, false)] + [InlineData(LogLevel.Information, false)] + [InlineData(LogLevel.Warning, false)] + [InlineData(LogLevel.Error, false)] + [InlineData(LogLevel.Critical, false)] + public void Log_WhenExtraKeysAsObject_AppendExtraKeys(LogLevel logLevel, bool logMethod) + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var message = Guid.NewGuid().ToString(); + + var configurations = new Mock(); + configurations.Setup(c => c.Service).Returns(service); + configurations.Setup(c => c.LogLevel).Returns(logLevel.ToString); + configurations.Setup(c => c.LoggerOutputCase).Returns(LoggerOutputCase.PascalCase.ToString); + var systemWrapper = new Mock(); + + var logger = new PowertoolsLogger(loggerName,configurations.Object, systemWrapper.Object, () => + new LoggerConfiguration + { + Service = service, + MinimumLevel = LogLevel.Trace, + }); + + var scopeKeys = new CustomMessage( + "Value 1", + "Value 2"); + + if(logMethod) + logger.Log(logLevel, scopeKeys, message); + else switch (logLevel) + { + case LogLevel.Trace: + logger.LogTrace(scopeKeys, message); + break; + case LogLevel.Debug: + logger.LogDebug(scopeKeys, message); + break; + case LogLevel.Information: + logger.LogInformation(scopeKeys, message); + break; + case LogLevel.Warning: + logger.LogWarning(scopeKeys, message); + break; + case LogLevel.Error: + logger.LogError(scopeKeys, message); + break; + case LogLevel.Critical: + logger.LogCritical(scopeKeys, message); + break; + case LogLevel.None: + break; + default: + throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null); + } + + systemWrapper.Verify(v => + v.LogLine( + It.Is + (s=> + s.Contains("PropOne") && + s.Contains("PropTwo") && + s.Contains(scopeKeys.PropOne) && + s.Contains(scopeKeys.PropTwo) + ) + ), Times.Once); + + Assert.Null(logger.CurrentScope?.ExtraKeys); + } + + [Fact] + public void Log_WhenException_LogsExceptionDetails() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var error = new InvalidOperationException("TestError"); + var logLevel = LogLevel.Information; + var randomSampleRate = 0.5; + + var configurations = new Mock(); + configurations.Setup(c => c.Service).Returns(service); + configurations.Setup(c => c.LogLevel).Returns(logLevel.ToString); + + var systemWrapper = new Mock(); + systemWrapper.Setup(c => c.GetRandom()).Returns(randomSampleRate); + + var logger = new PowertoolsLogger(loggerName, configurations.Object, systemWrapper.Object, () => + new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }); + + try + { + throw error; + } + catch (Exception ex) + { + logger.LogError(ex); + } + + // Assert + systemWrapper.Verify(v => + v.LogLine( + It.Is + (s => + s.Contains("\"exception\":{\"type\":\"" + error.GetType().FullName + "\",\"message\":\"" + error.Message + "\"") + ) + ), Times.Once); + } + + [Fact] + public void Log_WhenNestedException_LogsExceptionDetails() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var error = new InvalidOperationException("TestError"); + var logLevel = LogLevel.Information; + var randomSampleRate = 0.5; + + var configurations = new Mock(); + configurations.Setup(c => c.Service).Returns(service); + configurations.Setup(c => c.LogLevel).Returns(logLevel.ToString); + + var systemWrapper = new Mock(); + systemWrapper.Setup(c => c.GetRandom()).Returns(randomSampleRate); + + var logger = new PowertoolsLogger(loggerName, configurations.Object, systemWrapper.Object, () => + new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }); + + try + { + throw error; + } + catch (Exception ex) + { + logger.LogInformation( new CustomError("Test Object", ex)); + } + + // Assert + systemWrapper.Verify(v => + v.LogLine( + It.Is + (s => + s.Contains("\"message\":\"" + error.Message + "\"") + ) + ), Times.Once); + } + + [Fact] + public void Log_WhenByteArray_LogsByteArrayNumbers() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var bytes = new byte[10]; + new Random().NextBytes(bytes); + var logLevel = LogLevel.Information; + var randomSampleRate = 0.5; + + var configurations = new Mock(); + configurations.Setup(c => c.Service).Returns(service); + configurations.Setup(c => c.LogLevel).Returns(logLevel.ToString); + + var systemWrapper = new Mock(); + systemWrapper.Setup(c => c.GetRandom()).Returns(randomSampleRate); + + var logger = new PowertoolsLogger(loggerName, configurations.Object, systemWrapper.Object, () => + new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }); + + // Act + logger.LogInformation(new { Name = "Test Object", Bytes = bytes }); + + // Assert + systemWrapper.Verify(v => + v.LogLine( + It.Is + (s => + s.Contains("\"bytes\":[" + string.Join(",", bytes) + "]") + ) + ), Times.Once); + } + + [Fact] + public void Log_WhenMemoryStream_LogsBase64String() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + var bytes = new byte[10]; + new Random().NextBytes(bytes); + var memoryStream = new MemoryStream(bytes) + { + Position = 0 + }; + var logLevel = LogLevel.Information; + var randomSampleRate = 0.5; + + var configurations = new Mock(); + configurations.Setup(c => c.Service).Returns(service); + configurations.Setup(c => c.LogLevel).Returns(logLevel.ToString); + + var systemWrapper = new Mock(); + systemWrapper.Setup(c => c.GetRandom()).Returns(randomSampleRate); + + var logger = new PowertoolsLogger(loggerName, configurations.Object, systemWrapper.Object, () => + new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }); + + // Act + logger.LogInformation(new { Name = "Test Object", Stream = memoryStream }); + + // Assert + systemWrapper.Verify(v => + v.LogLine( + It.Is + (s => + s.Contains("\"stream\":\"" + Convert.ToBase64String(bytes) + "\"") + ) + ), Times.Once); + } + + [Fact] + public void Log_WhenMemoryStream_LogsBase64String_UnsafeRelaxedJsonEscaping() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var service = Guid.NewGuid().ToString(); + + // This will produce the encoded string dW5zYWZlIHN0cmluZyB+IHRlc3Q= (which has a plus sign to test unsafe escaping) + var bytes = Encoding.UTF8.GetBytes("unsafe string ~ test"); + + var memoryStream = new MemoryStream(bytes) + { + Position = 0 + }; + var logLevel = LogLevel.Information; + var randomSampleRate = 0.5; + + var configurations = new Mock(); + configurations.Setup(c => c.Service).Returns(service); + configurations.Setup(c => c.LogLevel).Returns(logLevel.ToString); + + var systemWrapper = new Mock(); + systemWrapper.Setup(c => c.GetRandom()).Returns(randomSampleRate); + + var logger = new PowertoolsLogger(loggerName, configurations.Object, systemWrapper.Object, () => + new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }); + + // Act + logger.LogInformation(new { Name = "Test Object", Stream = memoryStream }); + + // Assert + systemWrapper.Verify(v => + v.LogLine( + It.Is + (s => + s.Contains("\"stream\":\"" + Convert.ToBase64String(bytes) + "\"") + ) + ), Times.Once); + } + + [Fact] + public void Log_Set_Execution_Environment_Context() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + var assemblyName = "AWS.Lambda.Powertools.Logger"; + var assemblyVersion = "1.0.0"; + + var env = new Mock(); + env.Setup(x => x.GetAssemblyName(It.IsAny())).Returns(assemblyName); + env.Setup(x => x.GetAssemblyVersion(It.IsAny())).Returns(assemblyVersion); + + // Act + + var wrapper = new SystemWrapper(env.Object); + var conf = new PowertoolsConfigurations(wrapper); + + var logger = new PowertoolsLogger(loggerName,conf, wrapper, () => + new LoggerConfiguration + { + Service = null, + MinimumLevel = null + }); + logger.LogInformation("Test"); + + // Assert + env.Verify(v => + v.SetEnvironmentVariable( + "AWS_EXECUTION_ENV", $"{Constants.FeatureContextIdentifier}/Logger/{assemblyVersion}" + ), Times.Once); + + env.Verify(v => + v.GetEnvironmentVariable( + "AWS_EXECUTION_ENV" + ), Times.Once); + } + } +} + +public record CustomMessage(string PropOne, string PropTwo); + +public record CustomError(string message, Exception ex); + +[JsonSerializable(typeof(string))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(bool))] +[JsonSerializable(typeof(double))] +[JsonSerializable(typeof(Exception))] +[JsonSerializable(typeof(InvalidOperationException))] +[JsonSerializable(typeof(CustomMessage))] +[JsonSerializable(typeof(CustomError))] +public partial class TestSerializationContext : JsonSerializerContext +{ +} \ 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 74fa55fd..e2a9b656 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs @@ -16,8 +16,11 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text.Json.Serialization; using System.Threading.Tasks; using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Metrics; + using Moq; using Xunit; @@ -39,7 +42,7 @@ public void WhenCaptureColdStart_CreateSeparateBlob() Console.SetOut(consoleOut); var configurations = new Mock(); - + var metrics = new Metrics( configurations.Object, nameSpace: "dotnet-powertools-test", diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationWithSourceGenerationTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationWithSourceGenerationTests.cs new file mode 100644 index 00000000..dedc8785 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationWithSourceGenerationTests.cs @@ -0,0 +1,773 @@ +/* + * 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.IO; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Metrics; + +using Moq; +using Xunit; + +namespace AWS.Lambda.Powertools.Metrics.Tests +{ + [Collection("Sequential")] + public class EMFValidationWithSourceGenerationTests + { + public EMFValidationWithSourceGenerationTests() + { + MetricsContext.SetJsonSerializationContext(new SourceGeneratedSerializer()); + } + [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 = new Mock(); + + var metrics = new Metrics( + configurations.Object, + 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); + + 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 = new Mock(); + + var logger = new Metrics( + configurations.Object, + 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); + + 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 = new Mock(); + + var metrics = new Metrics( + configurations.Object, + nameSpace: "dotnet-powertools-test", + service: "testService" + ); + + var handler = new MetricsAspectHandler( + metrics, + false + ); + + var eventArgs = new AspectEventArgs { Name = methodName }; + + // Act + handler.OnEntry(eventArgs); + + for (var i = 0; i <= PowertoolsConfigurations.MaxMetrics; i++) + { + Metrics.AddMetric($"Metric Name {i + 1}", i, MetricUnit.Count); + + 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); + + var metricsOutput = consoleOut.ToString(); + + // 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(); + } + + [Trait("Category", "EMFLimits")] + [Fact] + public void WhenMaxDataPointsAreAddedToTheSameMetric_FlushAutomatically() + { + // Arrange + var methodName = Guid.NewGuid().ToString(); + var consoleOut = new StringWriter(); + Console.SetOut(consoleOut); + + var configurations = new Mock(); + + var metrics = new Metrics( + configurations.Object, + nameSpace: "dotnet-powertools-test", + service: "testService" + ); + + var handler = new MetricsAspectHandler( + metrics, + false + ); + + var eventArgs = new AspectEventArgs { Name = methodName }; + + // Act + handler.OnEntry(eventArgs); + + 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()); + } + } + + handler.OnExit(eventArgs); + + var metricsOutput = consoleOut.ToString(); + + // Assert + // flush the (MaxMetrics + 1) item only + Assert.Contains("[[\"Service\"]]}]},\"Service\":\"testService\",\"Metric Name\":100}", metricsOutput); + + // Reset + handler.ResetForTest(); + } + + [Trait("Category", "EMFLimits")] + [Fact] + public void WhenMoreThan9DimensionsAdded_ThrowArgumentOutOfRangeException() + { + // Arrange + var methodName = Guid.NewGuid().ToString(); + var configurations = new Mock(); + + var metrics = new Metrics( + configurations.Object, + 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); + + // Assert + Assert.Throws(act); + + // Reset + handler.ResetForTest(); + } + + [Trait("Category", "SchemaValidation")] + [Fact] + public void WhenNamespaceNotDefined_ThrowSchemaValidationException() + { + // Arrange + var methodName = Guid.NewGuid().ToString(); + var configurations = new Mock(); + + var metrics = new Metrics( + configurations.Object + ); + + 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); + }; + + // 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 = new Mock(); + + var metrics = new Metrics( + configurations.Object, + 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); + + var result = consoleOut.ToString(); + + // Assert + Assert.Contains("\"Dimensions\":[[\"Service\"],[\"functionVersion\"]]" + , result); + Assert.Contains("\"Service\":\"testService\",\"functionVersion\":\"$LATEST\"" + , result); + + // Reset + handler.ResetForTest(); + } + + [Trait("Category", "MetricsImplementation")] + [Fact] + public void WhenNamespaceIsDefined_AbleToRetrieveNamespace() + { + // Arrange + var methodName = Guid.NewGuid().ToString(); + var configurations = new Mock(); + var metrics = new Metrics(configurations.Object); + + var handler = new MetricsAspectHandler( + metrics, + false + ); + + var eventArgs = new AspectEventArgs { Name = methodName }; + + // Act + handler.OnEntry(eventArgs); + Metrics.SetNamespace("dotnet-powertools-test"); + + var result = Metrics.GetNamespace(); + + // Assert + Assert.Equal("dotnet-powertools-test", result); + + // Reset + handler.ResetForTest(); + } + + [Trait("Category", "MetricsImplementation")] + [Fact] + public void WhenMetricsDefined_AbleToAddMetadata() + { + // Arrange + var methodName = Guid.NewGuid().ToString(); + var consoleOut = new StringWriter(); + Console.SetOut(consoleOut); + + var configurations = new Mock(); + var metrics = new Metrics( + configurations.Object, + 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); + + var result = consoleOut.ToString(); + + // Assert + Assert.Contains("\"test_metadata\":\"test_value\"", result); + + // Reset + handler.ResetForTest(); + } + + [Trait("Category", "MetricsImplementation")] + [Fact] + 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 = new Mock(); + + var metrics = new Metrics( + configurations.Object, + nameSpace: "dotnet-powertools-test", + service: "testService" + ); + + var handler = new MetricsAspectHandler( + metrics, + false + ); + + var eventArgs = new AspectEventArgs { Name = methodName }; + + // Act + handler.OnEntry(eventArgs); + Metrics.SetDefaultDimensions(defaultDimensions); + Metrics.AddMetric("TestMetric", 1, MetricUnit.Count); + handler.OnExit(eventArgs); + + var result = consoleOut.ToString(); + + // Assert + Assert.Contains("\"Dimensions\":[[\"Service\"],[\"CustomDefaultDimension\"]", result); + Assert.Contains("\"CustomDefaultDimension\":\"CustomDefaultDimensionValue\"", result); + + // Reset + handler.ResetForTest(); + } + + [Trait("Category", "MetricsImplementation")] + [Fact] + public void WhenMetricIsNegativeValue_ThrowException() + { + // Arrange + var methodName = Guid.NewGuid().ToString(); + var configurations = new Mock(); + + var metrics = new Metrics( + configurations.Object, + 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); + }; + + // Assert + var exception = Assert.Throws(act); + Assert.Equal("'AddMetric' method requires a valid metrics value. Value must be >= 0.", exception.Message); + + // RESET + handler.ResetForTest(); + } + + [Trait("Category", "SchemaValidation")] + [Fact] + public void WhenDefaultDimensionSet_IgnoreDuplicates() + { + // Arrange + var methodName = Guid.NewGuid().ToString(); + var consoleOut = new StringWriter(); + Console.SetOut(consoleOut); + var configurations = new Mock(); + var defaultDimensions = new Dictionary { { "CustomDefaultDimension", "CustomDefaultDimensionValue" } }; + + var metrics = new Metrics( + configurations.Object, + nameSpace: "dotnet-powertools-test", + service: "testService" + ); + + 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); + + 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 = new Mock(); + + var metrics = new Metrics( + configurations.Object, + 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); + + var result = consoleOut.ToString(); + + // Assert + Assert.Contains("CloudWatchMetrics\":[{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"Time\",\"Unit\":\"Milliseconds\"}],\"Dimensions\":[[\"Service\"],[\"functionVersion\"]]}]},\"Service\":\"testService\",\"functionVersion\":\"$LATEST\",\"env\":\"dev\",\"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 = new Mock(); + + var metrics = new Metrics( + configurations.Object, + 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); + + 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 = new Mock(); + + var metrics = new Metrics( + configurations.Object, + 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); + + 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 = new Mock(); + + var metrics = new Metrics( + configurations.Object, + 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); + + var result = consoleOut.ToString(); + + // Assert + Assert.Contains("\"Metrics\":[{\"Name\":\"Time\",\"Unit\":\"Milliseconds\",\"StorageResolution\":1}]" + , result); + Assert.Contains("\"Time\":100.5" + , result); + + // Reset + handler.ResetForTest(); + } + + #region Helpers + + private List AllIndexesOf(string str, string value) + { + var indexes = new List(); + + if (string.IsNullOrEmpty(value)) return indexes; + + for (var index = 0; ; index += value.Length) + { + index = str.IndexOf(value, index, StringComparison.Ordinal); + if (index == -1) + return indexes; + 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 = new Mock(); + + var metrics = new Metrics(configurations.Object, + 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); + + var metricsOutput = consoleOut.ToString(); + + // Assert + Assert.Contains("{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"Metric Name\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\"]]", + metricsOutput); + + // Reset + handler.ResetForTest(); + } + } +} + +/// +/// This class is used to register the input event and return type for the FunctionHandler method with the System.Text.Json source generator. +/// There must be a JsonSerializable attribute for each type used as the input and return type or a runtime error will occur +/// from the JSON serializer unable to find the serialization information for unknown 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 TestJsonSerializerContext : JsonSerializerContext +{ +} From ef849c67d9d5732279e9d0d25ffa0ce079af117e Mon Sep 17 00:00:00 2001 From: James Eastham Date: Tue, 4 Jul 2023 18:35:16 +0100 Subject: [PATCH 2/4] #212 Update metrics to default to source generated serialization --- .../AWS.Lambda.Powertools.Metrics.csproj | 4 ---- .../Model/MetricsContext.cs | 2 +- .../Serializer/MetricsSerializationContext.cs | 20 ++++++++++++++++ .../EMFValidationTests.cs | 5 ++++ .../EMFValidationWithSourceGenerationTests.cs | 24 ------------------- 5 files changed, 26 insertions(+), 29 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 dbd62584..18f79afe 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 @@ -27,10 +27,6 @@ - - - - diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricsContext.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricsContext.cs index 6f4c8348..f8423407 100644 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricsContext.cs +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricsContext.cs @@ -34,7 +34,7 @@ public class MetricsContext : IDisposable ///
private RootNode _rootNode; - private static IPowerToolsSerializer _serializationContext = new SystemTextJsonSerializer(); + private static IPowerToolsSerializer _serializationContext = new SourceGeneratedSerializer(); /// /// Creates empty MetricsContext object 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..c9e2af40 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Serializer/MetricsSerializationContext.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AWS.Lambda.Powertools.Metrics; + +[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 +{ + +} \ 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 e2a9b656..d1eb562c 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs @@ -31,6 +31,11 @@ namespace AWS.Lambda.Powertools.Metrics.Tests [Collection("Sequential")] public class EmfValidationTests { + public EmfValidationTests() + { + MetricsContext.SetJsonSerializationContext(new SystemTextJsonSerializer()); + } + [Trait("Category", value: "SchemaValidation")] [Fact] public void WhenCaptureColdStart_CreateSeparateBlob() diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationWithSourceGenerationTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationWithSourceGenerationTests.cs index dedc8785..0e326cbd 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationWithSourceGenerationTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationWithSourceGenerationTests.cs @@ -29,10 +29,6 @@ namespace AWS.Lambda.Powertools.Metrics.Tests [Collection("Sequential")] public class EMFValidationWithSourceGenerationTests { - public EMFValidationWithSourceGenerationTests() - { - MetricsContext.SetJsonSerializationContext(new SourceGeneratedSerializer()); - } [Trait("Category", value: "SchemaValidation")] [Fact] public void WhenCaptureColdStart_CreateSeparateBlob() @@ -751,23 +747,3 @@ public async Task WhenMetricsAsyncRaceConditionItemSameKeyExists_ValidateLock() } } } - -/// -/// This class is used to register the input event and return type for the FunctionHandler method with the System.Text.Json source generator. -/// There must be a JsonSerializable attribute for each type used as the input and return type or a runtime error will occur -/// from the JSON serializer unable to find the serialization information for unknown 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 TestJsonSerializerContext : JsonSerializerContext -{ -} From 8ce878fe9bdb3bce5655fac4df464f73429a9341 Mon Sep 17 00:00:00 2001 From: jamesuk Date: Wed, 5 Jul 2023 08:00:27 +0100 Subject: [PATCH 3/4] #212 Add native AOT example --- .gitignore | 4 + .gitignore.bak | 27 + examples/PowerTools.NativeAOT/.gitignore | 638 ++++++++++++++++++ .../PowerTools.NativeAOTExample.sln | 52 ++ examples/PowerTools.NativeAOT/README.md | 119 ++++ examples/PowerTools.NativeAOT/build.ps1 | 4 + .../PowerTools.NativeAOT/events/event.json | 63 ++ examples/PowerTools.NativeAOT/global.json | 5 + examples/PowerTools.NativeAOT/omnisharp.json | 11 + .../HelloWorld/CustomSerializationContext.cs | 34 + .../src/HelloWorld/Dockerfile | 22 + .../src/HelloWorld/Function.cs | 172 +++++ .../src/HelloWorld/HelloWorld.csproj | 55 ++ .../src/HelloWorld/LookupRecord.cs | 45 ++ .../src/HelloWorld/Program.cs | 53 ++ .../HelloWorld/aws-lambda-tools-defaults.json | 12 + .../PowerTools.NativeAOT/template-docker.yaml | 60 ++ examples/PowerTools.NativeAOT/template.yaml | 61 ++ .../test/HelloWorld.Test/FunctionTest.cs | 108 +++ .../HelloWorld.Test/HelloWorld.Tests.csproj | 21 + 20 files changed, 1566 insertions(+) create mode 100644 .gitignore.bak create mode 100644 examples/PowerTools.NativeAOT/.gitignore create mode 100644 examples/PowerTools.NativeAOT/PowerTools.NativeAOTExample.sln create mode 100644 examples/PowerTools.NativeAOT/README.md create mode 100644 examples/PowerTools.NativeAOT/build.ps1 create mode 100644 examples/PowerTools.NativeAOT/events/event.json create mode 100644 examples/PowerTools.NativeAOT/global.json create mode 100644 examples/PowerTools.NativeAOT/omnisharp.json create mode 100644 examples/PowerTools.NativeAOT/src/HelloWorld/CustomSerializationContext.cs create mode 100644 examples/PowerTools.NativeAOT/src/HelloWorld/Dockerfile create mode 100644 examples/PowerTools.NativeAOT/src/HelloWorld/Function.cs create mode 100644 examples/PowerTools.NativeAOT/src/HelloWorld/HelloWorld.csproj create mode 100644 examples/PowerTools.NativeAOT/src/HelloWorld/LookupRecord.cs create mode 100644 examples/PowerTools.NativeAOT/src/HelloWorld/Program.cs create mode 100644 examples/PowerTools.NativeAOT/src/HelloWorld/aws-lambda-tools-defaults.json create mode 100644 examples/PowerTools.NativeAOT/template-docker.yaml create mode 100644 examples/PowerTools.NativeAOT/template.yaml create mode 100644 examples/PowerTools.NativeAOT/test/HelloWorld.Test/FunctionTest.cs create mode 100644 examples/PowerTools.NativeAOT/test/HelloWorld.Test/HelloWorld.Tests.csproj diff --git a/.gitignore b/.gitignore index 3e593c9a..865fcbd8 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,10 @@ deploy/** .vs/ .aws-sam +*.dll + +**/Assemblies/* + examples/SimpleLambda/.aws-sam examples/SimpleLambda/samconfig.toml diff --git a/.gitignore.bak b/.gitignore.bak new file mode 100644 index 00000000..7609afee --- /dev/null +++ b/.gitignore.bak @@ -0,0 +1,27 @@ +docs/node_modules +docs/.cache +docs/public + +apidocs/_site +apidocs/obj + +deploy/** + +.idea +.vscode +.vs/ +.aws-sam + +*.dll + +examples/SimpleLambda/.aws-sam +examples/SimpleLambda/samconfig.toml + +AWS.Lambda.Powertools.sln.DotSettings.user +[Oo]bj/** +[Bb]in/** +.DS_Store + +dist/ +site/ +samconfig.toml \ No newline at end of file diff --git a/examples/PowerTools.NativeAOT/.gitignore b/examples/PowerTools.NativeAOT/.gitignore new file mode 100644 index 00000000..af2786a1 --- /dev/null +++ b/examples/PowerTools.NativeAOT/.gitignore @@ -0,0 +1,638 @@ +samconfig.toml + +# Created by https://www.toptal.com/developers/gitignore/api/sam,linux,macos,windows,dotsettings,sublimetext,visualstudio,visualstudiocode,jetbrains +# Edit at https://www.toptal.com/developers/gitignore?templates=sam,linux,macos,windows,dotsettings,sublimetext,visualstudio,visualstudiocode,jetbrains + +### DotSettings ### +*.DotSettings + +### JetBrains ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### JetBrains Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### SAM ### +# Ignore build directories for the AWS Serverless Application Model (SAM) +# Info: https://aws.amazon.com/serverless/sam/ +# Docs: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-reference.html + +**/.aws-sam + +### SublimeText ### +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# Workspace files are user-specific +*.sublime-workspace + +# Project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using Sublime Text +# *.sublime-project + +# SFTP configuration file +sftp-config.json +sftp-config-alt*.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# Support for Project snippet scope + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +*.code-workspace + +# Local History for Visual Studio Code + +# Windows Installer files from build outputs + +# JetBrains Rider +*.sln.iml + +### VisualStudio Patch ### +# Additional files built by Visual Studio + +# End of https://www.toptal.com/developers/gitignore/api/sam,linux,macos,windows,dotsettings,sublimetext,visualstudio,visualstudiocode,jetbrains diff --git a/examples/PowerTools.NativeAOT/PowerTools.NativeAOTExample.sln b/examples/PowerTools.NativeAOT/PowerTools.NativeAOTExample.sln new file mode 100644 index 00000000..c9e94181 --- /dev/null +++ b/examples/PowerTools.NativeAOT/PowerTools.NativeAOTExample.sln @@ -0,0 +1,52 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HelloWorld", "src\HelloWorld\HelloWorld.csproj", "{F36CE828-C7B1-4BD1-AC4B-500C7ACD14E1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HelloWorld.Tests", "test\HelloWorld.Test\HelloWorld.Tests.csproj", "{9B31B9EA-52BC-47B6-B78B-AC16502D8D4A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Logging", "..\..\libraries\src\AWS.Lambda.Powertools.Logging\AWS.Lambda.Powertools.Logging.csproj", "{B6F1B81E-D74E-4332-9555-D5E2CA9635B8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Metrics", "..\..\libraries\src\AWS.Lambda.Powertools.Metrics\AWS.Lambda.Powertools.Metrics.csproj", "{591F6C70-991D-4056-818D-A373F70150E5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Tracing", "..\..\libraries\src\AWS.Lambda.Powertools.Tracing\AWS.Lambda.Powertools.Tracing.csproj", "{7605DAC5-4563-4D4E-96D4-5059F6FC8569}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Common", "..\..\libraries\src\AWS.Lambda.Powertools.Common\AWS.Lambda.Powertools.Common.csproj", "{41CF40C3-60D1-498E-9D4E-0AF33C6D530A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F36CE828-C7B1-4BD1-AC4B-500C7ACD14E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F36CE828-C7B1-4BD1-AC4B-500C7ACD14E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F36CE828-C7B1-4BD1-AC4B-500C7ACD14E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F36CE828-C7B1-4BD1-AC4B-500C7ACD14E1}.Release|Any CPU.Build.0 = Release|Any CPU + {9B31B9EA-52BC-47B6-B78B-AC16502D8D4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B31B9EA-52BC-47B6-B78B-AC16502D8D4A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B31B9EA-52BC-47B6-B78B-AC16502D8D4A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B31B9EA-52BC-47B6-B78B-AC16502D8D4A}.Release|Any CPU.Build.0 = Release|Any CPU + {B6F1B81E-D74E-4332-9555-D5E2CA9635B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6F1B81E-D74E-4332-9555-D5E2CA9635B8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6F1B81E-D74E-4332-9555-D5E2CA9635B8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6F1B81E-D74E-4332-9555-D5E2CA9635B8}.Release|Any CPU.Build.0 = Release|Any CPU + {591F6C70-991D-4056-818D-A373F70150E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {591F6C70-991D-4056-818D-A373F70150E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {591F6C70-991D-4056-818D-A373F70150E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {591F6C70-991D-4056-818D-A373F70150E5}.Release|Any CPU.Build.0 = Release|Any CPU + {7605DAC5-4563-4D4E-96D4-5059F6FC8569}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7605DAC5-4563-4D4E-96D4-5059F6FC8569}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7605DAC5-4563-4D4E-96D4-5059F6FC8569}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7605DAC5-4563-4D4E-96D4-5059F6FC8569}.Release|Any CPU.Build.0 = Release|Any CPU + {41CF40C3-60D1-498E-9D4E-0AF33C6D530A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {41CF40C3-60D1-498E-9D4E-0AF33C6D530A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {41CF40C3-60D1-498E-9D4E-0AF33C6D530A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {41CF40C3-60D1-498E-9D4E-0AF33C6D530A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/examples/PowerTools.NativeAOT/README.md b/examples/PowerTools.NativeAOT/README.md new file mode 100644 index 00000000..2c39c60a --- /dev/null +++ b/examples/PowerTools.NativeAOT/README.md @@ -0,0 +1,119 @@ +# Powertools for AWS Lambda (.NET) - Logging Example + +This project contains source code and supporting files for a serverless application that you can deploy with the AWS Serverless Application Model Command Line Interface (AWS SAM CLI). It includes the following files and folders. + +* src - Code for the application's Lambda function and Project Dockerfile. +* events - Invocation events that you can use to invoke the function. +* test - Unit tests for the application code. +* template.yaml - A template that defines the application's AWS resources. + +The application uses several AWS resources, including Lambda functions and an API Gateway API. These resources are defined in the `template.yaml` file in this project. You can update the template to add AWS resources through the same deployment process that updates your application code. + +If you prefer to use an integrated development environment (IDE) to build and test your application, you can use the AWS Toolkit. The AWS Toolkit is an open source plug-in for popular IDEs that uses the AWS SAM CLI to build and deploy serverless applications on AWS. The AWS Toolkit also adds a simplified step-through debugging experience for Lambda function code. See the following links to get started. + +* [Visual Studio Code](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/welcome.html) +* [Visual Studio](https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/welcome.html) +* [Rider](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) + +## Deploy the sample application + +The AWS SAM CLI is an extension of the AWS Command Line Interface (AWS CLI) that adds functionality for building and testing Lambda applications. It uses Docker to run your functions in an Amazon Linux environment that matches Lambda. It can also emulate your application's build environment and API. + +To use the AWS SAM CLI, you need the following tools. + +* AWS SAM CLI - [Install the AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) +* Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) + +You will need the following for local testing. +* .NET 6.0 - [Install .NET 6.0](https://www.microsoft.com/net/download) + +To build and deploy your application for the first time, run the following in your shell. Make sure the `template.yaml` file is in your current directory: + +```bash +sam build +sam deploy --guided +``` + +The first command will build a docker image from a Dockerfile and then copy the source of your application inside the Docker image. The second command will package and deploy your application to AWS, with a series of prompts: + +* **Stack Name**: The name of the stack to deploy to CloudFormation. This should be unique to your account and region, and a good starting point would be something matching your project name. +* **AWS Region**: The AWS region you want to deploy your app to. +* **Confirm changes before deploy**: If set to yes, any change sets will be shown to you before execution for manual review. If set to no, the AWS SAM CLI will automatically deploy application changes. +* **Allow SAM CLI IAM role creation**: Many AWS SAM templates, including this example, create AWS IAM roles required for the AWS Lambda function(s) included to access AWS services. By default, these are scoped down to minimum required permissions. To deploy an AWS CloudFormation stack which creates or modifies IAM roles, the `CAPABILITY_IAM` value for `capabilities` must be provided. If permission isn't provided through this prompt, to deploy this example you must explicitly pass `--capabilities CAPABILITY_IAM` to the `sam deploy` command. +* **Save arguments to samconfig.toml**: If set to yes, your choices will be saved to a configuration file inside the project, so that in the future you can just re-run `sam deploy` without parameters to deploy changes to your application. + +You can find your API Gateway Endpoint URL in the output values displayed after deployment. + +## Use the AWS SAM CLI to build and test locally + +Build your application with the `sam build` command. + +```bash +Logging$ sam build +``` + +The AWS SAM CLI builds a docker image from a Dockerfile and then installs dependencies defined in `src/HelloWorld.csproj` inside the docker image. The processed template file is saved in the `.aws-sam/build` folder. + +Test a single function by invoking it directly with a test event. An event is a JSON document that represents the input that the function receives from the event source. Test events are included in the `events` folder in this project. + +Run functions locally and invoke them with the `sam local invoke` command. + +```bash +Logging$ sam local invoke HelloWorldFunction --event events/event.json +``` + +The AWS SAM CLI can also emulate your application's API. Use the `sam local start-api` to run the API locally on port 3000. + +```bash +Logging$ sam local start-api +Logging$ curl http://localhost:3000/ +``` + +The AWS SAM CLI reads the application template to determine the API's routes and the functions that they invoke. The `Events` property on each function's definition includes the route and method for each path. + +```yaml + Events: + HelloWorld: + Type: Api + Properties: + Path: /hello + Method: get +``` + +## Add a resource to your application + +The application template uses AWS Serverless Application Model (AWS SAM) to define application resources. AWS SAM is an extension of AWS CloudFormation with a simpler syntax for configuring common serverless application resources such as functions, triggers, and APIs. For resources not included in [the AWS SAM specification](https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md), you can use standard [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) resource types. + +## Fetch, tail, and filter Lambda function logs + +To simplify troubleshooting, AWS SAM CLI has a command called `sam logs`. `sam logs` lets you fetch logs generated by your deployed Lambda function from the command line. In addition to printing the logs on the terminal, this command has several nifty features to help you quickly find the bug. + +`NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM. + +```bash +Logging$ sam logs -n HelloWorldFunction --stack-name Logging --tail +``` + +You can find more information and examples about filtering Lambda function logs in the [SAM CLI Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html). + +## Unit tests + +Tests are defined in the `test` folder in this project. + +```bash +Logging$ dotnet test test/HelloWorld.Test +``` + +## Cleanup + +To delete the sample application that you created, use the AWS SAM CLI. Assuming you used your project name for the stack name, you can run the following: + +```bash +Logging$ sam delete +``` + +## Resources + +See the [AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) for an introduction to SAM specification, the AWS SAM CLI, and serverless application concepts. + +Next, you can use AWS Serverless Application Repository to deploy ready to use Apps that go beyond hello world samples and learn how authors developed their applications: [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/) diff --git a/examples/PowerTools.NativeAOT/build.ps1 b/examples/PowerTools.NativeAOT/build.ps1 new file mode 100644 index 00000000..6e380d46 --- /dev/null +++ b/examples/PowerTools.NativeAOT/build.ps1 @@ -0,0 +1,4 @@ +dotnet publish -c Release -r linux-x64 ../../libraries/src/AWS.Lambda.Powertools.Logging/AWS.Lambda.Powertools.Logging.csproj -o ./src/HelloWorld/Assemblies +dotnet publish -c Release -r linux-x64 ../../libraries/src/AWS.Lambda.Powertools.Metrics/AWS.Lambda.Powertools.Metrics.csproj -o ./src/HelloWorld/Assemblies +dotnet publish -c Release -r linux-x64 ../../libraries/src/AWS.Lambda.Powertools.Tracing/AWS.Lambda.Powertools.Tracing.csproj -o ./src/HelloWorld/Assemblies +sam build \ No newline at end of file diff --git a/examples/PowerTools.NativeAOT/events/event.json b/examples/PowerTools.NativeAOT/events/event.json new file mode 100644 index 00000000..3822fada --- /dev/null +++ b/examples/PowerTools.NativeAOT/events/event.json @@ -0,0 +1,63 @@ +{ + "body": "{\"message\": \"hello world\"}", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } + } + \ No newline at end of file diff --git a/examples/PowerTools.NativeAOT/global.json b/examples/PowerTools.NativeAOT/global.json new file mode 100644 index 00000000..401762bf --- /dev/null +++ b/examples/PowerTools.NativeAOT/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "7.0.302" + } +} diff --git a/examples/PowerTools.NativeAOT/omnisharp.json b/examples/PowerTools.NativeAOT/omnisharp.json new file mode 100644 index 00000000..c42f8db9 --- /dev/null +++ b/examples/PowerTools.NativeAOT/omnisharp.json @@ -0,0 +1,11 @@ +{ + "fileOptions": { + "excludeSearchPatterns": [ + "**/bin/**/*", + "**/obj/**/*" + ] + }, + "msbuild": { + "Platform": "rhel.7.2-x64" + } +} \ No newline at end of file diff --git a/examples/PowerTools.NativeAOT/src/HelloWorld/CustomSerializationContext.cs b/examples/PowerTools.NativeAOT/src/HelloWorld/CustomSerializationContext.cs new file mode 100644 index 00000000..6dad7c40 --- /dev/null +++ b/examples/PowerTools.NativeAOT/src/HelloWorld/CustomSerializationContext.cs @@ -0,0 +1,34 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +namespace HelloWorld; + +using System.Text.Json.Serialization; + +using Amazon.Lambda.APIGatewayEvents; + +[JsonSerializable(typeof(APIGatewayProxyRequest))] +[JsonSerializable(typeof(APIGatewayProxyResponse))] +[JsonSerializable(typeof(LookupRecord))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(bool))] +[JsonSerializable(typeof(double))] +[JsonSerializable(typeof(Exception))] +[JsonSerializable(typeof(InvalidOperationException))] +public partial class CustomSerializationContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/examples/PowerTools.NativeAOT/src/HelloWorld/Dockerfile b/examples/PowerTools.NativeAOT/src/HelloWorld/Dockerfile new file mode 100644 index 00000000..0518f598 --- /dev/null +++ b/examples/PowerTools.NativeAOT/src/HelloWorld/Dockerfile @@ -0,0 +1,22 @@ +FROM mcr.microsoft.com/dotnet/sdk:7.0-alpine AS build-image + +ARG FUNCTION_DIR="/build" +ARG SAM_BUILD_MODE="run" +ENV PATH="/root/.dotnet/tools:${PATH}" +RUN apt-get update && apt-get -y install zip + +RUN mkdir $FUNCTION_DIR +WORKDIR $FUNCTION_DIR +COPY Function.cs Program.cs CustomSerializationContext.cs LookupRecord.cs HelloWorld.csproj aws-lambda-tools-defaults.json $FUNCTION_DIR/ +RUN dotnet tool install -g Amazon.Lambda.Tools + +# Build and Copy artifacts depending on build mode. +RUN mkdir -p build_artifacts +RUN if [ "$SAM_BUILD_MODE" = "debug" ]; then dotnet lambda package --configuration Debug; else dotnet lambda package --configuration Release; fi +RUN if [ "$SAM_BUILD_MODE" = "debug" ]; then cp -r /build/bin/Debug/net7.0/publish/* /build/build_artifacts; else cp -r /build/bin/Release/net7.0/publish/* /build/build_artifacts; fi + +FROM alpine +RUN apk add --no-cache libstdc++ +COPY --from=build-image /build/build_artifacts/ /var/task/ +# Command can be overwritten by providing a different command in the template directly. +CMD ["var/task/bootstrap"] \ No newline at end of file diff --git a/examples/PowerTools.NativeAOT/src/HelloWorld/Function.cs b/examples/PowerTools.NativeAOT/src/HelloWorld/Function.cs new file mode 100644 index 00000000..3652862b --- /dev/null +++ b/examples/PowerTools.NativeAOT/src/HelloWorld/Function.cs @@ -0,0 +1,172 @@ +namespace HelloWorld; + +using System.Text.Json; + +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.Core; +using Amazon.XRay.Recorder.Handlers.AwsSdk; + +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging; +using AWS.Lambda.Powertools.Metrics; +using AWS.Lambda.Powertools.Tracing; + +public class Function +{ + private static HttpClient? _httpClient; + private static IAmazonDynamoDB? _dynamoDbClient; + + /// + /// Function constructor + /// + public Function() + { + Logger.SetSerializer(new SourceGeneratedSerializer()); + + AWSSDKHandler.RegisterXRayForAllServices(); + _httpClient = new HttpClient(); + _dynamoDbClient = new AmazonDynamoDBClient(); + } + + /// + /// Test constructor + /// + public Function(IAmazonDynamoDB dynamoDbClient, HttpClient httpClient) + { + _httpClient = httpClient; + _dynamoDbClient = dynamoDbClient; + } + + [Tracing(CaptureMode = TracingCaptureMode.ResponseAndError)] + public async Task FunctionHandler( + APIGatewayProxyRequest apigwProxyEvent, + ILambdaContext context) + { + var requestContextRequestId = apigwProxyEvent.RequestContext.RequestId; + + var lookupInfo = new Dictionary + { + { "LookupInfo", new Dictionary { { "LookupId", requestContextRequestId } } } + }; + + // Appended keys are added to all subsequent log entries in the current execution. + // Call this method as early as possible in the Lambda handler. + // Typically this is value would be passed into the function via the event. + // Set the ClearState = true to force the removal of keys across invocations, + Logger.AppendKeys(lookupInfo); + + Logger.LogInformation("Getting ip address from external service"); + + // Add Metric to capture the amount of time + Metrics.PushSingleMetric( + "CallingIP", + 1, + MetricUnit.Count); + + var location = await GetCallingIp(); + + var lookupRecord = new LookupRecord( + requestContextRequestId, + "Hello Powertools for AWS Lambda (.NET)", + location); + + // Trace Fluent API + Tracing.WithSubsegment( + "LoggingResponse", + subsegment => + { + subsegment.AddAnnotation( + "AccountId", + apigwProxyEvent.RequestContext.AccountId); + subsegment.AddMetadata( + "LookupRecord", + lookupRecord); + }); + + try + { + await SaveRecordInDynamo(lookupRecord); + + Metrics.AddMetric( + "SuccessfulWrites", + 1, + MetricUnit.Count); + + return new APIGatewayProxyResponse + { + Body = JsonSerializer.Serialize( + lookupRecord, + typeof(LookupRecord), + CustomSerializationContext.Default), + StatusCode = 200, + Headers = new Dictionary { { "Content-Type", "application/json" } } + }; + } + catch (Exception e) + { + Logger.LogError(e.Message); + + return new APIGatewayProxyResponse + { + Body = e.Message, + StatusCode = 500, + Headers = new Dictionary { { "Content-Type", "application/json" } } + }; + } + } + + [Tracing(SegmentName = "Location service")] + private static async Task GetCallingIp() + { + if (_httpClient == null) return "0.0.0.0"; + _httpClient.DefaultRequestHeaders.Accept.Clear(); + _httpClient.DefaultRequestHeaders.Add("User-Agent", "AWS Lambda .Net Client"); + + try + { + Logger.LogInformation("Calling Check IP API"); + + var response = await _httpClient.GetStringAsync("https://checkip.amazonaws.com/").ConfigureAwait(false); + var ip = response.Replace( + "\n", + ""); + + Logger.LogInformation($"API response returned {ip}"); + + return ip; + } + catch (Exception ex) + { + Logger.LogError(ex); + throw; + } + } + + /// + /// Saves the lookup record in DynamoDB + /// + /// + /// A Task that can be used to poll or wait for results, or both. + [Tracing(SegmentName = "DynamoDB")] + private static async Task SaveRecordInDynamo(LookupRecord lookupRecord) + { + try + { + Logger.LogInformation($"Saving record with id {lookupRecord.LookupId}"); + + await _dynamoDbClient?.PutItemAsync(Environment.GetEnvironmentVariable("TABLE_NAME"), new Dictionary(3) + { + {"LookupId", new AttributeValue(lookupRecord.LookupId)}, + {"Greeting", new AttributeValue(lookupRecord.Greeting)}, + {"IpAddress", new AttributeValue(lookupRecord.IpAddress)}, + })!; + } + catch (AmazonDynamoDBException e) + { + Logger.LogCritical(e.Message); + throw; + } + } +} \ No newline at end of file diff --git a/examples/PowerTools.NativeAOT/src/HelloWorld/HelloWorld.csproj b/examples/PowerTools.NativeAOT/src/HelloWorld/HelloWorld.csproj new file mode 100644 index 00000000..96849631 --- /dev/null +++ b/examples/PowerTools.NativeAOT/src/HelloWorld/HelloWorld.csproj @@ -0,0 +1,55 @@ + + + Exe + net7.0 + enable + enable + Lambda + bootstrap + + true + + true + + true + partial + + + + + + + + + + + + + + + + + + + + + + + + + + + Assemblies\AWS.Lambda.Powertools.Common.dll + + + Assemblies\AWS.Lambda.Powertools.Logging.dll + + + Assemblies\AWS.Lambda.Powertools.Metrics.dll + + + Assemblies\AWS.Lambda.Powertools.Tracing.dll + + + diff --git a/examples/PowerTools.NativeAOT/src/HelloWorld/LookupRecord.cs b/examples/PowerTools.NativeAOT/src/HelloWorld/LookupRecord.cs new file mode 100644 index 00000000..f24ec9d2 --- /dev/null +++ b/examples/PowerTools.NativeAOT/src/HelloWorld/LookupRecord.cs @@ -0,0 +1,45 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + + +namespace HelloWorld; + +/// +/// Record to represent the data structure of Lookup +/// +[Serializable] +public class LookupRecord +{ + public LookupRecord() + { + } + + /// + /// Record to represent the data structure of Lookup + /// + /// Id of the lookup + /// Greeting phrase + /// IP address + public LookupRecord(string? lookupId, string? greeting, string? ipAddress) + { + this.LookupId = lookupId; + this.Greeting = greeting; + this.IpAddress = ipAddress; + } + + public string? LookupId { get; set; } + public string? Greeting { get; set; } + public string? IpAddress { get; set; } +} \ No newline at end of file diff --git a/examples/PowerTools.NativeAOT/src/HelloWorld/Program.cs b/examples/PowerTools.NativeAOT/src/HelloWorld/Program.cs new file mode 100644 index 00000000..8b363c4c --- /dev/null +++ b/examples/PowerTools.NativeAOT/src/HelloWorld/Program.cs @@ -0,0 +1,53 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.Core; +using Amazon.Lambda.Serialization.SystemTextJson; +using AWS.Lambda.Powertools.Logging; + +using Amazon.Lambda.RuntimeSupport; +using AWS.Lambda.Powertools.Metrics; +using AWS.Lambda.Powertools.Tracing; + +// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. +[assembly: LambdaSerializer(typeof(DefaultLambdaJsonSerializer))] + +namespace HelloWorld; + +public static class Program +{ + private static Function Function; + + private static async Task Main() + { + Function = new Function(); + + Func> handler = FunctionHandler; + await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer()) + .Build() + .RunAsync(); + } + + [Logging(LogEvent = true)] + [Metrics(CaptureColdStart = true)] + [Tracing(CaptureMode = TracingCaptureMode.ResponseAndError)] + public static async Task FunctionHandler(APIGatewayProxyRequest apigwProxyEvent, ILambdaContext context) + { + return await Function.FunctionHandler( + apigwProxyEvent, + context); + } +} \ No newline at end of file diff --git a/examples/PowerTools.NativeAOT/src/HelloWorld/aws-lambda-tools-defaults.json b/examples/PowerTools.NativeAOT/src/HelloWorld/aws-lambda-tools-defaults.json new file mode 100644 index 00000000..eb332f4b --- /dev/null +++ b/examples/PowerTools.NativeAOT/src/HelloWorld/aws-lambda-tools-defaults.json @@ -0,0 +1,12 @@ +{ + "Information": [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help", + "All the command line options for the Lambda command can be specified in this file." + ], + "profile": "", + "region": "", + "configuration": "Release", + "template": "template.yaml" +} \ No newline at end of file diff --git a/examples/PowerTools.NativeAOT/template-docker.yaml b/examples/PowerTools.NativeAOT/template-docker.yaml new file mode 100644 index 00000000..78651841 --- /dev/null +++ b/examples/PowerTools.NativeAOT/template-docker.yaml @@ -0,0 +1,60 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: > + Example project for Powertools for AWS Lambda (.NET) Logging utility + +# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst +Globals: + Function: + Timeout: 10 + Environment: + Variables: + POWERTOOLS_SERVICE_NAME: powertools-dotnet-logging-sample + POWERTOOLS_LOG_LEVEL: Debug + POWERTOOLS_LOGGER_CASE: PascalCase # Allowed values are: CamelCase, PascalCase and SnakeCase + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + MemorySize: 1024 + PackageType: Image + Environment: + Variables: + TABLE_NAME: !Ref PowertoolsLoggingTable + Events: + HelloWorld: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /hello + Method: get + Policies: + - DynamoDBCrudPolicy: # Policy template with placeholder value + TableName: !Ref PowertoolsLoggingTable + Metadata: + DockerTag: dotnet7aot-v1 + DockerContext: ./src/HelloWorld + Dockerfile: Dockerfile + DockerBuildArgs: + SAM_BUILD_MODE: run # debug or run + + PowertoolsLoggingTable: + Type: AWS::Serverless::SimpleTable + Properties: + TableName: PowertoolsLogging + PrimaryKey: + Name: LookupId + Type: String + +Outputs: + HelloWorldApi: + Description: "API Gateway endpoint URL for Prod stage for Hello World function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn + HelloWorldFunctionIamRole: + Description: "Implicit IAM Role created for Hello World function" + Value: !GetAtt HelloWorldFunctionRole.Arn diff --git a/examples/PowerTools.NativeAOT/template.yaml b/examples/PowerTools.NativeAOT/template.yaml new file mode 100644 index 00000000..3011fa23 --- /dev/null +++ b/examples/PowerTools.NativeAOT/template.yaml @@ -0,0 +1,61 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: > + Example project for Powertools for AWS Lambda (.NET) with Native AOT + +# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst +Globals: + Function: + Timeout: 10 + Tracing: Active + Environment: + Variables: + POWERTOOLS_SERVICE_NAME: powertools-dotnet-logging-sample + POWERTOOLS_LOGGER_CASE: PascalCase + POWERTOOLS_LOG_LEVEL: Debug + POWERTOOLS_METRICS_NAMESPACE: powertools-native-aot + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + Runtime: provided.al2 + CodeUri: ./src/HelloWorld/ + Handler: bootstrap + MemorySize: 1024 + Environment: + Variables: + TABLE_NAME: !Ref PowertoolsLoggingTable + Events: + HelloWorld: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /hello + Method: get + Policies: + - DynamoDBCrudPolicy: # Policy template with placeholder value + TableName: !Ref PowertoolsLoggingTable + - CloudWatchPutMetricPolicy: {} + Metadata: + BuildMethod: dotnet7 + + PowertoolsLoggingTable: + Type: AWS::Serverless::SimpleTable + Properties: + TableName: PowertoolsLogging + PrimaryKey: + Name: LookupId + Type: String + +Outputs: + HelloWorldApi: + Description: "API Gateway endpoint URL for Prod stage for Hello World function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn + HelloWorldFunctionIamRole: + Description: "Implicit IAM Role created for Hello World function" + Value: !GetAtt HelloWorldFunctionRole.Arn diff --git a/examples/PowerTools.NativeAOT/test/HelloWorld.Test/FunctionTest.cs b/examples/PowerTools.NativeAOT/test/HelloWorld.Test/FunctionTest.cs new file mode 100644 index 00000000..9b95544a --- /dev/null +++ b/examples/PowerTools.NativeAOT/test/HelloWorld.Test/FunctionTest.cs @@ -0,0 +1,108 @@ +/* + * 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.Net; +using System.Net.Http; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Amazon.DynamoDBv2.DataModel; +using Xunit; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.TestUtilities; +using Moq; +using Moq.Protected; +using Xunit.Abstractions; + +namespace HelloWorld.Tests +{ + public class FunctionTest + { + private readonly ITestOutputHelper _testOutputHelper; + + public FunctionTest(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + [Fact] + public async Task TestHelloWorldFunctionHandler() + { + // Arrange + var requestId = Guid.NewGuid().ToString("D"); + var accountId = Guid.NewGuid().ToString("D"); + var location = "192.158. 1.38"; + + var dynamoDbContext = new Mock(); + var handlerMock = new Mock(); + handlerMock + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny() + ) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(location) + }) + .Verifiable(); + + var request = new APIGatewayProxyRequest + { + RequestContext = new APIGatewayProxyRequest.ProxyRequestContext + { + RequestId = requestId, + AccountId = accountId + } + }; + + var context = new TestLambdaContext + { + FunctionName = "PowertoolsLoggingSample-HelloWorldFunction-Gg8rhPwO7Wa1", + FunctionVersion = "1", + MemoryLimitInMB = 215, + AwsRequestId = Guid.NewGuid().ToString("D") + }; + + var body = new Dictionary + { + { "LookupId", requestId }, + { "Greeting", "Hello Powertools for AWS Lambda (.NET)" }, + { "IpAddress", location }, + }; + + var expectedResponse = new APIGatewayProxyResponse + { + Body = JsonSerializer.Serialize(body), + StatusCode = 200, + Headers = new Dictionary { { "Content-Type", "application/json" } } + }; + + var function = new Function(dynamoDbContext.Object, new HttpClient(handlerMock.Object)); + var response = await function.FunctionHandler(request, context); + + _testOutputHelper.WriteLine("Lambda Response: \n" + response.Body); + _testOutputHelper.WriteLine("Expected Response: \n" + expectedResponse.Body); + + Assert.Equal(expectedResponse.Body, response.Body); + Assert.Equal(expectedResponse.Headers, response.Headers); + Assert.Equal(expectedResponse.StatusCode, response.StatusCode); + } + } +} \ No newline at end of file diff --git a/examples/PowerTools.NativeAOT/test/HelloWorld.Test/HelloWorld.Tests.csproj b/examples/PowerTools.NativeAOT/test/HelloWorld.Test/HelloWorld.Tests.csproj new file mode 100644 index 00000000..b4667d52 --- /dev/null +++ b/examples/PowerTools.NativeAOT/test/HelloWorld.Test/HelloWorld.Tests.csproj @@ -0,0 +1,21 @@ + + + net7.0 + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + \ No newline at end of file From bf4104192559e2c85c7b1031f45177e792c6c650 Mon Sep 17 00:00:00 2001 From: jamesuk Date: Fri, 3 Nov 2023 16:00:28 +0000 Subject: [PATCH 4/4] Update metrics example --- .../src/HelloWorld/Function.cs | 15 ++++----------- .../src/AWS.Lambda.Powertools.Metrics/Metrics.cs | 2 -- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/examples/PowerTools.NativeAOT/src/HelloWorld/Function.cs b/examples/PowerTools.NativeAOT/src/HelloWorld/Function.cs index 3652862b..a5131506 100644 --- a/examples/PowerTools.NativeAOT/src/HelloWorld/Function.cs +++ b/examples/PowerTools.NativeAOT/src/HelloWorld/Function.cs @@ -40,6 +40,8 @@ public Function(IAmazonDynamoDB dynamoDbClient, HttpClient httpClient) } [Tracing(CaptureMode = TracingCaptureMode.ResponseAndError)] + [Metrics] + [Logging(LogEvent = true)] public async Task FunctionHandler( APIGatewayProxyRequest apigwProxyEvent, ILambdaContext context) @@ -59,12 +61,6 @@ public async Task FunctionHandler( Logger.LogInformation("Getting ip address from external service"); - // Add Metric to capture the amount of time - Metrics.PushSingleMetric( - "CallingIP", - 1, - MetricUnit.Count); - var location = await GetCallingIp(); var lookupRecord = new LookupRecord( @@ -89,11 +85,6 @@ public async Task FunctionHandler( { await SaveRecordInDynamo(lookupRecord); - Metrics.AddMetric( - "SuccessfulWrites", - 1, - MetricUnit.Count); - return new APIGatewayProxyResponse { Body = JsonSerializer.Serialize( @@ -162,6 +153,8 @@ private static async Task SaveRecordInDynamo(LookupRecord lookupRecord) {"Greeting", new AttributeValue(lookupRecord.Greeting)}, {"IpAddress", new AttributeValue(lookupRecord.IpAddress)}, })!; + + Metrics.AddMetric("RecordSaved", 1, MetricUnit.Count); } catch (AmazonDynamoDBException e) { diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs index 5168ca6f..77891726 100644 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs @@ -20,8 +20,6 @@ namespace AWS.Lambda.Powertools.Metrics; -using System.Text.Json.Serialization; - /// /// Class Metrics. /// Implements the