diff --git a/docs/core/logging.md b/docs/core/logging.md
index 76bd3224..d3d59703 100644
--- a/docs/core/logging.md
+++ b/docs/core/logging.md
@@ -515,3 +515,95 @@ Below are some output examples for different casing.
"function_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72"
}
```
+
+## Custom Log formatter (Bring Your Own Formatter)
+
+You can customize the structure (keys and values) of your log entries by implementing a custom log formatter and override default log formatter using ``Logger.UseFormatter`` method. You can implement a custom log formatter by inheriting the ``ILogFormatter`` class and implementing the ``object FormatLogEntry(LogEntry logEntry)`` method.
+
+=== "Function.cs"
+
+ ```c# hl_lines="11"
+ /**
+ * Handler for requests to Lambda function.
+ */
+ public class Function
+ {
+ ///
+ /// Function constructor
+ ///
+ public Function()
+ {
+ Logger.UseFormatter(new CustomLogFormatter());
+ }
+
+ [Logging(CorrelationIdPath = "/headers/my_request_id_header", SamplingRate = 0.7)]
+ public async Task FunctionHandler
+ (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
+ {
+ ...
+ }
+ }
+ ```
+=== "CustomLogFormatter.cs"
+
+ ```c#
+ public class CustomLogFormatter : ILogFormatter
+ {
+ public object FormatLogEntry(LogEntry logEntry)
+ {
+ return new
+ {
+ Message = logEntry.Message,
+ Service = logEntry.Service,
+ CorrelationIds = new
+ {
+ AwsRequestId = logEntry.LambdaContext?.AwsRequestId,
+ XRayTraceId = logEntry.XRayTraceId,
+ CorrelationId = logEntry.CorrelationId
+ },
+ LambdaFunction = new
+ {
+ Name = logEntry.LambdaContext?.FunctionName,
+ Arn = logEntry.LambdaContext?.InvokedFunctionArn,
+ MemoryLimitInMB = logEntry.LambdaContext?.MemoryLimitInMB,
+ Version = logEntry.LambdaContext?.FunctionVersion,
+ ColdStart = logEntry.ColdStart,
+ },
+ Level = logEntry.Level.ToString(),
+ Timestamp = logEntry.Timestamp.ToString("o"),
+ Logger = new
+ {
+ Name = logEntry.Name,
+ SampleRate = logEntry.SamplingRate
+ },
+ };
+ }
+ }
+ ```
+
+=== "Example CloudWatch Logs excerpt"
+
+ ```json
+ {
+ "Message": "Test Message",
+ "Service": "lambda-example",
+ "CorrelationIds": {
+ "AwsRequestId": "52fdfc07-2182-154f-163f-5f0f9a621d72",
+ "XRayTraceId": "1-61b7add4-66532bb81441e1b060389429",
+ "CorrelationId": "correlation_id_value"
+ },
+ "LambdaFunction": {
+ "Name": "test",
+ "Arn": "arn:aws:lambda:eu-west-1:12345678910:function:test",
+ "MemorySize": 128,
+ "Version": "$LATEST",
+ "ColdStart": true
+ },
+ "Level": "Information",
+ "Timestamp": "2021-12-13T20:32:22.5774262Z",
+ "Logger": {
+ "Name": "AWS.Lambda.Powertools.Logging.Logger",
+ "SampleRate": 0.7
+ }
+ }
+ ```
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/ILogFormatter.cs b/libraries/src/AWS.Lambda.Powertools.Logging/ILogFormatter.cs
new file mode 100644
index 00000000..d880796c
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/ILogFormatter.cs
@@ -0,0 +1,29 @@
+/*
+ * 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.Logging;
+
+///
+/// Represents a type used to format Powertools log entries.
+///
+public interface ILogFormatter
+{
+ ///
+ /// Formats a log entry
+ ///
+ /// The log entry.
+ /// Formatted log entry as object.
+ object FormatLogEntry(LogEntry logEntry);
+}
\ 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 68561a2c..265090aa 100644
--- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs
@@ -206,20 +206,43 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except
if (!IsEnabled(logLevel))
return;
- var message = new Dictionary(StringComparer.Ordinal);
+ var timestamp = DateTime.UtcNow;
+ var message = CustomFormatter(state, exception, out var customMessage) && customMessage is not null
+ ? customMessage
+ : formatter(state, exception);
+
+ var logFormatter = Logger.GetFormatter();
+ var logEntry = logFormatter is null?
+ GetLogEntry(logLevel, timestamp, message, exception) :
+ GetFormattedLogEntry(logLevel, timestamp, message, exception, logFormatter);
+
+ _systemWrapper.LogLine(JsonSerializer.Serialize(logEntry, JsonSerializerOptions));
+ }
+
+ ///
+ /// Gets a log entry.
+ ///
+ /// Entry will be written on this level.
+ /// Entry timestamp.
+ /// The message to be written. Can be also an object.
+ /// The exception related to this entry.
+ private Dictionary GetLogEntry(LogLevel logLevel, DateTime timestamp, object message,
+ Exception exception)
+ {
+ var logEntry = new Dictionary(StringComparer.Ordinal);
// Add Custom Keys
foreach (var (key, value) in Logger.GetAllKeys())
- message.TryAdd(key, value);
+ logEntry.TryAdd(key, value);
// Add Lambda Context Keys
if (PowertoolsLambdaContext.Instance is not null)
{
- message.TryAdd(LoggingConstants.KeyFunctionName, PowertoolsLambdaContext.Instance.FunctionName);
- message.TryAdd(LoggingConstants.KeyFunctionVersion, PowertoolsLambdaContext.Instance.FunctionVersion);
- message.TryAdd(LoggingConstants.KeyFunctionMemorySize, PowertoolsLambdaContext.Instance.MemoryLimitInMB);
- message.TryAdd(LoggingConstants.KeyFunctionArn, PowertoolsLambdaContext.Instance.InvokedFunctionArn);
- message.TryAdd(LoggingConstants.KeyFunctionRequestId, PowertoolsLambdaContext.Instance.AwsRequestId);
+ 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);
}
// Add Extra Fields
@@ -228,24 +251,109 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except
foreach (var (key, value) in CurrentScope.ExtraKeys)
{
if (!string.IsNullOrWhiteSpace(key))
- message.TryAdd(key, value);
+ logEntry.TryAdd(key, value);
}
}
- message.TryAdd(LoggingConstants.KeyTimestamp, DateTime.UtcNow.ToString("o"));
- message.TryAdd(LoggingConstants.KeyLogLevel, logLevel.ToString());
- message.TryAdd(LoggingConstants.KeyService, Service);
- message.TryAdd(LoggingConstants.KeyLoggerName, _name);
- message.TryAdd(LoggingConstants.KeyMessage,
- CustomFormatter(state, exception, out var customMessage) && customMessage is not null
- ? customMessage
- : formatter(state, exception));
+ logEntry.TryAdd(LoggingConstants.KeyTimestamp, timestamp.ToString("o"));
+ logEntry.TryAdd(LoggingConstants.KeyLogLevel, logLevel.ToString());
+ logEntry.TryAdd(LoggingConstants.KeyService, Service);
+ logEntry.TryAdd(LoggingConstants.KeyLoggerName, _name);
+ logEntry.TryAdd(LoggingConstants.KeyMessage, message);
+
if (CurrentConfig.SamplingRate.HasValue)
- message.TryAdd(LoggingConstants.KeySamplingRate, CurrentConfig.SamplingRate.Value);
+ logEntry.TryAdd(LoggingConstants.KeySamplingRate, CurrentConfig.SamplingRate.Value);
if (exception != null)
- message.TryAdd(LoggingConstants.KeyException, exception);
+ logEntry.TryAdd(LoggingConstants.KeyException, exception);
- _systemWrapper.LogLine(JsonSerializer.Serialize(message, JsonSerializerOptions));
+ return logEntry;
+ }
+
+ ///
+ /// Gets a formatted log entry.
+ ///
+ /// Entry will be written on this level.
+ /// Entry timestamp.
+ /// The message to be written. Can be also an object.
+ /// The exception related to this entry.
+ /// The custom log entry formatter.
+ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, object message,
+ Exception exception, ILogFormatter logFormatter)
+ {
+ if (logFormatter is null)
+ return null;
+
+ var logEntry = new LogEntry
+ {
+ Timestamp = timestamp,
+ Level = logLevel,
+ Service = Service,
+ Name = _name,
+ Message = message,
+ Exception = exception,
+ SamplingRate = CurrentConfig.SamplingRate,
+ };
+
+ var extraKeys = new Dictionary();
+
+ // Add Custom Keys
+ foreach (var (key, value) in Logger.GetAllKeys())
+ {
+ switch (key)
+ {
+ case LoggingConstants.KeyColdStart:
+ logEntry.ColdStart = (bool)value;
+ break;
+ case LoggingConstants.KeyXRayTraceId:
+ logEntry.XRayTraceId = value as string;
+ break;
+ case LoggingConstants.KeyCorrelationId:
+ logEntry.CorrelationId = value as string;
+ break;
+ default:
+ extraKeys.TryAdd(key, value);
+ break;
+ }
+ }
+
+ // Add Extra Fields
+ if (CurrentScope?.ExtraKeys is not null)
+ {
+ foreach (var (key, value) in CurrentScope.ExtraKeys)
+ {
+ if (!string.IsNullOrWhiteSpace(key))
+ extraKeys.TryAdd(key, value);
+ }
+ }
+
+ if (extraKeys.Any())
+ logEntry.ExtraKeys = extraKeys;
+
+ // Add Lambda Context Keys
+ if (PowertoolsLambdaContext.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,
+ };
+ }
+
+ try
+ {
+ var logObject = logFormatter.FormatLogEntry(logEntry);
+ if (logObject is null)
+ throw new LogFormatException($"{logFormatter.GetType().FullName} returned Null value.");
+ return logObject;
+ }
+ catch (Exception e)
+ {
+ throw new LogFormatException(
+ $"{logFormatter.GetType().FullName} raised an exception: {e.Message}.", e);
+ }
}
///
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LogEntry.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LogEntry.cs
new file mode 100644
index 00000000..43171609
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/LogEntry.cs
@@ -0,0 +1,92 @@
+/*
+ * 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 Microsoft.Extensions.Logging;
+
+namespace AWS.Lambda.Powertools.Logging;
+
+///
+/// Powertools Log Entry
+///
+public class LogEntry
+{
+ ///
+ /// Indicates the cold start.
+ ///
+ /// The cold start value.
+ public bool ColdStart { get; internal set; }
+
+ ///
+ /// Gets the X-Ray trace identifier.
+ ///
+ /// The X-Ray trace identifier.
+ public string XRayTraceId { get; internal set; }
+
+ ///
+ /// Gets the correlation identifier.
+ ///
+ /// The correlation identifier.
+ public string CorrelationId { get; internal set; }
+
+ ///
+ /// Log entry timestamp in UTC.
+ ///
+ public DateTime Timestamp { get; internal set; }
+
+ ///
+ /// Log entry Level is used for logging.
+ ///
+ public LogLevel Level { get; internal set; }
+
+ ///
+ /// Service name is used for logging.
+ ///
+ public string Service { get; internal set; }
+
+ ///
+ /// Logger name is used for logging.
+ ///
+ public string Name { get; internal set; }
+
+ ///
+ /// Log entry Level is used for logging.
+ ///
+ public object Message { get; internal set; }
+
+ ///
+ /// Dynamically set a percentage of logs to DEBUG level.
+ /// This can be also set using the environment variable POWERTOOLS_LOGGER_SAMPLE_RATE.
+ ///
+ /// The sampling rate.
+ public double? SamplingRate { get; internal set; }
+
+ ///
+ /// Gets the appended additional keys to a log entry.
+ /// The extra keys.
+ ///
+ public Dictionary ExtraKeys { get; internal set; }
+
+ ///
+ /// The exception related to this entry.
+ ///
+ public Exception Exception { get; internal set; }
+
+ ///
+ /// The Lambda Context related to this entry.
+ ///
+ public LogEntryLambdaContext LambdaContext { get; internal set; }
+}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LogEntryLambdaContext.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LogEntryLambdaContext.cs
new file mode 100644
index 00000000..b83c933e
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/LogEntryLambdaContext.cs
@@ -0,0 +1,56 @@
+/*
+ * 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.Logging;
+
+///
+/// Powertools Log Entry Lambda Context
+///
+public class LogEntryLambdaContext
+{
+ ///
+ /// 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.
+ ///
+ public string AwsRequestId { get; internal set; }
+
+ ///
+ /// Name of the Lambda function that is running.
+ ///
+ public string FunctionName { get; internal 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.
+ ///
+ public string FunctionVersion { get; internal 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.
+ ///
+ public int MemoryLimitInMB { get; internal 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.
+ ///
+ public string InvokedFunctionArn { get; internal set; }
+}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LogFormatException.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LogFormatException.cs
new file mode 100644
index 00000000..c9248696
--- /dev/null
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/LogFormatException.cs
@@ -0,0 +1,41 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+using System;
+
+namespace AWS.Lambda.Powertools.Logging;
+
+///
+/// Exception thrown when LogFormatter returns invalid object or error:
+///
+public class LogFormatException : Exception
+{
+ ///
+ /// Creates a new LogFormatException
+ ///
+ public LogFormatException()
+ {
+ }
+
+ ///
+ public LogFormatException(string message) : base(message)
+ {
+ }
+
+ ///
+ public LogFormatException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs
index 0989abac..5b55acc5 100644
--- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs
@@ -43,6 +43,11 @@ public class Logger
/// The logger provider.
internal static ILoggerProvider LoggerProvider { get; set; }
+ ///
+ /// The logger formatter instance
+ ///
+ private static ILogFormatter _logFormatter;
+
///
/// Gets the scope.
///
@@ -404,7 +409,7 @@ public static void LogWarning(string message, params object[] args)
{
LoggerInstance.LogWarning(message, args);
}
-
+
#endregion
#region Error
@@ -533,7 +538,7 @@ public static void LogCritical(string message, params object[] args)
{
LoggerInstance.LogCritical(message, args);
}
-
+
#endregion
#region Log
@@ -606,9 +611,9 @@ public static void Log(LogLevel logLevel, EventId eventId, TState state,
}
#endregion
-
+
#endregion
-
+
#region JSON Logger Methods
///
@@ -758,7 +763,7 @@ public static void Log(LogLevel logLevel, Exception exception)
#region ExtraKeys Logger Methods
#region Debug
-
+
///
/// Formats and writes a debug log message.
///
@@ -810,11 +815,11 @@ public static void LogDebug(T extraKeys, string message, params object[] args
{
LoggerInstance.LogDebug(extraKeys, message, args);
}
-
+
#endregion
#region Trace
-
+
///
/// Formats and writes a trace log message.
///
@@ -870,7 +875,7 @@ public static void LogTrace(T extraKeys, string message, params object[] args
#endregion
#region Information
-
+
///
/// Formats and writes an informational log message.
///
@@ -922,7 +927,7 @@ public static void LogInformation(T extraKeys, string message, params object[
{
LoggerInstance.LogInformation(extraKeys, message, args);
}
-
+
#endregion
#region Warning
@@ -978,7 +983,7 @@ public static void LogWarning(T extraKeys, string message, params object[] ar
{
LoggerInstance.LogWarning(extraKeys, message, args);
}
-
+
#endregion
#region Error
@@ -1094,7 +1099,7 @@ public static void LogCritical(T extraKeys, string message, params object[] a
#endregion
#region Log
-
+
///
/// Formats and writes a log message at the specified log level.
///
@@ -1150,8 +1155,34 @@ public static void Log(LogLevel logLevel, T extraKeys, string message, params
{
LoggerInstance.Log(logLevel, extraKeys, message, args);
}
-
+
#endregion
#endregion
+
+ #region Custom Log Formatter
+
+ ///
+ /// Set the log formatter.
+ ///
+ /// The log formatter.
+ public static void UseFormatter(ILogFormatter logFormatter)
+ {
+ _logFormatter = logFormatter ?? throw new ArgumentNullException(nameof(logFormatter));
+ }
+
+ ///
+ /// Set the log formatter to default.
+ ///
+ public static void UseDefaultFormatter()
+ {
+ _logFormatter = null;
+ }
+
+ ///
+ /// Returns the log formatter.
+ ///
+ internal static ILogFormatter GetFormatter() => _logFormatter;
+
+ #endregion
}
\ 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
new file mode 100644
index 00000000..82c93e34
--- /dev/null
+++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/LogFormatterTest.cs
@@ -0,0 +1,248 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using AWS.Lambda.Powertools.Common;
+using AWS.Lambda.Powertools.Logging.Internal;
+using Microsoft.Extensions.Logging;
+using NSubstitute;
+using NSubstitute.ExceptionExtensions;
+using NSubstitute.ReturnsExtensions;
+using Xunit;
+
+namespace AWS.Lambda.Powertools.Logging.Tests
+{
+ [Collection("Sequential")]
+ public class LogFormatterTest
+ {
+ [Fact]
+ public void Log_WhenCustomFormatter_LogsCustomFormat()
+ {
+ // Arrange
+ const bool coldStart = false;
+ var xrayTraceId = Guid.NewGuid().ToString();
+ var correlationId = Guid.NewGuid().ToString();
+ var logLevel = LogLevel.Information;
+ var minimumLevel = LogLevel.Information;
+ var loggerName = Guid.NewGuid().ToString();
+ var service = Guid.NewGuid().ToString();
+ var message = Guid.NewGuid().ToString();
+
+ Logger.AppendKey(LoggingConstants.KeyColdStart, coldStart);
+ Logger.AppendKey(LoggingConstants.KeyXRayTraceId, xrayTraceId);
+ Logger.AppendKey(LoggingConstants.KeyCorrelationId, correlationId);
+
+ var configurations = Substitute.For();
+ configurations.Service.Returns(service);
+
+ var globalExtraKeys = new Dictionary
+ {
+ { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() },
+ { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }
+ };
+ Logger.AppendKeys(globalExtraKeys);
+
+ var lambdaContext = new LogEntryLambdaContext
+ {
+ FunctionName = Guid.NewGuid().ToString(),
+ FunctionVersion = Guid.NewGuid().ToString(),
+ InvokedFunctionArn = Guid.NewGuid().ToString(),
+ AwsRequestId = Guid.NewGuid().ToString(),
+ MemoryLimitInMB = (new Random()).Next()
+ };
+
+ var eventArgs = new AspectEventArgs
+ {
+ Name = Guid.NewGuid().ToString(),
+ Args = new object[]
+ {
+ new
+ {
+ Source = "Test"
+ },
+ lambdaContext
+ }
+ };
+ PowertoolsLambdaContext.Extract(eventArgs);
+
+ var logFormatter = Substitute.For();
+ var formattedLogEntry = new
+ {
+ Message = message,
+ Service = service,
+ CorrelationIds = new
+ {
+ lambdaContext.AwsRequestId,
+ XRayTraceId = xrayTraceId
+ },
+ LambdaFunction = new
+ {
+ Name = lambdaContext.FunctionName,
+ Arn = lambdaContext.InvokedFunctionArn,
+ MemorySize = lambdaContext.MemoryLimitInMB,
+ Version = lambdaContext.FunctionVersion,
+ ColdStart = coldStart,
+ },
+ Level = logLevel.ToString(),
+ Logger = new
+ {
+ Name = loggerName
+ }
+ };
+
+ logFormatter.FormatLogEntry(new LogEntry()).ReturnsForAnyArgs(formattedLogEntry);
+ Logger.UseFormatter(logFormatter);
+
+ var systemWrapper = Substitute.For();
+ var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () =>
+ new LoggerConfiguration
+ {
+ Service = service,
+ MinimumLevel = minimumLevel,
+ LoggerOutputCase = LoggerOutputCase.PascalCase
+ });
+
+ var scopeExtraKeys = new Dictionary
+ {
+ { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() },
+ { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }
+ };
+
+ // Act
+ logger.LogInformation(scopeExtraKeys, message);
+
+ // Assert
+ logFormatter.Received(1).FormatLogEntry(Arg.Is
+ (
+ x =>
+ x.ColdStart == coldStart &&
+ x.XRayTraceId == xrayTraceId &&
+ x.CorrelationId == correlationId &&
+ x.Service == service &&
+ x.Name == loggerName &&
+ x.Level == logLevel &&
+ x.Message.ToString() == message &&
+ x.Exception == null &&
+ x.ExtraKeys != null &&
+ 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 &&
+ x.LambdaContext.MemoryLimitInMB == lambdaContext.MemoryLimitInMB &&
+ x.LambdaContext.InvokedFunctionArn == lambdaContext.InvokedFunctionArn &&
+ x.LambdaContext.AwsRequestId == lambdaContext.AwsRequestId
+ ));
+ systemWrapper.Received(1).LogLine(JsonSerializer.Serialize(formattedLogEntry));
+
+ //Clean up
+ Logger.UseDefaultFormatter();
+ Logger.RemoveAllKeys();
+ PowertoolsLambdaContext.Clear();
+ LoggingAspectHandler.ResetForTest();
+ }
+ }
+
+ [Collection("Sequential")]
+ public class LogFormatterNullTest
+ {
+ [Fact]
+ public void Log_WhenCustomFormatterReturnNull_ThrowsLogFormatException()
+ {
+ // Arrange
+ var loggerName = Guid.NewGuid().ToString();
+ var service = Guid.NewGuid().ToString();
+ var message = Guid.NewGuid().ToString();
+
+ var configurations = Substitute.For();
+ configurations.Service.Returns(service);
+
+ var logFormatter = Substitute.For();
+ logFormatter.FormatLogEntry(new LogEntry()).ReturnsNullForAnyArgs();
+ Logger.UseFormatter(logFormatter);
+
+ var systemWrapper = Substitute.For();
+ var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () =>
+ new LoggerConfiguration
+ {
+ Service = service,
+ MinimumLevel = LogLevel.Information,
+ LoggerOutputCase = LoggerOutputCase.PascalCase
+ });
+
+ // Act
+ void Act() => logger.LogInformation(message);
+
+ // Assert
+ Assert.Throws(Act);
+ logFormatter.Received(1).FormatLogEntry(Arg.Any());
+ systemWrapper.DidNotReceiveWithAnyArgs().LogLine(Arg.Any());
+
+ //Clean up
+ Logger.UseDefaultFormatter();
+ }
+ }
+
+ [Collection("Sequential")]
+ public class LogFormatterExceptionTest
+ {
+ [Fact]
+ public void Log_WhenCustomFormatterRaisesException_ThrowsLogFormatException()
+ {
+ // Arrange
+ var loggerName = Guid.NewGuid().ToString();
+ var service = Guid.NewGuid().ToString();
+ var message = Guid.NewGuid().ToString();
+ var errorMessage = Guid.NewGuid().ToString();
+
+ var configurations = Substitute.For();
+ configurations.Service.Returns(service);
+
+ var logFormatter = Substitute.For();
+ logFormatter.FormatLogEntry(new LogEntry()).ThrowsForAnyArgs(new Exception(errorMessage));
+ Logger.UseFormatter(logFormatter);
+
+ var systemWrapper = Substitute.For();
+ var logger = new PowertoolsLogger(loggerName, configurations, systemWrapper, () =>
+ new LoggerConfiguration
+ {
+ Service = service,
+ MinimumLevel = LogLevel.Information,
+ LoggerOutputCase = LoggerOutputCase.PascalCase
+ });
+
+ // Act
+ void Act() => logger.LogInformation(message);
+
+ // Assert
+ Assert.Throws(Act);
+ logFormatter.Received(1).FormatLogEntry(Arg.Any());
+ systemWrapper.DidNotReceiveWithAnyArgs().LogLine(Arg.Any());
+
+ //Clean up
+ Logger.UseDefaultFormatter();
+ }
+ }
+}
\ No newline at end of file
diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs
index 7ba7b3a3..71db5932 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.UseDefaultFormatter();
+ }
+
private static void Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel logLevel, LogLevel minimumLevel)
{
// Arrange