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