From 6f50d34f43a0d9913c28ec1e6ebc557a4f700d46 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Fri, 2 Aug 2024 11:09:19 +0100
Subject: [PATCH 01/32] Initial commit. New Serialization context. .NET 8
serialization specific paths. New helper method to convert anonymous types to
dictionary, use for source generation .NET 8
---
.../AWS.Lambda.Powertools.Logging.csproj | 2 +
.../Internal/Converters/ExceptionConverter.cs | 6 +-
.../Helpers/PowertoolsLoggerHelpers.cs | 32 +++++++++
.../Internal/LoggingAspectHandler.cs | 37 +++++++---
.../Internal/PowertoolsConfigurations.cs | 2 +-
.../Internal/PowertoolsLogger.cs | 71 +++++++++++++------
.../LoggingSerializationContext.cs | 54 ++++++++++++++
libraries/src/Directory.Packages.props | 2 +
.../PowertoolsLoggerTest.cs | 6 +-
9 files changed, 177 insertions(+), 35 deletions(-)
create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs
create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/AWS.Lambda.Powertools.Logging.csproj b/libraries/src/AWS.Lambda.Powertools.Logging/AWS.Lambda.Powertools.Logging.csproj
index 6feca1a2..6cc22db7 100644
--- a/libraries/src/AWS.Lambda.Powertools.Logging/AWS.Lambda.Powertools.Logging.csproj
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/AWS.Lambda.Powertools.Logging.csproj
@@ -11,6 +11,8 @@
+
+
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..db74bc0c 100644
--- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionConverter.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ExceptionConverter.cs
@@ -17,6 +17,7 @@
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
+using AWS.Lambda.Powertools.Logging.Serializers;
namespace AWS.Lambda.Powertools.Logging.Internal.Converters;
@@ -84,9 +85,8 @@ public override void Write(Utf8JsonWriter writer, Exception value, JsonSerialize
case Type propType:
writer.WriteString(ApplyPropertyNamingPolicy(prop.Name, options), propType.FullName);
break;
- default:
- writer.WritePropertyName(ApplyPropertyNamingPolicy(prop.Name, options));
- JsonSerializer.Serialize(writer, prop.Value, options);
+ case string propString:
+ writer.WriteString(ApplyPropertyNamingPolicy(prop.Name, options), propString);
break;
}
}
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs
new file mode 100644
index 00000000..dd9ba457
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs
@@ -0,0 +1,32 @@
+/*
+ * 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.Linq;
+
+namespace AWS.Lambda.Powertools.Logging.Internal.Helpers;
+
+internal sealed class PowertoolsLoggerHelpers
+{
+ internal static object ObjectToDictionary(object anonymousObject)
+ {
+ if (anonymousObject.GetType().Namespace is not null)
+ {
+ return anonymousObject;
+ }
+
+ return anonymousObject.GetType().GetProperties()
+ .ToDictionary(prop => prop.Name, prop => ObjectToDictionary(prop.GetValue(anonymousObject, null)));
+ }
+}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs
index 441cdc22..ac92098b 100644
--- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs
@@ -1,12 +1,12 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
+ *
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
- *
+ *
* http://aws.amazon.com/apache2.0
- *
+ *
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
@@ -14,6 +14,7 @@
*/
using System;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Runtime.ExceptionServices;
@@ -21,6 +22,7 @@
using System.Text.Json.Serialization;
using AWS.Lambda.Powertools.Common;
using AWS.Lambda.Powertools.Logging.Internal.Converters;
+using AWS.Lambda.Powertools.Logging.Serializers;
using Microsoft.Extensions.Logging;
namespace AWS.Lambda.Powertools.Logging.Internal;
@@ -61,7 +63,7 @@ internal class LoggingAspectHandler : IMethodAspectHandler
/// The log level
///
private readonly LogLevel? _logLevel;
-
+
///
/// The logger output case
///
@@ -91,17 +93,17 @@ internal class LoggingAspectHandler : IMethodAspectHandler
/// The is context initialized
///
private bool _isContextInitialized;
-
+
///
/// Specify to clear Lambda Context on exit
///
private bool _clearLambdaContext;
-
+
///
/// The JsonSerializer options
///
private static JsonSerializerOptions _jsonSerializerOptions;
-
+
///
/// Get JsonSerializer options.
///
@@ -168,7 +170,7 @@ public void OnEntry(AspectEventArgs eventArgs)
Logger.LoggerProvider = new LoggerProvider(loggerConfig);
break;
case LoggerProvider:
- ((LoggerProvider) Logger.LoggerProvider).Configure(loggerConfig);
+ ((LoggerProvider)Logger.LoggerProvider).Configure(loggerConfig);
break;
}
@@ -274,7 +276,7 @@ private void CaptureLambdaContext(AspectEventArgs eventArgs)
_systemWrapper.LogLine(
"Skipping Lambda Context injection because ILambdaContext context parameter not found.");
}
-
+
///
/// Builds JsonSerializer options.
///
@@ -290,6 +292,11 @@ private static JsonSerializerOptions BuildJsonSerializerOptions()
jsonOptions.Converters.Add(new ConstantClassConverter());
jsonOptions.Converters.Add(new DateOnlyConverter());
jsonOptions.Converters.Add(new TimeOnlyConverter());
+
+#if NET8_0_OR_GREATER
+ jsonOptions.TypeInfoResolver = LoggingSerializationContext.Default;
+#endif
+
return jsonOptions;
}
@@ -297,6 +304,9 @@ private static JsonSerializerOptions BuildJsonSerializerOptions()
/// Captures the correlation identifier.
///
/// The event argument.
+ [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode",
+ Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
+ [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Everything is ok with serialization")]
private void CaptureCorrelationId(object eventArg)
{
if (string.IsNullOrWhiteSpace(_correlationIdPath))
@@ -319,7 +329,14 @@ private void CaptureCorrelationId(object eventArg)
try
{
var correlationId = string.Empty;
- var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(eventArg, JsonSerializerOptions));
+
+#if NET8_0_OR_GREATER
+ var jsonDoc =
+ JsonDocument.Parse(JsonSerializer.Serialize(eventArg, eventArg.GetType(), JsonSerializerOptions));
+#else
+ var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(eventArg, JsonSerializerOptions));
+#endif
+
var element = jsonDoc.RootElement;
for (var i = 0; i < correlationIdPaths.Length; i++)
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs
index 4d39cb16..232d7c49 100644
--- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs
@@ -67,7 +67,7 @@ internal static LoggerOutputCase GetLoggerOutputCase(this IPowertoolsConfigurati
return LoggingConstants.DefaultLoggerOutputCase;
}
- private static Dictionary AwsLogLevelMapper = new()
+ private static readonly Dictionary AwsLogLevelMapper = new()
{
{ "TRACE", "TRACE" },
{ "DEBUG", "DEBUG" },
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs
index e723ed22..a1733728 100644
--- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs
@@ -1,12 +1,12 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
+ *
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
- *
+ *
* http://aws.amazon.com/apache2.0
- *
+ *
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
@@ -15,11 +15,14 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Encodings.Web;
using System.Text.Json;
using AWS.Lambda.Powertools.Common;
using AWS.Lambda.Powertools.Logging.Internal.Converters;
+using AWS.Lambda.Powertools.Logging.Internal.Helpers;
+using AWS.Lambda.Powertools.Logging.Serializers;
using Microsoft.Extensions.Logging;
namespace AWS.Lambda.Powertools.Logging.Internal;
@@ -40,7 +43,7 @@ internal sealed class PowertoolsLogger : ILogger
/// The name
///
private readonly string _name;
-
+
///
/// The current configuration
///
@@ -55,7 +58,7 @@ internal sealed class PowertoolsLogger : ILogger
/// The system wrapper
///
private readonly ISystemWrapper _systemWrapper;
-
+
///
/// The JsonSerializer options
///
@@ -80,10 +83,10 @@ public PowertoolsLogger(
{
(_name, _powertoolsConfigurations, _systemWrapper, _getCurrentConfig) = (name,
powertoolsConfigurations, systemWrapper, getCurrentConfig);
-
+
_powertoolsConfigurations.SetExecutionEnvironment(this);
_currentConfig = GetCurrentConfig();
-
+
if (_lambdaLogLevelEnabled && _logLevel < _lambdaLogLevel)
{
var message =
@@ -148,10 +151,10 @@ internal void EndScope()
private static Dictionary GetScopeKeys(TState state)
{
var keys = new Dictionary();
-
- if (state is null)
+
+ if (state is null)
return keys;
-
+
switch (state)
{
case IEnumerable> pairs:
@@ -161,6 +164,7 @@ private static Dictionary GetScopeKeys(TState state)
if (!string.IsNullOrWhiteSpace(key))
keys.TryAdd(key, value);
}
+
break;
}
case IEnumerable> pairs:
@@ -170,6 +174,7 @@ private static Dictionary GetScopeKeys(TState state)
if (!string.IsNullOrWhiteSpace(key))
keys.TryAdd(key, value);
}
+
break;
}
default:
@@ -178,10 +183,11 @@ private static Dictionary GetScopeKeys(TState state)
{
keys.TryAdd(property.Name, property.GetValue(state));
}
+
break;
}
}
-
+
return keys;
}
@@ -204,6 +210,11 @@ public bool IsEnabled(LogLevel logLevel)
/// The exception related to this entry.
/// Function to create a message of the and .
/// The type of the object to be written.
+ [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode",
+ Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
+ [UnconditionalSuppressMessage("AOT",
+ "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.",
+ Justification = "Everything is ok with serialization")]
public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception,
Func formatter)
{
@@ -219,11 +230,16 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except
: formatter(state, exception);
var logFormatter = Logger.GetFormatter();
- var logEntry = logFormatter is null?
- GetLogEntry(logLevel, timestamp, message, exception) :
- GetFormattedLogEntry(logLevel, timestamp, message, exception, logFormatter);
+ var logEntry = logFormatter is null
+ ? GetLogEntry(logLevel, timestamp, message, exception)
+ : GetFormattedLogEntry(logLevel, timestamp, message, exception, logFormatter);
+
+#if NET8_0_OR_GREATER
+ _systemWrapper.LogLine(JsonSerializer.Serialize(logEntry, typeof(object), JsonSerializerOptions));
+#else
_systemWrapper.LogLine(JsonSerializer.Serialize(logEntry, JsonSerializerOptions));
+#endif
}
///
@@ -361,7 +377,11 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec
var logObject = logFormatter.FormatLogEntry(logEntry);
if (logObject is null)
throw new LogFormatException($"{logFormatter.GetType().FullName} returned Null value.");
+#if NET8_0_OR_GREATER
+ return PowertoolsLoggerHelpers.ObjectToDictionary(logObject);
+#else
return logObject;
+#endif
}
catch (Exception e)
{
@@ -369,6 +389,8 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec
$"{logFormatter.GetType().FullName} raised an exception: {e.Message}.", e);
}
}
+
+
///
/// Clears the configuration.
@@ -387,16 +409,16 @@ private LoggerConfiguration GetCurrentConfig()
var currConfig = _getCurrentConfig();
_logLevel = _powertoolsConfigurations.GetLogLevel(currConfig?.MinimumLevel);
var samplingRate = currConfig?.SamplingRate ?? _powertoolsConfigurations.LoggerSampleRate;
- var loggerOutputCase = _powertoolsConfigurations.GetLoggerOutputCase(currConfig?.LoggerOutputCase);
+ var loggerOutputCase = _powertoolsConfigurations.GetLoggerOutputCase(currConfig?.LoggerOutputCase);
_lambdaLogLevel = _powertoolsConfigurations.GetLambdaLogLevel();
_lambdaLogLevelEnabled = _lambdaLogLevel != LogLevel.None;
-
+
var minLogLevel = _logLevel;
if (_lambdaLogLevelEnabled)
{
minLogLevel = _lambdaLogLevel;
}
-
+
var config = new LoggerConfiguration
{
Service = currConfig?.Service,
@@ -445,8 +467,13 @@ private static bool CustomFormatter(TState state, Exception exception, o
if (exception is not null)
return false;
+#if NET8_0_OR_GREATER
+ var stateKeys = (state as IEnumerable>)?
+ .ToDictionary(i => i.Key, i => PowertoolsLoggerHelpers.ObjectToDictionary(i.Value));
+#else
var stateKeys = (state as IEnumerable>)?
.ToDictionary(i => i.Key, i => i.Value);
+#endif
if (stateKeys is null || stateKeys.Count != 2)
return false;
@@ -458,9 +485,10 @@ private static bool CustomFormatter(TState state, Exception exception, o
return false;
message = stateKeys.First(k => k.Key != "{OriginalFormat}").Value;
+
return true;
}
-
+
///
/// Builds JsonSerializer options.
///
@@ -490,9 +518,12 @@ private JsonSerializerOptions BuildJsonSerializerOptions()
jsonOptions.Converters.Add(new ConstantClassConverter());
jsonOptions.Converters.Add(new DateOnlyConverter());
jsonOptions.Converters.Add(new TimeOnlyConverter());
-
+
jsonOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
-
+#if NET8_0_OR_GREATER
+ jsonOptions.TypeInfoResolver = LoggingSerializationContext.Default;
+#endif
+
return jsonOptions;
}
}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs
new file mode 100644
index 00000000..bfa62789
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs
@@ -0,0 +1,54 @@
+/*
+ * 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 Amazon.Lambda.APIGatewayEvents;
+using Amazon.Lambda.ApplicationLoadBalancerEvents;
+
+namespace AWS.Lambda.Powertools.Logging.Serializers;
+
+#if NET8_0_OR_GREATER
+
+///
+/// Custom JSON serializer context for AWS.Lambda.Powertools.Logging
+///
+// [JsonSourceGenerationOptions(WriteIndented = true)]
+[JsonSerializable(typeof(Dictionary))]
+[JsonSerializable(typeof(bool))]
+[JsonSerializable(typeof(string))]
+[JsonSerializable(typeof(Int32))]
+[JsonSerializable(typeof(Double))]
+[JsonSerializable(typeof(DateOnly))]
+[JsonSerializable(typeof(TimeOnly))]
+[JsonSerializable(typeof(InvalidOperationException))]
+[JsonSerializable(typeof(Exception))]
+[JsonSerializable(typeof(IEnumerable
/// The current configuration.
- private static JsonSerializerOptions JsonSerializerOptions =>
- _jsonSerializerOptions ??= BuildJsonSerializerOptions();
+ private JsonSerializerOptions JsonSerializerOptions =>
+ _jsonSerializerOptions ??= _powertoolsConfigurations.BuildJsonSerializerOptions(_loggerOutputCase);
///
/// Initializes a new instance of the class.
@@ -230,7 +227,7 @@ public void OnExit(AspectEventArgs eventArgs)
if (!_isContextInitialized)
return;
if (_clearLambdaContext)
- PowertoolsLambdaContext.Clear();
+ LoggingLambdaContext.Clear();
if (_clearState)
Logger.RemoveAllKeys();
_initializeContext = true;
@@ -271,42 +268,21 @@ private void CaptureXrayTraceId()
///
private void CaptureLambdaContext(AspectEventArgs eventArgs)
{
- _clearLambdaContext = PowertoolsLambdaContext.Extract(eventArgs);
- if (PowertoolsLambdaContext.Instance is null && IsDebug())
+ _clearLambdaContext = LoggingLambdaContext.Extract(eventArgs);
+ if (LoggingLambdaContext.Instance is null && IsDebug())
_systemWrapper.LogLine(
"Skipping Lambda Context injection because ILambdaContext context parameter not found.");
}
- ///
- /// Builds JsonSerializer options.
- ///
- private static JsonSerializerOptions BuildJsonSerializerOptions()
- {
- var jsonOptions = new JsonSerializerOptions
- {
- DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
- };
- jsonOptions.Converters.Add(new ByteArrayConverter());
- jsonOptions.Converters.Add(new ExceptionConverter());
- jsonOptions.Converters.Add(new MemoryStreamConverter());
- jsonOptions.Converters.Add(new ConstantClassConverter());
- jsonOptions.Converters.Add(new DateOnlyConverter());
- jsonOptions.Converters.Add(new TimeOnlyConverter());
-
-#if NET8_0_OR_GREATER
- jsonOptions.TypeInfoResolver = LoggingSerializationContext.Default;
-#endif
-
- return jsonOptions;
- }
-
///
/// Captures the correlation identifier.
///
/// The event argument.
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode",
Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
- [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Everything is ok with serialization")]
+ [UnconditionalSuppressMessage("AOT",
+ "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.",
+ Justification = "Everything is ok with serialization")]
private void CaptureCorrelationId(object eventArg)
{
if (string.IsNullOrWhiteSpace(_correlationIdPath))
@@ -336,12 +312,12 @@ private void CaptureCorrelationId(object eventArg)
#else
var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(eventArg, JsonSerializerOptions));
#endif
-
var element = jsonDoc.RootElement;
for (var i = 0; i < correlationIdPaths.Length; i++)
{
- if (!element.TryGetProperty(correlationIdPaths[i], out var childElement))
+ var pathWithOutputCase = ConvertToOutputCase(correlationIdPaths[i]);
+ if (!element.TryGetProperty(pathWithOutputCase, out var childElement))
break;
element = childElement;
@@ -360,6 +336,32 @@ private void CaptureCorrelationId(object eventArg)
}
}
+ private string ConvertToOutputCase(string correlationIdPath)
+ {
+ return _loggerOutputCase switch
+ {
+ LoggerOutputCase.CamelCase => ToCamelCase(correlationIdPath),
+ LoggerOutputCase.PascalCase => ToPascalCase(correlationIdPath),
+ _ => ToSnakeCase(correlationIdPath), // default snake_case
+ };
+ }
+
+ private string ToSnakeCase(string correlationIdPath)
+ {
+ return string.Concat(correlationIdPath.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x : x.ToString()))
+ .ToLowerInvariant();
+ }
+
+ private string ToPascalCase(string correlationIdPath)
+ {
+ return char.ToUpperInvariant(correlationIdPath[0]) + correlationIdPath.Substring(1);
+ }
+
+ private string ToCamelCase(string correlationIdPath)
+ {
+ return char.ToLowerInvariant(correlationIdPath[0]) + correlationIdPath.Substring(1);
+ }
+
///
/// Logs the event.
///
@@ -407,7 +409,7 @@ internal static void ResetForTest()
{
_isColdStart = true;
_initializeContext = true;
- PowertoolsLambdaContext.Clear();
+ LoggingLambdaContext.Clear();
Logger.LoggerProvider = null;
Logger.RemoveAllKeys();
}
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingLambdaContext.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingLambdaContext.cs
new file mode 100644
index 00000000..a8846b15
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingLambdaContext.cs
@@ -0,0 +1,104 @@
+using System;
+using Amazon.Lambda.Core;
+using AWS.Lambda.Powertools.Common;
+
+namespace AWS.Lambda.Powertools.Logging.Internal;
+
+///
+/// Lambda Context
+///
+public class LoggingLambdaContext
+{
+ ///
+ /// The AWS request ID associated with the request.
+ /// This is the same ID returned to the client that called invoke().
+ /// This ID is reused for retries on the same request.
+ ///
+ internal string AwsRequestId { get; private set; }
+
+ /// Name of the Lambda function that is running.
+ internal string FunctionName { get; private set; }
+
+ ///
+ /// The Lambda function version that is executing.
+ /// If an alias is used to invoke the function, then this will be
+ /// the version the alias points to.
+ ///
+ internal string FunctionVersion { get; private set; }
+
+ ///
+ /// The ARN used to invoke this function.
+ /// It can be function ARN or alias ARN.
+ /// An unqualified ARN executes the $LATEST version and aliases execute
+ /// the function version they are pointing to.
+ ///
+ internal string InvokedFunctionArn { get; private set; }
+
+ ///
+ /// The CloudWatch log group name associated with the invoked function.
+ /// It can be null if the IAM user provided does not have permission for
+ /// CloudWatch actions.
+ ///
+ internal string LogGroupName { get; private set; }
+
+ ///
+ /// The CloudWatch log stream name for this function execution.
+ /// It can be null if the IAM user provided does not have permission
+ /// for CloudWatch actions.
+ ///
+ internal string LogStreamName { get; private set; }
+
+ ///
+ /// Memory limit, in MB, you configured for the Lambda function.
+ ///
+ internal int MemoryLimitInMB { get; private set; }
+
+ ///
+ /// The instance
+ ///
+ internal static LoggingLambdaContext Instance { get; private set; }
+
+ ///
+ /// Gets the Lambda context
+ ///
+ ///
+ ///
+ public static bool Extract(AspectEventArgs args)
+ {
+ if (Instance is not null)
+ return false;
+
+ if (args?.Args is null)
+ return false;
+ if (args.Method is null)
+ return false;
+
+ var index = Array.FindIndex(args.Method.GetParameters(), p => p.ParameterType == typeof(ILambdaContext));
+ if (index >= 0)
+ {
+ var x = (ILambdaContext)args.Args[index];
+
+ Instance = new LoggingLambdaContext
+ {
+ AwsRequestId = x.AwsRequestId,
+ FunctionName = x.FunctionName,
+ FunctionVersion = x.FunctionVersion,
+ InvokedFunctionArn = x.InvokedFunctionArn,
+ LogGroupName = x.LogGroupName,
+ LogStreamName = x.LogStreamName,
+ MemoryLimitInMB = x.MemoryLimitInMB
+ };
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Clear the extracted lambda context.
+ ///
+ internal static void Clear()
+ {
+ Instance = null;
+ }
+}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs
index 232d7c49..8f407aab 100644
--- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs
@@ -1,12 +1,12 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
+ *
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
- *
+ *
* http://aws.amazon.com/apache2.0
- *
+ *
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
@@ -15,7 +15,11 @@
using System;
using System.Collections.Generic;
+using System.Text.Encodings.Web;
+using System.Text.Json;
using AWS.Lambda.Powertools.Common;
+using AWS.Lambda.Powertools.Logging.Internal.Converters;
+using AWS.Lambda.Powertools.Logging.Serializers;
using Microsoft.Extensions.Logging;
namespace AWS.Lambda.Powertools.Logging.Internal;
@@ -42,10 +46,11 @@ internal static LogLevel GetLogLevel(this IPowertoolsConfigurations powertoolsCo
return LoggingConstants.DefaultLogLevel;
}
-
+
internal static LogLevel GetLambdaLogLevel(this IPowertoolsConfigurations powertoolsConfigurations)
{
- AwsLogLevelMapper.TryGetValue((powertoolsConfigurations.AWSLambdaLogLevel ?? "").Trim().ToUpper(), out var awsLogLevel);
+ AwsLogLevelMapper.TryGetValue((powertoolsConfigurations.AWSLambdaLogLevel ?? "").Trim().ToUpper(),
+ out var awsLogLevel);
if (Enum.TryParse(awsLogLevel, true, out LogLevel result))
{
@@ -54,7 +59,7 @@ internal static LogLevel GetLambdaLogLevel(this IPowertoolsConfigurations powert
return LogLevel.None;
}
-
+
internal static LoggerOutputCase GetLoggerOutputCase(this IPowertoolsConfigurations powertoolsConfigurations,
LoggerOutputCase? loggerOutputCase = null)
{
@@ -76,4 +81,42 @@ internal static LoggerOutputCase GetLoggerOutputCase(this IPowertoolsConfigurati
{ "ERROR", "ERROR" },
{ "FATAL", "CRITICAL" }
};
+
+ internal static JsonSerializerOptions BuildJsonSerializerOptions(
+ this IPowertoolsConfigurations powertoolsConfigurations, LoggerOutputCase? loggerOutputCase = null)
+ {
+ var jsonOptions = powertoolsConfigurations.GetLoggerOutputCase(loggerOutputCase) switch
+ {
+ LoggerOutputCase.CamelCase => new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DictionaryKeyPolicy = JsonNamingPolicy.CamelCase
+ },
+ LoggerOutputCase.PascalCase => new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = PascalCaseNamingPolicy.Instance,
+ DictionaryKeyPolicy = PascalCaseNamingPolicy.Instance
+ },
+ _ => new JsonSerializerOptions //defaults to snake_case
+ {
+ PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance,
+ DictionaryKeyPolicy = SnakeCaseNamingPolicy.Instance
+ }
+ };
+
+ jsonOptions.Converters.Add(new ByteArrayConverter());
+ jsonOptions.Converters.Add(new ExceptionConverter());
+ jsonOptions.Converters.Add(new MemoryStreamConverter());
+ jsonOptions.Converters.Add(new ConstantClassConverter());
+ jsonOptions.Converters.Add(new DateOnlyConverter());
+ jsonOptions.Converters.Add(new TimeOnlyConverter());
+
+ jsonOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
+
+#if NET8_0_OR_GREATER
+ jsonOptions.TypeInfoResolver = LoggingSerializationContext.Default;
+#endif
+
+ return jsonOptions;
+ }
}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs
index a1733728..a2867168 100644
--- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs
@@ -118,7 +118,7 @@ public PowertoolsLogger(
///
/// The current configuration.
private JsonSerializerOptions JsonSerializerOptions =>
- _jsonSerializerOptions ??= BuildJsonSerializerOptions();
+ _jsonSerializerOptions ??= _powertoolsConfigurations.BuildJsonSerializerOptions(CurrentConfig.LoggerOutputCase);
internal PowertoolsLoggerScope CurrentScope { get; private set; }
@@ -259,13 +259,13 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times
logEntry.TryAdd(key, value);
// Add Lambda Context Keys
- if (PowertoolsLambdaContext.Instance is not null)
+ if (LoggingLambdaContext.Instance is not null)
{
- logEntry.TryAdd(LoggingConstants.KeyFunctionName, PowertoolsLambdaContext.Instance.FunctionName);
- logEntry.TryAdd(LoggingConstants.KeyFunctionVersion, PowertoolsLambdaContext.Instance.FunctionVersion);
- logEntry.TryAdd(LoggingConstants.KeyFunctionMemorySize, PowertoolsLambdaContext.Instance.MemoryLimitInMB);
- logEntry.TryAdd(LoggingConstants.KeyFunctionArn, PowertoolsLambdaContext.Instance.InvokedFunctionArn);
- logEntry.TryAdd(LoggingConstants.KeyFunctionRequestId, PowertoolsLambdaContext.Instance.AwsRequestId);
+ logEntry.TryAdd(LoggingConstants.KeyFunctionName, LoggingLambdaContext.Instance.FunctionName);
+ logEntry.TryAdd(LoggingConstants.KeyFunctionVersion, LoggingLambdaContext.Instance.FunctionVersion);
+ logEntry.TryAdd(LoggingConstants.KeyFunctionMemorySize, LoggingLambdaContext.Instance.MemoryLimitInMB);
+ logEntry.TryAdd(LoggingConstants.KeyFunctionArn, LoggingLambdaContext.Instance.InvokedFunctionArn);
+ logEntry.TryAdd(LoggingConstants.KeyFunctionRequestId, LoggingLambdaContext.Instance.AwsRequestId);
}
// Add Extra Fields
@@ -360,15 +360,15 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec
logEntry.ExtraKeys = extraKeys;
// Add Lambda Context Keys
- if (PowertoolsLambdaContext.Instance is not null)
+ if (LoggingLambdaContext.Instance is not null)
{
logEntry.LambdaContext = new LogEntryLambdaContext
{
- FunctionName = PowertoolsLambdaContext.Instance.FunctionName,
- FunctionVersion = PowertoolsLambdaContext.Instance.FunctionVersion,
- MemoryLimitInMB = PowertoolsLambdaContext.Instance.MemoryLimitInMB,
- InvokedFunctionArn = PowertoolsLambdaContext.Instance.InvokedFunctionArn,
- AwsRequestId = PowertoolsLambdaContext.Instance.AwsRequestId,
+ FunctionName = LoggingLambdaContext.Instance.FunctionName,
+ FunctionVersion = LoggingLambdaContext.Instance.FunctionVersion,
+ MemoryLimitInMB = LoggingLambdaContext.Instance.MemoryLimitInMB,
+ InvokedFunctionArn = LoggingLambdaContext.Instance.InvokedFunctionArn,
+ AwsRequestId = LoggingLambdaContext.Instance.AwsRequestId,
};
}
@@ -488,42 +488,4 @@ private static bool CustomFormatter(TState state, Exception exception, o
return true;
}
-
- ///
- /// Builds JsonSerializer options.
- ///
- private JsonSerializerOptions BuildJsonSerializerOptions()
- {
- var jsonOptions = CurrentConfig.LoggerOutputCase switch
- {
- LoggerOutputCase.CamelCase => new JsonSerializerOptions(JsonSerializerDefaults.Web)
- {
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
- DictionaryKeyPolicy = JsonNamingPolicy.CamelCase
- },
- LoggerOutputCase.PascalCase => new JsonSerializerOptions
- {
- PropertyNamingPolicy = PascalCaseNamingPolicy.Instance,
- DictionaryKeyPolicy = PascalCaseNamingPolicy.Instance
- },
- _ => new JsonSerializerOptions
- {
- PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance,
- DictionaryKeyPolicy = SnakeCaseNamingPolicy.Instance
- }
- };
- jsonOptions.Converters.Add(new ByteArrayConverter());
- jsonOptions.Converters.Add(new ExceptionConverter());
- jsonOptions.Converters.Add(new MemoryStreamConverter());
- jsonOptions.Converters.Add(new ConstantClassConverter());
- jsonOptions.Converters.Add(new DateOnlyConverter());
- jsonOptions.Converters.Add(new TimeOnlyConverter());
-
- jsonOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
-#if NET8_0_OR_GREATER
- jsonOptions.TypeInfoResolver = LoggingSerializationContext.Default;
-#endif
-
- return jsonOptions;
- }
}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs
index bfa62789..a03de4f2 100644
--- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs
@@ -44,8 +44,13 @@ namespace AWS.Lambda.Powertools.Logging.Serializers;
[JsonSerializable(typeof(Byte[]))]
[JsonSerializable(typeof(MemoryStream))]
[JsonSerializable(typeof(APIGatewayProxyRequest))]
+[JsonSerializable(typeof(APIGatewayProxyResponse))]
+[JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestContext))]
[JsonSerializable(typeof(ApplicationLoadBalancerRequest))]
[JsonSerializable(typeof(LogEntry))]
+[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest))]
+[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyResponse))]
+[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext))]
internal partial class LoggingSerializationContext : JsonSerializerContext
{
diff --git a/libraries/src/Directory.Build.props b/libraries/src/Directory.Build.props
index 8f412fa4..ebe62090 100644
--- a/libraries/src/Directory.Build.props
+++ b/libraries/src/Directory.Build.props
@@ -21,7 +21,7 @@
-
+
true
true
true
diff --git a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsLambdaContextTest.cs b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsLambdaContextTest.cs
deleted file mode 100644
index d6f87fae..00000000
--- a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsLambdaContextTest.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-using System;
-using Xunit;
-
-namespace AWS.Lambda.Powertools.Common.Tests;
-
-public class PowertoolsLambdaContextTest
-{
- private class TestLambdaContext
- {
- public string AwsRequestId { get; set; }
- public string FunctionName { get; set; }
- public string FunctionVersion { get; set; }
- public string InvokedFunctionArn { get; set; }
- public string LogGroupName { get; set; }
- public string LogStreamName { get; set; }
- public int MemoryLimitInMB { get; set; }
- }
-
- private static TestLambdaContext NewLambdaContext()
- {
- return new TestLambdaContext
- {
- AwsRequestId = Guid.NewGuid().ToString(),
- FunctionName = Guid.NewGuid().ToString(),
- FunctionVersion = Guid.NewGuid().ToString(),
- InvokedFunctionArn = Guid.NewGuid().ToString(),
- LogGroupName = Guid.NewGuid().ToString(),
- LogStreamName = Guid.NewGuid().ToString(),
- MemoryLimitInMB = new Random().Next()
- };
- }
-
- [Fact]
- public void Extract_WhenHasLambdaContextArgument_InitializesLambdaContextInfo()
- {
- // Arrange
- var lambdaContext = NewLambdaContext();
- var eventArg = new {Source = "Test"};
- var eventArgs = new AspectEventArgs
- {
- Name = Guid.NewGuid().ToString(),
- Args = new object []
- {
- eventArg,
- lambdaContext
- }
- };
-
- // Act && Assert
- PowertoolsLambdaContext.Clear();
- Assert.Null(PowertoolsLambdaContext.Instance);
- Assert.True(PowertoolsLambdaContext.Extract(eventArgs));
- Assert.NotNull(PowertoolsLambdaContext.Instance);
- Assert.False(PowertoolsLambdaContext.Extract(eventArgs));
- Assert.Equal(PowertoolsLambdaContext.Instance.AwsRequestId, lambdaContext.AwsRequestId);
- Assert.Equal(PowertoolsLambdaContext.Instance.FunctionName, lambdaContext.FunctionName);
- Assert.Equal(PowertoolsLambdaContext.Instance.FunctionVersion, lambdaContext.FunctionVersion);
- Assert.Equal(PowertoolsLambdaContext.Instance.InvokedFunctionArn, lambdaContext.InvokedFunctionArn);
- Assert.Equal(PowertoolsLambdaContext.Instance.LogGroupName, lambdaContext.LogGroupName);
- Assert.Equal(PowertoolsLambdaContext.Instance.LogStreamName, lambdaContext.LogStreamName);
- Assert.Equal(PowertoolsLambdaContext.Instance.MemoryLimitInMB, lambdaContext.MemoryLimitInMB);
- PowertoolsLambdaContext.Clear();
- Assert.Null(PowertoolsLambdaContext.Instance);
- }
-}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LambdaContextTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LambdaContextTest.cs
new file mode 100644
index 00000000..55a8f928
--- /dev/null
+++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LambdaContextTest.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Reflection;
+using Amazon.Lambda.Core;
+using Amazon.Lambda.TestUtilities;
+using AWS.Lambda.Powertools.Common;
+using AWS.Lambda.Powertools.Logging.Internal;
+using NSubstitute;
+using Xunit;
+
+namespace AWS.Lambda.Powertools.Logging.Tests;
+
+public class LambdaContextTest
+{
+ [Fact]
+ public void Extract_WhenHasLambdaContextArgument_InitializesLambdaContextInfo()
+ {
+ // Arrange
+ var lambdaContext = new TestLambdaContext
+ {
+ AwsRequestId = Guid.NewGuid().ToString(),
+ FunctionName = Guid.NewGuid().ToString(),
+ FunctionVersion = Guid.NewGuid().ToString(),
+ InvokedFunctionArn = Guid.NewGuid().ToString(),
+ LogGroupName = Guid.NewGuid().ToString(),
+ LogStreamName = Guid.NewGuid().ToString(),
+ MemoryLimitInMB = new Random().Next()
+ };
+
+ var args = Substitute.For();
+ var method = Substitute.For();
+ var parameter = Substitute.For();
+
+ // Setup parameter
+ parameter.ParameterType.Returns(typeof(ILambdaContext));
+
+ // Setup method
+ method.GetParameters().Returns(new[] { parameter });
+
+ // Setup args
+ args.Method = method;
+ args.Args = new object[] { lambdaContext };
+
+ // Act && Assert
+ LoggingLambdaContext.Clear();
+ Assert.Null(LoggingLambdaContext.Instance);
+ Assert.True(LoggingLambdaContext.Extract(args));
+ Assert.NotNull(LoggingLambdaContext.Instance);
+ Assert.Equal(LoggingLambdaContext.Instance.AwsRequestId, lambdaContext.AwsRequestId);
+ Assert.Equal(LoggingLambdaContext.Instance.FunctionName, lambdaContext.FunctionName);
+ Assert.Equal(LoggingLambdaContext.Instance.FunctionVersion, lambdaContext.FunctionVersion);
+ Assert.Equal(LoggingLambdaContext.Instance.InvokedFunctionArn, lambdaContext.InvokedFunctionArn);
+ Assert.Equal(LoggingLambdaContext.Instance.LogGroupName, lambdaContext.LogGroupName);
+ Assert.Equal(LoggingLambdaContext.Instance.LogStreamName, lambdaContext.LogStreamName);
+ Assert.Equal(LoggingLambdaContext.Instance.MemoryLimitInMB, lambdaContext.MemoryLimitInMB);
+ LoggingLambdaContext.Clear();
+ Assert.Null(LoggingLambdaContext.Instance);
+ }
+
+ [Fact]
+ public void Extract_When_Args_Null_Returns_False()
+ {
+ // Arrange
+ var args = Substitute.For();
+
+ // Act && Assert
+ LoggingLambdaContext.Clear();
+ Assert.Null(LoggingLambdaContext.Instance);
+ Assert.False(LoggingLambdaContext.Extract(args));
+ }
+
+ [Fact]
+ public void Extract_When_Method_Null_Returns_False()
+ {
+ // Arrange
+ var args = Substitute.For();
+ args.Args = Array.Empty
internal static class ExceptionPropertyExtractor
{
-
- // Note: Leave code comments for future reference
-
///
/// The property extractors
///
private static readonly Dictionary>>> PropertyExtractors = new()
{
{ typeof(Exception), GetBaseExceptionProperties },
- // Add more specific exception types here
- // { typeof(ArgumentException), GetArgumentExceptionProperties },
- // { typeof(InvalidOperationException), GetInvalidOperationExceptionProperties },
- // { typeof(NullReferenceException), GetBaseExceptionProperties },
- // ... add more as needed
};
///
@@ -35,14 +27,6 @@ internal static class ExceptionPropertyExtractor
///
public static IEnumerable> ExtractProperties(Exception exception)
{
- // var exceptionType = exception.GetType();
-
- // if (PropertyExtractors.TryGetValue(exceptionType, out var extractor))
- // {
- // return extractor(exception);
- // }
-
- // If we don't have a specific extractor, use the base Exception extractor
return GetBaseExceptionProperties(exception);
}
@@ -56,23 +40,5 @@ private static IEnumerable> GetBaseExceptionPropert
yield return new KeyValuePair(nameof(Exception.Message), ex.Message);
yield return new KeyValuePair(nameof(Exception.Source), ex.Source);
yield return new KeyValuePair(nameof(Exception.StackTrace), ex.StackTrace);
- // Add any other properties you want to extract
}
-
- // private static IEnumerable> GetArgumentExceptionProperties(Exception ex)
- // {
- // var argEx = (ArgumentException)ex;
- // foreach (var prop in GetBaseExceptionProperties(ex))
- // {
- // yield return prop;
- // }
- // yield return new KeyValuePair(nameof(ArgumentException.ParamName), argEx.ParamName);
- // }
- //
- // private static IEnumerable> GetInvalidOperationExceptionProperties(Exception ex)
- // {
- // // InvalidOperationException doesn't have any additional properties
- // // but we include it here as an example
- // return GetBaseExceptionProperties(ex);
- // }
}
\ No newline at end of file
From 42138f7db885de2e408a2350a732a3f7c07ea6a4 Mon Sep 17 00:00:00 2001
From: Henrique <999396+hjgraca@users.noreply.github.com>
Date: Wed, 11 Sep 2024 15:59:22 +0100
Subject: [PATCH 07/32] refactor, change the way we create Logger in tests to
use LoggerProvider
---
.../Helpers/PowertoolsLoggerHelpers.cs | 60 ++++
.../Internal/LoggerProvider.cs | 42 ++-
.../Internal/LoggingAspectHandler.cs | 90 +----
.../Internal/PowertoolsConfigurations.cs | 122 -------
.../PowertoolsConfigurationsExtension.cs | 163 +++++++++
.../Internal/PowertoolsLogger.cs | 126 +------
.../AWS.Lambda.Powertools.Logging/Logger.cs | 3 -
.../LoggingAttribute.cs | 27 +-
.../LoggingSerializationContext.cs | 22 +-
.../Serializers/PowertoolsLambdaSerializer.cs | 79 +++++
.../PowertoolsLoggingSerializer.cs | 182 ++++++++++
.../LogFormatterTest.cs | 67 ++--
.../LoggingAttributeTest.cs | 45 ++-
.../PowertoolsLoggerTest.cs | 334 ++++++++++--------
14 files changed, 825 insertions(+), 537 deletions(-)
delete mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs
create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs
create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLambdaSerializer.cs
create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs
index 55bc7053..428deb68 100644
--- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerHelpers.cs
@@ -14,11 +14,23 @@
*/
using System.Linq;
+using AWS.Lambda.Powertools.Common;
namespace AWS.Lambda.Powertools.Logging.Internal.Helpers;
+///
+/// Class PowertoolsLoggerHelpers.
+///
internal static class PowertoolsLoggerHelpers
{
+ ///
+ /// Converts an object to a dictionary.
+ ///
+ /// The object to convert.
+ ///
+ /// If the object has a namespace, returns the object as-is.
+ /// Otherwise, returns a dictionary representation of the object's properties.
+ ///
internal static object ObjectToDictionary(object anonymousObject)
{
if (anonymousObject.GetType().Namespace is not null)
@@ -29,4 +41,52 @@ internal static object ObjectToDictionary(object anonymousObject)
return anonymousObject.GetType().GetProperties()
.ToDictionary(prop => prop.Name, prop => ObjectToDictionary(prop.GetValue(anonymousObject, null)));
}
+
+ ///
+ /// Converts the input string to the configured output case.
+ ///
+ /// The string to convert.
+ ///
+ /// The input string converted to the configured case (camel, pascal, or snake case).
+ ///
+ internal static string ConvertToOutputCase(string correlationIdPath)
+ {
+ return PowertoolsConfigurations.Instance.GetLoggerOutputCase() switch
+ {
+ LoggerOutputCase.CamelCase => ToCamelCase(correlationIdPath),
+ LoggerOutputCase.PascalCase => ToPascalCase(correlationIdPath),
+ _ => ToSnakeCase(correlationIdPath), // default snake_case
+ };
+ }
+
+ ///
+ /// Converts a string to snake_case.
+ ///
+ /// The string to convert.
+ /// The input string converted to snake_case.
+ private static string ToSnakeCase(string correlationIdPath)
+ {
+ return string.Concat(correlationIdPath.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x : x.ToString()))
+ .ToLowerInvariant();
+ }
+
+ ///
+ /// Converts a string to PascalCase.
+ ///
+ /// The string to convert.
+ /// The input string converted to PascalCase.
+ private static string ToPascalCase(string correlationIdPath)
+ {
+ return char.ToUpperInvariant(correlationIdPath[0]) + correlationIdPath.Substring(1);
+ }
+
+ ///
+ /// Converts a string to camelCase.
+ ///
+ /// The string to convert.
+ /// The input string converted to camelCase.
+ private static string ToCamelCase(string correlationIdPath)
+ {
+ return char.ToLowerInvariant(correlationIdPath[0]) + correlationIdPath.Substring(1);
+ }
}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs
index 5b18ff97..e11791c8 100644
--- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs
@@ -1,12 +1,12 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
+ *
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
- *
+ *
* http://aws.amazon.com/apache2.0
- *
+ *
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
@@ -27,12 +27,21 @@ namespace AWS.Lambda.Powertools.Logging.Internal;
///
public sealed class LoggerProvider : ILoggerProvider
{
+ ///
+ /// The powertools configurations
+ ///
+ private readonly IPowertoolsConfigurations _powertoolsConfigurations;
+
+ ///
+ /// The system wrapper
+ ///
+ private readonly ISystemWrapper _systemWrapper;
+
///
/// The loggers
///
private readonly ConcurrentDictionary _loggers = new();
-
///
/// The current configuration
///
@@ -42,9 +51,14 @@ public sealed class LoggerProvider : ILoggerProvider
/// Initializes a new instance of the class.
///
/// The configuration.
- public LoggerProvider(IOptions config)
+ ///
+ ///
+ public LoggerProvider(IOptions config, IPowertoolsConfigurations powertoolsConfigurations, ISystemWrapper systemWrapper)
{
- _currentConfig = config?.Value;
+ _powertoolsConfigurations = powertoolsConfigurations;
+ _systemWrapper = systemWrapper;
+
+ _currentConfig= powertoolsConfigurations.SetCurrentConfig(config?.Value, systemWrapper);
}
///
@@ -56,9 +70,9 @@ public ILogger CreateLogger(string categoryName)
{
return _loggers.GetOrAdd(categoryName,
name => new PowertoolsLogger(name,
- PowertoolsConfigurations.Instance,
- SystemWrapper.Instance,
- GetCurrentConfig));
+ _powertoolsConfigurations,
+ _systemWrapper,
+ _currentConfig));
}
///
@@ -69,14 +83,6 @@ public void Dispose()
_loggers.Clear();
}
- ///
- /// Gets the current configuration.
- ///
- /// LoggerConfiguration.
- private LoggerConfiguration GetCurrentConfig()
- {
- return _currentConfig;
- }
///
/// Configures the loggers.
@@ -91,4 +97,4 @@ internal void Configure(IOptions config)
foreach (var logger in _loggers.Values)
logger.ClearConfig();
}
-}
+}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs
index a436b645..f3564bda 100644
--- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectHandler.cs
@@ -14,12 +14,13 @@
*/
using System;
-using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Text.Json;
using AWS.Lambda.Powertools.Common;
+using AWS.Lambda.Powertools.Logging.Internal.Helpers;
+using AWS.Lambda.Powertools.Logging.Serializers;
using Microsoft.Extensions.Logging;
namespace AWS.Lambda.Powertools.Logging.Internal;
@@ -61,26 +62,11 @@ internal class LoggingAspectHandler : IMethodAspectHandler
///
private readonly LogLevel? _logLevel;
- ///
- /// The logger output case
- ///
- private readonly LoggerOutputCase? _loggerOutputCase;
-
///
/// The Powertools for AWS Lambda (.NET) configurations
///
private readonly IPowertoolsConfigurations _powertoolsConfigurations;
- ///
- /// The sampling rate
- ///
- private readonly double? _samplingRate;
-
- ///
- /// Service name
- ///
- private readonly string _service;
-
///
/// The system wrapper
///
@@ -97,24 +83,15 @@ internal class LoggingAspectHandler : IMethodAspectHandler
private bool _clearLambdaContext;
///
- /// The JsonSerializer options
+ /// The configuration
///
- private static JsonSerializerOptions _jsonSerializerOptions;
-
- ///
- /// Get JsonSerializer options.
- ///
- /// The current configuration.
- private JsonSerializerOptions JsonSerializerOptions =>
- _jsonSerializerOptions ??= _powertoolsConfigurations.BuildJsonSerializerOptions(_loggerOutputCase);
+ private readonly LoggerConfiguration _config;
///
/// Initializes a new instance of the class.
///
- /// Service name
+ ///
/// The log level.
- /// The logger output case.
- /// The sampling rate.
/// if set to true [log event].
/// The correlation identifier path.
/// if set to true [clear state].
@@ -122,10 +99,8 @@ internal class LoggingAspectHandler : IMethodAspectHandler
/// The system wrapper.
internal LoggingAspectHandler
(
- string service,
+ LoggerConfiguration config,
LogLevel? logLevel,
- LoggerOutputCase? loggerOutputCase,
- double? samplingRate,
bool? logEvent,
string correlationIdPath,
bool clearState,
@@ -133,15 +108,13 @@ internal LoggingAspectHandler
ISystemWrapper systemWrapper
)
{
- _service = service;
_logLevel = logLevel;
- _loggerOutputCase = loggerOutputCase;
- _samplingRate = samplingRate;
_logEvent = logEvent;
_clearState = clearState;
_correlationIdPath = correlationIdPath;
_powertoolsConfigurations = powertoolsConfigurations;
_systemWrapper = systemWrapper;
+ _config = config;
}
///
@@ -153,21 +126,13 @@ ISystemWrapper systemWrapper
///
public void OnEntry(AspectEventArgs eventArgs)
{
- var loggerConfig = new LoggerConfiguration
- {
- Service = _service,
- MinimumLevel = _logLevel,
- SamplingRate = _samplingRate,
- LoggerOutputCase = _loggerOutputCase
- };
-
switch (Logger.LoggerProvider)
{
case null:
- Logger.LoggerProvider = new LoggerProvider(loggerConfig);
+ Logger.LoggerProvider = new LoggerProvider(_config, _powertoolsConfigurations, _systemWrapper );
break;
case LoggerProvider:
- ((LoggerProvider)Logger.LoggerProvider).Configure(loggerConfig);
+ ((LoggerProvider)Logger.LoggerProvider).Configure(_config);
break;
}
@@ -278,11 +243,6 @@ private void CaptureLambdaContext(AspectEventArgs eventArgs)
/// Captures the correlation identifier.
///
/// The event argument.
- [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode",
- Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
- [UnconditionalSuppressMessage("AOT",
- "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.",
- Justification = "Everything is ok with serialization")]
private void CaptureCorrelationId(object eventArg)
{
if (string.IsNullOrWhiteSpace(_correlationIdPath))
@@ -308,15 +268,15 @@ private void CaptureCorrelationId(object eventArg)
#if NET8_0_OR_GREATER
var jsonDoc =
- JsonDocument.Parse(JsonSerializer.Serialize(eventArg, eventArg.GetType(), JsonSerializerOptions));
+ JsonDocument.Parse(PowertoolsLoggingSerializer.Serialize(eventArg, eventArg.GetType()));
#else
- var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(eventArg, JsonSerializerOptions));
+ var jsonDoc = JsonDocument.Parse(PowertoolsLoggingSerializer.Serialize(eventArg));
#endif
var element = jsonDoc.RootElement;
for (var i = 0; i < correlationIdPaths.Length; i++)
{
- var pathWithOutputCase = ConvertToOutputCase(correlationIdPaths[i]);
+ var pathWithOutputCase = PowertoolsLoggerHelpers.ConvertToOutputCase(correlationIdPaths[i]);
if (!element.TryGetProperty(pathWithOutputCase, out var childElement))
break;
@@ -336,32 +296,6 @@ private void CaptureCorrelationId(object eventArg)
}
}
- private string ConvertToOutputCase(string correlationIdPath)
- {
- return _loggerOutputCase switch
- {
- LoggerOutputCase.CamelCase => ToCamelCase(correlationIdPath),
- LoggerOutputCase.PascalCase => ToPascalCase(correlationIdPath),
- _ => ToSnakeCase(correlationIdPath), // default snake_case
- };
- }
-
- private string ToSnakeCase(string correlationIdPath)
- {
- return string.Concat(correlationIdPath.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x : x.ToString()))
- .ToLowerInvariant();
- }
-
- private string ToPascalCase(string correlationIdPath)
- {
- return char.ToUpperInvariant(correlationIdPath[0]) + correlationIdPath.Substring(1);
- }
-
- private string ToCamelCase(string correlationIdPath)
- {
- return char.ToLowerInvariant(correlationIdPath[0]) + correlationIdPath.Substring(1);
- }
-
///
/// Logs the event.
///
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs
deleted file mode 100644
index 8f407aab..00000000
--- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurations.cs
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed
- * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-using System;
-using System.Collections.Generic;
-using System.Text.Encodings.Web;
-using System.Text.Json;
-using AWS.Lambda.Powertools.Common;
-using AWS.Lambda.Powertools.Logging.Internal.Converters;
-using AWS.Lambda.Powertools.Logging.Serializers;
-using Microsoft.Extensions.Logging;
-
-namespace AWS.Lambda.Powertools.Logging.Internal;
-
-///
-/// Class PowertoolsConfigurationsExtension.
-///
-internal static class PowertoolsConfigurationsExtension
-{
- ///
- /// Gets the log level.
- ///
- /// The Powertools for AWS Lambda (.NET) configurations.
- /// The log level.
- /// LogLevel.
- internal static LogLevel GetLogLevel(this IPowertoolsConfigurations powertoolsConfigurations,
- LogLevel? logLevel = null)
- {
- if (logLevel.HasValue)
- return logLevel.Value;
-
- if (Enum.TryParse((powertoolsConfigurations.LogLevel ?? "").Trim(), true, out LogLevel result))
- return result;
-
- return LoggingConstants.DefaultLogLevel;
- }
-
- internal static LogLevel GetLambdaLogLevel(this IPowertoolsConfigurations powertoolsConfigurations)
- {
- AwsLogLevelMapper.TryGetValue((powertoolsConfigurations.AWSLambdaLogLevel ?? "").Trim().ToUpper(),
- out var awsLogLevel);
-
- if (Enum.TryParse(awsLogLevel, true, out LogLevel result))
- {
- return result;
- }
-
- return LogLevel.None;
- }
-
- internal static LoggerOutputCase GetLoggerOutputCase(this IPowertoolsConfigurations powertoolsConfigurations,
- LoggerOutputCase? loggerOutputCase = null)
- {
- if (loggerOutputCase.HasValue)
- return loggerOutputCase.Value;
-
- if (Enum.TryParse((powertoolsConfigurations.LoggerOutputCase ?? "").Trim(), true, out LoggerOutputCase result))
- return result;
-
- return LoggingConstants.DefaultLoggerOutputCase;
- }
-
- private static readonly Dictionary AwsLogLevelMapper = new()
- {
- { "TRACE", "TRACE" },
- { "DEBUG", "DEBUG" },
- { "INFO", "INFORMATION" },
- { "WARN", "WARNING" },
- { "ERROR", "ERROR" },
- { "FATAL", "CRITICAL" }
- };
-
- internal static JsonSerializerOptions BuildJsonSerializerOptions(
- this IPowertoolsConfigurations powertoolsConfigurations, LoggerOutputCase? loggerOutputCase = null)
- {
- var jsonOptions = powertoolsConfigurations.GetLoggerOutputCase(loggerOutputCase) switch
- {
- LoggerOutputCase.CamelCase => new JsonSerializerOptions
- {
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
- DictionaryKeyPolicy = JsonNamingPolicy.CamelCase
- },
- LoggerOutputCase.PascalCase => new JsonSerializerOptions
- {
- PropertyNamingPolicy = PascalCaseNamingPolicy.Instance,
- DictionaryKeyPolicy = PascalCaseNamingPolicy.Instance
- },
- _ => new JsonSerializerOptions //defaults to snake_case
- {
- PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance,
- DictionaryKeyPolicy = SnakeCaseNamingPolicy.Instance
- }
- };
-
- jsonOptions.Converters.Add(new ByteArrayConverter());
- jsonOptions.Converters.Add(new ExceptionConverter());
- jsonOptions.Converters.Add(new MemoryStreamConverter());
- jsonOptions.Converters.Add(new ConstantClassConverter());
- jsonOptions.Converters.Add(new DateOnlyConverter());
- jsonOptions.Converters.Add(new TimeOnlyConverter());
-
- jsonOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
-
-#if NET8_0_OR_GREATER
- jsonOptions.TypeInfoResolver = LoggingSerializationContext.Default;
-#endif
-
- return jsonOptions;
- }
-}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs
new file mode 100644
index 00000000..d31caba9
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs
@@ -0,0 +1,163 @@
+/*
+ * 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 AWS.Lambda.Powertools.Common;
+using AWS.Lambda.Powertools.Logging.Serializers;
+using Microsoft.Extensions.Logging;
+
+namespace AWS.Lambda.Powertools.Logging.Internal;
+
+///
+/// Class PowertoolsConfigurationsExtension.
+///
+internal static class PowertoolsConfigurationsExtension
+{
+ ///
+ /// Gets the log level.
+ ///
+ /// The Powertools for AWS Lambda (.NET) configurations.
+ /// The log level.
+ /// LogLevel.
+ internal static LogLevel GetLogLevel(this IPowertoolsConfigurations powertoolsConfigurations,
+ LogLevel? logLevel = null)
+ {
+ if (logLevel.HasValue)
+ return logLevel.Value;
+
+ if (Enum.TryParse((powertoolsConfigurations.LogLevel ?? "").Trim(), true, out LogLevel result))
+ return result;
+
+ return LoggingConstants.DefaultLogLevel;
+ }
+
+ ///
+ /// Lambda Log Level Mapper
+ ///
+ ///
+ ///
+ internal static LogLevel GetLambdaLogLevel(this IPowertoolsConfigurations powertoolsConfigurations)
+ {
+ AwsLogLevelMapper.TryGetValue((powertoolsConfigurations.AWSLambdaLogLevel ?? "").Trim().ToUpper(),
+ out var awsLogLevel);
+
+ if (Enum.TryParse(awsLogLevel, true, out LogLevel result))
+ {
+ return result;
+ }
+
+ return LogLevel.None;
+ }
+
+ ///
+ /// Determines the logger output case based on configuration and input.
+ ///
+ /// The Powertools configurations.
+ /// Optional explicit logger output case.
+ /// The determined LoggerOutputCase.
+ internal static LoggerOutputCase GetLoggerOutputCase(this IPowertoolsConfigurations powertoolsConfigurations,
+ LoggerOutputCase? loggerOutputCase = null)
+ {
+ if (loggerOutputCase.HasValue)
+ return loggerOutputCase.Value;
+
+ if (Enum.TryParse((powertoolsConfigurations.LoggerOutputCase ?? "").Trim(), true, out LoggerOutputCase result))
+ return result;
+
+ return LoggingConstants.DefaultLoggerOutputCase;
+ }
+
+ ///
+ /// Maps AWS log level to .NET log level
+ ///
+ private static readonly Dictionary AwsLogLevelMapper = new()
+ {
+ { "TRACE", "TRACE" },
+ { "DEBUG", "DEBUG" },
+ { "INFO", "INFORMATION" },
+ { "WARN", "WARNING" },
+ { "ERROR", "ERROR" },
+ { "FATAL", "CRITICAL" }
+ };
+
+ ///
+ /// Gets the current configuration.
+ ///
+ /// AWS.Lambda.Powertools.Logging.LoggerConfiguration.
+ public static LoggerConfiguration SetCurrentConfig(this IPowertoolsConfigurations powertoolsConfigurations,
+ LoggerConfiguration config, ISystemWrapper systemWrapper)
+ {
+ config ??= new LoggerConfiguration();
+
+ var logLevel = powertoolsConfigurations.GetLogLevel(config?.MinimumLevel);
+ var lambdaLogLevel = powertoolsConfigurations.GetLambdaLogLevel();
+ var lambdaLogLevelEnabled = powertoolsConfigurations.LambdaLogLevelEnabled();
+
+ if (lambdaLogLevelEnabled && logLevel < lambdaLogLevel)
+ {
+ systemWrapper.LogLine(
+ $"Current log level ({logLevel}) does not match AWS Lambda Advanced Logging Controls minimum log level ({lambdaLogLevel}). This can lead to data loss, consider adjusting them.");
+ }
+
+ var samplingRate = config?.SamplingRate ?? powertoolsConfigurations.LoggerSampleRate;
+ var loggerOutputCase = powertoolsConfigurations.GetLoggerOutputCase(config?.LoggerOutputCase);
+ var service = config?.Service ?? powertoolsConfigurations.Service;
+
+
+ var minLogLevel = logLevel;
+ if (lambdaLogLevelEnabled)
+ {
+ minLogLevel = lambdaLogLevel;
+ }
+
+ config.Service = service;
+ config.MinimumLevel = minLogLevel;
+ config.SamplingRate = samplingRate;
+ config.LoggerOutputCase = loggerOutputCase;
+
+ PowertoolsLoggingSerializer.ConfigureNamingPolicy(config.LoggerOutputCase);
+
+ if (!samplingRate.HasValue)
+ return config;
+
+ if (samplingRate.Value < 0 || samplingRate.Value > 1)
+ {
+ if (minLogLevel is LogLevel.Debug or LogLevel.Trace)
+ systemWrapper.LogLine(
+ $"Skipping sampling rate configuration because of invalid value. Sampling rate: {samplingRate.Value}");
+ config.SamplingRate = null;
+ return config;
+ }
+
+ if (samplingRate.Value == 0)
+ return config;
+
+ var sample = systemWrapper.GetRandom();
+ if (samplingRate.Value > sample)
+ {
+ systemWrapper.LogLine(
+ $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate.Value}, Sampler Value: {sample}.");
+ config.MinimumLevel = LogLevel.Debug;
+ }
+
+ return config;
+ }
+
+ internal static bool LambdaLogLevelEnabled(this IPowertoolsConfigurations powertoolsConfigurations)
+ {
+ return powertoolsConfigurations.GetLambdaLogLevel() != LogLevel.None;
+ }
+}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs
index a2867168..0c37a6e2 100644
--- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs
@@ -15,12 +15,8 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using System.Linq;
-using System.Text.Encodings.Web;
-using System.Text.Json;
using AWS.Lambda.Powertools.Common;
-using AWS.Lambda.Powertools.Logging.Internal.Converters;
using AWS.Lambda.Powertools.Logging.Internal.Helpers;
using AWS.Lambda.Powertools.Logging.Serializers;
using Microsoft.Extensions.Logging;
@@ -34,11 +30,6 @@ namespace AWS.Lambda.Powertools.Logging.Internal;
///
internal sealed class PowertoolsLogger : ILogger
{
- ///
- /// The get current configuration
- ///
- private readonly Func _getCurrentConfig;
-
///
/// The name
///
@@ -47,7 +38,7 @@ internal sealed class PowertoolsLogger : ILogger
///
/// The current configuration
///
- private LoggerConfiguration _currentConfig;
+ private static LoggerConfiguration _currentConfig;
///
/// The Powertools for AWS Lambda (.NET) configurations
@@ -59,67 +50,41 @@ internal sealed class PowertoolsLogger : ILogger
///
private readonly ISystemWrapper _systemWrapper;
- ///
- /// The JsonSerializer options
- ///
- private JsonSerializerOptions _jsonSerializerOptions;
-
- private LogLevel _lambdaLogLevel;
- private LogLevel _logLevel;
- private bool _lambdaLogLevelEnabled;
-
///
/// Initializes a new instance of the class.
///
/// The name.
/// The Powertools for AWS Lambda (.NET) configurations.
/// The system wrapper.
- /// The get current configuration.
+ ///
public PowertoolsLogger(
string name,
IPowertoolsConfigurations powertoolsConfigurations,
ISystemWrapper systemWrapper,
- Func getCurrentConfig)
+ LoggerConfiguration currentConfig)
{
- (_name, _powertoolsConfigurations, _systemWrapper, _getCurrentConfig) = (name,
- powertoolsConfigurations, systemWrapper, getCurrentConfig);
+ (_name, _powertoolsConfigurations, _systemWrapper, _currentConfig) = (name,
+ powertoolsConfigurations, systemWrapper, currentConfig);
_powertoolsConfigurations.SetExecutionEnvironment(this);
- _currentConfig = GetCurrentConfig();
-
- if (_lambdaLogLevelEnabled && _logLevel < _lambdaLogLevel)
- {
- var message =
- $"Current log level ({_logLevel}) does not match AWS Lambda Advanced Logging Controls minimum log level ({_lambdaLogLevel}). This can lead to data loss, consider adjusting them.";
- this.LogWarning(message);
- }
}
- private LoggerConfiguration CurrentConfig => _currentConfig ??= GetCurrentConfig();
-
///
/// Sets the minimum level.
///
/// The minimum level.
private LogLevel MinimumLevel =>
- CurrentConfig.MinimumLevel ?? LoggingConstants.DefaultLogLevel;
+ _currentConfig.MinimumLevel ?? LoggingConstants.DefaultLogLevel;
///
/// Sets the service.
///
/// The service.
private string Service =>
- !string.IsNullOrWhiteSpace(CurrentConfig.Service)
- ? CurrentConfig.Service
+ !string.IsNullOrWhiteSpace(_currentConfig.Service)
+ ? _currentConfig.Service
: _powertoolsConfigurations.Service;
- ///
- /// Get JsonSerializer options.
- ///
- /// The current configuration.
- private JsonSerializerOptions JsonSerializerOptions =>
- _jsonSerializerOptions ??= _powertoolsConfigurations.BuildJsonSerializerOptions(CurrentConfig.LoggerOutputCase);
-
internal PowertoolsLoggerScope CurrentScope { get; private set; }
///
@@ -210,11 +175,6 @@ public bool IsEnabled(LogLevel logLevel)
/// The exception related to this entry.
/// Function to create a message of the and .
/// The type of the object to be written.
- [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode",
- Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
- [UnconditionalSuppressMessage("AOT",
- "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.",
- Justification = "Everything is ok with serialization")]
public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception,
Func formatter)
{
@@ -236,9 +196,9 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except
#if NET8_0_OR_GREATER
- _systemWrapper.LogLine(JsonSerializer.Serialize(logEntry, typeof(object), JsonSerializerOptions));
+ _systemWrapper.LogLine(PowertoolsLoggingSerializer.Serialize(logEntry, typeof(object)));
#else
- _systemWrapper.LogLine(JsonSerializer.Serialize(logEntry, JsonSerializerOptions));
+ _systemWrapper.LogLine(PowertoolsLoggingSerializer.Serialize(logEntry));
#endif
}
@@ -279,8 +239,10 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times
}
var keyLogLevel = LoggingConstants.KeyLogLevel;
+ var lambdaLogLevelEnabled = _powertoolsConfigurations.LambdaLogLevelEnabled();
+
// If ALC is enabled and PascalCase we need to convert Level to LogLevel for it to be parsed and sent to CW
- if (_lambdaLogLevelEnabled && CurrentConfig.LoggerOutputCase == LoggerOutputCase.PascalCase)
+ if (lambdaLogLevelEnabled && _currentConfig.LoggerOutputCase == LoggerOutputCase.PascalCase)
{
keyLogLevel = "LogLevel";
}
@@ -291,8 +253,8 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times
logEntry.TryAdd(LoggingConstants.KeyLoggerName, _name);
logEntry.TryAdd(LoggingConstants.KeyMessage, message);
- if (CurrentConfig.SamplingRate.HasValue)
- logEntry.TryAdd(LoggingConstants.KeySamplingRate, CurrentConfig.SamplingRate.Value);
+ if (_currentConfig.SamplingRate.HasValue)
+ logEntry.TryAdd(LoggingConstants.KeySamplingRate, _currentConfig.SamplingRate.Value);
if (exception != null)
logEntry.TryAdd(LoggingConstants.KeyException, exception);
@@ -321,7 +283,7 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec
Name = _name,
Message = message,
Exception = exception,
- SamplingRate = CurrentConfig.SamplingRate,
+ SamplingRate = _currentConfig.SamplingRate,
};
var extraKeys = new Dictionary();
@@ -389,8 +351,7 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec
$"{logFormatter.GetType().FullName} raised an exception: {e.Message}.", e);
}
}
-
-
+
///
/// Clears the configuration.
@@ -400,59 +361,6 @@ internal void ClearConfig()
_currentConfig = null;
}
- ///
- /// Gets the current configuration.
- ///
- /// AWS.Lambda.Powertools.Logging.LoggerConfiguration.
- private LoggerConfiguration GetCurrentConfig()
- {
- var currConfig = _getCurrentConfig();
- _logLevel = _powertoolsConfigurations.GetLogLevel(currConfig?.MinimumLevel);
- var samplingRate = currConfig?.SamplingRate ?? _powertoolsConfigurations.LoggerSampleRate;
- var loggerOutputCase = _powertoolsConfigurations.GetLoggerOutputCase(currConfig?.LoggerOutputCase);
- _lambdaLogLevel = _powertoolsConfigurations.GetLambdaLogLevel();
- _lambdaLogLevelEnabled = _lambdaLogLevel != LogLevel.None;
-
- var minLogLevel = _logLevel;
- if (_lambdaLogLevelEnabled)
- {
- minLogLevel = _lambdaLogLevel;
- }
-
- var config = new LoggerConfiguration
- {
- Service = currConfig?.Service,
- MinimumLevel = minLogLevel,
- SamplingRate = samplingRate,
- LoggerOutputCase = loggerOutputCase
- };
-
- if (!samplingRate.HasValue)
- return config;
-
- if (samplingRate.Value < 0 || samplingRate.Value > 1)
- {
- if (minLogLevel is LogLevel.Debug or LogLevel.Trace)
- _systemWrapper.LogLine(
- $"Skipping sampling rate configuration because of invalid value. Sampling rate: {samplingRate.Value}");
- config.SamplingRate = null;
- return config;
- }
-
- if (samplingRate.Value == 0)
- return config;
-
- var sample = _systemWrapper.GetRandom();
- if (samplingRate.Value > sample)
- {
- _systemWrapper.LogLine(
- $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate.Value}, Sampler Value: {sample}.");
- config.MinimumLevel = LogLevel.Debug;
- }
-
- return config;
- }
-
///
/// Formats message for a log entry.
///
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs
index 5b55acc5..ad3611cb 100644
--- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs
@@ -16,7 +16,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using AWS.Lambda.Powertools.Logging.Internal;
using Microsoft.Extensions.Logging;
namespace AWS.Lambda.Powertools.Logging;
@@ -65,8 +64,6 @@ public static ILogger Create(string categoryName)
if (string.IsNullOrWhiteSpace(categoryName))
throw new ArgumentNullException(nameof(categoryName));
- LoggerProvider ??= new LoggerProvider(null);
-
return LoggerProvider.CreateLogger(categoryName);
}
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs
index d60ee045..43537b59 100644
--- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs
@@ -128,10 +128,6 @@ public class LoggingAttribute : MethodAspectAttribute
///
private LogLevel? _logLevel;
- ///
- /// The logger output case
- ///
- private LoggerOutputCase? _loggerOutputCase;
///
/// The sampling rate
@@ -195,17 +191,13 @@ public bool LogEvent
///
/// true if [clear state]; otherwise, false.
public bool ClearState { get; set; } = false;
-
+
///
/// Specify output case for logging (SnakeCase, by default).
/// This can be also set using the environment variable POWERTOOLS_LOGGER_CASE.
///
/// The log level.
- public LoggerOutputCase LoggerOutputCase
- {
- get => _loggerOutputCase ?? LoggingConstants.DefaultLoggerOutputCase;
- set => _loggerOutputCase = value;
- }
+ public LoggerOutputCase? LoggerOutputCase { get; set; }
///
/// Creates the handler.
@@ -213,13 +205,18 @@ public LoggerOutputCase LoggerOutputCase
/// IMethodAspectHandler.
protected override IMethodAspectHandler CreateHandler()
{
+ var config = new LoggerConfiguration
+ {
+ Service = Service,
+ LoggerOutputCase = LoggerOutputCase,
+ SamplingRate = SamplingRate,
+ };
+
return new LoggingAspectHandler
(
- Service,
- _logLevel,
- _loggerOutputCase,
- _samplingRate,
- _logEvent,
+ config,
+ LogLevel,
+ LogEvent,
CorrelationIdPath,
ClearState,
PowertoolsConfigurations.Instance,
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs
index f68eed5a..022b0a5d 100644
--- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/LoggingSerializationContext.cs
@@ -45,19 +45,25 @@ namespace AWS.Lambda.Powertools.Logging.Serializers;
[JsonSerializable(typeof(MemoryStream))]
[JsonSerializable(typeof(APIGatewayProxyRequest))]
[JsonSerializable(typeof(APIGatewayProxyResponse))]
-[JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestContext), TypeInfoPropertyName = "APIGatewayProxyRequestContext")]
-[JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestClientCert), TypeInfoPropertyName = "APIGatewayProxyRequestProxyRequestClientCert")]
-[JsonSerializable(typeof(APIGatewayProxyRequest.ClientCertValidity), TypeInfoPropertyName = "APIGatewayProxyRequestClientCertValidity")]
+[JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestContext),
+ TypeInfoPropertyName = "APIGatewayProxyRequestContext")]
+[JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestClientCert),
+ TypeInfoPropertyName = "APIGatewayProxyRequestProxyRequestClientCert")]
+[JsonSerializable(typeof(APIGatewayProxyRequest.ClientCertValidity),
+ TypeInfoPropertyName = "APIGatewayProxyRequestClientCertValidity")]
[JsonSerializable(typeof(ApplicationLoadBalancerRequest))]
[JsonSerializable(typeof(LogEntry))]
[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest))]
[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyResponse))]
-[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext), TypeInfoPropertyName = "APIGatewayHttpApiV2ProxyRequestContext")]
-[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestClientCert), TypeInfoPropertyName = "APIGatewayHttpApiV2ProxyRequestProxyRequestClientCert")]
-[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ClientCertValidity), TypeInfoPropertyName = "APIGatewayHttpApiV2ProxyRequestClientCertValidity")]
-internal partial class LoggingSerializationContext : JsonSerializerContext
+[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext),
+ TypeInfoPropertyName = "APIGatewayHttpApiV2ProxyRequestContext")]
+[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestClientCert),
+ TypeInfoPropertyName = "APIGatewayHttpApiV2ProxyRequestProxyRequestClientCert")]
+[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ClientCertValidity),
+ TypeInfoPropertyName = "APIGatewayHttpApiV2ProxyRequestClientCertValidity")]
+public partial class PowertoolsLoggingSerializationContext : JsonSerializerContext
{
-
}
+
#endif
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLambdaSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLambdaSerializer.cs
new file mode 100644
index 00000000..1ff6588f
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLambdaSerializer.cs
@@ -0,0 +1,79 @@
+/*
+ * 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.IO;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Amazon.Lambda.Core;
+
+namespace AWS.Lambda.Powertools.Logging.Serializers;
+
+#if NET8_0_OR_GREATER
+///
+/// Provides a custom Lambda serializer that combines multiple JsonSerializerContexts.
+///
+public class PowertoolsLambdaSerializer : ILambdaSerializer
+{
+ ///
+ /// Initializes a new instance of PowertoolsLambdaSerializer.
+ ///
+ /// The customer's JsonSerializerContext.
+ public PowertoolsLambdaSerializer(JsonSerializerContext customerContext)
+ {
+ PowertoolsLoggingSerializer.AddSerializerContext(customerContext);
+ }
+
+ ///
+ /// Deserializes the input stream to the specified type.
+ ///
+ public T Deserialize(Stream requestStream)
+ {
+ if (!requestStream.CanSeek)
+ {
+ using var ms = new MemoryStream();
+ requestStream.CopyTo(ms);
+ ms.Position = 0;
+ requestStream = ms;
+ }
+
+ var typeInfo = PowertoolsLoggingSerializer.GetTypeInfo(typeof(T));
+ if (typeInfo == null)
+ {
+ throw new InvalidOperationException(
+ $"Type {typeof(T)} is not known to the serializer. Ensure it's included in the JsonSerializerContext.");
+ }
+
+ return (T)JsonSerializer.Deserialize(requestStream, typeInfo)!;
+ }
+
+ ///
+ /// Serializes the specified object and writes the result to the output stream.
+ ///
+ public void Serialize(T response, Stream responseStream)
+ {
+ var typeInfo = PowertoolsLoggingSerializer.GetTypeInfo(typeof(T));
+ if (typeInfo == null)
+ {
+ throw new InvalidOperationException(
+ $"Type {typeof(T)} is not known to the serializer. Ensure it's included in the JsonSerializerContext.");
+ }
+
+ using var writer = new Utf8JsonWriter(responseStream, new JsonWriterOptions { SkipValidation = true });
+ JsonSerializer.Serialize(writer, response, typeInfo);
+ }
+}
+
+#endif
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs
new file mode 100644
index 00000000..1344b8fa
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs
@@ -0,0 +1,182 @@
+/*
+ * 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.Text.Encodings.Web;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+using AWS.Lambda.Powertools.Common;
+using AWS.Lambda.Powertools.Logging.Internal.Converters;
+
+namespace AWS.Lambda.Powertools.Logging.Serializers;
+
+///
+/// Provides serialization functionality for Powertools logging.
+///
+internal class PowertoolsLoggingSerializer
+{
+ private static JsonSerializerOptions _serializerOptions;
+ private static readonly List _additionalContexts = new List();
+ private static LoggerOutputCase _currentOutputCase = LoggerOutputCase.SnakeCase;
+
+ ///
+ /// Gets the JsonSerializerOptions instance.
+ ///
+ internal static JsonSerializerOptions SerializerOptions
+ {
+ get
+ {
+ if (_serializerOptions == null)
+ {
+ if (_serializerOptions == null)
+ {
+ _serializerOptions = BuildJsonSerializerOptions();
+ }
+ }
+
+ return _serializerOptions;
+ }
+ }
+
+ ///
+ /// Configures the naming policy for the serializer.
+ ///
+ /// The case to use for serialization.
+ public static void ConfigureNamingPolicy(LoggerOutputCase? loggerOutputCase)
+ {
+ if (loggerOutputCase == null || loggerOutputCase == _currentOutputCase)
+ {
+ return;
+ }
+
+ _currentOutputCase = loggerOutputCase.Value;
+ var newOptions = BuildJsonSerializerOptions();
+
+#if NET8_0_OR_GREATER
+ foreach (var context in _additionalContexts)
+ {
+ newOptions.TypeInfoResolverChain.Add(context);
+ }
+#endif
+
+ _serializerOptions = newOptions;
+ }
+
+#if NET8_0_OR_GREATER
+
+ ///
+ /// Serializes an object to a JSON string.
+ ///
+ /// The object to serialize.
+ /// The type of the object to serialize.
+ /// A JSON string representation of the object.
+ /// Thrown when the input type is not known to the serializer.
+ public static string Serialize(object value, Type inputType)
+ {
+ var typeInfo = GetTypeInfo(inputType);
+
+ if (typeInfo == null)
+ {
+ throw new InvalidOperationException(
+ $"Type {inputType} is not known to the serializer. Ensure it's included in the JsonSerializerContext.");
+ }
+
+ return JsonSerializer.Serialize(value, typeInfo);
+ }
+
+ ///
+ /// Adds a JsonSerializerContext to the serializer options.
+ ///
+ /// The JsonSerializerContext to add.
+ /// Thrown when the context is null.
+ internal static void AddSerializerContext(JsonSerializerContext context)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+
+ if (!_additionalContexts.Contains(context))
+ {
+ _additionalContexts.Add(context);
+ _serializerOptions?.TypeInfoResolverChain.Add(context);
+ }
+ }
+
+ ///
+ /// Gets the JsonTypeInfo for a given type.
+ ///
+ /// The type to get information for.
+ /// The JsonTypeInfo for the specified type, or null if not found.
+ internal static JsonTypeInfo GetTypeInfo(Type type)
+ {
+ return SerializerOptions.TypeInfoResolver?.GetTypeInfo(type, SerializerOptions);
+ }
+#endif
+
+#if NET6_0
+ ///
+ /// Serializes an object to a JSON string.
+ ///
+ /// The object to serialize.
+ /// A JSON string representation of the object.
+ public static string Serialize(object value)
+ {
+ return JsonSerializer.Serialize(value, SerializerOptions);
+ }
+#endif
+
+ ///
+ /// Builds and configures the JsonSerializerOptions.
+ ///
+ /// A configured JsonSerializerOptions instance.
+ private static JsonSerializerOptions BuildJsonSerializerOptions()
+ {
+ var jsonOptions = new JsonSerializerOptions();
+
+ switch (_currentOutputCase)
+ {
+ case LoggerOutputCase.CamelCase:
+ jsonOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+ jsonOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
+ break;
+ case LoggerOutputCase.PascalCase:
+ jsonOptions.PropertyNamingPolicy = PascalCaseNamingPolicy.Instance;
+ jsonOptions.DictionaryKeyPolicy = PascalCaseNamingPolicy.Instance;
+ break;
+ default: // Snake case
+ jsonOptions.PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance;
+ jsonOptions.DictionaryKeyPolicy = SnakeCaseNamingPolicy.Instance;
+ break;
+ }
+
+ jsonOptions.Converters.Add(new ByteArrayConverter());
+ jsonOptions.Converters.Add(new ExceptionConverter());
+ jsonOptions.Converters.Add(new MemoryStreamConverter());
+ jsonOptions.Converters.Add(new ConstantClassConverter());
+ jsonOptions.Converters.Add(new DateOnlyConverter());
+ jsonOptions.Converters.Add(new TimeOnlyConverter());
+
+ jsonOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
+
+#if NET8_0_OR_GREATER
+ jsonOptions.TypeInfoResolverChain.Add(PowertoolsLoggingSerializationContext.Default);
+ foreach (var context in _additionalContexts)
+ {
+ jsonOptions.TypeInfoResolverChain.Add(context);
+ }
+#endif
+ return jsonOptions;
+ }
+}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LogFormatterTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LogFormatterTest.cs
index 620ebe5e..aadb3a2f 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LogFormatterTest.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LogFormatterTest.cs
@@ -1,12 +1,12 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
+ *
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
- *
+ *
* http://aws.amazon.com/apache2.0
- *
+ *
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
@@ -22,6 +22,7 @@
using Amazon.Lambda.TestUtilities;
using AWS.Lambda.Powertools.Common;
using AWS.Lambda.Powertools.Logging.Internal;
+using AWS.Lambda.Powertools.Logging.Serializers;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using NSubstitute.ReturnsExtensions;
@@ -53,6 +54,13 @@ public void Log_WhenCustomFormatter_LogsCustomFormat()
var configurations = Substitute.For();
configurations.Service.Returns(service);
+ var loggerConfiguration = new LoggerConfiguration
+ {
+ Service = service,
+ MinimumLevel = minimumLevel,
+ LoggerOutputCase = LoggerOutputCase.PascalCase
+ };
+
var globalExtraKeys = new Dictionary
{
{ Guid.NewGuid().ToString(), Guid.NewGuid().ToString() },
@@ -68,11 +76,11 @@ public void Log_WhenCustomFormatter_LogsCustomFormat()
AwsRequestId = Guid.NewGuid().ToString(),
MemoryLimitInMB = (new Random()).Next()
};
-
+
var args = Substitute.For();
var method = Substitute.For();
var parameter = Substitute.For();
-
+
// Setup parameter
parameter.ParameterType.Returns(typeof(ILambdaContext));
@@ -81,12 +89,12 @@ public void Log_WhenCustomFormatter_LogsCustomFormat()
// Setup args
args.Method = method;
- args.Args = new object[] { lambdaContext };
-
+ args.Args = new object[] { lambdaContext };
+
// Act
-
+
LoggingLambdaContext.Extract(args);
-
+
var logFormatter = Substitute.For();
var formattedLogEntry = new
{
@@ -116,13 +124,9 @@ public void Log_WhenCustomFormatter_LogsCustomFormat()
Logger.UseFormatter(logFormatter);
var systemWrapper = Substitute.For();
- var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () =>
- new LoggerConfiguration
- {
- Service = service,
- MinimumLevel = minimumLevel,
- LoggerOutputCase = LoggerOutputCase.PascalCase
- });
+
+ var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper);
+ var logger = provider.CreateLogger(loggerName);
var scopeExtraKeys = new Dictionary
{
@@ -146,16 +150,16 @@ public void Log_WhenCustomFormatter_LogsCustomFormat()
x.Message.ToString() == message &&
x.Exception == null &&
x.ExtraKeys != null && (
- x.ExtraKeys.Count != globalExtraKeys.Count + scopeExtraKeys.Count || (
- x.ExtraKeys.Count == globalExtraKeys.Count + scopeExtraKeys.Count &&
- x.ExtraKeys.ContainsKey(globalExtraKeys.First().Key) &&
- x.ExtraKeys[globalExtraKeys.First().Key] == globalExtraKeys.First().Value &&
- x.ExtraKeys.ContainsKey(globalExtraKeys.Last().Key) &&
- x.ExtraKeys[globalExtraKeys.Last().Key] == globalExtraKeys.Last().Value &&
- x.ExtraKeys.ContainsKey(scopeExtraKeys.First().Key) &&
- x.ExtraKeys[scopeExtraKeys.First().Key] == scopeExtraKeys.First().Value &&
- x.ExtraKeys.ContainsKey(scopeExtraKeys.Last().Key) &&
- x.ExtraKeys[scopeExtraKeys.Last().Key] == scopeExtraKeys.Last().Value ) ) &&
+ x.ExtraKeys.Count != globalExtraKeys.Count + scopeExtraKeys.Count || (
+ x.ExtraKeys.Count == globalExtraKeys.Count + scopeExtraKeys.Count &&
+ x.ExtraKeys.ContainsKey(globalExtraKeys.First().Key) &&
+ x.ExtraKeys[globalExtraKeys.First().Key] == globalExtraKeys.First().Value &&
+ x.ExtraKeys.ContainsKey(globalExtraKeys.Last().Key) &&
+ x.ExtraKeys[globalExtraKeys.Last().Key] == globalExtraKeys.Last().Value &&
+ x.ExtraKeys.ContainsKey(scopeExtraKeys.First().Key) &&
+ x.ExtraKeys[scopeExtraKeys.First().Key] == scopeExtraKeys.First().Value &&
+ x.ExtraKeys.ContainsKey(scopeExtraKeys.Last().Key) &&
+ x.ExtraKeys[scopeExtraKeys.Last().Key] == scopeExtraKeys.Last().Value)) &&
x.LambdaContext != null &&
x.LambdaContext.FunctionName == lambdaContext.FunctionName &&
x.LambdaContext.FunctionVersion == lambdaContext.FunctionVersion &&
@@ -163,8 +167,9 @@ public void Log_WhenCustomFormatter_LogsCustomFormat()
x.LambdaContext.InvokedFunctionArn == lambdaContext.InvokedFunctionArn &&
x.LambdaContext.AwsRequestId == lambdaContext.AwsRequestId
));
- systemWrapper.Received(1).LogLine(JsonSerializer.Serialize(formattedLogEntry));
+ systemWrapper.Received(1).LogLine(JsonSerializer.Serialize(formattedLogEntry));
+
//Clean up
Logger.UseDefaultFormatter();
Logger.RemoveAllKeys();
@@ -172,7 +177,7 @@ public void Log_WhenCustomFormatter_LogsCustomFormat()
LoggingAspectHandler.ResetForTest();
}
}
-
+
[Collection("Sequential")]
public class LogFormatterNullTest
{
@@ -192,7 +197,7 @@ public void Log_WhenCustomFormatterReturnNull_ThrowsLogFormatException()
Logger.UseFormatter(logFormatter);
var systemWrapper = Substitute.For();
- var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () =>
+ var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper,
new LoggerConfiguration
{
Service = service,
@@ -212,7 +217,7 @@ public void Log_WhenCustomFormatterReturnNull_ThrowsLogFormatException()
Logger.UseDefaultFormatter();
}
}
-
+
[Collection("Sequential")]
public class LogFormatterExceptionTest
{
@@ -233,7 +238,7 @@ public void Log_WhenCustomFormatterRaisesException_ThrowsLogFormatException()
Logger.UseFormatter(logFormatter);
var systemWrapper = Substitute.For();
- var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () =>
+ var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper,
new LoggerConfiguration
{
Service = service,
diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LoggingAttributeTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LoggingAttributeTest.cs
index cb0cad09..c13c9759 100644
--- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LoggingAttributeTest.cs
+++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LoggingAttributeTest.cs
@@ -49,9 +49,13 @@ public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContext()
Args = Array.Empty