From 01638c92e0545780b603e1dfe4fe61df16e04b0d Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 6 Mar 2025 18:58:28 +0000 Subject: [PATCH 01/49] refactor to support ILogger --- .../Logger.ExtraKeysLogs.cs | 439 +++++++ .../Logger.Formatter.cs | 49 + .../Logger.JsonLogs.cs | 168 +++ .../Logger.Scope.cs | 97 ++ .../Logger.StandardLogs.cs | 463 +++++++ .../AWS.Lambda.Powertools.Logging/Logger.cs | 1110 +---------------- 6 files changed, 1231 insertions(+), 1095 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Logger.ExtraKeysLogs.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Logger.Formatter.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Logger.JsonLogs.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Logger.Scope.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Logger.StandardLogs.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.ExtraKeysLogs.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.ExtraKeysLogs.cs new file mode 100644 index 00000000..be00722a --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.ExtraKeysLogs.cs @@ -0,0 +1,439 @@ +/* + * 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 Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging; + +public partial class Logger +{ + #region ExtraKeys Logger Methods + + #region Debug + + /// + /// Formats and writes a debug log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// The exception to log. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogDebug(extraKeys, 0, exception, "Error while processing request from {Address}", address) + public static void LogDebug(T extraKeys, EventId eventId, Exception exception, string message, + params object[] args) where T : class + { + LoggerInstance.LogDebug(extraKeys, eventId, exception, message, args); + } + + /// + /// Formats and writes a debug log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogDebug(extraKeys, 0, "Processing request from {Address}", address) + public static void LogDebug(T extraKeys, EventId eventId, string message, params object[] args) where T : class + { + LoggerInstance.LogDebug(extraKeys, eventId, message, args); + } + + /// + /// Formats and writes a debug log message. + /// + /// Additional keys will be appended to the log entry. + /// The exception to log. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogDebug(extraKeys, exception, "Error while processing request from {Address}", address) + public static void LogDebug(T extraKeys, Exception exception, string message, params object[] args) + where T : class + { + LoggerInstance.LogDebug(extraKeys, exception, message, args); + } + + /// + /// Formats and writes a debug log message. + /// + /// Additional keys will be appended to the log entry. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogDebug(extraKeys, "Processing request from {Address}", address) + public static void LogDebug(T extraKeys, string message, params object[] args) where T : class + { + LoggerInstance.LogDebug(extraKeys, message, args); + } + + #endregion + + #region Trace + + /// + /// Formats and writes a trace log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// The exception to log. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogTrace(extraKeys, 0, exception, "Error while processing request from {Address}", address) + public static void LogTrace(T extraKeys, EventId eventId, Exception exception, string message, + params object[] args) where T : class + { + LoggerInstance.LogTrace(extraKeys, eventId, exception, message, args); + } + + /// + /// Formats and writes a trace log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogTrace(extraKeys, 0, "Processing request from {Address}", address) + public static void LogTrace(T extraKeys, EventId eventId, string message, params object[] args) where T : class + { + LoggerInstance.LogTrace(extraKeys, eventId, message, args); + } + + /// + /// Formats and writes a trace log message. + /// + /// Additional keys will be appended to the log entry. + /// The exception to log. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogTrace(extraKeys, exception, "Error while processing request from {Address}", address) + public static void LogTrace(T extraKeys, Exception exception, string message, params object[] args) + where T : class + { + LoggerInstance.LogTrace(extraKeys, exception, message, args); + } + + /// + /// Formats and writes a trace log message. + /// + /// Additional keys will be appended to the log entry. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogTrace(extraKeys, "Processing request from {Address}", address) + public static void LogTrace(T extraKeys, string message, params object[] args) where T : class + { + LoggerInstance.LogTrace(extraKeys, message, args); + } + + #endregion + + #region Information + + /// + /// Formats and writes an informational log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// The exception to log. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogInformation(extraKeys, 0, exception, "Error while processing request from {Address}", address) + public static void LogInformation(T extraKeys, EventId eventId, Exception exception, string message, + params object[] args) where T : class + { + LoggerInstance.LogInformation(extraKeys, eventId, exception, message, args); + } + + /// + /// Formats and writes an informational log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogInformation(extraKeys, 0, "Processing request from {Address}", address) + public static void LogInformation(T extraKeys, EventId eventId, string message, params object[] args) + where T : class + { + LoggerInstance.LogInformation(extraKeys, eventId, message, args); + } + + /// + /// Formats and writes an informational log message. + /// + /// Additional keys will be appended to the log entry. + /// The exception to log. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogInformation(extraKeys, exception, "Error while processing request from {Address}", address) + public static void LogInformation(T extraKeys, Exception exception, string message, params object[] args) + where T : class + { + LoggerInstance.LogInformation(extraKeys, exception, message, args); + } + + /// + /// Formats and writes an informational log message. + /// + /// Additional keys will be appended to the log entry. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogInformation(extraKeys, "Processing request from {Address}", address) + public static void LogInformation(T extraKeys, string message, params object[] args) where T : class + { + LoggerInstance.LogInformation(extraKeys, message, args); + } + + #endregion + + #region Warning + + /// + /// Formats and writes a warning log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// The exception to log. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogWarning(extraKeys, 0, exception, "Error while processing request from {Address}", address) + public static void LogWarning(T extraKeys, EventId eventId, Exception exception, string message, + params object[] args) where T : class + { + LoggerInstance.LogWarning(extraKeys, eventId, exception, message, args); + } + + /// + /// Formats and writes a warning log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogWarning(extraKeys, 0, "Processing request from {Address}", address) + public static void LogWarning(T extraKeys, EventId eventId, string message, params object[] args) where T : class + { + LoggerInstance.LogWarning(extraKeys, eventId, message, args); + } + + /// + /// Formats and writes a warning log message. + /// + /// Additional keys will be appended to the log entry. + /// The exception to log. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogWarning(extraKeys, exception, "Error while processing request from {Address}", address) + public static void LogWarning(T extraKeys, Exception exception, string message, params object[] args) + where T : class + { + LoggerInstance.LogWarning(extraKeys, exception, message, args); + } + + /// + /// Formats and writes a warning log message. + /// + /// Additional keys will be appended to the log entry. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogWarning(extraKeys, "Processing request from {Address}", address) + public static void LogWarning(T extraKeys, string message, params object[] args) where T : class + { + LoggerInstance.LogWarning(extraKeys, message, args); + } + + #endregion + + #region Error + + /// + /// Formats and writes an error log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// The exception to log. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogError(extraKeys, 0, exception, "Error while processing request from {Address}", address) + public static void LogError(T extraKeys, EventId eventId, Exception exception, string message, + params object[] args) where T : class + { + LoggerInstance.LogError(extraKeys, eventId, exception, message, args); + } + + /// + /// Formats and writes an error log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogError(extraKeys, 0, "Processing request from {Address}", address) + public static void LogError(T extraKeys, EventId eventId, string message, params object[] args) where T : class + { + LoggerInstance.LogError(extraKeys, eventId, message, args); + } + + /// + /// Formats and writes an error log message. + /// + /// Additional keys will be appended to the log entry. + /// The exception to log. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogError(extraKeys, exception, "Error while processing request from {Address}", address) + public static void LogError(T extraKeys, Exception exception, string message, params object[] args) + where T : class + { + LoggerInstance.LogError(extraKeys, exception, message, args); + } + + /// + /// Formats and writes an error log message. + /// + /// Additional keys will be appended to the log entry. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogError(extraKeys, "Processing request from {Address}", address) + public static void LogError(T extraKeys, string message, params object[] args) where T : class + { + LoggerInstance.LogError(extraKeys, message, args); + } + + #endregion + + #region Critical + + /// + /// Formats and writes a critical log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// The exception to log. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogCritical(extraKeys, 0, exception, "Error while processing request from {Address}", address) + public static void LogCritical(T extraKeys, EventId eventId, Exception exception, string message, + params object[] args) where T : class + { + LoggerInstance.LogCritical(extraKeys, eventId, exception, message, args); + } + + /// + /// Formats and writes a critical log message. + /// + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogCritical(extraKeys, 0, "Processing request from {Address}", address) + public static void LogCritical(T extraKeys, EventId eventId, string message, params object[] args) + where T : class + { + LoggerInstance.LogCritical(extraKeys, eventId, message, args); + } + + /// + /// Formats and writes a critical log message. + /// + /// Additional keys will be appended to the log entry. + /// The exception to log. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogCritical(extraKeys, exception, "Error while processing request from {Address}", address) + public static void LogCritical(T extraKeys, Exception exception, string message, params object[] args) + where T : class + { + LoggerInstance.LogCritical(extraKeys, exception, message, args); + } + + /// + /// Formats and writes a critical log message. + /// + /// Additional keys will be appended to the log entry. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogCritical(extraKeys, "Processing request from {Address}", address) + public static void LogCritical(T extraKeys, string message, params object[] args) where T : class + { + LoggerInstance.LogCritical(extraKeys, message, args); + } + + #endregion + + #region Log + + /// + /// Formats and writes a log message at the specified log level. + /// + /// Entry will be written on this level. + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// The exception to log. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.Log(LogLevel.Information, extraKeys, 0, exception, "Error while processing request from {Address}", address) + public static void Log(LogLevel logLevel, T extraKeys, EventId eventId, Exception exception, string message, + params object[] args) where T : class + { + LoggerInstance.Log(logLevel, extraKeys, eventId, exception, message, args); + } + + /// + /// Formats and writes a log message at the specified log level. + /// + /// Entry will be written on this level. + /// Additional keys will be appended to the log entry. + /// The event id associated with the log. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.Log(LogLevel.Information, extraKeys, 0, "Processing request from {Address}", address) + public static void Log(LogLevel logLevel, T extraKeys, EventId eventId, string message, params object[] args) + where T : class + { + LoggerInstance.Log(logLevel, extraKeys, eventId, message, args); + } + + /// + /// Formats and writes a log message at the specified log level. + /// + /// Entry will be written on this level. + /// Additional keys will be appended to the log entry. + /// The exception to log. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.Log(LogLevel.Information, extraKeys, exception, "Error while processing request from {Address}", address) + public static void Log(LogLevel logLevel, T extraKeys, Exception exception, string message, params object[] args) + where T : class + { + LoggerInstance.Log(logLevel, extraKeys, exception, message, args); + } + + /// + /// Formats and writes a log message at the specified log level. + /// + /// Entry will be written on this level. + /// Additional keys will be appended to the log entry. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.Log(LogLevel.Information, extraKeys, "Processing request from {Address}", address) + public static void Log(LogLevel logLevel, T extraKeys, string message, params object[] args) where T : class + { + LoggerInstance.Log(logLevel, extraKeys, message, args); + } + + #endregion + + #endregion +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Formatter.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Formatter.cs new file mode 100644 index 00000000..62a5bb51 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Formatter.cs @@ -0,0 +1,49 @@ +/* + * 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 AWS.Lambda.Powertools.Logging.Internal; + +namespace AWS.Lambda.Powertools.Logging; + +public partial class Logger +{ + #region Custom Log Formatter + + /// + /// Set the log formatter. + /// + /// The log formatter. + /// WARNING: This method should not be called when using AOT. ILogFormatter should be passed to PowertoolsSourceGeneratorSerializer constructor + 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 +} diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.JsonLogs.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.JsonLogs.cs new file mode 100644 index 00000000..680f766e --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.JsonLogs.cs @@ -0,0 +1,168 @@ +/* + * 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 Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging; + +public partial class Logger +{ + #region JSON Logger Methods + + /// + /// Formats and writes a trace log message as JSON. + /// + /// The object to be serialized as JSON. + /// logger.LogTrace(new {User = user, Address = address}) + public static void LogTrace(object message) + { + LoggerInstance.LogTrace(message); + } + + /// + /// Formats and writes an trace log message. + /// + /// The exception to log. + /// logger.LogTrace(exception) + public static void LogTrace(Exception exception) + { + LoggerInstance.LogTrace(exception); + } + + /// + /// Formats and writes a debug log message as JSON. + /// + /// The object to be serialized as JSON. + /// logger.LogDebug(new {User = user, Address = address}) + public static void LogDebug(object message) + { + LoggerInstance.LogDebug(message); + } + + /// + /// Formats and writes an debug log message. + /// + /// The exception to log. + /// logger.LogDebug(exception) + public static void LogDebug(Exception exception) + { + LoggerInstance.LogDebug(exception); + } + + /// + /// Formats and writes an information log message as JSON. + /// + /// The object to be serialized as JSON. + /// logger.LogInformation(new {User = user, Address = address}) + public static void LogInformation(object message) + { + LoggerInstance.LogInformation(message); + } + + /// + /// Formats and writes an information log message. + /// + /// The exception to log. + /// logger.LogInformation(exception) + public static void LogInformation(Exception exception) + { + LoggerInstance.LogInformation(exception); + } + + /// + /// Formats and writes a warning log message as JSON. + /// + /// The object to be serialized as JSON. + /// logger.LogWarning(new {User = user, Address = address}) + public static void LogWarning(object message) + { + LoggerInstance.LogWarning(message); + } + + /// + /// Formats and writes an warning log message. + /// + /// The exception to log. + /// logger.LogWarning(exception) + public static void LogWarning(Exception exception) + { + LoggerInstance.LogWarning(exception); + } + + /// + /// Formats and writes a error log message as JSON. + /// + /// The object to be serialized as JSON. + /// logger.LogCritical(new {User = user, Address = address}) + public static void LogError(object message) + { + LoggerInstance.LogError(message); + } + + /// + /// Formats and writes an error log message. + /// + /// The exception to log. + /// logger.LogError(exception) + public static void LogError(Exception exception) + { + LoggerInstance.LogError(exception); + } + + /// + /// Formats and writes a critical log message as JSON. + /// + /// The object to be serialized as JSON. + /// logger.LogCritical(new {User = user, Address = address}) + public static void LogCritical(object message) + { + LoggerInstance.LogCritical(message); + } + + /// + /// Formats and writes an critical log message. + /// + /// The exception to log. + /// logger.LogCritical(exception) + public static void LogCritical(Exception exception) + { + LoggerInstance.LogCritical(exception); + } + + /// + /// Formats and writes a log message as JSON at the specified log level. + /// + /// Entry will be written on this level. + /// The object to be serialized as JSON. + /// logger.Log(LogLevel.Information, new {User = user, Address = address}) + public static void Log(LogLevel logLevel, object message) + { + LoggerInstance.Log(logLevel, message); + } + + /// + /// Formats and writes a log message at the specified log level. + /// + /// Entry will be written on this level. + /// The exception to log. + /// logger.Log(LogLevel.Information, exception) + public static void Log(LogLevel logLevel, Exception exception) + { + LoggerInstance.Log(logLevel, exception); + } + + #endregion +} diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Scope.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Scope.cs new file mode 100644 index 00000000..d5561327 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Scope.cs @@ -0,0 +1,97 @@ +/* + * 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 AWS.Lambda.Powertools.Logging.Internal.Helpers; + +namespace AWS.Lambda.Powertools.Logging; + +public partial class Logger +{ + #region Scope Variables + + /// + /// Appending additional key to the log context. + /// + /// The key. + /// The value. + /// key + /// value + public static void AppendKey(string key, object value) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentNullException(nameof(key)); + +#if NET8_0_OR_GREATER + Scope[key] = PowertoolsLoggerHelpers.ObjectToDictionary(value) ?? + throw new ArgumentNullException(nameof(value)); +#else + Scope[key] = value ?? throw new ArgumentNullException(nameof(value)); +#endif + } + + /// + /// Appending additional key to the log context. + /// + /// The list of keys. + public static void AppendKeys(IEnumerable> keys) + { + foreach (var (key, value) in keys) + AppendKey(key, value); + } + + /// + /// Appending additional key to the log context. + /// + /// The list of keys. + public static void AppendKeys(IEnumerable> keys) + { + foreach (var (key, value) in keys) + AppendKey(key, value); + } + + /// + /// Remove additional keys from the log context. + /// + /// The list of keys. + public static void RemoveKeys(params string[] keys) + { + if (keys == null) return; + foreach (var key in keys) + if (Scope.ContainsKey(key)) + Scope.Remove(key); + } + + /// + /// Returns all additional keys added to the log context. + /// + /// IEnumerable<KeyValuePair<System.String, System.Object>>. + public static IEnumerable> GetAllKeys() + { + return Scope.AsEnumerable(); + } + + /// + /// Removes all additional keys from the log context. + /// + internal static void RemoveAllKeys() + { + Scope.Clear(); + } + + #endregion +} diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.StandardLogs.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.StandardLogs.cs new file mode 100644 index 00000000..c403a8ef --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.StandardLogs.cs @@ -0,0 +1,463 @@ +/* + * 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 Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging; + +public partial class Logger +{ + #region Core Logger Methods + + #region Debug + + /// + /// Formats and writes a debug log message. + /// + /// The event id associated with the log. + /// The exception to log. + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogDebug(0, exception, "Error while processing request from {Address}", address) + public static void LogDebug(EventId eventId, Exception exception, string message, params object[] args) + { + LoggerInstance.LogDebug(eventId, exception, message, args); + } + + /// + /// Formats and writes a debug log message. + /// + /// The event id associated with the log. + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogDebug(0, "Processing request from {Address}", address) + public static void LogDebug(EventId eventId, string message, params object[] args) + { + LoggerInstance.LogDebug(eventId, message, args); + } + + /// + /// Formats and writes a debug log message. + /// + /// The exception to log. + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogDebug(exception, "Error while processing request from {Address}", address) + public static void LogDebug(Exception exception, string message, params object[] args) + { + LoggerInstance.LogDebug(exception, message, args); + } + + /// + /// Formats and writes a debug log message. + /// + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogDebug("Processing request from {Address}", address) + public static void LogDebug(string message, params object[] args) + { + LoggerInstance.LogDebug(message, args); + } + + #endregion + + #region Trace + + /// + /// Formats and writes a trace log message. + /// + /// The event id associated with the log. + /// The exception to log. + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogTrace(0, exception, "Error while processing request from {Address}", address) + public static void LogTrace(EventId eventId, Exception exception, string message, params object[] args) + { + LoggerInstance.LogTrace(eventId, exception, message, args); + } + + /// + /// Formats and writes a trace log message. + /// + /// The event id associated with the log. + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogTrace(0, "Processing request from {Address}", address) + public static void LogTrace(EventId eventId, string message, params object[] args) + { + LoggerInstance.LogTrace(eventId, message, args); + } + + /// + /// Formats and writes a trace log message. + /// + /// The exception to log. + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogTrace(exception, "Error while processing request from {Address}", address) + public static void LogTrace(Exception exception, string message, params object[] args) + { + LoggerInstance.LogTrace(exception, message, args); + } + + /// + /// Formats and writes a trace log message. + /// + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogTrace("Processing request from {Address}", address) + public static void LogTrace(string message, params object[] args) + { + LoggerInstance.LogTrace(message, args); + } + + #endregion + + #region Information + + /// + /// Formats and writes an informational log message. + /// + /// The event id associated with the log. + /// The exception to log. + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogInformation(0, exception, "Error while processing request from {Address}", address) + public static void LogInformation(EventId eventId, Exception exception, string message, params object[] args) + { + LoggerInstance.LogInformation(eventId, exception, message, args); + } + + /// + /// Formats and writes an informational log message. + /// + /// The event id associated with the log. + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogInformation(0, "Processing request from {Address}", address) + public static void LogInformation(EventId eventId, string message, params object[] args) + { + LoggerInstance.LogInformation(eventId, message, args); + } + + /// + /// Formats and writes an informational log message. + /// + /// The exception to log. + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogInformation(exception, "Error while processing request from {Address}", address) + public static void LogInformation(Exception exception, string message, params object[] args) + { + LoggerInstance.LogInformation(exception, message, args); + } + + /// + /// Formats and writes an informational log message. + /// + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogInformation("Processing request from {Address}", address) + public static void LogInformation(string message, params object[] args) + { + LoggerInstance.LogInformation(message, args); + } + + #endregion + + #region Warning + + /// + /// Formats and writes a warning log message. + /// + /// The event id associated with the log. + /// The exception to log. + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogWarning(0, exception, "Error while processing request from {Address}", address) + public static void LogWarning(EventId eventId, Exception exception, string message, params object[] args) + { + LoggerInstance.LogWarning(eventId, exception, message, args); + } + + /// + /// Formats and writes a warning log message. + /// + /// The event id associated with the log. + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogWarning(0, "Processing request from {Address}", address) + public static void LogWarning(EventId eventId, string message, params object[] args) + { + LoggerInstance.LogWarning(eventId, message, args); + } + + /// + /// Formats and writes a warning log message. + /// + /// The exception to log. + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogWarning(exception, "Error while processing request from {Address}", address) + public static void LogWarning(Exception exception, string message, params object[] args) + { + LoggerInstance.LogWarning(exception, message, args); + } + + /// + /// Formats and writes a warning log message. + /// + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogWarning("Processing request from {Address}", address) + public static void LogWarning(string message, params object[] args) + { + LoggerInstance.LogWarning(message, args); + } + + #endregion + + #region Error + + /// + /// Formats and writes an error log message. + /// + /// The event id associated with the log. + /// The exception to log. + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogError(0, exception, "Error while processing request from {Address}", address) + public static void LogError(EventId eventId, Exception exception, string message, params object[] args) + { + LoggerInstance.LogError(eventId, exception, message, args); + } + + /// + /// Formats and writes an error log message. + /// + /// The event id associated with the log. + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogError(0, "Processing request from {Address}", address) + public static void LogError(EventId eventId, string message, params object[] args) + { + LoggerInstance.LogError(eventId, message, args); + } + + /// + /// Formats and writes an error log message. + /// + /// The exception to log. + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogError(exception, "Error while processing request from {Address}", address) + public static void LogError(Exception exception, string message, params object[] args) + { + LoggerInstance.LogError(exception, message, args); + } + + /// + /// Formats and writes an error log message. + /// + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogError("Processing request from {Address}", address) + public static void LogError(string message, params object[] args) + { + LoggerInstance.LogError(message, args); + } + + #endregion + + #region Critical + + /// + /// Formats and writes a critical log message. + /// + /// The event id associated with the log. + /// The exception to log. + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogCritical(0, exception, "Error while processing request from {Address}", address) + public static void LogCritical(EventId eventId, Exception exception, string message, params object[] args) + { + LoggerInstance.LogCritical(eventId, exception, message, args); + } + + /// + /// Formats and writes a critical log message. + /// + /// The event id associated with the log. + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogCritical(0, "Processing request from {Address}", address) + public static void LogCritical(EventId eventId, string message, params object[] args) + { + LoggerInstance.LogCritical(eventId, message, args); + } + + /// + /// Formats and writes a critical log message. + /// + /// The exception to log. + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogCritical(exception, "Error while processing request from {Address}", address) + public static void LogCritical(Exception exception, string message, params object[] args) + { + LoggerInstance.LogCritical(exception, message, args); + } + + /// + /// Formats and writes a critical log message. + /// + /// + /// Format string of the log message in message template format. Example: + /// "User {User} logged in from {Address}" + /// + /// An object array that contains zero or more objects to format. + /// Logger.LogCritical("Processing request from {Address}", address) + public static void LogCritical(string message, params object[] args) + { + LoggerInstance.LogCritical(message, args); + } + + #endregion + + #region Log + + /// + /// Formats and writes a log message at the specified log level. + /// + /// Entry will be written on this level. + /// Format string of the log message. + /// An object array that contains zero or more objects to format. + public static void Log(LogLevel logLevel, string message, params object[] args) + { + LoggerInstance.Log(logLevel, message, args); + } + + /// + /// Formats and writes a log message at the specified log level. + /// + /// Entry will be written on this level. + /// The event id associated with the log. + /// Format string of the log message. + /// An object array that contains zero or more objects to format. + public static void Log(LogLevel logLevel, EventId eventId, string message, params object[] args) + { + LoggerInstance.Log(logLevel, eventId, message, args); + } + + /// + /// Formats and writes a log message at the specified log level. + /// + /// Entry will be written on this level. + /// The exception to log. + /// Format string of the log message. + /// An object array that contains zero or more objects to format. + public static void Log(LogLevel logLevel, Exception exception, string message, params object[] args) + { + LoggerInstance.Log(logLevel, exception, message, args); + } + + /// + /// Formats and writes a log message at the specified log level. + /// + /// Entry will be written on this level. + /// The event id associated with the log. + /// The exception to log. + /// Format string of the log message. + /// An object array that contains zero or more objects to format. + public static void Log(LogLevel logLevel, EventId eventId, Exception exception, string message, + params object[] args) + { + LoggerInstance.Log(logLevel, eventId, exception, message, args); + } + + #endregion + + #endregion +} \ 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 4271de83..e0aa1266 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs @@ -25,7 +25,7 @@ namespace AWS.Lambda.Powertools.Logging; /// /// Class Logger. /// -public class Logger +public partial class Logger : ILogger { /// /// The logger instance @@ -82,524 +82,36 @@ public static ILogger Create() return Create(typeof(T).FullName); } - #region Scope Variables - - /// - /// Appending additional key to the log context. - /// - /// The key. - /// The value. - /// key - /// value - public static void AppendKey(string key, object value) - { - if (string.IsNullOrWhiteSpace(key)) - throw new ArgumentNullException(nameof(key)); - -#if NET8_0_OR_GREATER - Scope[key] = PowertoolsLoggerHelpers.ObjectToDictionary(value) ?? - throw new ArgumentNullException(nameof(value)); -#else - Scope[key] = value ?? throw new ArgumentNullException(nameof(value)); -#endif - } - - /// - /// Appending additional key to the log context. - /// - /// The list of keys. - public static void AppendKeys(IEnumerable> keys) - { - foreach (var (key, value) in keys) - AppendKey(key, value); - } - - /// - /// Appending additional key to the log context. - /// - /// The list of keys. - public static void AppendKeys(IEnumerable> keys) - { - foreach (var (key, value) in keys) - AppendKey(key, value); - } - - /// - /// Remove additional keys from the log context. - /// - /// The list of keys. - public static void RemoveKeys(params string[] keys) - { - if (keys == null) return; - foreach (var key in keys) - if (Scope.ContainsKey(key)) - Scope.Remove(key); - } - - /// - /// Returns all additional keys added to the log context. - /// - /// IEnumerable<KeyValuePair<System.String, System.Object>>. - public static IEnumerable> GetAllKeys() - { - return Scope.AsEnumerable(); - } - - /// - /// Removes all additional keys from the log context. - /// - internal static void RemoveAllKeys() - { - Scope.Clear(); - } - internal static void ClearLoggerInstance() { _loggerInstance = null; } - #endregion - - #region Core Logger Methods - - #region Debug - - /// - /// Formats and writes a debug log message. - /// - /// The event id associated with the log. - /// The exception to log. - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// Logger.LogDebug(0, exception, "Error while processing request from {Address}", address) - public static void LogDebug(EventId eventId, Exception exception, string message, params object[] args) - { - LoggerInstance.LogDebug(eventId, exception, message, args); - } - - /// - /// Formats and writes a debug log message. - /// - /// The event id associated with the log. - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// Logger.LogDebug(0, "Processing request from {Address}", address) - public static void LogDebug(EventId eventId, string message, params object[] args) - { - LoggerInstance.LogDebug(eventId, message, args); - } - - /// - /// Formats and writes a debug log message. - /// - /// The exception to log. - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// Logger.LogDebug(exception, "Error while processing request from {Address}", address) - public static void LogDebug(Exception exception, string message, params object[] args) - { - LoggerInstance.LogDebug(exception, message, args); - } - - /// - /// Formats and writes a debug log message. - /// - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// Logger.LogDebug("Processing request from {Address}", address) - public static void LogDebug(string message, params object[] args) - { - LoggerInstance.LogDebug(message, args); - } - - #endregion - - #region Trace - - /// - /// Formats and writes a trace log message. - /// - /// The event id associated with the log. - /// The exception to log. - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// Logger.LogTrace(0, exception, "Error while processing request from {Address}", address) - public static void LogTrace(EventId eventId, Exception exception, string message, params object[] args) - { - LoggerInstance.LogTrace(eventId, exception, message, args); - } - - /// - /// Formats and writes a trace log message. - /// - /// The event id associated with the log. - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// Logger.LogTrace(0, "Processing request from {Address}", address) - public static void LogTrace(EventId eventId, string message, params object[] args) - { - LoggerInstance.LogTrace(eventId, message, args); - } - - /// - /// Formats and writes a trace log message. - /// - /// The exception to log. - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// Logger.LogTrace(exception, "Error while processing request from {Address}", address) - public static void LogTrace(Exception exception, string message, params object[] args) - { - LoggerInstance.LogTrace(exception, message, args); - } - - /// - /// Formats and writes a trace log message. - /// - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// Logger.LogTrace("Processing request from {Address}", address) - public static void LogTrace(string message, params object[] args) - { - LoggerInstance.LogTrace(message, args); - } - - #endregion - - #region Information - - /// - /// Formats and writes an informational log message. - /// - /// The event id associated with the log. - /// The exception to log. - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// Logger.LogInformation(0, exception, "Error while processing request from {Address}", address) - public static void LogInformation(EventId eventId, Exception exception, string message, params object[] args) - { - LoggerInstance.LogInformation(eventId, exception, message, args); - } - - /// - /// Formats and writes an informational log message. - /// - /// The event id associated with the log. - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// Logger.LogInformation(0, "Processing request from {Address}", address) - public static void LogInformation(EventId eventId, string message, params object[] args) - { - LoggerInstance.LogInformation(eventId, message, args); - } - - /// - /// Formats and writes an informational log message. - /// - /// The exception to log. - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// Logger.LogInformation(exception, "Error while processing request from {Address}", address) - public static void LogInformation(Exception exception, string message, params object[] args) - { - LoggerInstance.LogInformation(exception, message, args); - } - - /// - /// Formats and writes an informational log message. - /// - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// Logger.LogInformation("Processing request from {Address}", address) - public static void LogInformation(string message, params object[] args) - { - LoggerInstance.LogInformation(message, args); - } - - #endregion - - #region Warning - - /// - /// Formats and writes a warning log message. - /// - /// The event id associated with the log. - /// The exception to log. - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// Logger.LogWarning(0, exception, "Error while processing request from {Address}", address) - public static void LogWarning(EventId eventId, Exception exception, string message, params object[] args) - { - LoggerInstance.LogWarning(eventId, exception, message, args); - } - - /// - /// Formats and writes a warning log message. - /// - /// The event id associated with the log. - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// Logger.LogWarning(0, "Processing request from {Address}", address) - public static void LogWarning(EventId eventId, string message, params object[] args) - { - LoggerInstance.LogWarning(eventId, message, args); - } - - /// - /// Formats and writes a warning log message. - /// - /// The exception to log. - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// Logger.LogWarning(exception, "Error while processing request from {Address}", address) - public static void LogWarning(Exception exception, string message, params object[] args) - { - LoggerInstance.LogWarning(exception, message, args); - } - - /// - /// Formats and writes a warning log message. - /// - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// Logger.LogWarning("Processing request from {Address}", address) - public static void LogWarning(string message, params object[] args) - { - LoggerInstance.LogWarning(message, args); - } - - #endregion - - #region Error - - /// - /// Formats and writes an error log message. - /// - /// The event id associated with the log. - /// The exception to log. - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// Logger.LogError(0, exception, "Error while processing request from {Address}", address) - public static void LogError(EventId eventId, Exception exception, string message, params object[] args) - { - LoggerInstance.LogError(eventId, exception, message, args); - } - - /// - /// Formats and writes an error log message. - /// - /// The event id associated with the log. - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// Logger.LogError(0, "Processing request from {Address}", address) - public static void LogError(EventId eventId, string message, params object[] args) - { - LoggerInstance.LogError(eventId, message, args); - } - - /// - /// Formats and writes an error log message. - /// - /// The exception to log. - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// > - /// Logger.LogError(exception, "Error while processing request from {Address}", address) - public static void LogError(Exception exception, string message, params object[] args) - { - LoggerInstance.LogError(exception, message, args); - } - - /// - /// Formats and writes an error log message. - /// - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// Logger.LogError("Processing request from {Address}", address) - public static void LogError(string message, params object[] args) - { - LoggerInstance.LogError(message, args); - } - - #endregion - - #region Critical - - /// - /// Formats and writes a critical log message. - /// - /// The event id associated with the log. - /// The exception to log. - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// Logger.LogCritical(0, exception, "Error while processing request from {Address}", address) - public static void LogCritical(EventId eventId, Exception exception, string message, params object[] args) - { - LoggerInstance.LogCritical(eventId, exception, message, args); - } - - /// - /// Formats and writes a critical log message. - /// - /// The event id associated with the log. - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// Logger.LogCritical(0, "Processing request from {Address}", address) - public static void LogCritical(EventId eventId, string message, params object[] args) - { - LoggerInstance.LogCritical(eventId, message, args); - } - - /// - /// Formats and writes a critical log message. - /// - /// The exception to log. - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// Logger.LogCritical(exception, "Error while processing request from {Address}", address) - public static void LogCritical(Exception exception, string message, params object[] args) - { - LoggerInstance.LogCritical(exception, message, args); - } - - /// - /// Formats and writes a critical log message. - /// - /// - /// Format string of the log message in message template format. Example: - /// "User {User} logged in from {Address}" - /// - /// An object array that contains zero or more objects to format. - /// Logger.LogCritical("Processing request from {Address}", address) - public static void LogCritical(string message, params object[] args) - { - LoggerInstance.LogCritical(message, args); - } - - #endregion - - #region Log + #region ILogger Interface Implementation /// - /// Formats and writes a log message at the specified log level. + /// Begins a logical operation scope. /// - /// Entry will be written on this level. - /// Format string of the log message. - /// An object array that contains zero or more objects to format. - public static void Log(LogLevel logLevel, string message, params object[] args) + /// The type of state to begin scope for. + /// The identifier for the scope. + /// An that ends the logical operation scope on dispose. + public IDisposable BeginScope(TState state) { - LoggerInstance.Log(logLevel, message, args); + return LoggerInstance.BeginScope(state); } /// - /// Formats and writes a log message at the specified log level. + /// Checks if the given is enabled. /// - /// Entry will be written on this level. - /// The event id associated with the log. - /// Format string of the log message. - /// An object array that contains zero or more objects to format. - public static void Log(LogLevel logLevel, EventId eventId, string message, params object[] args) + /// Level to be checked. + /// true if enabled. + public bool IsEnabled(LogLevel logLevel) { - LoggerInstance.Log(logLevel, eventId, message, args); + return LoggerInstance.IsEnabled(logLevel); } /// - /// Formats and writes a log message at the specified log level. - /// - /// Entry will be written on this level. - /// The exception to log. - /// Format string of the log message. - /// An object array that contains zero or more objects to format. - public static void Log(LogLevel logLevel, Exception exception, string message, params object[] args) - { - LoggerInstance.Log(logLevel, exception, message, args); - } - - /// - /// Formats and writes a log message at the specified log level. - /// - /// Entry will be written on this level. - /// The event id associated with the log. - /// The exception to log. - /// Format string of the log message. - /// An object array that contains zero or more objects to format. - public static void Log(LogLevel logLevel, EventId eventId, Exception exception, string message, - params object[] args) - { - LoggerInstance.Log(logLevel, eventId, exception, message, args); - } - - /// - /// Writes a log entry. + /// Writes a log entry. /// /// The type of the object to be written. /// Entry will be written on this level. @@ -610,603 +122,11 @@ public static void Log(LogLevel logLevel, EventId eventId, Exception exception, /// Function to create a message of the /// and . /// - public static void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { LoggerInstance.Log(logLevel, eventId, state, exception, formatter); } #endregion - - #endregion - - #region JSON Logger Methods - - /// - /// Formats and writes a trace log message as JSON. - /// - /// The object to be serialized as JSON. - /// logger.LogTrace(new {User = user, Address = address}) - public static void LogTrace(object message) - { - LoggerInstance.LogTrace(message); - } - - /// - /// Formats and writes an trace log message. - /// - /// The exception to log. - /// logger.LogTrace(exception) - public static void LogTrace(Exception exception) - { - LoggerInstance.LogTrace(exception); - } - - /// - /// Formats and writes a debug log message as JSON. - /// - /// The object to be serialized as JSON. - /// logger.LogDebug(new {User = user, Address = address}) - public static void LogDebug(object message) - { - LoggerInstance.LogDebug(message); - } - - /// - /// Formats and writes an debug log message. - /// - /// The exception to log. - /// logger.LogDebug(exception) - public static void LogDebug(Exception exception) - { - LoggerInstance.LogDebug(exception); - } - - /// - /// Formats and writes an information log message as JSON. - /// - /// The object to be serialized as JSON. - /// logger.LogInformation(new {User = user, Address = address}) - public static void LogInformation(object message) - { - LoggerInstance.LogInformation(message); - } - - /// - /// Formats and writes an information log message. - /// - /// The exception to log. - /// logger.LogInformation(exception) - public static void LogInformation(Exception exception) - { - LoggerInstance.LogInformation(exception); - } - - /// - /// Formats and writes a warning log message as JSON. - /// - /// The object to be serialized as JSON. - /// logger.LogWarning(new {User = user, Address = address}) - public static void LogWarning(object message) - { - LoggerInstance.LogWarning(message); - } - - /// - /// Formats and writes an warning log message. - /// - /// The exception to log. - /// logger.LogWarning(exception) - public static void LogWarning(Exception exception) - { - LoggerInstance.LogWarning(exception); - } - - /// - /// Formats and writes a error log message as JSON. - /// - /// The object to be serialized as JSON. - /// logger.LogCritical(new {User = user, Address = address}) - public static void LogError(object message) - { - LoggerInstance.LogError(message); - } - - /// - /// Formats and writes an error log message. - /// - /// The exception to log. - /// logger.LogError(exception) - public static void LogError(Exception exception) - { - LoggerInstance.LogError(exception); - } - - /// - /// Formats and writes a critical log message as JSON. - /// - /// The object to be serialized as JSON. - /// logger.LogCritical(new {User = user, Address = address}) - public static void LogCritical(object message) - { - LoggerInstance.LogCritical(message); - } - - /// - /// Formats and writes an critical log message. - /// - /// The exception to log. - /// logger.LogCritical(exception) - public static void LogCritical(Exception exception) - { - LoggerInstance.LogCritical(exception); - } - - /// - /// Formats and writes a log message as JSON at the specified log level. - /// - /// Entry will be written on this level. - /// The object to be serialized as JSON. - /// logger.Log(LogLevel.Information, new {User = user, Address = address}) - public static void Log(LogLevel logLevel, object message) - { - LoggerInstance.Log(logLevel, message); - } - - /// - /// Formats and writes a log message at the specified log level. - /// - /// Entry will be written on this level. - /// The exception to log. - /// logger.Log(LogLevel.Information, exception) - public static void Log(LogLevel logLevel, Exception exception) - { - LoggerInstance.Log(logLevel, exception); - } - - #endregion - - #region ExtraKeys Logger Methods - - #region Debug - - /// - /// Formats and writes a debug log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// The exception to log. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogDebug(extraKeys, 0, exception, "Error while processing request from {Address}", address) - public static void LogDebug(T extraKeys, EventId eventId, Exception exception, string message, - params object[] args) where T : class - { - LoggerInstance.LogDebug(extraKeys, eventId, exception, message, args); - } - - /// - /// Formats and writes a debug log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogDebug(extraKeys, 0, "Processing request from {Address}", address) - public static void LogDebug(T extraKeys, EventId eventId, string message, params object[] args) where T : class - { - LoggerInstance.LogDebug(extraKeys, eventId, message, args); - } - - /// - /// Formats and writes a debug log message. - /// - /// Additional keys will be appended to the log entry. - /// The exception to log. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogDebug(extraKeys, exception, "Error while processing request from {Address}", address) - public static void LogDebug(T extraKeys, Exception exception, string message, params object[] args) - where T : class - { - LoggerInstance.LogDebug(extraKeys, exception, message, args); - } - - /// - /// Formats and writes a debug log message. - /// - /// Additional keys will be appended to the log entry. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogDebug(extraKeys, "Processing request from {Address}", address) - public static void LogDebug(T extraKeys, string message, params object[] args) where T : class - { - LoggerInstance.LogDebug(extraKeys, message, args); - } - - #endregion - - #region Trace - - /// - /// Formats and writes a trace log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// The exception to log. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogTrace(extraKeys, 0, exception, "Error while processing request from {Address}", address) - public static void LogTrace(T extraKeys, EventId eventId, Exception exception, string message, - params object[] args) where T : class - { - LoggerInstance.LogTrace(extraKeys, eventId, exception, message, args); - } - - /// - /// Formats and writes a trace log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogTrace(extraKeys, 0, "Processing request from {Address}", address) - public static void LogTrace(T extraKeys, EventId eventId, string message, params object[] args) where T : class - { - LoggerInstance.LogTrace(extraKeys, eventId, message, args); - } - - /// - /// Formats and writes a trace log message. - /// - /// Additional keys will be appended to the log entry. - /// The exception to log. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogTrace(extraKeys, exception, "Error while processing request from {Address}", address) - public static void LogTrace(T extraKeys, Exception exception, string message, params object[] args) - where T : class - { - LoggerInstance.LogTrace(extraKeys, exception, message, args); - } - - /// - /// Formats and writes a trace log message. - /// - /// Additional keys will be appended to the log entry. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogTrace(extraKeys, "Processing request from {Address}", address) - public static void LogTrace(T extraKeys, string message, params object[] args) where T : class - { - LoggerInstance.LogTrace(extraKeys, message, args); - } - - #endregion - - #region Information - - /// - /// Formats and writes an informational log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// The exception to log. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogInformation(extraKeys, 0, exception, "Error while processing request from {Address}", address) - public static void LogInformation(T extraKeys, EventId eventId, Exception exception, string message, - params object[] args) where T : class - { - LoggerInstance.LogInformation(extraKeys, eventId, exception, message, args); - } - - /// - /// Formats and writes an informational log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogInformation(extraKeys, 0, "Processing request from {Address}", address) - public static void LogInformation(T extraKeys, EventId eventId, string message, params object[] args) - where T : class - { - LoggerInstance.LogInformation(extraKeys, eventId, message, args); - } - - /// - /// Formats and writes an informational log message. - /// - /// Additional keys will be appended to the log entry. - /// The exception to log. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogInformation(extraKeys, exception, "Error while processing request from {Address}", address) - public static void LogInformation(T extraKeys, Exception exception, string message, params object[] args) - where T : class - { - LoggerInstance.LogInformation(extraKeys, exception, message, args); - } - - /// - /// Formats and writes an informational log message. - /// - /// Additional keys will be appended to the log entry. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogInformation(extraKeys, "Processing request from {Address}", address) - public static void LogInformation(T extraKeys, string message, params object[] args) where T : class - { - LoggerInstance.LogInformation(extraKeys, message, args); - } - - #endregion - - #region Warning - - /// - /// Formats and writes a warning log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// The exception to log. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogWarning(extraKeys, 0, exception, "Error while processing request from {Address}", address) - public static void LogWarning(T extraKeys, EventId eventId, Exception exception, string message, - params object[] args) where T : class - { - LoggerInstance.LogWarning(extraKeys, eventId, exception, message, args); - } - - /// - /// Formats and writes a warning log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogWarning(extraKeys, 0, "Processing request from {Address}", address) - public static void LogWarning(T extraKeys, EventId eventId, string message, params object[] args) where T : class - { - LoggerInstance.LogWarning(extraKeys, eventId, message, args); - } - - /// - /// Formats and writes a warning log message. - /// - /// Additional keys will be appended to the log entry. - /// The exception to log. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogWarning(extraKeys, exception, "Error while processing request from {Address}", address) - public static void LogWarning(T extraKeys, Exception exception, string message, params object[] args) - where T : class - { - LoggerInstance.LogWarning(extraKeys, exception, message, args); - } - - /// - /// Formats and writes a warning log message. - /// - /// Additional keys will be appended to the log entry. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogWarning(extraKeys, "Processing request from {Address}", address) - public static void LogWarning(T extraKeys, string message, params object[] args) where T : class - { - LoggerInstance.LogWarning(extraKeys, message, args); - } - - #endregion - - #region Error - - /// - /// Formats and writes an error log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// The exception to log. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogError(extraKeys, 0, exception, "Error while processing request from {Address}", address) - public static void LogError(T extraKeys, EventId eventId, Exception exception, string message, - params object[] args) where T : class - { - LoggerInstance.LogError(extraKeys, eventId, exception, message, args); - } - - /// - /// Formats and writes an error log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogError(extraKeys, 0, "Processing request from {Address}", address) - public static void LogError(T extraKeys, EventId eventId, string message, params object[] args) where T : class - { - LoggerInstance.LogError(extraKeys, eventId, message, args); - } - - /// - /// Formats and writes an error log message. - /// - /// Additional keys will be appended to the log entry. - /// The exception to log. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogError(extraKeys, exception, "Error while processing request from {Address}", address) - public static void LogError(T extraKeys, Exception exception, string message, params object[] args) - where T : class - { - LoggerInstance.LogError(extraKeys, exception, message, args); - } - - /// - /// Formats and writes an error log message. - /// - /// Additional keys will be appended to the log entry. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogError(extraKeys, "Processing request from {Address}", address) - public static void LogError(T extraKeys, string message, params object[] args) where T : class - { - LoggerInstance.LogError(extraKeys, message, args); - } - - #endregion - - #region Critical - - /// - /// Formats and writes a critical log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// The exception to log. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogCritical(extraKeys, 0, exception, "Error while processing request from {Address}", address) - public static void LogCritical(T extraKeys, EventId eventId, Exception exception, string message, - params object[] args) where T : class - { - LoggerInstance.LogCritical(extraKeys, eventId, exception, message, args); - } - - /// - /// Formats and writes a critical log message. - /// - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogCritical(extraKeys, 0, "Processing request from {Address}", address) - public static void LogCritical(T extraKeys, EventId eventId, string message, params object[] args) - where T : class - { - LoggerInstance.LogCritical(extraKeys, eventId, message, args); - } - - /// - /// Formats and writes a critical log message. - /// - /// Additional keys will be appended to the log entry. - /// The exception to log. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogCritical(extraKeys, exception, "Error while processing request from {Address}", address) - public static void LogCritical(T extraKeys, Exception exception, string message, params object[] args) - where T : class - { - LoggerInstance.LogCritical(extraKeys, exception, message, args); - } - - /// - /// Formats and writes a critical log message. - /// - /// Additional keys will be appended to the log entry. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.LogCritical(extraKeys, "Processing request from {Address}", address) - public static void LogCritical(T extraKeys, string message, params object[] args) where T : class - { - LoggerInstance.LogCritical(extraKeys, message, args); - } - - #endregion - - #region Log - - /// - /// Formats and writes a log message at the specified log level. - /// - /// Entry will be written on this level. - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// The exception to log. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.Log(LogLevel.Information, extraKeys, 0, exception, "Error while processing request from {Address}", address) - public static void Log(LogLevel logLevel, T extraKeys, EventId eventId, Exception exception, string message, - params object[] args) where T : class - { - LoggerInstance.Log(logLevel, extraKeys, eventId, exception, message, args); - } - - /// - /// Formats and writes a log message at the specified log level. - /// - /// Entry will be written on this level. - /// Additional keys will be appended to the log entry. - /// The event id associated with the log. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.Log(LogLevel.Information, extraKeys, 0, "Processing request from {Address}", address) - public static void Log(LogLevel logLevel, T extraKeys, EventId eventId, string message, params object[] args) - where T : class - { - LoggerInstance.Log(logLevel, extraKeys, eventId, message, args); - } - - /// - /// Formats and writes a log message at the specified log level. - /// - /// Entry will be written on this level. - /// Additional keys will be appended to the log entry. - /// The exception to log. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.Log(LogLevel.Information, extraKeys, exception, "Error while processing request from {Address}", address) - public static void Log(LogLevel logLevel, T extraKeys, Exception exception, string message, params object[] args) - where T : class - { - LoggerInstance.Log(logLevel, extraKeys, exception, message, args); - } - - /// - /// Formats and writes a log message at the specified log level. - /// - /// Entry will be written on this level. - /// Additional keys will be appended to the log entry. - /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" - /// An object array that contains zero or more objects to format. - /// logger.Log(LogLevel.Information, extraKeys, "Processing request from {Address}", address) - public static void Log(LogLevel logLevel, T extraKeys, string message, params object[] args) where T : class - { - LoggerInstance.Log(logLevel, extraKeys, message, args); - } - - #endregion - - #endregion - - #region Custom Log Formatter - - /// - /// Set the log formatter. - /// - /// The log formatter. - /// WARNING: This method should not be called when using AOT. ILogFormatter should be passed to PowertoolsSourceGeneratorSerializer constructor - 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 From 477ad84f403853bd9e5bacde1b4bda9c3e21dcf5 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 17 Mar 2025 19:24:33 +0000 Subject: [PATCH 02/49] refactor: change Logger class to static and enhance logging capabilities --- .../AWS.Lambda.Powertools.Logging.csproj | 1 + .../BuilderExtensions.cs | 60 ++++++ .../Internal/LoggerProvider.cs | 120 +++++++++-- .../Internal/LoggingAspect.cs | 150 +++++++------- .../Internal/LoggingAspectFactory.cs | 2 +- .../PowertoolsConfigurationsExtension.cs | 119 +++++------ .../Internal/PowertoolsLogger.cs | 66 ++---- .../Logger.Formatter.cs | 4 +- .../Logger.JsonLogs.cs | 2 +- .../Logger.Scope.cs | 12 +- .../Logger.StandardLogs.cs | 33 +-- .../AWS.Lambda.Powertools.Logging/Logger.cs | 190 +++++++++--------- .../LoggerExtensions.cs | 15 ++ ...on.cs => PowertoolsLoggerConfiguration.cs} | 10 +- .../PowertoolsLoggerFactory.cs | 73 +++++++ .../PowertoolsLoggerFactoryBuilder.cs | 83 ++++++++ libraries/src/Directory.Packages.props | 7 +- 17 files changed, 607 insertions(+), 340 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs rename libraries/src/AWS.Lambda.Powertools.Logging/{LoggerConfiguration.cs => PowertoolsLoggerConfiguration.cs} (88%) create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactoryBuilder.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 a4a1478f..ccf8c3ea 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 @@ -15,6 +15,7 @@ + diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs new file mode 100644 index 00000000..0b977275 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs @@ -0,0 +1,60 @@ +using System; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Configuration; +using Microsoft.Extensions.Options; + +namespace AWS.Lambda.Powertools.Logging; + +public static class BuilderExtensions +{ + // Track if we're in the middle of configuration to prevent recursion + private static bool _configuring = false; + + // Single base method that all other overloads call + public static ILoggingBuilder AddPowertoolsLogger( + this ILoggingBuilder builder, + Action? configure = null, + bool fromLoggerConfigure = false) + { + // Add configuration + builder.AddConfiguration(); + + // Register the provider + builder.Services.TryAddEnumerable( + ServiceDescriptor.Singleton()); + + LoggerProviderOptions.RegisterProviderOptions + (builder.Services); + + // Apply configuration if provided + if (configure != null) + { + // Create and apply configuration + var options = new PowertoolsLoggerConfiguration(); + configure(options); + + // Configure options for DI + builder.Services.Configure(configure); + + // Configure static Logger (if not already in a configuration cycle) + if (!fromLoggerConfigure && !_configuring) + { + try + { + _configuring = true; + Logger.Configure(options); + } + finally + { + _configuring = false; + } + } + } + + return builder; + } +} \ 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 94bb1c0d..09735162 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs @@ -13,8 +13,10 @@ * permissions and limitations under the License. */ +using System; using System.Collections.Concurrent; using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging.Serializers; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -25,13 +27,14 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// Implements the /// /// -public sealed class LoggerProvider : ILoggerProvider +[ProviderAlias("PowertoolsLogger")] +internal sealed class LoggerProvider : ILoggerProvider { /// /// The powertools configurations /// private readonly IPowertoolsConfigurations _powertoolsConfigurations; - + /// /// The system wrapper /// @@ -40,8 +43,10 @@ public sealed class LoggerProvider : ILoggerProvider /// /// The loggers /// - private readonly ConcurrentDictionary _loggers = new(); + private readonly ConcurrentDictionary _loggers = new(StringComparer.OrdinalIgnoreCase); + private readonly IDisposable? _onChangeToken; + private PowertoolsLoggerConfiguration _currentConfig; /// /// Initializes a new instance of the class. @@ -49,19 +54,28 @@ public sealed class LoggerProvider : ILoggerProvider /// The configuration. /// /// - public LoggerProvider(IOptions config, IPowertoolsConfigurations powertoolsConfigurations, ISystemWrapper systemWrapper) + public LoggerProvider(IOptionsMonitor config, + IPowertoolsConfigurations powertoolsConfigurations, + ISystemWrapper systemWrapper) { + _currentConfig = config.CurrentValue; _powertoolsConfigurations = powertoolsConfigurations; _systemWrapper = systemWrapper; - _powertoolsConfigurations.SetCurrentConfig(config?.Value, systemWrapper); + _onChangeToken = config.OnChange(updatedConfig => _currentConfig = updatedConfig); + + // TODO: FIx this + // It was moved bellow + // _powertoolsConfigurations.SetCurrentConfig(_currentConfig, systemWrapper); } - + /// /// Initializes a new instance of the class. /// /// The configuration. - public LoggerProvider(IOptions config) - : this(config, PowertoolsConfigurations.Instance, SystemWrapper.Instance) { } + public LoggerProvider(IOptionsMonitor config) + : this(config, PowertoolsConfigurations.Instance, SystemWrapper.Instance) + { + } /// /// Creates a new instance. @@ -70,10 +84,91 @@ public LoggerProvider(IOptions config) /// The instance of that was created. public ILogger CreateLogger(string categoryName) { - return _loggers.GetOrAdd(categoryName, - name => PowertoolsLogger.CreateLogger(name, - _powertoolsConfigurations, - _systemWrapper)); + return _loggers.GetOrAdd(categoryName, name => new PowertoolsLogger(name, + GetCurrentConfig, + _systemWrapper)); + } + + private PowertoolsLoggerConfiguration GetCurrentConfig() + { + var config = _currentConfig; + + ApplyPowertoolsConfig(config); + + return config; + } + + private void ApplyPowertoolsConfig(PowertoolsLoggerConfiguration config) + { + 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."); + } + + // // Set service + config.Service ??= _powertoolsConfigurations.Service; + + + // // Set output case + if (config.LoggerOutputCase == LoggerOutputCase.Default) + { + var loggerOutputCase = _powertoolsConfigurations.GetLoggerOutputCase(config.LoggerOutputCase); + config.LoggerOutputCase = loggerOutputCase; + // TODO: Fix this + } + + PowertoolsLoggingSerializer.ConfigureNamingPolicy(config.LoggerOutputCase); + + // + + // + // // Set log level + // var minLogLevel = lambdaLogLevelEnabled ? lambdaLogLevel : logLevel; + // config.MinimumLevel = minLogLevel; + + config.LogLevelKey = _powertoolsConfigurations.LambdaLogLevelEnabled() && + config.LoggerOutputCase == LoggerOutputCase.PascalCase + ? "LogLevel" + : LoggingConstants.KeyLogLevel; + + // Set sampling rate + // var samplingRate = config.SamplingRate > 0 ? config.SamplingRate : _powertoolsConfigurations.LoggerSampleRate; + // samplingRate = ValidateSamplingRate(samplingRate, minLogLevel, _systemWrapper); + // + // config.SamplingRate = samplingRate; + // + // if (samplingRate > 0) + // { + // double sample = _systemWrapper.GetRandom(); + // + // if (sample <= samplingRate) + // { + // _systemWrapper.LogLine( + // $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate}, Sampler Value: {sample}."); + // config.MinimumLevel = LogLevel.Debug; + // } + // } + } + + private static double ValidateSamplingRate(double samplingRate, LogLevel minLogLevel, ISystemWrapper systemWrapper) + { + if (samplingRate < 0 || samplingRate > 1) + { + if (minLogLevel is LogLevel.Debug or LogLevel.Trace) + { + systemWrapper.LogLine( + $"Skipping sampling rate configuration because of invalid value. Sampling rate: {samplingRate}"); + } + + return 0; + } + + return samplingRate; } /// @@ -82,5 +177,6 @@ public ILogger CreateLogger(string categoryName) public void Dispose() { _loggers.Clear(); + _onChangeToken?.Dispose(); } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index c92566e2..bf875281 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -14,6 +14,8 @@ */ using System; +using System.Collections.Concurrent; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -49,21 +51,6 @@ public class LoggingAspect /// private bool _clearState; - /// - /// The correlation identifier path - /// - private string _correlationIdPath; - - /// - /// The Powertools for AWS Lambda (.NET) configurations - /// - private readonly IPowertoolsConfigurations _powertoolsConfigurations; - - /// - /// The system wrapper - /// - private readonly ISystemWrapper _systemWrapper; - /// /// The is context initialized /// @@ -73,21 +60,50 @@ public class LoggingAspect /// Specify to clear Lambda Context on exit /// private bool _clearLambdaContext; + + private ILogger _logger; + private readonly bool _LogEventEnv; + private readonly string _xRayTraceId; + private bool _isDebug; - /// - /// The configuration - /// - private LoggerConfiguration _config; /// /// Initializes a new instance of the class. /// /// The Powertools configurations. - /// The system wrapper. - public LoggingAspect(IPowertoolsConfigurations powertoolsConfigurations, ISystemWrapper systemWrapper) + public LoggingAspect(IPowertoolsConfigurations powertoolsConfigurations) { - _powertoolsConfigurations = powertoolsConfigurations; - _systemWrapper = systemWrapper; + _LogEventEnv = powertoolsConfigurations.LoggerLogEvent; + _xRayTraceId = powertoolsConfigurations.XRayTraceId; + } + + private void InitializeLogger(LoggingAttribute trigger) + { + // Always configure when we have explicit trigger settings + bool hasExplicitSettings = (trigger.LogLevel != LogLevel.None || + !string.IsNullOrEmpty(trigger.Service) || + trigger.LoggerOutputCase != default || + trigger.SamplingRate > 0); + + // Configure logger if not configured or we have explicit settings + if (!Logger.IsConfigured || hasExplicitSettings) + { + // Create configuration with default values when not explicitly specified + Logger.Configure(new PowertoolsLoggerConfiguration + { + // Use sensible defaults if not specified in the attribute + MinimumLevel = trigger.LogLevel != LogLevel.None ? trigger.LogLevel : LogLevel.Information, + Service = !string.IsNullOrEmpty(trigger.Service) ? trigger.Service : "service_undefined", + LoggerOutputCase = trigger.LoggerOutputCase != default ? trigger.LoggerOutputCase : LoggerOutputCase.SnakeCase, + SamplingRate = trigger.SamplingRate > 0 ? trigger.SamplingRate : 1.0 + }); + } + + // Get logger after configuration + _logger = Logger.GetLogger(); + + // TODO: Fix this + // _isDebug = config.MinimumLevel <= LogLevel.Debug; } /// @@ -126,24 +142,18 @@ public void OnEntry( Triggers = triggers }; - _config = new LoggerConfiguration - { - Service = trigger.Service, - LoggerOutputCase = trigger.LoggerOutputCase, - SamplingRate = trigger.SamplingRate, - MinimumLevel = trigger.LogLevel - }; var logEvent = trigger.LogEvent; - _correlationIdPath = trigger.CorrelationIdPath; _clearState = trigger.ClearState; - Logger.LoggerProvider = new LoggerProvider(_config, _powertoolsConfigurations, _systemWrapper); + InitializeLogger(trigger); if (!_initializeContext) return; + + _isDebug = LogLevel.Debug >= trigger.LogLevel; - Logger.AppendKey(LoggingConstants.KeyColdStart, _isColdStart); + _logger.AppendKey(LoggingConstants.KeyColdStart, _isColdStart); _isColdStart = false; _initializeContext = false; @@ -152,8 +162,8 @@ public void OnEntry( var eventObject = eventArgs.Args.FirstOrDefault(); CaptureXrayTraceId(); CaptureLambdaContext(eventArgs); - CaptureCorrelationId(eventObject); - if (logEvent || _powertoolsConfigurations.LoggerLogEvent) + CaptureCorrelationId(eventObject, trigger.CorrelationIdPath); + if (logEvent || _LogEventEnv) LogEvent(eventObject); } catch (Exception exception) @@ -175,32 +185,18 @@ public void OnExit() if (_clearLambdaContext) LoggingLambdaContext.Clear(); if (_clearState) - Logger.RemoveAllKeys(); + _logger.RemoveAllKeys(); _initializeContext = true; } - /// - /// Determines whether this instance is debug. - /// - /// true if this instance is debug; otherwise, false. - private bool IsDebug() - { - return LogLevel.Debug >= _powertoolsConfigurations.GetLogLevel(_config.MinimumLevel); - } - /// /// Captures the xray trace identifier. /// private void CaptureXrayTraceId() { - var xRayTraceId = _powertoolsConfigurations.XRayTraceId; - if (string.IsNullOrWhiteSpace(xRayTraceId)) + if (string.IsNullOrWhiteSpace(_xRayTraceId)) return; - - xRayTraceId = xRayTraceId - .Split(';', StringSplitOptions.RemoveEmptyEntries)[0].Replace("Root=", ""); - - Logger.AppendKey(LoggingConstants.KeyXRayTraceId, xRayTraceId); + _logger.AppendKey(LoggingConstants.KeyXRayTraceId, _xRayTraceId.Split(';', StringSplitOptions.RemoveEmptyEntries)[0].Replace("Root=", "")); } /// @@ -213,8 +209,8 @@ private void CaptureXrayTraceId() private void CaptureLambdaContext(AspectEventArgs eventArgs) { _clearLambdaContext = LoggingLambdaContext.Extract(eventArgs); - if (LoggingLambdaContext.Instance is null && IsDebug()) - _systemWrapper.LogLine( + if (LoggingLambdaContext.Instance is null && _isDebug) + Debug.WriteLine( "Skipping Lambda Context injection because ILambdaContext context parameter not found."); } @@ -222,12 +218,12 @@ private void CaptureLambdaContext(AspectEventArgs eventArgs) /// Captures the correlation identifier. /// /// The event argument. - private void CaptureCorrelationId(object eventArg) + private void CaptureCorrelationId(object eventArg, string correlationIdPath) { - if (string.IsNullOrWhiteSpace(_correlationIdPath)) + if (string.IsNullOrWhiteSpace(correlationIdPath)) return; - var correlationIdPaths = _correlationIdPath + var correlationIdPaths = correlationIdPath .Split(CorrelationIdPaths.Separator, StringSplitOptions.RemoveEmptyEntries); if (!correlationIdPaths.Any()) @@ -235,8 +231,8 @@ private void CaptureCorrelationId(object eventArg) if (eventArg is null) { - if (IsDebug()) - _systemWrapper.LogLine( + if (_isDebug) + Debug.WriteLine( "Skipping CorrelationId capture because event parameter not found."); return; } @@ -254,23 +250,25 @@ private void CaptureCorrelationId(object eventArg) { // For casing parsing to be removed from Logging v2 when we get rid of outputcase // without this CorrelationIdPaths.ApiGatewayRest would not work - var pathWithOutputCase = - _powertoolsConfigurations.ConvertToOutputCase(correlationIdPaths[i], _config.LoggerOutputCase); - if (!element.TryGetProperty(pathWithOutputCase, out var childElement)) - break; - - element = childElement; + + // TODO: fix this + // var pathWithOutputCase = + // _powertoolsConfigurations.ConvertToOutputCase(correlationIdPaths[i], _config.LoggerOutputCase); + // if (!element.TryGetProperty(pathWithOutputCase, out var childElement)) + // break; + // + // element = childElement; if (i == correlationIdPaths.Length - 1) correlationId = element.ToString(); } if (!string.IsNullOrWhiteSpace(correlationId)) - Logger.AppendKey(LoggingConstants.KeyCorrelationId, correlationId); + _logger.AppendKey(LoggingConstants.KeyCorrelationId, correlationId); } catch (Exception e) { - if (IsDebug()) - _systemWrapper.LogLine( + if (_isDebug) + Debug.WriteLine( $"Skipping CorrelationId capture because of error caused while parsing the event object {e.Message}."); } } @@ -285,30 +283,30 @@ private void LogEvent(object eventArg) { case null: { - if (IsDebug()) - _systemWrapper.LogLine( + if (_isDebug) + Debug.WriteLine( "Skipping Event Log because event parameter not found."); break; } case Stream: try { - Logger.LogInformation(eventArg); + _logger.LogInformation(eventArg); } catch (Exception e) { - Logger.LogError(e, "Failed to log event from supplied input stream."); + _logger.LogError(e, "Failed to log event from supplied input stream."); } break; default: try { - Logger.LogInformation(eventArg); + _logger.LogInformation(eventArg); } catch (Exception e) { - Logger.LogError(e, "Failed to log event from supplied input object."); + _logger.LogError(e, "Failed to log event from supplied input object."); } break; @@ -321,8 +319,6 @@ private void LogEvent(object eventArg) internal static void ResetForTest() { LoggingLambdaContext.Clear(); - Logger.LoggerProvider = null; - Logger.RemoveAllKeys(); - Logger.ClearLoggerInstance(); + // _logger.RemoveAllKeys(); } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs index 5feae3cf..72ea5645 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs @@ -30,6 +30,6 @@ internal static class LoggingAspectFactory /// An instance of the LoggingAspect class. public static object GetInstance(Type type) { - return new LoggingAspect(PowertoolsConfigurations.Instance, SystemWrapper.Instance); + return new LoggingAspect(PowertoolsConfigurations.Instance); } } \ 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 index 148bb540..d513c185 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs @@ -29,7 +29,7 @@ namespace AWS.Lambda.Powertools.Logging.Internal; internal static class PowertoolsConfigurationsExtension { private static readonly object _lock = new object(); - private static LoggerConfiguration _config; + private static PowertoolsLoggerConfiguration _config; /// /// Maps AWS log level to .NET log level @@ -95,37 +95,37 @@ internal static LoggerOutputCase GetLoggerOutputCase(this IPowertoolsConfigurati /// Gets the current configuration. /// /// AWS.Lambda.Powertools.Logging.LoggerConfiguration. - internal static void SetCurrentConfig(this IPowertoolsConfigurations powertoolsConfigurations, LoggerConfiguration config, ISystemWrapper systemWrapper) - { - lock (_lock) - { - _config = 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."); - } - - // Set service - _config.Service = _config.Service ?? powertoolsConfigurations.Service; - - // Set output case - var loggerOutputCase = powertoolsConfigurations.GetLoggerOutputCase(_config.LoggerOutputCase); - _config.LoggerOutputCase = loggerOutputCase; - PowertoolsLoggingSerializer.ConfigureNamingPolicy(loggerOutputCase); - - // Set log level - var minLogLevel = lambdaLogLevelEnabled ? lambdaLogLevel : logLevel; - _config.MinimumLevel = minLogLevel; - - // Set sampling rate - SetSamplingRate(powertoolsConfigurations, systemWrapper, minLogLevel); - } - } + // internal static void SetCurrentConfig(this IPowertoolsConfigurations powertoolsConfigurations, LoggerConfiguration config, ISystemWrapper systemWrapper) + // { + // lock (_lock) + // { + // _config = 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."); + // } + // + // // Set service + // _config.Service = _config.Service ?? powertoolsConfigurations.Service; + // + // // Set output case + // var loggerOutputCase = powertoolsConfigurations.GetLoggerOutputCase(_config.LoggerOutputCase); + // _config.LoggerOutputCase = loggerOutputCase; + // PowertoolsLoggingSerializer.ConfigureNamingPolicy(loggerOutputCase); + // + // // Set log level + // var minLogLevel = lambdaLogLevelEnabled ? lambdaLogLevel : logLevel; + // _config.MinimumLevel = minLogLevel; + // + // // Set sampling rate + // SetSamplingRate(powertoolsConfigurations, systemWrapper, minLogLevel); + // } + // } /// /// Set sampling rate @@ -134,24 +134,24 @@ internal static void SetCurrentConfig(this IPowertoolsConfigurations powertoolsC /// /// /// - private static void SetSamplingRate(IPowertoolsConfigurations powertoolsConfigurations, ISystemWrapper systemWrapper, LogLevel minLogLevel) - { - var samplingRate = _config.SamplingRate > 0 ? _config.SamplingRate : powertoolsConfigurations.LoggerSampleRate; - samplingRate = ValidateSamplingRate(samplingRate, minLogLevel, systemWrapper); - - _config.SamplingRate = samplingRate; - - if (samplingRate > 0) - { - double sample = systemWrapper.GetRandom(); - - if (sample <= samplingRate) - { - systemWrapper.LogLine($"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate}, Sampler Value: {sample}."); - _config.MinimumLevel = LogLevel.Debug; - } - } - } + // private static void SetSamplingRate(IPowertoolsConfigurations powertoolsConfigurations, ISystemWrapper systemWrapper, LogLevel minLogLevel) + // { + // var samplingRate = _config.SamplingRate > 0 ? _config.SamplingRate : powertoolsConfigurations.LoggerSampleRate; + // samplingRate = ValidateSamplingRate(samplingRate, minLogLevel, systemWrapper); + // + // _config.SamplingRate = samplingRate; + // + // if (samplingRate > 0) + // { + // double sample = systemWrapper.GetRandom(); + // + // if (sample <= samplingRate) + // { + // systemWrapper.LogLine($"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate}, Sampler Value: {sample}."); + // _config.MinimumLevel = LogLevel.Debug; + // } + // } + // } /// /// Validate Sampling rate @@ -309,23 +309,4 @@ private static string ToCamelCase(string input) return char.ToLowerInvariant(pascalCase[0]) + pascalCase.Substring(1); } - /// - /// Determines whether [is log level enabled]. - /// - /// The Powertools for AWS Lambda (.NET) configurations. - /// The log level. - /// true if [is log level enabled]; otherwise, false. - internal static bool IsLogLevelEnabled(this IPowertoolsConfigurations powertoolsConfigurations, LogLevel logLevel) - { - return logLevel != LogLevel.None && logLevel >= _config.MinimumLevel; - } - - /// - /// Gets the current configuration. - /// - /// AWS.Lambda.Powertools.Logging.LoggerConfiguration. - internal static LoggerConfiguration CurrentConfig(this IPowertoolsConfigurations powertoolsConfigurations) - { - return _config; - } } \ 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 6e72d102..3c6ae09a 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -34,18 +34,19 @@ internal sealed class PowertoolsLogger : ILogger /// /// The name /// - private readonly string _name; + private readonly string _categoryName; /// /// The current configuration /// - private readonly IPowertoolsConfigurations _powertoolsConfigurations; + private readonly PowertoolsLoggerConfiguration _currentConfig; /// /// The system wrapper /// private readonly ISystemWrapper _systemWrapper; + /// /// The current scope /// @@ -54,32 +55,20 @@ internal sealed class PowertoolsLogger : ILogger /// /// Private constructor - Is initialized on CreateLogger /// - /// The name. + /// The name. /// The Powertools for AWS Lambda (.NET) configurations. /// The system wrapper. - private PowertoolsLogger( - string name, - IPowertoolsConfigurations powertoolsConfigurations, + public PowertoolsLogger( + string categoryName, + Func getCurrentConfig, ISystemWrapper systemWrapper) { - _name = name; - _powertoolsConfigurations = powertoolsConfigurations; + _categoryName = categoryName; + _currentConfig = getCurrentConfig(); _systemWrapper = systemWrapper; - _powertoolsConfigurations.SetExecutionEnvironment(this); - } - - /// - /// Initializes a new instance of the class. - /// - /// The name. - /// The Powertools for AWS Lambda (.NET) configurations. - /// The system wrapper. - internal static PowertoolsLogger CreateLogger(string name, - IPowertoolsConfigurations powertoolsConfigurations, - ISystemWrapper systemWrapper) - { - return new PowertoolsLogger(name, powertoolsConfigurations, systemWrapper); + // TODO: Fix + // _powertoolsConfigurations.SetExecutionEnvironment(this); } /// @@ -108,7 +97,7 @@ internal void EndScope() /// The log level. /// bool. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool IsEnabled(LogLevel logLevel) => _powertoolsConfigurations.IsLogLevelEnabled(logLevel); + public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None && logLevel >= _currentConfig.MinimumLevel; /// /// Writes a log entry. @@ -175,15 +164,13 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times } } - var keyLogLevel = GetLogLevelKey(); - logEntry.TryAdd(LoggingConstants.KeyTimestamp, timestamp.ToString("o")); - logEntry.TryAdd(keyLogLevel, logLevel.ToString()); - logEntry.TryAdd(LoggingConstants.KeyService, _powertoolsConfigurations.CurrentConfig().Service); - logEntry.TryAdd(LoggingConstants.KeyLoggerName, _name); + logEntry.TryAdd(_currentConfig.LogLevelKey, logLevel.ToString()); + logEntry.TryAdd(LoggingConstants.KeyService, _currentConfig.Service); + logEntry.TryAdd(LoggingConstants.KeyLoggerName, _categoryName); logEntry.TryAdd(LoggingConstants.KeyMessage, message); - if (_powertoolsConfigurations.CurrentConfig().SamplingRate > 0) - logEntry.TryAdd(LoggingConstants.KeySamplingRate, _powertoolsConfigurations.CurrentConfig().SamplingRate); + if (_currentConfig.SamplingRate > 0) + logEntry.TryAdd(LoggingConstants.KeySamplingRate, _currentConfig.SamplingRate); if (exception != null) logEntry.TryAdd(LoggingConstants.KeyException, exception); @@ -208,11 +195,11 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec { Timestamp = timestamp, Level = logLevel, - Service = _powertoolsConfigurations.CurrentConfig().Service, - Name = _name, + Service = _currentConfig.Service, + Name = _categoryName, Message = message, Exception = exception, - SamplingRate = _powertoolsConfigurations.CurrentConfig().SamplingRate, + SamplingRate = _currentConfig.SamplingRate, }; var extraKeys = new Dictionary(); @@ -311,19 +298,6 @@ private static bool CustomFormatter(TState state, Exception exception, o return true; } - /// - /// Gets the log level key. - /// - /// System.String. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private string GetLogLevelKey() - { - return _powertoolsConfigurations.LambdaLogLevelEnabled() && - _powertoolsConfigurations.CurrentConfig().LoggerOutputCase == LoggerOutputCase.PascalCase - ? "LogLevel" - : LoggingConstants.KeyLogLevel; - } - /// /// Adds the lambda context keys. /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Formatter.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Formatter.cs index 62a5bb51..515ff2f0 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Formatter.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Formatter.cs @@ -18,8 +18,10 @@ namespace AWS.Lambda.Powertools.Logging; -public partial class Logger +public static partial class Logger { + private static ILogFormatter _logFormatter; + #region Custom Log Formatter /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.JsonLogs.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.JsonLogs.cs index 680f766e..1221a282 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.JsonLogs.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.JsonLogs.cs @@ -18,7 +18,7 @@ namespace AWS.Lambda.Powertools.Logging; -public partial class Logger +public static partial class Logger { #region JSON Logger Methods diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Scope.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Scope.cs index d5561327..94642122 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Scope.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Scope.cs @@ -20,9 +20,13 @@ namespace AWS.Lambda.Powertools.Logging; -public partial class Logger +public static partial class Logger { - #region Scope Variables + /// + /// Gets the scope. + /// + /// The scope. + private static IDictionary Scope { get; } = new Dictionary(StringComparer.Ordinal); /// /// Appending additional key to the log context. @@ -68,7 +72,7 @@ public static void AppendKeys(IEnumerable> keys) /// Remove additional keys from the log context. /// /// The list of keys. - public static void RemoveKeys(params string[] keys) + public static void RemoveKeys(params string[] keys) { if (keys == null) return; foreach (var key in keys) @@ -92,6 +96,4 @@ internal static void RemoveAllKeys() { Scope.Clear(); } - - #endregion } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.StandardLogs.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.StandardLogs.cs index c403a8ef..f157c0cb 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.StandardLogs.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.StandardLogs.cs @@ -18,12 +18,8 @@ namespace AWS.Lambda.Powertools.Logging; -public partial class Logger +public static partial class Logger { - #region Core Logger Methods - - #region Debug - /// /// Formats and writes a debug log message. /// @@ -84,10 +80,6 @@ public static void LogDebug(string message, params object[] args) LoggerInstance.LogDebug(message, args); } - #endregion - - #region Trace - /// /// Formats and writes a trace log message. /// @@ -148,10 +140,6 @@ public static void LogTrace(string message, params object[] args) LoggerInstance.LogTrace(message, args); } - #endregion - - #region Information - /// /// Formats and writes an informational log message. /// @@ -212,10 +200,6 @@ public static void LogInformation(string message, params object[] args) LoggerInstance.LogInformation(message, args); } - #endregion - - #region Warning - /// /// Formats and writes a warning log message. /// @@ -276,10 +260,6 @@ public static void LogWarning(string message, params object[] args) LoggerInstance.LogWarning(message, args); } - #endregion - - #region Error - /// /// Formats and writes an error log message. /// @@ -340,10 +320,6 @@ public static void LogError(string message, params object[] args) LoggerInstance.LogError(message, args); } - #endregion - - #region Critical - /// /// Formats and writes a critical log message. /// @@ -404,10 +380,6 @@ public static void LogCritical(string message, params object[] args) LoggerInstance.LogCritical(message, args); } - #endregion - - #region Log - /// /// Formats and writes a log message at the specified log level. /// @@ -457,7 +429,4 @@ public static void Log(LogLevel logLevel, EventId eventId, Exception exception, LoggerInstance.Log(logLevel, eventId, exception, message, args); } - #endregion - - #endregion } \ 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 e0aa1266..9059feaf 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs @@ -14,10 +14,10 @@ */ using System; -using System.Collections.Generic; -using System.Linq; +using System.Threading; using AWS.Lambda.Powertools.Logging.Internal; -using AWS.Lambda.Powertools.Logging.Internal.Helpers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging; @@ -25,108 +25,118 @@ namespace AWS.Lambda.Powertools.Logging; /// /// Class Logger. /// -public partial class Logger : ILogger +public static partial class Logger { - /// - /// The logger instance - /// - private static ILogger _loggerInstance; - - /// - /// Gets the logger instance. - /// - /// The logger instance. - private static ILogger LoggerInstance => _loggerInstance ??= Create(); - - /// - /// Gets or sets the logger provider. - /// - /// The logger provider. - internal static ILoggerProvider LoggerProvider { get; set; } + // Use Lazy for thread-safe initialization + private static Lazy _factoryLazy; + private static Lazy _defaultLoggerLazy; + + // Static constructor to ensure initialization + // static Logger() + // { + // // Create default configuration with sensible defaults + // var defaultConfig = new PowertoolsLoggerConfiguration + // { + // MinimumLevel = LogLevel.Information, // Default to Information level + // Service = "LambdaFunction", // Default service name + // LoggerOutputCase = LoggerOutputCase.SnakeCase, // Default case + // SamplingRate = 1.0 // Default to log everything + // }; + // + // // Initialize with default factory + // _factoryLazy = new Lazy(() => + // LoggerFactory.Create(builder => + // builder.AddPowertoolsLogger(config => + // { + // config.MinimumLevel = defaultConfig.MinimumLevel; + // config.Service = defaultConfig.Service; + // config.LoggerOutputCase = defaultConfig.LoggerOutputCase; + // config.SamplingRate = defaultConfig.SamplingRate; + // })), + // LazyThreadSafetyMode.ExecutionAndPublication); + // + // _defaultLoggerLazy = new Lazy(() => + // _factoryLazy.Value.CreateLogger("PowertoolsLogger")); + // + // // Not yet explicitly configured + // _isConfigured = false; + // } + + // Flag to track if custom configuration has been applied + private static bool _isConfigured; + + // Properties to access the lazy-initialized instances + private static ILoggerFactory Factory => _factoryLazy.Value; + private static ILogger LoggerInstance => _defaultLoggerLazy.Value; /// - /// The logger formatter instance + /// Indicates whether the Logger has been configured with custom settings /// - private static ILogFormatter _logFormatter; - - /// - /// Gets the scope. - /// - /// The scope. - private static IDictionary Scope { get; } = new Dictionary(StringComparer.Ordinal); - - /// - /// Creates a new instance. - /// - /// The category name for messages produced by the logger. - /// The instance of that was created. - /// categoryName - public static ILogger Create(string categoryName) - { - if (string.IsNullOrWhiteSpace(categoryName)) - throw new ArgumentNullException(nameof(categoryName)); + public static bool IsConfigured => _isConfigured; - // Needed for when using Logger directly with decorator - LoggerProvider ??= new LoggerProvider(null); - return LoggerProvider.CreateLogger(categoryName); - } - - /// - /// Creates a new instance. - /// - /// - /// The instance of that was created. - public static ILogger Create() + // Allow manual configuration using options + public static void Configure(Action configureOptions) { - return Create(typeof(T).FullName); + var options = new PowertoolsLoggerConfiguration(); + configureOptions(options); + Configure(options); } - - internal static void ClearLoggerInstance() + + // Configure with existing factory + public static void Configure(ILoggerFactory loggerFactory) { - _loggerInstance = null; - } + Interlocked.Exchange(ref _factoryLazy, + new Lazy(() => loggerFactory)); - #region ILogger Interface Implementation + Interlocked.Exchange(ref _defaultLoggerLazy, + new Lazy(() => Factory.CreateLogger("PowertoolsLogger"))); - /// - /// Begins a logical operation scope. - /// - /// The type of state to begin scope for. - /// The identifier for the scope. - /// An that ends the logical operation scope on dispose. - public IDisposable BeginScope(TState state) - { - return LoggerInstance.BeginScope(state); + _isConfigured = true; } - /// - /// Checks if the given is enabled. - /// - /// Level to be checked. - /// true if enabled. - public bool IsEnabled(LogLevel logLevel) + // Directly configure from a PowertoolsLoggerConfiguration + internal static void Configure(PowertoolsLoggerConfiguration options) { - return LoggerInstance.IsEnabled(logLevel); + if (options == null) throw new ArgumentNullException(nameof(options)); + + // Create a factory with our provider + var factory = LoggerFactory.Create(builder => + { + // Use AddPowertoolsLogger but with fromLoggerConfigure=true to prevent recursion + builder.AddPowertoolsLogger(config => + { + config.Service = options.Service; + config.MinimumLevel = options.MinimumLevel; + config.LoggerOutputCase = options.LoggerOutputCase; + config.SamplingRate = options.SamplingRate; + // Copy other properties as needed + }, true); + }); + + // Update factory and logger + Interlocked.Exchange(ref _factoryLazy, + new Lazy(() => factory)); + + Interlocked.Exchange(ref _defaultLoggerLazy, + new Lazy(() => Factory.CreateLogger("PowertoolsLogger"))); + + _isConfigured = true; } - /// - /// Writes a log entry. - /// - /// The type of the object to be written. - /// Entry will be written on this level. - /// Id of the event. - /// The entry to be written. Can be also an object. - /// The exception related to this entry. - /// - /// Function to create a message of the - /// and . - /// - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, - Func formatter) - { - LoggerInstance.Log(logLevel, eventId, state, exception, formatter); - } - #endregion + // Get a logger for a specific category + public static ILogger GetLogger() => GetLogger(typeof(T).Name); + + public static ILogger GetLogger(string category) => Factory.CreateLogger(category); + + // For testing purposes + // internal static void Reset() + // { + // Interlocked.Exchange(ref _factoryLazy, + // new Lazy(() => new PowertoolsLoggerFactory())); + + // Interlocked.Exchange(ref _defaultLoggerLazy, + // new Lazy(() => Factory.CreateLogger())); + // } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LoggerExtensions.cs index 200cf46e..e04a8fd7 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LoggerExtensions.cs @@ -652,4 +652,19 @@ public static void Log(this ILogger logger, LogLevel logLevel, T extraKeys, s #endregion #endregion + + public static void AppendKey(this ILogger logger, string key, object value) + { + Logger.AppendKey(key, value); + } + + internal static void RemoveAllKeys(this ILogger logger) + { + Logger.RemoveAllKeys(); + } + + public static void RemoveKeys(this ILogger logger, params string[] keys) + { + Logger.RemoveKeys(keys); + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs similarity index 88% rename from libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs rename to libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs index aab959af..b1d8dd84 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs @@ -24,14 +24,14 @@ namespace AWS.Lambda.Powertools.Logging; /// /// /// -public class LoggerConfiguration : IOptions +public class PowertoolsLoggerConfiguration : IOptions { /// /// Service name is used for logging. /// This can be also set using the environment variable POWERTOOLS_SERVICE_NAME. /// /// The service. - public string Service { get; set; } + public string? Service { get; set; } = null; /// /// Specify the minimum log level for logging (Information, by default). @@ -51,7 +51,7 @@ public class LoggerConfiguration : IOptions /// The default configured options instance /// /// The value. - LoggerConfiguration IOptions.Value => this; + PowertoolsLoggerConfiguration IOptions.Value => this; /// /// The logger output case. @@ -59,4 +59,8 @@ public class LoggerConfiguration : IOptions /// /// The logger output case. public LoggerOutputCase LoggerOutputCase { get; set; } = LoggerOutputCase.Default; + + internal string LogLevelKey { get; set; } + + } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs new file mode 100644 index 00000000..a939ab9c --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs @@ -0,0 +1,73 @@ +using System; +using AWS.Lambda.Powertools.Logging.Internal; +using Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging; + +public sealed class PowertoolsLoggerFactory : IDisposable +{ + private readonly ILoggerFactory _factory; + + public PowertoolsLoggerFactory(ILoggerFactory? loggerFactory = null) + { + _factory = loggerFactory ?? LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(); + }); + } + + public PowertoolsLoggerFactory() : this(LoggerFactory.Create(builder => { builder.AddPowertoolsLogger(); })) + { + } + + public static PowertoolsLoggerFactory Create(Action configureOptions) + { + var options = new PowertoolsLoggerConfiguration(); + configureOptions(options); + + var factory = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + // Copy basic properties + config.Service = options.Service; + config.MinimumLevel = options.MinimumLevel; + config.LoggerOutputCase = options.LoggerOutputCase; + config.SamplingRate = options.SamplingRate; + + // // Copy additional contexts using the public API + // foreach (var ctx in options.GetAdditionalContexts()) + // { + // config.AddJsonContext(ctx); + // } + // + // // Copy log level colors + // foreach (var kvp in options.LogLevelToColorMap) + // { + // config.LogLevelToColorMap[kvp.Key] = kvp.Value; + // } + }); + }); + + Logger.Configure(factory); + return new PowertoolsLoggerFactory(factory); + } + + // Add builder pattern support + public static PowertoolsLoggerFactoryBuilder CreateBuilder() + { + return new PowertoolsLoggerFactoryBuilder(); + } + + public ILogger CreateLogger() => CreateLogger(typeof(T).FullName ?? typeof(T).Name); + + public ILogger CreateLogger(string category) + { + return _factory.CreateLogger(category); + } + + public void Dispose() + { + _factory?.Dispose(); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactoryBuilder.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactoryBuilder.cs new file mode 100644 index 00000000..870c8ada --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactoryBuilder.cs @@ -0,0 +1,83 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging; + +public class PowertoolsLoggerFactoryBuilder +{ + private readonly PowertoolsLoggerConfiguration _configuration = new(); + + // public PowertoolsLoggerFactoryBuilder UseEnvironmentVariables(bool enabled) + // { + // _configuration.UseEnvironmentVariables = enabled; + // return this; + // } + // + // public PowertoolsLoggerFactoryBuilder SetLogLevelColor(AppLogLevel level, ConsoleColor color) + // { + // _configuration.LogLevelToColorMap[level] = color; + // return this; + // } + // + // public PowertoolsLoggerFactoryBuilder SetEventId(int eventId) + // { + // _configuration.EventId = eventId; + // return this; + // } + // + // public PowertoolsLoggerFactoryBuilder SetJsonOptions(JsonSerializerOptions options) + // { + // _configuration.JsonOptions = options; + // return this; + // } + // + // public PowertoolsLoggerFactoryBuilder UseJsonOutput(bool enabled = true) + // { + // _configuration.UseJsonOutput = enabled; + // return this; + // } + // + // public PowertoolsLoggerFactoryBuilder SetTimestampFormat(string format) + // { + // _configuration.TimestampFormat = format; + // return this; + // } + // + // public PowertoolsLoggerFactoryBuilder UseJsonContext(JsonSerializerContext context) + // { + // _configuration.JsonContext = context; + // return this; + // } + // + // public PowertoolsLoggerFactoryBuilder AddJsonContext(JsonSerializerContext context) + // { + // _configuration.AddJsonContext(context); + // return this; + // } + // + // public PowertoolsLoggerFactory Build() + // { + // var factory = LoggerFactory.Create(builder => + // { + // builder.AddPowertoolsLogger(config => + // { + // config.UseEnvironmentVariables = _configuration.UseEnvironmentVariables; + // config.EventId = _configuration.EventId; + // config.JsonOptions = _configuration.JsonOptions; + // config.UseJsonOutput = _configuration.UseJsonOutput; // Add this line + // config.TimestampFormat = _configuration.TimestampFormat; // Add this line + // config.JsonContext = _configuration.JsonContext; + // + // foreach (var kvp in _configuration.LogLevelToColorMap) + // { + // config.LogLevelToColorMap[kvp.Key] = kvp.Value; + // } + // }); + // }); + // + // Logger.Configure(factory); // Configure the static logger + // return new PowertoolsLoggerFactory(factory); + // } +} \ No newline at end of file diff --git a/libraries/src/Directory.Packages.props b/libraries/src/Directory.Packages.props index c5af6311..a28320f2 100644 --- a/libraries/src/Directory.Packages.props +++ b/libraries/src/Directory.Packages.props @@ -4,16 +4,16 @@ - + - + - + @@ -22,5 +22,6 @@ + \ No newline at end of file From 1e1716e2e1b9a6ac2746cb787d503f02a56a695f Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 18 Mar 2025 13:10:08 +0000 Subject: [PATCH 03/49] feat(logging): add GetLogOutput method and CompositeJsonTypeInfoResolver for enhanced logging capabilities --- .../Core/ISystemWrapper.cs | 11 + .../Core/SystemWrapper.cs | 37 ++- .../BuilderExtensions.cs | 48 +++- .../Internal/LoggerProvider.cs | 85 +++--- .../Internal/LoggingAspect.cs | 23 +- .../AWS.Lambda.Powertools.Logging/Logger.cs | 79 +++--- .../PowertoolsLoggerConfiguration.cs | 255 +++++++++++++++++- .../CompositeJsonTypeInfoResolver.cs | 59 ++++ .../PowertoolsLoggingSerializer.cs | 83 +++++- libraries/tests/Directory.Packages.props | 6 +- 10 files changed, 551 insertions(+), 135 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Serializers/CompositeJsonTypeInfoResolver.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/ISystemWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/ISystemWrapper.cs index 8a035984..4e7ad4f0 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/ISystemWrapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/ISystemWrapper.cs @@ -70,4 +70,15 @@ public interface ISystemWrapper /// /// The TextWriter instance where to write to void SetOut(TextWriter writeTo); + + /// + /// Sets console error output + /// Useful for testing and checking the console error output + /// + /// var consoleError = new StringWriter(); + /// SystemWrapper.Instance.SetError(consoleError); + /// + /// + /// + string GetLogOutput(); } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs index 8f42bda4..d972955b 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs @@ -27,10 +27,6 @@ namespace AWS.Lambda.Powertools.Common; public class SystemWrapper : ISystemWrapper { private static IPowertoolsEnvironment _powertoolsEnvironment; - - /// - /// The instance - /// private static ISystemWrapper _instance; /// @@ -56,38 +52,26 @@ public SystemWrapper(IPowertoolsEnvironment powertoolsEnvironment) /// The instance. public static ISystemWrapper Instance => _instance ??= new SystemWrapper(PowertoolsEnvironment.Instance); - /// - /// Gets the environment variable. - /// - /// The variable. - /// System.String. + + /// public string GetEnvironmentVariable(string variable) { return _powertoolsEnvironment.GetEnvironmentVariable(variable); } - /// - /// Logs the specified value. - /// - /// The value. + /// public void Log(string value) { Console.Write(value); } - /// - /// Logs the line. - /// - /// The value. + /// public void LogLine(string value) { Console.WriteLine(value); } - /// - /// Gets random number - /// - /// System.Double. + /// public double GetRandom() { return new Random().NextDouble(); @@ -152,4 +136,15 @@ private string ParseAssemblyName(string assemblyName) return $"{Constants.FeatureContextIdentifier}/{assemblyName}"; } + + /// + public string GetLogOutput() + { + if (Console.Out is StringWriter sw) + { + return sw.ToString(); + } + + return "Console.Out is not a StringWriter - no captured output available"; + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs index 0b977275..e8d7a669 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs @@ -1,6 +1,7 @@ using System; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Serializers; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; @@ -23,23 +24,29 @@ public static ILoggingBuilder AddPowertoolsLogger( // Add configuration builder.AddConfiguration(); - // Register the provider - builder.Services.TryAddEnumerable( - ServiceDescriptor.Singleton()); - - LoggerProviderOptions.RegisterProviderOptions - (builder.Services); - // Apply configuration if provided if (configure != null) { - // Create and apply configuration + // Create initial configuration var options = new PowertoolsLoggerConfiguration(); configure(options); + + // IMPORTANT: Set the minimum level directly on the builder + if (options.MinimumLevel != LogLevel.None) + { + builder.SetMinimumLevel(options.MinimumLevel); + } + // Configure options for DI builder.Services.Configure(configure); + // Register services + RegisterServices(builder); + + // Apply the output case configuration + PowertoolsLoggingSerializer.ConfigureNamingPolicy(options.LoggerOutputCase); + // Configure static Logger (if not already in a configuration cycle) if (!fromLoggerConfigure && !_configuring) { @@ -54,7 +61,32 @@ public static ILoggingBuilder AddPowertoolsLogger( } } } + else + { + // Register services even if no configuration was provided + RegisterServices(builder); + } return builder; } + + private static void RegisterServices(ILoggingBuilder builder) + { + // Register ISystemWrapper if not already registered + builder.Services.TryAddSingleton(); + + // Register IPowertoolsEnvironment if it exists + builder.Services.TryAddSingleton(); + + // Register IPowertoolsConfigurations with all its dependencies + builder.Services.TryAddSingleton(sp => + new PowertoolsConfigurations(sp.GetRequiredService())); + + // Register the provider + builder.Services.TryAddEnumerable( + ServiceDescriptor.Singleton()); + + LoggerProviderOptions.RegisterProviderOptions + (builder.Services); + } } \ 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 09735162..f241cb47 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs @@ -100,59 +100,78 @@ private PowertoolsLoggerConfiguration GetCurrentConfig() private void ApplyPowertoolsConfig(PowertoolsLoggerConfiguration config) { - var logLevel = _powertoolsConfigurations.GetLogLevel(config.MinimumLevel); + var logLevel = _powertoolsConfigurations.GetLogLevel(LogLevel.None); var lambdaLogLevel = _powertoolsConfigurations.GetLambdaLogLevel(); var lambdaLogLevelEnabled = _powertoolsConfigurations.LambdaLogLevelEnabled(); - if (lambdaLogLevelEnabled && logLevel < lambdaLogLevel) + // Check for explicit config + bool hasExplicitLevel = config.MinimumLevel != LogLevel.None; + + // Warn if Lambda log level doesn't match + if (lambdaLogLevelEnabled && hasExplicitLevel && config.MinimumLevel < 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."); + $"Current log level ({config.MinimumLevel}) does not match AWS Lambda Advanced Logging Controls minimum log level ({lambdaLogLevel}). This can lead to data loss, consider adjusting them."); } - // // Set service - config.Service ??= _powertoolsConfigurations.Service; - + // Set service from environment if not explicitly set + if (string.IsNullOrEmpty(config.Service)) + { + config.Service = _powertoolsConfigurations.Service; + } - // // Set output case + // Set output case from environment if not explicitly set if (config.LoggerOutputCase == LoggerOutputCase.Default) { var loggerOutputCase = _powertoolsConfigurations.GetLoggerOutputCase(config.LoggerOutputCase); config.LoggerOutputCase = loggerOutputCase; - // TODO: Fix this } + // Set log level from environment ONLY if not explicitly set + if (!hasExplicitLevel) + { + var minLogLevel = lambdaLogLevelEnabled ? lambdaLogLevel : logLevel; + config.MinimumLevel = minLogLevel != LogLevel.None ? minLogLevel : LoggingConstants.DefaultLogLevel; + } + + // Always configure the serializer with the output case PowertoolsLoggingSerializer.ConfigureNamingPolicy(config.LoggerOutputCase); - // - - // - // // Set log level - // var minLogLevel = lambdaLogLevelEnabled ? lambdaLogLevel : logLevel; - // config.MinimumLevel = minLogLevel; - + // Configure the log level key based on output case config.LogLevelKey = _powertoolsConfigurations.LambdaLogLevelEnabled() && - config.LoggerOutputCase == LoggerOutputCase.PascalCase + config.LoggerOutputCase == LoggerOutputCase.PascalCase ? "LogLevel" : LoggingConstants.KeyLogLevel; + + // Handle sampling rate - BUT DON'T MODIFY MINIMUM LEVEL + ProcessSamplingRate(config); + } - // Set sampling rate - // var samplingRate = config.SamplingRate > 0 ? config.SamplingRate : _powertoolsConfigurations.LoggerSampleRate; - // samplingRate = ValidateSamplingRate(samplingRate, minLogLevel, _systemWrapper); - // - // config.SamplingRate = samplingRate; - // - // if (samplingRate > 0) - // { - // double sample = _systemWrapper.GetRandom(); - // - // if (sample <= samplingRate) - // { - // _systemWrapper.LogLine( - // $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate}, Sampler Value: {sample}."); - // config.MinimumLevel = LogLevel.Debug; - // } - // } + private void ProcessSamplingRate(PowertoolsLoggerConfiguration config) + { + var samplingRate = config.SamplingRate > 0 + ? config.SamplingRate + : _powertoolsConfigurations.LoggerSampleRate; + + samplingRate = ValidateSamplingRate(samplingRate, config.MinimumLevel, _systemWrapper); + config.SamplingRate = samplingRate; + + // Only notify if sampling is configured + if (samplingRate > 0) + { + double sample = _systemWrapper.GetRandom(); + + // Instead of changing log level, just indicate sampling status + if (sample <= samplingRate) + { + _systemWrapper.LogLine( + $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate}, Sampler Value: {sample}."); + config.MinimumLevel = LogLevel.Debug; + + // Store sampling decision in config without changing log level + config.SamplingEnabled = true; + } + } } private static double ValidateSamplingRate(double samplingRate, LogLevel minLogLevel, ISystemWrapper systemWrapper) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index bf875281..f733608a 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -79,7 +79,7 @@ public LoggingAspect(IPowertoolsConfigurations powertoolsConfigurations) private void InitializeLogger(LoggingAttribute trigger) { - // Always configure when we have explicit trigger settings + // Always check for explicit settings bool hasExplicitSettings = (trigger.LogLevel != LogLevel.None || !string.IsNullOrEmpty(trigger.Service) || trigger.LoggerOutputCase != default || @@ -89,21 +89,24 @@ private void InitializeLogger(LoggingAttribute trigger) if (!Logger.IsConfigured || hasExplicitSettings) { // Create configuration with default values when not explicitly specified - Logger.Configure(new PowertoolsLoggerConfiguration + var config = new PowertoolsLoggerConfiguration { // Use sensible defaults if not specified in the attribute MinimumLevel = trigger.LogLevel != LogLevel.None ? trigger.LogLevel : LogLevel.Information, Service = !string.IsNullOrEmpty(trigger.Service) ? trigger.Service : "service_undefined", LoggerOutputCase = trigger.LoggerOutputCase != default ? trigger.LoggerOutputCase : LoggerOutputCase.SnakeCase, SamplingRate = trigger.SamplingRate > 0 ? trigger.SamplingRate : 1.0 - }); + }; + + // Configure the logger with our configuration + Logger.Configure(config); } // Get logger after configuration _logger = Logger.GetLogger(); - // TODO: Fix this - // _isDebug = config.MinimumLevel <= LogLevel.Debug; + // Set debug flag based on the minimum level from Logger + _isDebug = Logger.GetConfiguration().MinimumLevel <= LogLevel.Debug; } /// @@ -150,8 +153,6 @@ public void OnEntry( if (!_initializeContext) return; - - _isDebug = LogLevel.Debug >= trigger.LogLevel; _logger.AppendKey(LoggingConstants.KeyColdStart, _isColdStart); @@ -210,7 +211,7 @@ private void CaptureLambdaContext(AspectEventArgs eventArgs) { _clearLambdaContext = LoggingLambdaContext.Extract(eventArgs); if (LoggingLambdaContext.Instance is null && _isDebug) - Debug.WriteLine( + _logger.LogDebug( "Skipping Lambda Context injection because ILambdaContext context parameter not found."); } @@ -232,7 +233,7 @@ private void CaptureCorrelationId(object eventArg, string correlationIdPath) if (eventArg is null) { if (_isDebug) - Debug.WriteLine( + _logger.LogDebug( "Skipping CorrelationId capture because event parameter not found."); return; } @@ -268,7 +269,7 @@ private void CaptureCorrelationId(object eventArg, string correlationIdPath) catch (Exception e) { if (_isDebug) - Debug.WriteLine( + _logger.LogDebug( $"Skipping CorrelationId capture because of error caused while parsing the event object {e.Message}."); } } @@ -284,7 +285,7 @@ private void LogEvent(object eventArg) case null: { if (_isDebug) - Debug.WriteLine( + _logger.LogDebug( "Skipping Event Log because event parameter not found."); break; } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs index 9059feaf..0f13d9a2 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs @@ -31,49 +31,21 @@ public static partial class Logger private static Lazy _factoryLazy; private static Lazy _defaultLoggerLazy; - // Static constructor to ensure initialization - // static Logger() - // { - // // Create default configuration with sensible defaults - // var defaultConfig = new PowertoolsLoggerConfiguration - // { - // MinimumLevel = LogLevel.Information, // Default to Information level - // Service = "LambdaFunction", // Default service name - // LoggerOutputCase = LoggerOutputCase.SnakeCase, // Default case - // SamplingRate = 1.0 // Default to log everything - // }; - // - // // Initialize with default factory - // _factoryLazy = new Lazy(() => - // LoggerFactory.Create(builder => - // builder.AddPowertoolsLogger(config => - // { - // config.MinimumLevel = defaultConfig.MinimumLevel; - // config.Service = defaultConfig.Service; - // config.LoggerOutputCase = defaultConfig.LoggerOutputCase; - // config.SamplingRate = defaultConfig.SamplingRate; - // })), - // LazyThreadSafetyMode.ExecutionAndPublication); - // - // _defaultLoggerLazy = new Lazy(() => - // _factoryLazy.Value.CreateLogger("PowertoolsLogger")); - // - // // Not yet explicitly configured - // _isConfigured = false; - // } - - // Flag to track if custom configuration has been applied - private static bool _isConfigured; + // Add a backing field + private static bool _isConfigured = false; // Properties to access the lazy-initialized instances private static ILoggerFactory Factory => _factoryLazy.Value; private static ILogger LoggerInstance => _defaultLoggerLazy.Value; /// - /// Indicates whether the Logger has been configured with custom settings + /// Gets a value indicating whether the logger is configured. /// + /// true if the logger is configured; otherwise, false. public static bool IsConfigured => _isConfigured; + // Add this field to the Logger class + private static PowertoolsLoggerConfiguration _currentConfig; // Allow manual configuration using options public static void Configure(Action configureOptions) @@ -100,17 +72,27 @@ internal static void Configure(PowertoolsLoggerConfiguration options) { if (options == null) throw new ArgumentNullException(nameof(options)); - // Create a factory with our provider + // Store the configuration + _currentConfig = options.Clone(); + + // Create a factory with our provider - CRITICAL PART var factory = LoggerFactory.Create(builder => { - // Use AddPowertoolsLogger but with fromLoggerConfigure=true to prevent recursion + // IMPORTANT - Set the minimum level directly on the logging builder! + // This ensures it's respected by the logging infrastructure + if (_currentConfig.MinimumLevel != LogLevel.None) + { + builder.SetMinimumLevel(_currentConfig.MinimumLevel); + } + + // Add PowertoolsLogger with the same configuration builder.AddPowertoolsLogger(config => { - config.Service = options.Service; - config.MinimumLevel = options.MinimumLevel; - config.LoggerOutputCase = options.LoggerOutputCase; - config.SamplingRate = options.SamplingRate; - // Copy other properties as needed + // Transfer all settings + config.Service = _currentConfig.Service; + config.MinimumLevel = _currentConfig.MinimumLevel; + config.LoggerOutputCase = _currentConfig.LoggerOutputCase; + config.SamplingRate = _currentConfig.SamplingRate; }, true); }); @@ -124,6 +106,21 @@ internal static void Configure(PowertoolsLoggerConfiguration options) _isConfigured = true; } + // Add this method to the Logger class + // Get the current configuration + public static PowertoolsLoggerConfiguration GetConfiguration() + { + // Ensure logger is initialized + _ = LoggerInstance; + + // Create a new configuration with current settings + if (_currentConfig == null) + { + _currentConfig = new PowertoolsLoggerConfiguration(); + } + + return _currentConfig; + } // Get a logger for a specific category public static ILogger GetLogger() => GetLogger(typeof(T).Name); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs index b1d8dd84..4643fe06 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs @@ -13,54 +13,281 @@ * permissions and limitations under the License. */ +using System; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging.Serializers; namespace AWS.Lambda.Powertools.Logging; /// -/// Class LoggerConfiguration. -/// Implements the -/// +/// Class PowertoolsLoggerConfiguration. +/// Implements the /// -/// public class PowertoolsLoggerConfiguration : IOptions { + public const string ConfigurationSectionName = "PowertoolsLogger"; + /// /// Service name is used for logging. /// This can be also set using the environment variable POWERTOOLS_SERVICE_NAME. /// - /// The service. public string? Service { get; set; } = null; /// /// Specify the minimum log level for logging (Information, by default). /// This can be also set using the environment variable POWERTOOLS_LOG_LEVEL. /// - /// The minimum level. public LogLevel MinimumLevel { get; set; } = LogLevel.None; /// /// 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; set; } /// - /// The default configured options instance + /// Whether sampling was enabled for the current request /// - /// The value. - PowertoolsLoggerConfiguration IOptions.Value => this; + public bool SamplingEnabled { get; set; } /// /// The logger output case. /// This can be also set using the environment variable POWERTOOLS_LOGGER_CASE. /// - /// The logger output case. public LoggerOutputCase LoggerOutputCase { get; set; } = LoggerOutputCase.Default; - internal string LogLevelKey { get; set; } - - + /// + /// Internal key used for log level in output + /// + internal string LogLevelKey { get; set; } = "level"; + + /// + /// JSON serializer options to use for log serialization + /// + private JsonSerializerOptions? _jsonOptions; + public JsonSerializerOptions? JsonOptions + { + get => _jsonOptions; + set + { + _jsonOptions = value; + if (_jsonOptions != null) + { +#if NET8_0_OR_GREATER + HandleJsonOptionsTypeResolver(_jsonOptions); +#endif + ApplyJsonOptions(); + } + } + } + +#if NET8_0_OR_GREATER + /// + /// Default JSON serializer context + /// + private JsonSerializerContext? _jsonContext = PowertoolsLoggingSerializationContext.Default; + private readonly List _additionalContexts = new(); + + /// + /// Main JSON context to use for serialization + /// + public JsonSerializerContext? JsonContext + { + get => _jsonContext; + set + { + _jsonContext = value; + ApplyJsonContext(); + } + } + + /// + /// Add additional JsonSerializerContext for client types + /// + public void AddJsonContext(JsonSerializerContext context) + { + if (context != null && !_additionalContexts.Contains(context)) + { + _additionalContexts.Add(context); + ApplyAdditionalJsonContext(context); + } + } + + /// + /// Get all additional contexts + /// + public IReadOnlyList GetAdditionalContexts() + { + return _additionalContexts.AsReadOnly(); + } + + private IJsonTypeInfoResolver? _customTypeInfoResolver = null; + private List? _customTypeInfoResolvers; + + /// + /// Process JSON options type resolver information + /// + public void HandleJsonOptionsTypeResolver(JsonSerializerOptions options) + { + if (options == null) return; + + // Check for TypeInfoResolver + if (options.TypeInfoResolver != null && + options.TypeInfoResolver != GetCompositeResolver()) + { + _customTypeInfoResolver = options.TypeInfoResolver; + } + + // Check for TypeInfoResolverChain + if (options.TypeInfoResolverChain != null && options.TypeInfoResolverChain.Count > 0) + { + foreach (var resolver in options.TypeInfoResolverChain) + { + // If it's a JsonSerializerContext, add it to our additional contexts + if (resolver is JsonSerializerContext context) + { + AddJsonContext(context); + } + // Otherwise store it as a custom resolver + else if (resolver is IJsonTypeInfoResolver customResolver && + customResolver != GetCompositeResolver() && + _customTypeInfoResolver != customResolver) + { + // If we already have a different custom resolver, we need to store multiple + if (_customTypeInfoResolver != null) + { + _customTypeInfoResolvers ??= new List(); + if (!_customTypeInfoResolvers.Contains(customResolver)) + { + _customTypeInfoResolvers.Add(customResolver); + } + } + else + { + _customTypeInfoResolver = customResolver; + } + } + } + } + } + + /// + /// Get a composite resolver that includes all configured resolvers + /// + public IJsonTypeInfoResolver GetCompositeResolver() + { + var resolvers = new List(); + + // Add custom resolver if provided + if (_customTypeInfoResolver != null) + { + resolvers.Add(_customTypeInfoResolver); + } + + // Add additional custom resolvers if any + if (_customTypeInfoResolvers != null) + { + foreach (var resolver in _customTypeInfoResolvers) + { + resolvers.Add(resolver); + } + } + + // Add default context + if (_jsonContext != null) + { + resolvers.Add(_jsonContext); + } + + // Add additional contexts + foreach (var context in _additionalContexts) + { + resolvers.Add(context); + } + + return new CompositeJsonTypeInfoResolver(resolvers.ToArray()); + } + + /// + /// Apply JSON context to serializer + /// + private void ApplyJsonContext() + { + if (_jsonContext != null) + { + PowertoolsLoggingSerializer.SetDefaultContext(_jsonContext); + } + } + + /// + /// Apply additional JSON context to serializer + /// + private void ApplyAdditionalJsonContext(JsonSerializerContext context) + { + PowertoolsLoggingSerializer.AddSerializerContext(context); + } +#endif + + /// + /// Apply JSON options to the serializer + /// + private void ApplyJsonOptions() + { + if (_jsonOptions != null) + { + PowertoolsLoggingSerializer.ConfigureJsonOptions(_jsonOptions); + } + } + + /// + /// Apply output case configuration + /// + public void ApplyOutputCase() + { + PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase); + } + + /// + /// Clone this configuration + /// + public PowertoolsLoggerConfiguration Clone() + { + var clone = new PowertoolsLoggerConfiguration + { + Service = Service, + MinimumLevel = MinimumLevel, + SamplingRate = SamplingRate, + SamplingEnabled = SamplingEnabled, // Copy the sampling decision + LoggerOutputCase = LoggerOutputCase, + LogLevelKey = LogLevelKey, + JsonOptions = JsonOptions + }; + +#if NET8_0_OR_GREATER + clone._jsonContext = _jsonContext; + foreach (var context in _additionalContexts) + { + clone._additionalContexts.Add(context); + } + + clone._customTypeInfoResolver = _customTypeInfoResolver; + + if (_customTypeInfoResolvers != null) + { + clone._customTypeInfoResolvers = new List(_customTypeInfoResolvers); + } +#endif + + return clone; + } + + // IOptions implementation + PowertoolsLoggerConfiguration IOptions.Value => this; } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/CompositeJsonTypeInfoResolver.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/CompositeJsonTypeInfoResolver.cs new file mode 100644 index 00000000..c665f960 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/CompositeJsonTypeInfoResolver.cs @@ -0,0 +1,59 @@ +/* + * 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. + */ + +#if NET8_0_OR_GREATER + +using System; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; + +namespace AWS.Lambda.Powertools.Logging.Serializers +{ + /// + /// Combines multiple IJsonTypeInfoResolver instances into one + /// + internal class CompositeJsonTypeInfoResolver : IJsonTypeInfoResolver + { + private readonly IJsonTypeInfoResolver[] _resolvers; + + /// + /// Creates a new composite resolver from multiple resolvers + /// + /// Array of resolvers to use + public CompositeJsonTypeInfoResolver(IJsonTypeInfoResolver[] resolvers) + { + _resolvers = resolvers ?? throw new ArgumentNullException(nameof(resolvers)); + } + + + /// + /// Gets JSON type info by trying each resolver in order (.NET Standard 2.0 version) + /// + public JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) + { + foreach (var resolver in _resolvers) + { + var typeInfo = resolver?.GetTypeInfo(type, options); + if (typeInfo != null) + { + return typeInfo; + } + } + + return null; + } + } +} +#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 index 97aabc06..ef54d3a5 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -36,6 +36,7 @@ internal static class PowertoolsLoggingSerializer { private static LoggerOutputCase _currentOutputCase; private static JsonSerializerOptions _jsonOptions; + private static readonly object _lock = new object(); private static readonly ConcurrentBag AdditionalContexts = new ConcurrentBag(); @@ -45,7 +46,19 @@ internal static class PowertoolsLoggingSerializer /// internal static JsonSerializerOptions GetSerializerOptions() { - return _jsonOptions ?? BuildJsonSerializerOptions(); + // Double-checked locking pattern for thread safety while ensuring we only build once + if (_jsonOptions == null) + { + lock (_lock) + { + if (_jsonOptions == null) + { + BuildJsonSerializerOptions(); + } + } + } + + return _jsonOptions; } /// @@ -54,7 +67,19 @@ internal static JsonSerializerOptions GetSerializerOptions() /// The case to use for serialization. internal static void ConfigureNamingPolicy(LoggerOutputCase loggerOutputCase) { - _currentOutputCase = loggerOutputCase; + if (_currentOutputCase != loggerOutputCase) + { + lock (_lock) + { + _currentOutputCase = loggerOutputCase; + + // Only rebuild options if they already exist + if (_jsonOptions != null) + { + BuildJsonSerializerOptions(); + } + } + } } /// @@ -120,8 +145,9 @@ internal static JsonTypeInfo GetTypeInfo(Type type) /// Builds and configures the JsonSerializerOptions. /// /// A configured JsonSerializerOptions instance. - private static JsonSerializerOptions BuildJsonSerializerOptions() + private static void BuildJsonSerializerOptions() { + // This should already be in a lock when called _jsonOptions = new JsonSerializerOptions(); switch (_currentOutputCase) @@ -173,7 +199,6 @@ private static JsonSerializerOptions BuildJsonSerializerOptions() } } #endif - return _jsonOptions; } #if NET8_0_OR_GREATER @@ -195,4 +220,54 @@ internal static void ClearOptions() { _jsonOptions = null; } + + /// + /// Sets the default JSON context to use + /// + internal static void SetDefaultContext(JsonSerializerContext context) + { + lock (_lock) + { + // Reset options to ensure they're rebuilt with the new context + _jsonOptions = null; + } + } + + /// + /// Configure the serializer with specific JSON options + /// + internal static void ConfigureJsonOptions(JsonSerializerOptions options) + { + if (options == null) return; + + lock (_lock) + { + _jsonOptions = options; + + // Add required converters if they're not already present + var converters = new[] + { + typeof(ByteArrayConverter), + typeof(ExceptionConverter), + typeof(MemoryStreamConverter), + typeof(ConstantClassConverter), + typeof(DateOnlyConverter), + typeof(TimeOnlyConverter), + typeof(LogLevelJsonConverter), + }; + + foreach (var converterType in converters) + { + if (!_jsonOptions.Converters.Any(c => c.GetType() == converterType)) + { + // Add the converter through reflection to avoid direct instantiation + var converter = Activator.CreateInstance(converterType) as JsonConverter; + if (converter != null) + { + _jsonOptions.Converters.Add(converter); + } + } + } + } + } } \ No newline at end of file diff --git a/libraries/tests/Directory.Packages.props b/libraries/tests/Directory.Packages.props index 516a0e93..c6868210 100644 --- a/libraries/tests/Directory.Packages.props +++ b/libraries/tests/Directory.Packages.props @@ -4,7 +4,7 @@ - + @@ -13,13 +13,13 @@ - + - + \ No newline at end of file From a494c2b955d12f4b4a8416bced9f73f976ff7666 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 18 Mar 2025 13:56:05 +0000 Subject: [PATCH 04/49] refactor(logging): enhance IsEnabled method for improved log level handling --- .../Internal/PowertoolsLogger.cs | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs index 3c6ae09a..3e9100d7 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -56,7 +56,7 @@ internal sealed class PowertoolsLogger : ILogger /// Private constructor - Is initialized on CreateLogger /// /// The name. - /// The Powertools for AWS Lambda (.NET) configurations. + /// /// The system wrapper. public PowertoolsLogger( string categoryName, @@ -97,7 +97,22 @@ internal void EndScope() /// The log level. /// bool. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None && logLevel >= _currentConfig.MinimumLevel; + public bool IsEnabled(LogLevel logLevel) + { + // If we have no explicit minimum level, use the default + var effectiveMinLevel = _currentConfig.MinimumLevel != LogLevel.None + ? _currentConfig.MinimumLevel + : LoggingConstants.DefaultLogLevel; + + // Log diagnostic info for Debug/Trace levels + if (logLevel <= LogLevel.Debug) + { + return logLevel >= effectiveMinLevel; + } + + // Standard check + return logLevel >= effectiveMinLevel; + } /// /// Writes a log entry. @@ -111,11 +126,13 @@ internal void EndScope() public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { - if (formatter is null) - throw new ArgumentNullException(nameof(formatter)); - if (!IsEnabled(logLevel)) + { return; + } + + if (formatter is null) + throw new ArgumentNullException(nameof(formatter)); var timestamp = DateTime.UtcNow; var message = CustomFormatter(state, exception, out var customMessage) && customMessage is not null From 8c0386664d6b9a8e7414255e6477deeb51188fa8 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:55:26 +0000 Subject: [PATCH 05/49] feat(logging): introduce custom logger output and enhance configuration options --- .../Core/IPowertoolsEnvironment.cs | 6 ++ .../Core/ISystemWrapper.cs | 31 ------- .../Core/PowertoolsConfigurations.cs | 25 +++--- .../Core/PowertoolsEnvironment.cs | 48 ++++++++++ .../Core/SystemWrapper.cs | 89 +------------------ .../Core/TestLoggerOutput.cs | 51 +++++++++++ .../BuilderExtensions.cs | 2 +- .../Internal/LoggerProvider.cs | 26 ++---- .../AWS.Lambda.Powertools.Logging/Logger.cs | 25 ++++-- .../PowertoolsLoggerConfiguration.cs | 7 ++ 10 files changed, 152 insertions(+), 158 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.Common/Core/TestLoggerOutput.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsEnvironment.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsEnvironment.cs index 059cfb7e..6f57aabb 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsEnvironment.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsEnvironment.cs @@ -34,4 +34,10 @@ public interface IPowertoolsEnvironment /// /// Assembly Version in the Major.Minor.Build format string GetAssemblyVersion(T type); + + /// + /// Sets the execution Environment Variable (AWS_EXECUTION_ENV) + /// + /// + void SetExecutionEnvironment(T type); } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/ISystemWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/ISystemWrapper.cs index 4e7ad4f0..745118d7 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/ISystemWrapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/ISystemWrapper.cs @@ -22,13 +22,6 @@ namespace AWS.Lambda.Powertools.Common; /// public interface ISystemWrapper { - /// - /// Gets the environment variable. - /// - /// The variable. - /// System.String. - string GetEnvironmentVariable(string variable); - /// /// Logs the specified value. /// @@ -47,19 +40,6 @@ public interface ISystemWrapper /// System.Double. double GetRandom(); - /// - /// Sets the environment variable. - /// - /// The variable. - /// - void SetEnvironmentVariable(string variable, string value); - - /// - /// Sets the execution Environment Variable (AWS_EXECUTION_ENV) - /// - /// - void SetExecutionEnvironment(T type); - /// /// Sets console output /// Useful for testing and checking the console output @@ -70,15 +50,4 @@ public interface ISystemWrapper /// /// The TextWriter instance where to write to void SetOut(TextWriter writeTo); - - /// - /// Sets console error output - /// Useful for testing and checking the console error output - /// - /// var consoleError = new StringWriter(); - /// SystemWrapper.Instance.SetError(consoleError); - /// - /// - /// - string GetLogOutput(); } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs index 3933972d..2fce2191 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs @@ -24,6 +24,8 @@ namespace AWS.Lambda.Powertools.Common; /// public class PowertoolsConfigurations : IPowertoolsConfigurations { + private readonly IPowertoolsEnvironment _powertoolsEnvironment; + /// /// The maximum dimensions /// @@ -39,18 +41,13 @@ public class PowertoolsConfigurations : IPowertoolsConfigurations /// private static IPowertoolsConfigurations _instance; - /// - /// The system wrapper - /// - private readonly ISystemWrapper _systemWrapper; - /// /// Initializes a new instance of the class. /// /// The system wrapper. - internal PowertoolsConfigurations(ISystemWrapper systemWrapper) + internal PowertoolsConfigurations(IPowertoolsEnvironment powertoolsEnvironment) { - _systemWrapper = systemWrapper; + _powertoolsEnvironment = powertoolsEnvironment; } /// @@ -58,7 +55,7 @@ internal PowertoolsConfigurations(ISystemWrapper systemWrapper) /// /// The instance. public static IPowertoolsConfigurations Instance => - _instance ??= new PowertoolsConfigurations(SystemWrapper.Instance); + _instance ??= new PowertoolsConfigurations(PowertoolsEnvironment.Instance); /// /// Gets the environment variable. @@ -67,7 +64,7 @@ internal PowertoolsConfigurations(ISystemWrapper systemWrapper) /// System.String. public string GetEnvironmentVariable(string variable) { - return _systemWrapper.GetEnvironmentVariable(variable); + return _powertoolsEnvironment.GetEnvironmentVariable(variable); } /// @@ -78,7 +75,7 @@ public string GetEnvironmentVariable(string variable) /// System.String. public string GetEnvironmentVariableOrDefault(string variable, string defaultValue) { - var result = _systemWrapper.GetEnvironmentVariable(variable); + var result = _powertoolsEnvironment.GetEnvironmentVariable(variable); return string.IsNullOrWhiteSpace(result) ? defaultValue : result; } @@ -90,7 +87,7 @@ public string GetEnvironmentVariableOrDefault(string variable, string defaultVal /// System.Int32. public int GetEnvironmentVariableOrDefault(string variable, int defaultValue) { - var result = _systemWrapper.GetEnvironmentVariable(variable); + var result = _powertoolsEnvironment.GetEnvironmentVariable(variable); return int.TryParse(result, out var parsedValue) ? parsedValue : defaultValue; } @@ -102,7 +99,7 @@ public int GetEnvironmentVariableOrDefault(string variable, int defaultValue) /// true if XXXX, false otherwise. public bool GetEnvironmentVariableOrDefault(string variable, bool defaultValue) { - return bool.TryParse(_systemWrapper.GetEnvironmentVariable(variable), out var result) + return bool.TryParse(_powertoolsEnvironment.GetEnvironmentVariable(variable), out var result) ? result : defaultValue; } @@ -160,7 +157,7 @@ public bool GetEnvironmentVariableOrDefault(string variable, bool defaultValue) /// /// The logger sample rate. public double LoggerSampleRate => - double.TryParse(_systemWrapper.GetEnvironmentVariable(Constants.LoggerSampleRateNameEnv), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var result) + double.TryParse(_powertoolsEnvironment.GetEnvironmentVariable(Constants.LoggerSampleRateNameEnv), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var result) ? result : 0; @@ -201,7 +198,7 @@ public bool GetEnvironmentVariableOrDefault(string variable, bool defaultValue) /// public void SetExecutionEnvironment(T type) { - _systemWrapper.SetExecutionEnvironment(type); + _powertoolsEnvironment.SetExecutionEnvironment(type); } /// diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsEnvironment.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsEnvironment.cs index 3ad5317c..7abb379a 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsEnvironment.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsEnvironment.cs @@ -1,4 +1,5 @@ using System; +using System.Text; namespace AWS.Lambda.Powertools.Common; @@ -40,4 +41,51 @@ public string GetAssemblyVersion(T type) var version = type.GetType().Assembly.GetName().Version; return version != null ? $"{version.Major}.{version.Minor}.{version.Build}" : string.Empty; } + + public void SetExecutionEnvironment(T type) + { + const string envName = Constants.AwsExecutionEnvironmentVariableName; + var envValue = new StringBuilder(); + var currentEnvValue = GetEnvironmentVariable(envName); + var assemblyName = ParseAssemblyName(GetAssemblyName(type)); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if (!string.IsNullOrEmpty(currentEnvValue)) + { + // Avoid duplication - should not happen since the calling Instances are Singletons - defensive purposes + if (currentEnvValue.Contains(assemblyName)) + { + return; + } + + envValue.Append($"{currentEnvValue} "); + } + + var assemblyVersion = GetAssemblyVersion(type); + + envValue.Append($"{assemblyName}/{assemblyVersion}"); + + SetEnvironmentVariable(envName, envValue.ToString()); + } + + /// + /// Parsing the name to conform with the required naming convention for the UserAgent header (PTFeature/Name/Version) + /// Fallback to Assembly Name on exception + /// + /// + /// + private string ParseAssemblyName(string assemblyName) + { + try + { + var parsedName = assemblyName.Substring(assemblyName.LastIndexOf(".", StringComparison.Ordinal) + 1); + return $"{Constants.FeatureContextIdentifier}/{parsedName}"; + } + catch + { + //NOOP + } + + return $"{Constants.FeatureContextIdentifier}/{assemblyName}"; + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs index d972955b..cc7da82d 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs @@ -15,7 +15,6 @@ using System; using System.IO; -using System.Text; namespace AWS.Lambda.Powertools.Common; @@ -24,19 +23,13 @@ namespace AWS.Lambda.Powertools.Common; /// Implements the /// /// -public class SystemWrapper : ISystemWrapper +internal class SystemWrapper : ISystemWrapper { - private static IPowertoolsEnvironment _powertoolsEnvironment; - private static ISystemWrapper _instance; - /// /// Prevents a default instance of the class from being created. /// - public SystemWrapper(IPowertoolsEnvironment powertoolsEnvironment) + public SystemWrapper() { - _powertoolsEnvironment = powertoolsEnvironment; - _instance ??= this; - // Clear AWS SDK Console injected parameters StdOut and StdErr var standardOutput = new StreamWriter(Console.OpenStandardOutput()); standardOutput.AutoFlush = true; @@ -46,19 +39,6 @@ public SystemWrapper(IPowertoolsEnvironment powertoolsEnvironment) Console.SetError(errordOutput); } - /// - /// Gets the instance. - /// - /// The instance. - public static ISystemWrapper Instance => _instance ??= new SystemWrapper(PowertoolsEnvironment.Instance); - - - /// - public string GetEnvironmentVariable(string variable) - { - return _powertoolsEnvironment.GetEnvironmentVariable(variable); - } - /// public void Log(string value) { @@ -77,74 +57,9 @@ public double GetRandom() return new Random().NextDouble(); } - /// - public void SetEnvironmentVariable(string variable, string value) - { - _powertoolsEnvironment.SetEnvironmentVariable(variable, value); - } - - /// - public void SetExecutionEnvironment(T type) - { - const string envName = Constants.AwsExecutionEnvironmentVariableName; - var envValue = new StringBuilder(); - var currentEnvValue = GetEnvironmentVariable(envName); - var assemblyName = ParseAssemblyName(_powertoolsEnvironment.GetAssemblyName(type)); - - // If there is an existing execution environment variable add the annotations package as a suffix. - if (!string.IsNullOrEmpty(currentEnvValue)) - { - // Avoid duplication - should not happen since the calling Instances are Singletons - defensive purposes - if (currentEnvValue.Contains(assemblyName)) - { - return; - } - - envValue.Append($"{currentEnvValue} "); - } - - var assemblyVersion = _powertoolsEnvironment.GetAssemblyVersion(type); - - envValue.Append($"{assemblyName}/{assemblyVersion}"); - - SetEnvironmentVariable(envName, envValue.ToString()); - } - /// public void SetOut(TextWriter writeTo) { Console.SetOut(writeTo); } - - /// - /// Parsing the name to conform with the required naming convention for the UserAgent header (PTFeature/Name/Version) - /// Fallback to Assembly Name on exception - /// - /// - /// - private string ParseAssemblyName(string assemblyName) - { - try - { - var parsedName = assemblyName.Substring(assemblyName.LastIndexOf(".", StringComparison.Ordinal) + 1); - return $"{Constants.FeatureContextIdentifier}/{parsedName}"; - } - catch - { - //NOOP - } - - return $"{Constants.FeatureContextIdentifier}/{assemblyName}"; - } - - /// - public string GetLogOutput() - { - if (Console.Out is StringWriter sw) - { - return sw.ToString(); - } - - return "Console.Out is not a StringWriter - no captured output available"; - } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/TestLoggerOutput.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/TestLoggerOutput.cs new file mode 100644 index 00000000..5a70b529 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/TestLoggerOutput.cs @@ -0,0 +1,51 @@ +using System; +using System.IO; +using System.Text; + +namespace AWS.Lambda.Powertools.Common.Tests; + +/// +/// Test logger output +/// +public class TestLoggerOutput : ISystemWrapper +{ + /// + /// Buffer for all the log messages written to the logger. + /// + public StringBuilder Buffer { get; } = new StringBuilder(); + + /// + /// Logs the specified value. + /// + /// + public void Log(string value) + { + Buffer.Append(value); + Console.Write(value); + } + + /// + /// Logs the line. + /// + public void LogLine(string value) + { + Buffer.AppendLine(value); + Console.WriteLine(value); + } + + /// + /// Gets random number + /// + public double GetRandom() + { + return 0.7; + } + + /// + /// Sets console output + /// + public void SetOut(TextWriter writeTo) + { + Console.SetOut(writeTo); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs index e8d7a669..923d432a 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs @@ -80,7 +80,7 @@ private static void RegisterServices(ILoggingBuilder builder) // Register IPowertoolsConfigurations with all its dependencies builder.Services.TryAddSingleton(sp => - new PowertoolsConfigurations(sp.GetRequiredService())); + new PowertoolsConfigurations(sp.GetRequiredService())); // Register the provider builder.Services.TryAddEnumerable( diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs index f241cb47..ffddbfc4 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs @@ -56,25 +56,17 @@ internal sealed class LoggerProvider : ILoggerProvider /// public LoggerProvider(IOptionsMonitor config, IPowertoolsConfigurations powertoolsConfigurations, - ISystemWrapper systemWrapper) + ISystemWrapper? systemWrapper = null) { - _currentConfig = config.CurrentValue; + // Use custom system wrapper if provided through config + var currentConfig = config.CurrentValue; + _systemWrapper = currentConfig.LoggerOutput ?? systemWrapper ?? new SystemWrapper(); + _powertoolsConfigurations = powertoolsConfigurations; - _systemWrapper = systemWrapper; - _onChangeToken = config.OnChange(updatedConfig => _currentConfig = updatedConfig); + _currentConfig = currentConfig; - // TODO: FIx this - // It was moved bellow - // _powertoolsConfigurations.SetCurrentConfig(_currentConfig, systemWrapper); - } - - /// - /// Initializes a new instance of the class. - /// - /// The configuration. - public LoggerProvider(IOptionsMonitor config) - : this(config, PowertoolsConfigurations.Instance, SystemWrapper.Instance) - { + _onChangeToken = config.OnChange(updatedConfig => _currentConfig = updatedConfig); + ApplyPowertoolsConfig(_currentConfig); } /// @@ -85,7 +77,7 @@ public LoggerProvider(IOptionsMonitor config) public ILogger CreateLogger(string categoryName) { return _loggers.GetOrAdd(categoryName, name => new PowertoolsLogger(name, - GetCurrentConfig, + () => _currentConfig, _systemWrapper)); } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs index 0f13d9a2..457d87d5 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs @@ -15,6 +15,7 @@ using System; using System.Threading; +using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -74,25 +75,33 @@ internal static void Configure(PowertoolsLoggerConfiguration options) // Store the configuration _currentConfig = options.Clone(); + + // Create a system wrapper if needed + var systemWrapper = options.LoggerOutput ?? new SystemWrapper(); - // Create a factory with our provider - CRITICAL PART + // Create a factory with our provider var factory = LoggerFactory.Create(builder => { - // IMPORTANT - Set the minimum level directly on the logging builder! - // This ensures it's respected by the logging infrastructure - if (_currentConfig.MinimumLevel != LogLevel.None) + // Set minimum level directly on builder + if (options.MinimumLevel != LogLevel.None) { - builder.SetMinimumLevel(_currentConfig.MinimumLevel); + builder.SetMinimumLevel(options.MinimumLevel); } - - // Add PowertoolsLogger with the same configuration + + // Add our provider - the config's OutputLogger will be used + builder.Services.AddSingleton(systemWrapper); + builder.AddPowertoolsLogger(config => { - // Transfer all settings config.Service = _currentConfig.Service; config.MinimumLevel = _currentConfig.MinimumLevel; config.LoggerOutputCase = _currentConfig.LoggerOutputCase; config.SamplingRate = _currentConfig.SamplingRate; + config.LoggerOutput = _currentConfig.LoggerOutput; + config.JsonOptions = _currentConfig.JsonOptions; +#if NET8_0_OR_GREATER + config.JsonContext = _currentConfig.JsonContext; +#endif }, true); }); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs index 4643fe06..de6543e1 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs @@ -68,6 +68,12 @@ public class PowertoolsLoggerConfiguration : IOptions internal string LogLevelKey { get; set; } = "level"; + /// + /// Custom output logger to use instead of Console + /// + public ISystemWrapper? LoggerOutput { get; set; } + + /// /// JSON serializer options to use for log serialization /// @@ -266,6 +272,7 @@ public PowertoolsLoggerConfiguration Clone() SamplingRate = SamplingRate, SamplingEnabled = SamplingEnabled, // Copy the sampling decision LoggerOutputCase = LoggerOutputCase, + LoggerOutput = LoggerOutput, LogLevelKey = LogLevelKey, JsonOptions = JsonOptions }; From cfd2b289fad6c8bc4473c44ec6b8d909badb1d38 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 19 Mar 2025 11:30:16 +0000 Subject: [PATCH 06/49] add log formatting --- .../Internal/LoggerProvider.cs | 3 - .../Internal/PowertoolsLogger.cs | 197 +++++++++++++++++- .../PowertoolsLoggerConfiguration.cs | 6 - 3 files changed, 191 insertions(+), 15 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs index ffddbfc4..6b1ddcbb 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs @@ -159,9 +159,6 @@ private void ProcessSamplingRate(PowertoolsLoggerConfiguration config) _systemWrapper.LogLine( $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate}, Sampler Value: {sample}."); config.MinimumLevel = LogLevel.Debug; - - // Store sampling decision in config without changing log level - config.SamplingEnabled = true; } } } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs index 3e9100d7..8793113e 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -135,14 +135,34 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except throw new ArgumentNullException(nameof(formatter)); var timestamp = DateTime.UtcNow; + + // Extract structured logging parameters + var structuredParameters = ExtractStructuredParameters(state, out string messageTemplate); + + // Format the message using the provided formatter var message = CustomFormatter(state, exception, out var customMessage) && customMessage is not null ? customMessage : formatter(state, exception); + + // // For better object representation in messages + // if (message != null && message.ToString().Contains(".") && + // message.ToString().EndsWith("Class") && + // structuredParameters.Count > 0) + // { + // // This might be a ToString() of an object class - let's try to use a better representation + // var objName = message.ToString().Split('.').Last(); + // if (structuredParameters.Count == 1) + // { + // // If we have a single parameter, try to represent it nicely in the log + // var param = structuredParameters.First(); + // message = $"{param.Key}: {(param.Value != null ? "[object]" : "null")}"; + // } + // } var logFormatter = Logger.GetFormatter(); var logEntry = logFormatter is null - ? GetLogEntry(logLevel, timestamp, message, exception) - : GetFormattedLogEntry(logLevel, timestamp, message, exception, logFormatter); + ? GetLogEntry(logLevel, timestamp, message, exception, structuredParameters) + : GetFormattedLogEntry(logLevel, timestamp, message, exception, logFormatter, structuredParameters); _systemWrapper.LogLine(PowertoolsLoggingSerializer.Serialize(logEntry, typeof(object))); } @@ -155,7 +175,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except /// 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) + Exception exception, Dictionary structuredParameters = null) { var logEntry = new Dictionary(); @@ -181,6 +201,16 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times } } + // Add structured parameters + if (structuredParameters != null) + { + foreach (var (key, value) in structuredParameters) + { + if (!string.IsNullOrWhiteSpace(key)) + logEntry.TryAdd(key, value); + } + } + logEntry.TryAdd(LoggingConstants.KeyTimestamp, timestamp.ToString("o")); logEntry.TryAdd(_currentConfig.LogLevelKey, logLevel.ToString()); logEntry.TryAdd(LoggingConstants.KeyService, _currentConfig.Service); @@ -188,8 +218,12 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times logEntry.TryAdd(LoggingConstants.KeyMessage, message); if (_currentConfig.SamplingRate > 0) logEntry.TryAdd(LoggingConstants.KeySamplingRate, _currentConfig.SamplingRate); + + // Use the AddExceptionDetails method instead of adding exception directly if (exception != null) - logEntry.TryAdd(LoggingConstants.KeyException, exception); + { + AddExceptionDetails(logEntry, exception); + } return logEntry; } @@ -203,7 +237,7 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times /// The exception related to this entry. /// The custom log entry formatter. private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, object message, - Exception exception, ILogFormatter logFormatter) + Exception exception, ILogFormatter logFormatter, Dictionary structuredParameters) { if (logFormatter is null) return null; @@ -215,7 +249,7 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec Service = _currentConfig.Service, Name = _categoryName, Message = message, - Exception = exception, + Exception = exception, // Keep this to maintain compatibility SamplingRate = _currentConfig.SamplingRate, }; @@ -250,6 +284,29 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec extraKeys.TryAdd(key, value); } } + + // Add structured parameters + if (structuredParameters != null) + { + foreach (var (key, value) in structuredParameters) + { + if (!string.IsNullOrWhiteSpace(key)) + extraKeys.TryAdd(key, value); + } + } + + // Add detailed exception information + if (exception != null) + { + var exceptionDetails = new Dictionary(); + AddExceptionDetails(exceptionDetails, exception); + + // Add exception details to extra keys + foreach (var (key, value) in exceptionDetails) + { + extraKeys.TryAdd(key, value); + } + } if (extraKeys.Any()) logEntry.ExtraKeys = extraKeys; @@ -390,4 +447,132 @@ private static Dictionary GetScopeKeys(TState state) return keys; } + + /// + /// Extracts structured parameter key-value pairs from the log state + /// + private Dictionary ExtractStructuredParameters(TState state, out string messageTemplate) + { + messageTemplate = string.Empty; + var parameters = new Dictionary(); + + if (state is IEnumerable> stateProps) + { + // Dictionary to store format specifiers for each parameter + var formatSpecifiers = new Dictionary(); + + // First pass - extract template and identify format specifiers + foreach (var prop in stateProps) + { + // The original message template is stored with key "{OriginalFormat}" + if (prop.Key == "{OriginalFormat}" && prop.Value is string template) + { + messageTemplate = template; + + // Extract format specifiers from the template + var matches = System.Text.RegularExpressions.Regex.Matches( + template, + @"{([@\w]+)(?::([^{}]+))?}"); + + foreach (System.Text.RegularExpressions.Match match in matches) + { + string paramName = match.Groups[1].Value; + if (match.Groups.Count > 2 && match.Groups[2].Success) + { + formatSpecifiers[paramName] = match.Groups[2].Value; + } + } + + continue; + } + } + + // Second pass - process values with extracted format specifiers + foreach (var prop in stateProps) + { + if (prop.Key == "{OriginalFormat}") + continue; + + // Extract parameter name without braces + string paramName = ExtractParameterName(prop.Key); + if (string.IsNullOrEmpty(paramName)) + continue; + + // Handle special serialization designators (like @) + bool useStructuredSerialization = paramName.StartsWith("@"); + string actualParamName = useStructuredSerialization ? paramName.Substring(1) : paramName; + + // Apply formatting if a format specifier exists and the value can be formatted + if (!useStructuredSerialization && + formatSpecifiers.TryGetValue(paramName, out string format) && + prop.Value is IFormattable formattable) + { + // Format the value using the specified format + string formattedValue = formattable.ToString(format, System.Globalization.CultureInfo.InvariantCulture); + + // Try to preserve the numeric type if possible + if (double.TryParse(formattedValue, out double numericValue)) + { + parameters[actualParamName] = numericValue; + } + else + { + parameters[actualParamName] = formattedValue; + } + } + else if (useStructuredSerialization) + { + // Serialize the entire object + parameters[actualParamName] = prop.Value; + } + else + { + // For regular objects, use the value directly + parameters[actualParamName] = prop.Value; + } + } + } + + return parameters; + } + + /// + /// Extracts the parameter name from a template placeholder (e.g. "{paramName}" or "{paramName:format}") + /// + private string ExtractParameterName(string key) + { + // If it's already a proper parameter name without braces, return it + if (!key.StartsWith("{") || !key.EndsWith("}")) + return key; + + // Remove the braces + var nameWithPossibleFormat = key.Substring(1, key.Length - 2); + + // If there's a format specifier, remove it + var colonIndex = nameWithPossibleFormat.IndexOf(':'); + return colonIndex > 0 + ? nameWithPossibleFormat.Substring(0, colonIndex) + : nameWithPossibleFormat; + } + + private void AddExceptionDetails(Dictionary logEntry, Exception exception) + { + if (exception == null) + return; + + logEntry.TryAdd("errorType", exception.GetType().FullName); + logEntry.TryAdd("errorMessage", exception.Message); + + // Add stack trace as array of strings for better readability + var stackFrames = exception.StackTrace?.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + if (stackFrames?.Length > 0) + { + var cleanedStackTrace = new List + { + $"{exception.GetType().FullName}: {exception.Message}" + }; + cleanedStackTrace.AddRange(stackFrames); + logEntry.TryAdd("stackTrace", cleanedStackTrace); + } + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs index de6543e1..2185ec68 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs @@ -52,11 +52,6 @@ public class PowertoolsLoggerConfiguration : IOptions public double SamplingRate { get; set; } - /// - /// Whether sampling was enabled for the current request - /// - public bool SamplingEnabled { get; set; } - /// /// The logger output case. /// This can be also set using the environment variable POWERTOOLS_LOGGER_CASE. @@ -270,7 +265,6 @@ public PowertoolsLoggerConfiguration Clone() Service = Service, MinimumLevel = MinimumLevel, SamplingRate = SamplingRate, - SamplingEnabled = SamplingEnabled, // Copy the sampling decision LoggerOutputCase = LoggerOutputCase, LoggerOutput = LoggerOutput, LogLevelKey = LogLevelKey, From 70ba886ed3aa2fd3293a02617ef60e3dbc4d01cb Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 19 Mar 2025 13:01:11 +0000 Subject: [PATCH 07/49] combine context, remove json key --- .../Internal/PowertoolsLogger.cs | 85 ++++++++++++------- .../Logger.Scope.cs | 9 ++ .../LoggerExtensions.cs | 59 ++++++++++++- .../PowertoolsLoggerConfiguration.cs | 29 ++++++- .../PowertoolsLoggingSerializer.cs | 62 ++++++++++++-- 5 files changed, 203 insertions(+), 41 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs index 8793113e..1657d8b5 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -136,34 +136,37 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except var timestamp = DateTime.UtcNow; - // Extract structured logging parameters - var structuredParameters = ExtractStructuredParameters(state, out string messageTemplate); + // Check if state is a direct object (not structured logging) + bool isDirectObjectLog = state != null && + !(state is IEnumerable>) && + !(state is string); - // Format the message using the provided formatter - var message = CustomFormatter(state, exception, out var customMessage) && customMessage is not null - ? customMessage - : formatter(state, exception); - - // // For better object representation in messages - // if (message != null && message.ToString().Contains(".") && - // message.ToString().EndsWith("Class") && - // structuredParameters.Count > 0) - // { - // // This might be a ToString() of an object class - let's try to use a better representation - // var objName = message.ToString().Split('.').Last(); - // if (structuredParameters.Count == 1) - // { - // // If we have a single parameter, try to represent it nicely in the log - // var param = structuredParameters.First(); - // message = $"{param.Key}: {(param.Value != null ? "[object]" : "null")}"; - // } - // } + // Extract structured parameters for template-style logging + var structuredParameters = isDirectObjectLog + ? new Dictionary() + : ExtractStructuredParameters(state, out string messageTemplate); + + // Format the message + object message; + if (isDirectObjectLog) + { + // For direct object logging, use the object itself + message = state; + } + else + { + // For structured logging or regular string messages + message = CustomFormatter(state, exception, out var customMessage) && customMessage is not null + ? customMessage + : formatter(state, exception); + } + // Get log entry var logFormatter = Logger.GetFormatter(); var logEntry = logFormatter is null ? GetLogEntry(logLevel, timestamp, message, exception, structuredParameters) : GetFormattedLogEntry(logLevel, timestamp, message, exception, logFormatter, structuredParameters); - + _systemWrapper.LogLine(PowertoolsLoggingSerializer.Serialize(logEntry, typeof(object))); } @@ -182,7 +185,10 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times // Add Custom Keys foreach (var (key, value) in Logger.GetAllKeys()) { - logEntry.TryAdd(key, value); + if (key != "json") // Skip the json key + { + logEntry.TryAdd(key, value); + } } // Add Lambda Context Keys @@ -196,18 +202,22 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times { foreach (var (key, value) in CurrentScope.ExtraKeys) { - if (!string.IsNullOrWhiteSpace(key)) + if (!string.IsNullOrWhiteSpace(key) && key != "json") + { logEntry.TryAdd(key, value); + } } } // Add structured parameters - if (structuredParameters != null) + if (structuredParameters != null && structuredParameters.Count > 0) { foreach (var (key, value) in structuredParameters) { - if (!string.IsNullOrWhiteSpace(key)) + if (!string.IsNullOrWhiteSpace(key) && key != "json") + { logEntry.TryAdd(key, value); + } } } @@ -236,6 +246,7 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times /// The message to be written. Can be also an object. /// The exception related to this entry. /// The custom log entry formatter. + /// The structured parameters. private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, object message, Exception exception, ILogFormatter logFormatter, Dictionary structuredParameters) { @@ -258,6 +269,8 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec // Add Custom Keys foreach (var (key, value) in Logger.GetAllKeys()) { + if (key == "json") continue; // Skip json key + switch (key) { case LoggingConstants.KeyColdStart: @@ -280,18 +293,22 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec { foreach (var (key, value) in CurrentScope.ExtraKeys) { - if (!string.IsNullOrWhiteSpace(key)) + if (!string.IsNullOrWhiteSpace(key) && key != "json") + { extraKeys.TryAdd(key, value); + } } } // Add structured parameters - if (structuredParameters != null) + if (structuredParameters != null && structuredParameters.Count > 0) { foreach (var (key, value) in structuredParameters) { - if (!string.IsNullOrWhiteSpace(key)) + if (!string.IsNullOrWhiteSpace(key) && key != "json") + { extraKeys.TryAdd(key, value); + } } } @@ -322,6 +339,7 @@ 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 @@ -456,6 +474,15 @@ private Dictionary ExtractStructuredParameters(TState st messageTemplate = string.Empty; var parameters = new Dictionary(); + // Handle direct object logging - when an object is passed directly without a message template + if (state != null && + !(state is IEnumerable>) && + !(state is string)) + { + // No structured parameters for direct object logging + return parameters; + } + if (state is IEnumerable> stateProps) { // Dictionary to store format specifiers for each parameter diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Scope.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Scope.cs index 94642122..23526cd5 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Scope.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Scope.cs @@ -96,4 +96,13 @@ internal static void RemoveAllKeys() { Scope.Clear(); } + + /// + /// Removes a key from the log context. + /// + public static void RemoveKey(string key) + { + if (Scope.ContainsKey(key)) + Scope.Remove(key); + } } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LoggerExtensions.cs index e04a8fd7..0c1c6de8 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LoggerExtensions.cs @@ -14,6 +14,7 @@ */ using System; +using System.Collections.Generic; using AWS.Lambda.Powertools.Logging.Internal; using Microsoft.Extensions.Logging; @@ -652,19 +653,73 @@ public static void Log(this ILogger logger, LogLevel logLevel, T extraKeys, s #endregion #endregion - + + + /// + /// Appending additional key to the log context. + /// + /// + /// The list of keys. + public static void AppendKeys(this ILogger logger,IEnumerable> keys) + { + Logger.AppendKeys(keys); + } + + /// + /// Appending additional key to the log context. + /// + /// + /// The list of keys. + public static void AppendKeys(this ILogger logger,IEnumerable> keys) + { + Logger.AppendKeys(keys); + } + + /// + /// Appending additional key to the log context. + /// + /// + /// The key. + /// The value. + /// key + /// value public static void AppendKey(this ILogger logger, string key, object value) { Logger.AppendKey(key, value); } + /// + /// Returns all additional keys added to the log context. + /// + /// IEnumerable<KeyValuePair<System.String, System.Object>>. + public static IEnumerable> GetAllKeys(this ILogger logger) + { + return Logger.GetAllKeys(); + } + + /// + /// Removes all additional keys from the log context. + /// internal static void RemoveAllKeys(this ILogger logger) { Logger.RemoveAllKeys(); } - + + /// + /// Remove additional keys from the log context. + /// + /// + /// The list of keys. public static void RemoveKeys(this ILogger logger, params string[] keys) { Logger.RemoveKeys(keys); } + + /// + /// Removes a key from the log context. + /// + public static void RemoveKey(this ILogger logger, string key) + { + Logger.RemoveKey(key); + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs index 2185ec68..54aaf0a7 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs @@ -22,6 +22,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Common.Utils; using AWS.Lambda.Powertools.Logging.Serializers; namespace AWS.Lambda.Powertools.Logging; @@ -106,6 +107,13 @@ public JsonSerializerContext? JsonContext { _jsonContext = value; ApplyJsonContext(); + + // If we have existing JSON options, update their type resolver + if (_jsonOptions != null && !RuntimeFeatureWrapper.IsDynamicCodeSupported) + { + // Reset the type resolver chain to rebuild it + _jsonOptions.TypeInfoResolver = GetCompositeResolver(); + } } } @@ -114,10 +122,21 @@ public JsonSerializerContext? JsonContext /// public void AddJsonContext(JsonSerializerContext context) { - if (context != null && !_additionalContexts.Contains(context)) + if (context == null) + return; + + // Don't add duplicates + if (!_additionalContexts.Contains(context)) { _additionalContexts.Add(context); ApplyAdditionalJsonContext(context); + + // If we have existing JSON options, update their type resolver + if (_jsonOptions != null && !RuntimeFeatureWrapper.IsDynamicCodeSupported) + { + // Reset the type resolver chain to rebuild it + _jsonOptions.TypeInfoResolver = GetCompositeResolver(); + } } } @@ -139,11 +158,17 @@ public void HandleJsonOptionsTypeResolver(JsonSerializerOptions options) { if (options == null) return; - // Check for TypeInfoResolver + // Check for TypeInfoResolver and ensure it's not lost if (options.TypeInfoResolver != null && options.TypeInfoResolver != GetCompositeResolver()) { _customTypeInfoResolver = options.TypeInfoResolver; + + // If it's a JsonSerializerContext, also add it to our contexts + if (_customTypeInfoResolver is JsonSerializerContext jsonContext) + { + AddJsonContext(jsonContext); + } } // Check for TypeInfoResolverChain diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs index ef54d3a5..cc207191 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -97,19 +97,42 @@ internal static string Serialize(object value, Type inputType) #else if (RuntimeFeatureWrapper.IsDynamicCodeSupported) { - var options = GetSerializerOptions(); + var jsonSerializerOptions = GetSerializerOptions(); #pragma warning disable - return JsonSerializer.Serialize(value, options); + return JsonSerializer.Serialize(value, jsonSerializerOptions); } - - var typeInfo = GetTypeInfo(inputType); - if (typeInfo == null) + + var options = GetSerializerOptions(); + + // Try to serialize using the configured TypeInfoResolver + try + { + var typeInfo = GetTypeInfo(inputType); + if (typeInfo != null) + { + return JsonSerializer.Serialize(value, typeInfo); + } + } + catch (InvalidOperationException) + { + // Failed to get typeinfo, will fall back to trying the serializer directly + } + + // Fall back to direct serialization which may work if the resolver chain can handle it + try + { + return JsonSerializer.Serialize(value, inputType, options); + } + catch (JsonException ex) { throw new JsonSerializerException( - $"Type {inputType} is not known to the serializer. Ensure it's included in the JsonSerializerContext."); + $"Type {inputType} is not known to the serializer. Ensure it's included in the JsonSerializerContext.", ex); + } + catch (InvalidOperationException ex) + { + throw new JsonSerializerException( + $"Type {inputType} is not known to the serializer. Ensure it's included in the JsonSerializerContext.", ex); } - - return JsonSerializer.Serialize(value, typeInfo); #endif } @@ -139,6 +162,26 @@ internal static JsonTypeInfo GetTypeInfo(Type type) var options = GetSerializerOptions(); return options.TypeInfoResolver?.GetTypeInfo(type, options); } + + /// + /// Checks if a type is supported by any of the configured type resolvers + /// + internal static bool IsTypeSupportedByAnyResolver(Type type) + { + var options = GetSerializerOptions(); + if (options.TypeInfoResolver == null) + return false; + + try + { + var typeInfo = options.TypeInfoResolver.GetTypeInfo(type, options); + return typeInfo != null; + } + catch + { + return false; + } + } #endif /// @@ -192,7 +235,10 @@ private static void BuildJsonSerializerOptions() // Only add TypeInfoResolver if AOT mode if (!RuntimeFeatureWrapper.IsDynamicCodeSupported) { + // Always ensure our default context is in the chain first _jsonOptions.TypeInfoResolverChain.Add(PowertoolsLoggingSerializationContext.Default); + + // Add all registered contexts foreach (var context in AdditionalContexts) { _jsonOptions.TypeInfoResolverChain.Add(context); From 1f5ea65d3078ace1ef8cc4a00ab2d8d9495cf47c Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 19 Mar 2025 15:02:22 +0000 Subject: [PATCH 08/49] add new LoggerFactory, add builder --- .../BuilderExtensions.cs | 4 +- .../Internal/LoggerProvider.cs | 14 ++-- .../Internal/LoggingAspect.cs | 6 +- .../Internal/PowertoolsLogger.cs | 52 ++---------- .../AWS.Lambda.Powertools.Logging/Logger.cs | 21 +++-- .../PowertoolsLoggerBuilder.cs | 79 ++++++++++++++++++ .../PowertoolsLoggerConfiguration.cs | 51 +++--------- .../PowertoolsLoggerFactory.cs | 18 ++-- .../PowertoolsLoggerFactoryBuilder.cs | 83 ------------------- .../PowertoolsLoggerFactoryExtensions.cs | 19 +++++ 10 files changed, 149 insertions(+), 198 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs delete mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactoryBuilder.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactoryExtensions.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs index 923d432a..5f627b2d 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs @@ -33,9 +33,9 @@ public static ILoggingBuilder AddPowertoolsLogger( // IMPORTANT: Set the minimum level directly on the builder - if (options.MinimumLevel != LogLevel.None) + if (options.MinimumLogLevel != LogLevel.None) { - builder.SetMinimumLevel(options.MinimumLevel); + builder.SetMinimumLevel(options.MinimumLogLevel); } // Configure options for DI diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs index 6b1ddcbb..852d9962 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs @@ -76,6 +76,8 @@ public LoggerProvider(IOptionsMonitor config, /// The instance of that was created. public ILogger CreateLogger(string categoryName) { + _powertoolsConfigurations.SetExecutionEnvironment(typeof(PowertoolsLogger)); + return _loggers.GetOrAdd(categoryName, name => new PowertoolsLogger(name, () => _currentConfig, _systemWrapper)); @@ -97,13 +99,13 @@ private void ApplyPowertoolsConfig(PowertoolsLoggerConfiguration config) var lambdaLogLevelEnabled = _powertoolsConfigurations.LambdaLogLevelEnabled(); // Check for explicit config - bool hasExplicitLevel = config.MinimumLevel != LogLevel.None; + bool hasExplicitLevel = config.MinimumLogLevel != LogLevel.None; // Warn if Lambda log level doesn't match - if (lambdaLogLevelEnabled && hasExplicitLevel && config.MinimumLevel < lambdaLogLevel) + if (lambdaLogLevelEnabled && hasExplicitLevel && config.MinimumLogLevel < lambdaLogLevel) { _systemWrapper.LogLine( - $"Current log level ({config.MinimumLevel}) does not match AWS Lambda Advanced Logging Controls minimum log level ({lambdaLogLevel}). This can lead to data loss, consider adjusting them."); + $"Current log level ({config.MinimumLogLevel}) does not match AWS Lambda Advanced Logging Controls minimum log level ({lambdaLogLevel}). This can lead to data loss, consider adjusting them."); } // Set service from environment if not explicitly set @@ -123,7 +125,7 @@ private void ApplyPowertoolsConfig(PowertoolsLoggerConfiguration config) if (!hasExplicitLevel) { var minLogLevel = lambdaLogLevelEnabled ? lambdaLogLevel : logLevel; - config.MinimumLevel = minLogLevel != LogLevel.None ? minLogLevel : LoggingConstants.DefaultLogLevel; + config.MinimumLogLevel = minLogLevel != LogLevel.None ? minLogLevel : LoggingConstants.DefaultLogLevel; } // Always configure the serializer with the output case @@ -145,7 +147,7 @@ private void ProcessSamplingRate(PowertoolsLoggerConfiguration config) ? config.SamplingRate : _powertoolsConfigurations.LoggerSampleRate; - samplingRate = ValidateSamplingRate(samplingRate, config.MinimumLevel, _systemWrapper); + samplingRate = ValidateSamplingRate(samplingRate, config.MinimumLogLevel, _systemWrapper); config.SamplingRate = samplingRate; // Only notify if sampling is configured @@ -158,7 +160,7 @@ private void ProcessSamplingRate(PowertoolsLoggerConfiguration config) { _systemWrapper.LogLine( $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate}, Sampler Value: {sample}."); - config.MinimumLevel = LogLevel.Debug; + config.MinimumLogLevel = LogLevel.Debug; } } } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index f733608a..4e41fc82 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -92,7 +92,7 @@ private void InitializeLogger(LoggingAttribute trigger) var config = new PowertoolsLoggerConfiguration { // Use sensible defaults if not specified in the attribute - MinimumLevel = trigger.LogLevel != LogLevel.None ? trigger.LogLevel : LogLevel.Information, + MinimumLogLevel = trigger.LogLevel != LogLevel.None ? trigger.LogLevel : LogLevel.Information, Service = !string.IsNullOrEmpty(trigger.Service) ? trigger.Service : "service_undefined", LoggerOutputCase = trigger.LoggerOutputCase != default ? trigger.LoggerOutputCase : LoggerOutputCase.SnakeCase, SamplingRate = trigger.SamplingRate > 0 ? trigger.SamplingRate : 1.0 @@ -103,10 +103,10 @@ private void InitializeLogger(LoggingAttribute trigger) } // Get logger after configuration - _logger = Logger.GetLogger(); + _logger = Logger.GetPowertoolsLogger(); // Set debug flag based on the minimum level from Logger - _isDebug = Logger.GetConfiguration().MinimumLevel <= LogLevel.Debug; + _isDebug = Logger.GetConfiguration().MinimumLogLevel <= LogLevel.Debug; } /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs index 1657d8b5..396839df 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -66,9 +66,6 @@ public PowertoolsLogger( _categoryName = categoryName; _currentConfig = getCurrentConfig(); _systemWrapper = systemWrapper; - - // TODO: Fix - // _powertoolsConfigurations.SetExecutionEnvironment(this); } /// @@ -100,8 +97,8 @@ internal void EndScope() public bool IsEnabled(LogLevel logLevel) { // If we have no explicit minimum level, use the default - var effectiveMinLevel = _currentConfig.MinimumLevel != LogLevel.None - ? _currentConfig.MinimumLevel + var effectiveMinLevel = _currentConfig.MinimumLogLevel != LogLevel.None + ? _currentConfig.MinimumLogLevel : LoggingConstants.DefaultLogLevel; // Log diagnostic info for Debug/Trace levels @@ -136,30 +133,13 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except var timestamp = DateTime.UtcNow; - // Check if state is a direct object (not structured logging) - bool isDirectObjectLog = state != null && - !(state is IEnumerable>) && - !(state is string); - // Extract structured parameters for template-style logging - var structuredParameters = isDirectObjectLog - ? new Dictionary() - : ExtractStructuredParameters(state, out string messageTemplate); + var structuredParameters = ExtractStructuredParameters(state, out string messageTemplate); // Format the message - object message; - if (isDirectObjectLog) - { - // For direct object logging, use the object itself - message = state; - } - else - { - // For structured logging or regular string messages - message = CustomFormatter(state, exception, out var customMessage) && customMessage is not null - ? customMessage - : formatter(state, exception); - } + var message = CustomFormatter(state, exception, out var customMessage) && customMessage is not null + ? customMessage + : formatter(state, exception); // Get log entry var logFormatter = Logger.GetFormatter(); @@ -185,10 +165,7 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times // Add Custom Keys foreach (var (key, value) in Logger.GetAllKeys()) { - if (key != "json") // Skip the json key - { - logEntry.TryAdd(key, value); - } + logEntry.TryAdd(key, value); } // Add Lambda Context Keys @@ -202,7 +179,7 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times { foreach (var (key, value) in CurrentScope.ExtraKeys) { - if (!string.IsNullOrWhiteSpace(key) && key != "json") + if (!string.IsNullOrWhiteSpace(key)) { logEntry.TryAdd(key, value); } @@ -269,8 +246,6 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec // Add Custom Keys foreach (var (key, value) in Logger.GetAllKeys()) { - if (key == "json") continue; // Skip json key - switch (key) { case LoggingConstants.KeyColdStart: @@ -293,7 +268,7 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec { foreach (var (key, value) in CurrentScope.ExtraKeys) { - if (!string.IsNullOrWhiteSpace(key) && key != "json") + if (!string.IsNullOrWhiteSpace(key)) { extraKeys.TryAdd(key, value); } @@ -474,15 +449,6 @@ private Dictionary ExtractStructuredParameters(TState st messageTemplate = string.Empty; var parameters = new Dictionary(); - // Handle direct object logging - when an object is passed directly without a message template - if (state != null && - !(state is IEnumerable>) && - !(state is string)) - { - // No structured parameters for direct object logging - return parameters; - } - if (state is IEnumerable> stateProps) { // Dictionary to store format specifiers for each parameter diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs index 457d87d5..d25fb151 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs @@ -57,13 +57,13 @@ public static void Configure(Action configureOpti } // Configure with existing factory - public static void Configure(ILoggerFactory loggerFactory) + internal static void Configure(ILoggerFactory loggerFactory) { Interlocked.Exchange(ref _factoryLazy, new Lazy(() => loggerFactory)); Interlocked.Exchange(ref _defaultLoggerLazy, - new Lazy(() => Factory.CreateLogger("PowertoolsLogger"))); + new Lazy(() => Factory.CreatePowertoolsLogger())); _isConfigured = true; } @@ -83,9 +83,9 @@ internal static void Configure(PowertoolsLoggerConfiguration options) var factory = LoggerFactory.Create(builder => { // Set minimum level directly on builder - if (options.MinimumLevel != LogLevel.None) + if (options.MinimumLogLevel != LogLevel.None) { - builder.SetMinimumLevel(options.MinimumLevel); + builder.SetMinimumLevel(options.MinimumLogLevel); } // Add our provider - the config's OutputLogger will be used @@ -94,14 +94,12 @@ internal static void Configure(PowertoolsLoggerConfiguration options) builder.AddPowertoolsLogger(config => { config.Service = _currentConfig.Service; - config.MinimumLevel = _currentConfig.MinimumLevel; + config.MinimumLogLevel = _currentConfig.MinimumLogLevel; config.LoggerOutputCase = _currentConfig.LoggerOutputCase; config.SamplingRate = _currentConfig.SamplingRate; config.LoggerOutput = _currentConfig.LoggerOutput; config.JsonOptions = _currentConfig.JsonOptions; -#if NET8_0_OR_GREATER - config.JsonContext = _currentConfig.JsonContext; -#endif + }, true); }); @@ -110,14 +108,13 @@ internal static void Configure(PowertoolsLoggerConfiguration options) new Lazy(() => factory)); Interlocked.Exchange(ref _defaultLoggerLazy, - new Lazy(() => Factory.CreateLogger("PowertoolsLogger"))); + new Lazy(() => Factory.CreatePowertoolsLogger())); _isConfigured = true; } - // Add this method to the Logger class // Get the current configuration - public static PowertoolsLoggerConfiguration GetConfiguration() + internal static PowertoolsLoggerConfiguration GetConfiguration() { // Ensure logger is initialized _ = LoggerInstance; @@ -136,6 +133,8 @@ public static PowertoolsLoggerConfiguration GetConfiguration() public static ILogger GetLogger(string category) => Factory.CreateLogger(category); + public static ILogger GetPowertoolsLogger() => Factory.CreatePowertoolsLogger(); + // For testing purposes // internal static void Reset() // { diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs new file mode 100644 index 00000000..b5d10747 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs @@ -0,0 +1,79 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using AWS.Lambda.Powertools.Common; +using Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging; + +public class PowertoolsLoggerBuilder +{ + private readonly PowertoolsLoggerConfiguration _configuration = new(); + + public PowertoolsLoggerBuilder WithService(string service) + { + _configuration.Service = service; + return this; + } + + public PowertoolsLoggerBuilder WithSamplingRate(double rate) + { + _configuration.SamplingRate = rate; + return this; + } + + public PowertoolsLoggerBuilder WithMinimumLogLevel(LogLevel level) + { + _configuration.MinimumLogLevel = level; + return this; + } + + public PowertoolsLoggerBuilder WithJsonOptions(JsonSerializerOptions options) + { + _configuration.JsonOptions = options; + return this; + } + + public PowertoolsLoggerBuilder WithTimestampFormat(string format) + { + _configuration.TimestampFormat = format; + return this; + } + + public PowertoolsLoggerBuilder WithOutputCase(LoggerOutputCase outputCase) + { + _configuration.LoggerOutputCase = outputCase; + return this; + } + + public PowertoolsLoggerBuilder WithOutput(ISystemWrapper output) + { + _configuration.LoggerOutput = output; + return this; + } + + public ILogger Build() + { + var factory = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = _configuration.Service; + config.SamplingRate = _configuration.SamplingRate; + config.MinimumLogLevel = _configuration.MinimumLogLevel; + config.LoggerOutputCase = _configuration.LoggerOutputCase; + config.LoggerOutput = _configuration.LoggerOutput; + config.JsonOptions = _configuration.JsonOptions; + config.TimestampFormat = _configuration.TimestampFormat; // Add this line + + // foreach (var context in _configuration.GetAdditionalContexts()) + // { + // config.AddJsonContext(context); + // } + }); + }); + + Logger.Configure(factory); // Configure the static logger + return factory.CreatePowertoolsLogger(); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs index 54aaf0a7..242226c9 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs @@ -33,7 +33,7 @@ namespace AWS.Lambda.Powertools.Logging; /// public class PowertoolsLoggerConfiguration : IOptions { - public const string ConfigurationSectionName = "PowertoolsLogger"; + public const string ConfigurationSectionName = "AWS.Lambda.Powertools.Logging.Logger"; /// /// Service name is used for logging. @@ -45,7 +45,7 @@ public class PowertoolsLoggerConfiguration : IOptionsPOWERTOOLS_LOG_LEVEL. /// - public LogLevel MinimumLevel { get; set; } = LogLevel.None; + public LogLevel MinimumLogLevel { get; set; } = LogLevel.None; /// /// Dynamically set a percentage of logs to DEBUG level. @@ -69,7 +69,6 @@ public class PowertoolsLoggerConfiguration : IOptions public ISystemWrapper? LoggerOutput { get; set; } - /// /// JSON serializer options to use for log serialization /// @@ -97,30 +96,10 @@ public JsonSerializerOptions? JsonOptions private JsonSerializerContext? _jsonContext = PowertoolsLoggingSerializationContext.Default; private readonly List _additionalContexts = new(); - /// - /// Main JSON context to use for serialization - /// - public JsonSerializerContext? JsonContext - { - get => _jsonContext; - set - { - _jsonContext = value; - ApplyJsonContext(); - - // If we have existing JSON options, update their type resolver - if (_jsonOptions != null && !RuntimeFeatureWrapper.IsDynamicCodeSupported) - { - // Reset the type resolver chain to rebuild it - _jsonOptions.TypeInfoResolver = GetCompositeResolver(); - } - } - } - /// /// Add additional JsonSerializerContext for client types /// - public void AddJsonContext(JsonSerializerContext context) + internal void AddJsonContext(JsonSerializerContext context) { if (context == null) return; @@ -143,7 +122,7 @@ public void AddJsonContext(JsonSerializerContext context) /// /// Get all additional contexts /// - public IReadOnlyList GetAdditionalContexts() + internal IReadOnlyList GetAdditionalContexts() { return _additionalContexts.AsReadOnly(); } @@ -154,7 +133,7 @@ public IReadOnlyList GetAdditionalContexts() /// /// Process JSON options type resolver information /// - public void HandleJsonOptionsTypeResolver(JsonSerializerOptions options) + internal void HandleJsonOptionsTypeResolver(JsonSerializerOptions options) { if (options == null) return; @@ -207,7 +186,7 @@ public void HandleJsonOptionsTypeResolver(JsonSerializerOptions options) /// /// Get a composite resolver that includes all configured resolvers /// - public IJsonTypeInfoResolver GetCompositeResolver() + internal IJsonTypeInfoResolver GetCompositeResolver() { var resolvers = new List(); @@ -241,17 +220,6 @@ public IJsonTypeInfoResolver GetCompositeResolver() return new CompositeJsonTypeInfoResolver(resolvers.ToArray()); } - /// - /// Apply JSON context to serializer - /// - private void ApplyJsonContext() - { - if (_jsonContext != null) - { - PowertoolsLoggingSerializer.SetDefaultContext(_jsonContext); - } - } - /// /// Apply additional JSON context to serializer /// @@ -275,7 +243,7 @@ private void ApplyJsonOptions() /// /// Apply output case configuration /// - public void ApplyOutputCase() + internal void ApplyOutputCase() { PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase); } @@ -283,12 +251,12 @@ public void ApplyOutputCase() /// /// Clone this configuration /// - public PowertoolsLoggerConfiguration Clone() + internal PowertoolsLoggerConfiguration Clone() { var clone = new PowertoolsLoggerConfiguration { Service = Service, - MinimumLevel = MinimumLevel, + MinimumLogLevel = MinimumLogLevel, SamplingRate = SamplingRate, LoggerOutputCase = LoggerOutputCase, LoggerOutput = LoggerOutput, @@ -316,4 +284,5 @@ public PowertoolsLoggerConfiguration Clone() // IOptions implementation PowertoolsLoggerConfiguration IOptions.Value => this; + public string TimestampFormat { get; set; } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs index a939ab9c..7794b787 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs @@ -4,7 +4,7 @@ namespace AWS.Lambda.Powertools.Logging; -public sealed class PowertoolsLoggerFactory : IDisposable +internal sealed class PowertoolsLoggerFactory : IDisposable { private readonly ILoggerFactory _factory; @@ -31,7 +31,7 @@ public static PowertoolsLoggerFactory Create(Action() => CreateLogger(typeof(T).FullName ?? typeof(T).Name); @@ -66,6 +61,11 @@ public ILogger CreateLogger(string category) return _factory.CreateLogger(category); } + public ILogger CreatePowertoolsLogger() + { + return _factory.CreatePowertoolsLogger(); + } + public void Dispose() { _factory?.Dispose(); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactoryBuilder.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactoryBuilder.cs deleted file mode 100644 index 870c8ada..00000000 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactoryBuilder.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.Extensions.Logging; - -namespace AWS.Lambda.Powertools.Logging; - -public class PowertoolsLoggerFactoryBuilder -{ - private readonly PowertoolsLoggerConfiguration _configuration = new(); - - // public PowertoolsLoggerFactoryBuilder UseEnvironmentVariables(bool enabled) - // { - // _configuration.UseEnvironmentVariables = enabled; - // return this; - // } - // - // public PowertoolsLoggerFactoryBuilder SetLogLevelColor(AppLogLevel level, ConsoleColor color) - // { - // _configuration.LogLevelToColorMap[level] = color; - // return this; - // } - // - // public PowertoolsLoggerFactoryBuilder SetEventId(int eventId) - // { - // _configuration.EventId = eventId; - // return this; - // } - // - // public PowertoolsLoggerFactoryBuilder SetJsonOptions(JsonSerializerOptions options) - // { - // _configuration.JsonOptions = options; - // return this; - // } - // - // public PowertoolsLoggerFactoryBuilder UseJsonOutput(bool enabled = true) - // { - // _configuration.UseJsonOutput = enabled; - // return this; - // } - // - // public PowertoolsLoggerFactoryBuilder SetTimestampFormat(string format) - // { - // _configuration.TimestampFormat = format; - // return this; - // } - // - // public PowertoolsLoggerFactoryBuilder UseJsonContext(JsonSerializerContext context) - // { - // _configuration.JsonContext = context; - // return this; - // } - // - // public PowertoolsLoggerFactoryBuilder AddJsonContext(JsonSerializerContext context) - // { - // _configuration.AddJsonContext(context); - // return this; - // } - // - // public PowertoolsLoggerFactory Build() - // { - // var factory = LoggerFactory.Create(builder => - // { - // builder.AddPowertoolsLogger(config => - // { - // config.UseEnvironmentVariables = _configuration.UseEnvironmentVariables; - // config.EventId = _configuration.EventId; - // config.JsonOptions = _configuration.JsonOptions; - // config.UseJsonOutput = _configuration.UseJsonOutput; // Add this line - // config.TimestampFormat = _configuration.TimestampFormat; // Add this line - // config.JsonContext = _configuration.JsonContext; - // - // foreach (var kvp in _configuration.LogLevelToColorMap) - // { - // config.LogLevelToColorMap[kvp.Key] = kvp.Value; - // } - // }); - // }); - // - // Logger.Configure(factory); // Configure the static logger - // return new PowertoolsLoggerFactory(factory); - // } -} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactoryExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactoryExtensions.cs new file mode 100644 index 00000000..edec07fd --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactoryExtensions.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging; + +/// +/// Extensions for ILoggerFactory +/// +public static class PowertoolsLoggerFactoryExtensions +{ + /// + /// Creates a new Powertools Logger instance using the Powertools full name. + /// + /// The factory. + /// The that was created. + public static ILogger CreatePowertoolsLogger(this ILoggerFactory factory) + { + return new PowertoolsLoggerFactory(factory).CreateLogger(PowertoolsLoggerConfiguration.ConfigurationSectionName); + } +} \ No newline at end of file From d865fa26ff9c134ad2388f936861ba167abab2ec Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 19 Mar 2025 16:29:17 +0000 Subject: [PATCH 09/49] refactor builder, configurations add timestamp format --- .../BuilderExtensions.cs | 18 +-- .../Helpers/ConfigurationExtensions.cs | 29 ++++ .../Internal/PowertoolsLogger.cs | 2 +- .../AWS.Lambda.Powertools.Logging/Logger.cs | 32 +---- .../PowertoolsLoggerBuilder.cs | 16 +-- .../PowertoolsLoggerConfiguration.cs | 73 +--------- .../PowertoolsLoggerFactory.cs | 24 ++-- .../PowertoolsLoggingSerializer.cs | 130 ++++++------------ 8 files changed, 104 insertions(+), 220 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/ConfigurationExtensions.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs index 5f627b2d..33143f3c 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs @@ -18,8 +18,7 @@ public static class BuilderExtensions // Single base method that all other overloads call public static ILoggingBuilder AddPowertoolsLogger( this ILoggingBuilder builder, - Action? configure = null, - bool fromLoggerConfigure = false) + Action? configure = null) { // Add configuration builder.AddConfiguration(); @@ -30,14 +29,15 @@ public static ILoggingBuilder AddPowertoolsLogger( // Create initial configuration var options = new PowertoolsLoggerConfiguration(); configure(options); - - + // IMPORTANT: Set the minimum level directly on the builder - if (options.MinimumLogLevel != LogLevel.None) + if (options.MinimumLogLevel != LogLevel.None) { builder.SetMinimumLevel(options.MinimumLogLevel); } + // Add filters here + // Configure options for DI builder.Services.Configure(configure); @@ -48,7 +48,7 @@ public static ILoggingBuilder AddPowertoolsLogger( PowertoolsLoggingSerializer.ConfigureNamingPolicy(options.LoggerOutputCase); // Configure static Logger (if not already in a configuration cycle) - if (!fromLoggerConfigure && !_configuring) + if (!_configuring) { try { @@ -74,12 +74,12 @@ private static void RegisterServices(ILoggingBuilder builder) { // Register ISystemWrapper if not already registered builder.Services.TryAddSingleton(); - + // Register IPowertoolsEnvironment if it exists builder.Services.TryAddSingleton(); - + // Register IPowertoolsConfigurations with all its dependencies - builder.Services.TryAddSingleton(sp => + builder.Services.TryAddSingleton(sp => new PowertoolsConfigurations(sp.GetRequiredService())); // Register the provider diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/ConfigurationExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/ConfigurationExtensions.cs new file mode 100644 index 00000000..4a8f6c8e --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/ConfigurationExtensions.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging.Internal.Helpers; + +/// +/// Extension methods for handling configuration copying between PowertoolsLogger configurations +/// +internal static class ConfigurationExtensions +{ + /// + /// Copies configuration values from source to destination configuration + /// + /// The destination configuration to copy values to + /// The source configuration to copy values from + /// The updated destination configuration + public static PowertoolsLoggerConfiguration CopyFrom(this PowertoolsLoggerConfiguration destination, PowertoolsLoggerConfiguration source) + { + destination.Service = source.Service; + destination.SamplingRate = source.SamplingRate; + destination.MinimumLogLevel = source.MinimumLogLevel; + destination.LoggerOutputCase = source.LoggerOutputCase; + destination.LoggerOutput = source.LoggerOutput; + destination.JsonOptions = source.JsonOptions; + destination.TimestampFormat = source.TimestampFormat; + + return destination; + } +} \ 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 396839df..902b40af 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -198,7 +198,7 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times } } - logEntry.TryAdd(LoggingConstants.KeyTimestamp, timestamp.ToString("o")); + logEntry.TryAdd(LoggingConstants.KeyTimestamp, timestamp.ToString( _currentConfig.TimestampFormat ?? "o")); logEntry.TryAdd(_currentConfig.LogLevelKey, logLevel.ToString()); logEntry.TryAdd(LoggingConstants.KeyService, _currentConfig.Service); logEntry.TryAdd(LoggingConstants.KeyLoggerName, _categoryName); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs index d25fb151..bdcdbe72 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs @@ -73,39 +73,9 @@ internal static void Configure(PowertoolsLoggerConfiguration options) { if (options == null) throw new ArgumentNullException(nameof(options)); - // Store the configuration - _currentConfig = options.Clone(); - - // Create a system wrapper if needed - var systemWrapper = options.LoggerOutput ?? new SystemWrapper(); - - // Create a factory with our provider - var factory = LoggerFactory.Create(builder => - { - // Set minimum level directly on builder - if (options.MinimumLogLevel != LogLevel.None) - { - builder.SetMinimumLevel(options.MinimumLogLevel); - } - - // Add our provider - the config's OutputLogger will be used - builder.Services.AddSingleton(systemWrapper); - - builder.AddPowertoolsLogger(config => - { - config.Service = _currentConfig.Service; - config.MinimumLogLevel = _currentConfig.MinimumLogLevel; - config.LoggerOutputCase = _currentConfig.LoggerOutputCase; - config.SamplingRate = _currentConfig.SamplingRate; - config.LoggerOutput = _currentConfig.LoggerOutput; - config.JsonOptions = _currentConfig.JsonOptions; - - }, true); - }); - // Update factory and logger Interlocked.Exchange(ref _factoryLazy, - new Lazy(() => factory)); + new Lazy(() => PowertoolsLoggerFactory.Create(options))); Interlocked.Exchange(ref _defaultLoggerLazy, new Lazy(() => Factory.CreatePowertoolsLogger())); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs index b5d10747..b4411f74 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs @@ -2,6 +2,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging.Internal.Helpers; using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging; @@ -58,21 +59,10 @@ public ILogger Build() { builder.AddPowertoolsLogger(config => { - config.Service = _configuration.Service; - config.SamplingRate = _configuration.SamplingRate; - config.MinimumLogLevel = _configuration.MinimumLogLevel; - config.LoggerOutputCase = _configuration.LoggerOutputCase; - config.LoggerOutput = _configuration.LoggerOutput; - config.JsonOptions = _configuration.JsonOptions; - config.TimestampFormat = _configuration.TimestampFormat; // Add this line - - // foreach (var context in _configuration.GetAdditionalContexts()) - // { - // config.AddJsonContext(context); - // } + config.CopyFrom(_configuration); }); }); - + Logger.Configure(factory); // Configure the static logger return factory.CreatePowertoolsLogger(); } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs index 242226c9..27e3ef2a 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs @@ -40,6 +40,11 @@ public class PowertoolsLoggerConfiguration : IOptionsPOWERTOOLS_SERVICE_NAME. /// public string? Service { get; set; } = null; + + /// + /// Timestamp format for logging. + /// + public string? TimestampFormat { get; set; } /// /// Specify the minimum log level for logging (Information, by default). @@ -149,38 +154,6 @@ internal void HandleJsonOptionsTypeResolver(JsonSerializerOptions options) AddJsonContext(jsonContext); } } - - // Check for TypeInfoResolverChain - if (options.TypeInfoResolverChain != null && options.TypeInfoResolverChain.Count > 0) - { - foreach (var resolver in options.TypeInfoResolverChain) - { - // If it's a JsonSerializerContext, add it to our additional contexts - if (resolver is JsonSerializerContext context) - { - AddJsonContext(context); - } - // Otherwise store it as a custom resolver - else if (resolver is IJsonTypeInfoResolver customResolver && - customResolver != GetCompositeResolver() && - _customTypeInfoResolver != customResolver) - { - // If we already have a different custom resolver, we need to store multiple - if (_customTypeInfoResolver != null) - { - _customTypeInfoResolvers ??= new List(); - if (!_customTypeInfoResolvers.Contains(customResolver)) - { - _customTypeInfoResolvers.Add(customResolver); - } - } - else - { - _customTypeInfoResolver = customResolver; - } - } - } - } } /// @@ -236,7 +209,7 @@ private void ApplyJsonOptions() { if (_jsonOptions != null) { - PowertoolsLoggingSerializer.ConfigureJsonOptions(_jsonOptions); + PowertoolsLoggingSerializer.BuildJsonSerializerOptions(_jsonOptions); } } @@ -248,41 +221,7 @@ internal void ApplyOutputCase() PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase); } - /// - /// Clone this configuration - /// - internal PowertoolsLoggerConfiguration Clone() - { - var clone = new PowertoolsLoggerConfiguration - { - Service = Service, - MinimumLogLevel = MinimumLogLevel, - SamplingRate = SamplingRate, - LoggerOutputCase = LoggerOutputCase, - LoggerOutput = LoggerOutput, - LogLevelKey = LogLevelKey, - JsonOptions = JsonOptions - }; - -#if NET8_0_OR_GREATER - clone._jsonContext = _jsonContext; - foreach (var context in _additionalContexts) - { - clone._additionalContexts.Add(context); - } - - clone._customTypeInfoResolver = _customTypeInfoResolver; - - if (_customTypeInfoResolvers != null) - { - clone._customTypeInfoResolvers = new List(_customTypeInfoResolvers); - } -#endif - - return clone; - } // IOptions implementation PowertoolsLoggerConfiguration IOptions.Value => this; - public string TimestampFormat { get; set; } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs index 7794b787..7a0793a9 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs @@ -1,5 +1,6 @@ using System; using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Internal.Helpers; using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging; @@ -25,27 +26,24 @@ public static PowertoolsLoggerFactory Create(Action { builder.AddPowertoolsLogger(config => { - // Copy basic properties - config.Service = options.Service; - config.MinimumLogLevel = options.MinimumLogLevel; - config.LoggerOutputCase = options.LoggerOutputCase; - config.SamplingRate = options.SamplingRate; - - // // Copy additional contexts using the public API - // foreach (var ctx in options.GetAdditionalContexts()) - // { - // config.AddJsonContext(ctx); - // } - // + config.CopyFrom(options); }); }); Logger.Configure(factory); - return new PowertoolsLoggerFactory(factory); + return factory; } // Add builder pattern support diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs index cc207191..9407dcfa 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -188,32 +188,59 @@ internal static bool IsTypeSupportedByAnyResolver(Type type) /// Builds and configures the JsonSerializerOptions. /// /// A configured JsonSerializerOptions instance. - private static void BuildJsonSerializerOptions() + internal static void BuildJsonSerializerOptions(JsonSerializerOptions options = null) { - // This should already be in a lock when called - _jsonOptions = new JsonSerializerOptions(); - - switch (_currentOutputCase) + lock (_lock) { - 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 + // This should already be in a lock when called + _jsonOptions = options ?? 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 #if NET8_0_OR_GREATER - _jsonOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; - _jsonOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; + _jsonOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; + _jsonOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; #else _jsonOptions.PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance; _jsonOptions.DictionaryKeyPolicy = SnakeCaseNamingPolicy.Instance; #endif - break; + break; + } + + AddConverters(); + + _jsonOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + _jsonOptions.PropertyNameCaseInsensitive = true; + +#if NET8_0_OR_GREATER + + // Only add TypeInfoResolver if AOT mode + if (!RuntimeFeatureWrapper.IsDynamicCodeSupported) + { + // Always ensure our default context is in the chain first + _jsonOptions.TypeInfoResolverChain.Add(PowertoolsLoggingSerializationContext.Default); + + // Add all registered contexts + foreach (var context in AdditionalContexts) + { + _jsonOptions.TypeInfoResolverChain.Add(context); + } + } +#endif } + } + private static void AddConverters() + { _jsonOptions.Converters.Add(new ByteArrayConverter()); _jsonOptions.Converters.Add(new ExceptionConverter()); _jsonOptions.Converters.Add(new MemoryStreamConverter()); @@ -226,25 +253,6 @@ private static void BuildJsonSerializerOptions() #elif NET6_0 _jsonOptions.Converters.Add(new LogLevelJsonConverter()); #endif - - _jsonOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; - _jsonOptions.PropertyNameCaseInsensitive = true; - -#if NET8_0_OR_GREATER - - // Only add TypeInfoResolver if AOT mode - if (!RuntimeFeatureWrapper.IsDynamicCodeSupported) - { - // Always ensure our default context is in the chain first - _jsonOptions.TypeInfoResolverChain.Add(PowertoolsLoggingSerializationContext.Default); - - // Add all registered contexts - foreach (var context in AdditionalContexts) - { - _jsonOptions.TypeInfoResolverChain.Add(context); - } - } -#endif } #if NET8_0_OR_GREATER @@ -266,54 +274,4 @@ internal static void ClearOptions() { _jsonOptions = null; } - - /// - /// Sets the default JSON context to use - /// - internal static void SetDefaultContext(JsonSerializerContext context) - { - lock (_lock) - { - // Reset options to ensure they're rebuilt with the new context - _jsonOptions = null; - } - } - - /// - /// Configure the serializer with specific JSON options - /// - internal static void ConfigureJsonOptions(JsonSerializerOptions options) - { - if (options == null) return; - - lock (_lock) - { - _jsonOptions = options; - - // Add required converters if they're not already present - var converters = new[] - { - typeof(ByteArrayConverter), - typeof(ExceptionConverter), - typeof(MemoryStreamConverter), - typeof(ConstantClassConverter), - typeof(DateOnlyConverter), - typeof(TimeOnlyConverter), - typeof(LogLevelJsonConverter), - }; - - foreach (var converterType in converters) - { - if (!_jsonOptions.Converters.Any(c => c.GetType() == converterType)) - { - // Add the converter through reflection to avoid direct instantiation - var converter = Activator.CreateInstance(converterType) as JsonConverter; - if (converter != null) - { - _jsonOptions.Converters.Add(converter); - } - } - } - } - } } \ No newline at end of file From c635752400ac7da4e2be979b7f5da59d620e0d00 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 19 Mar 2025 19:52:59 +0000 Subject: [PATCH 10/49] log formatter --- .../Helpers/ConfigurationExtensions.cs | 1 + .../Internal/Helpers/LoggerFactoryHelper.cs | 36 +++++++++++++++++++ .../PowertoolsLoggerBuilder.cs | 16 ++++----- .../PowertoolsLoggerConfiguration.cs | 5 +++ .../PowertoolsLoggerFactory.cs | 11 +----- 5 files changed, 50 insertions(+), 19 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/ConfigurationExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/ConfigurationExtensions.cs index 4a8f6c8e..914c36cb 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/ConfigurationExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/ConfigurationExtensions.cs @@ -23,6 +23,7 @@ public static PowertoolsLoggerConfiguration CopyFrom(this PowertoolsLoggerConfig destination.LoggerOutput = source.LoggerOutput; destination.JsonOptions = source.JsonOptions; destination.TimestampFormat = source.TimestampFormat; + destination.LogFormatter = source.LogFormatter; return destination; } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs new file mode 100644 index 00000000..aaa4f684 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging.Internal.Helpers; + +/// +/// Helper class for creating and configuring logger factories +/// +internal static class LoggerFactoryHelper +{ + /// + /// Creates and configures a logger factory with the provided configuration + /// + /// The Powertools logger configuration to apply + /// The configured logger factory + public static ILoggerFactory CreateAndConfigureFactory(PowertoolsLoggerConfiguration configuration) + { + var factory = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.CopyFrom(configuration); + }); + }); + + // Configure the static logger with the factory + Logger.Configure(factory); + + // Apply formatter if one is specified + if (configuration.LogFormatter != null) + { + Logger.UseFormatter(configuration.LogFormatter); + } + + return factory; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs index b4411f74..32084e08 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs @@ -52,18 +52,16 @@ public PowertoolsLoggerBuilder WithOutput(ISystemWrapper output) _configuration.LoggerOutput = output; return this; } + + public PowertoolsLoggerBuilder WithFormatter(ILogFormatter formatter) + { + _configuration.LogFormatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); + return this; + } public ILogger Build() { - var factory = LoggerFactory.Create(builder => - { - builder.AddPowertoolsLogger(config => - { - config.CopyFrom(_configuration); - }); - }); - - Logger.Configure(factory); // Configure the static logger + var factory = LoggerFactoryHelper.CreateAndConfigureFactory(_configuration); return factory.CreatePowertoolsLogger(); } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs index 27e3ef2a..5bf25db4 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs @@ -74,6 +74,11 @@ public class PowertoolsLoggerConfiguration : IOptions public ISystemWrapper? LoggerOutput { get; set; } + /// + /// Custom log formatter to use for formatting log entries + /// + public ILogFormatter? LogFormatter { get; set; } + /// /// JSON serializer options to use for log serialization /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs index 7a0793a9..18640b7b 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs @@ -34,16 +34,7 @@ public static PowertoolsLoggerFactory Create(Action - { - builder.AddPowertoolsLogger(config => - { - config.CopyFrom(options); - }); - }); - - Logger.Configure(factory); - return factory; + return LoggerFactoryHelper.CreateAndConfigureFactory(options); } // Add builder pattern support From 490a4495cb785fcaef463e3ca461713560a20469 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 20 Mar 2025 12:39:49 +0000 Subject: [PATCH 11/49] update to PowertoolsLoggerProvider --- .../src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs | 4 ++-- .../{LoggerProvider.cs => PowertoolsLoggerProvider.cs} | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) rename libraries/src/AWS.Lambda.Powertools.Logging/Internal/{LoggerProvider.cs => PowertoolsLoggerProvider.cs} (96%) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs index 33143f3c..16a873ae 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs @@ -84,9 +84,9 @@ private static void RegisterServices(ILoggingBuilder builder) // Register the provider builder.Services.TryAddEnumerable( - ServiceDescriptor.Singleton()); + ServiceDescriptor.Singleton()); LoggerProviderOptions.RegisterProviderOptions - (builder.Services); + (builder.Services); } } \ 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/PowertoolsLoggerProvider.cs similarity index 96% rename from libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs rename to libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs index 852d9962..440b8985 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs @@ -28,7 +28,7 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// /// [ProviderAlias("PowertoolsLogger")] -internal sealed class LoggerProvider : ILoggerProvider +internal sealed class PowertoolsLoggerProvider : ILoggerProvider { /// /// The powertools configurations @@ -49,12 +49,12 @@ internal sealed class LoggerProvider : ILoggerProvider private PowertoolsLoggerConfiguration _currentConfig; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration. /// /// - public LoggerProvider(IOptionsMonitor config, + public PowertoolsLoggerProvider(IOptionsMonitor config, IPowertoolsConfigurations powertoolsConfigurations, ISystemWrapper? systemWrapper = null) { From 498f3e1d83933fa4d0061714d4e5de1e81202fc4 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 20 Mar 2025 18:49:46 +0000 Subject: [PATCH 12/49] log buffering 1 --- .../BuilderExtensions.cs | 96 +++++++---- .../Internal/BufferingLoggerProvider.cs | 87 ++++++++++ .../Internal/LogBuffer.cs | 101 ++++++++++++ .../Internal/LogBufferManager.cs | 65 ++++++++ .../Internal/LoggingAspect.cs | 1 + .../Internal/PowertoolsBufferingLogger.cs | 155 ++++++++++++++++++ .../Internal/PowertoolsLogger.cs | 23 ++- .../LogBufferingOptions.cs | 44 +++++ .../Logger.Buffer.cs | 40 +++++ .../LoggerExtensions.cs | 20 +++ .../PowertoolsLoggerBuilder.cs | 18 ++ .../PowertoolsLoggerConfiguration.cs | 5 + 12 files changed, 614 insertions(+), 41 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Internal/BufferingLoggerProvider.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Internal/LogBuffer.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Internal/LogBufferManager.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsBufferingLogger.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/LogBufferingOptions.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Logger.Buffer.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs index 16a873ae..36cabbdf 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; using AWS.Lambda.Powertools.Logging.Serializers; @@ -23,54 +24,50 @@ public static ILoggingBuilder AddPowertoolsLogger( // Add configuration builder.AddConfiguration(); - // Apply configuration if provided - if (configure != null) + // If no configuration was provided, register services with defaults + if (configure == null) { - // Create initial configuration - var options = new PowertoolsLoggerConfiguration(); - configure(options); + RegisterServices(builder); + return builder; + } + + // Create initial configuration + var options = new PowertoolsLoggerConfiguration(); + configure(options); - // IMPORTANT: Set the minimum level directly on the builder - if (options.MinimumLogLevel != LogLevel.None) - { - builder.SetMinimumLevel(options.MinimumLogLevel); - } - - // Add filters here + // IMPORTANT: Set the minimum level directly on the builder + if (options.MinimumLogLevel != LogLevel.None) + { + builder.SetMinimumLevel(options.MinimumLogLevel); + } - // Configure options for DI - builder.Services.Configure(configure); + // Configure options for DI + builder.Services.Configure(configure); - // Register services - RegisterServices(builder); + // Register services with the options + RegisterServices(builder, options); - // Apply the output case configuration - PowertoolsLoggingSerializer.ConfigureNamingPolicy(options.LoggerOutputCase); + // Apply the output case configuration + PowertoolsLoggingSerializer.ConfigureNamingPolicy(options.LoggerOutputCase); - // Configure static Logger (if not already in a configuration cycle) - if (!_configuring) + // Configure static Logger (if not already in a configuration cycle) + if (!_configuring) + { + try { - try - { - _configuring = true; - Logger.Configure(options); - } - finally - { - _configuring = false; - } + _configuring = true; + Logger.Configure(options); + } + finally + { + _configuring = false; } - } - else - { - // Register services even if no configuration was provided - RegisterServices(builder); } return builder; } - private static void RegisterServices(ILoggingBuilder builder) + private static void RegisterServices(ILoggingBuilder builder, PowertoolsLoggerConfiguration options = null) { // Register ISystemWrapper if not already registered builder.Services.TryAddSingleton(); @@ -82,7 +79,34 @@ private static void RegisterServices(ILoggingBuilder builder) builder.Services.TryAddSingleton(sp => new PowertoolsConfigurations(sp.GetRequiredService())); - // Register the provider + // If buffering is enabled, register it before the standard provider + if (options?.LogBufferingOptions?.Enabled == true) + { + // Add a filter for the buffer provider to capture logs at the buffer threshold + builder.AddFilter( + null, + options.LogBufferingOptions.BufferAtLogLevel); + + // Register the buffering provider + builder.Services.AddSingleton(sp => + { + var optionsMonitor = sp.GetRequiredService>(); + var powertoolsConfigs = sp.GetService() ?? + new PowertoolsConfigurations(sp.GetService() ?? + new PowertoolsEnvironment()); + var output = sp.GetService() ?? new SystemWrapper(); + + // Create a dedicated provider for buffering + var powerToolsProvider = new PowertoolsLoggerProvider(optionsMonitor, powertoolsConfigs, output); + + // Return the buffering provider + return new BufferingLoggerProvider( + powerToolsProvider, + optionsMonitor); + }); + } + + // Register the regular provider builder.Services.TryAddEnumerable( ServiceDescriptor.Singleton()); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/BufferingLoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/BufferingLoggerProvider.cs new file mode 100644 index 00000000..3f0f6c23 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/BufferingLoggerProvider.cs @@ -0,0 +1,87 @@ +/* + * 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.Concurrent; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace AWS.Lambda.Powertools.Logging.Internal; + +/// +/// Logger provider that supports buffering logs +/// +[ProviderAlias("PowertoolsBuffering")] +internal partial class BufferingLoggerProvider : ILoggerProvider +{ + private readonly ILoggerProvider _innerProvider; + private readonly ConcurrentDictionary _loggers = new(); + private readonly IOptionsMonitor _options; + + public BufferingLoggerProvider( + ILoggerProvider innerProvider, + IOptionsMonitor options) + { + _innerProvider = innerProvider ?? throw new ArgumentNullException(nameof(innerProvider)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + + // Register with the buffer manager + LogBufferManager.RegisterProvider(this); + } + + public ILogger CreateLogger(string categoryName) + { + return _loggers.GetOrAdd( + categoryName, + name => new BufferingLogger( + _innerProvider.CreateLogger(name), + _options, + name)); + } + + public void Dispose() + { + // Flush all buffers before disposing + foreach (var logger in _loggers.Values) + { + logger.FlushBuffer(); + } + + _innerProvider.Dispose(); + _loggers.Clear(); + } + + /// + /// Flush all buffered logs + /// + public void FlushBuffers() + { + foreach (var logger in _loggers.Values) + { + logger.FlushBuffer(); + } + } + + /// + /// Clear all buffered logs + /// + public void ClearBuffers() + { + foreach (var logger in _loggers.Values) + { + logger.ClearBuffer(); + } + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LogBuffer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LogBuffer.cs new file mode 100644 index 00000000..435d7133 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LogBuffer.cs @@ -0,0 +1,101 @@ +/* + * 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.Concurrent; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging.Internal; + +/// +/// A simplified buffer for storing log entries +/// +internal class LogBuffer +{ + // Simple storage for buffered messages + private readonly ConcurrentQueue _buffer = new(); + + // Keep track of approximate buffer size + private int _currentSize = 0; + + /// + /// Add a log entry to the buffer + /// + public void Add(string logEntry, int maxBytes) + { + // Estimate size (very roughly) + var size = 100 + (logEntry?.Length ?? 0) * 2; + + // Check if buffer is full - drop oldest entries until we have space + if (_currentSize + size > maxBytes) + { + while (_buffer.TryDequeue(out _) && _currentSize + size > maxBytes) + { + _currentSize -= 100; // Rough size per entry + } + + // Safety check - don't allow negative sizes + if (_currentSize < 0) _currentSize = 0; + } + + // Add to buffer + _buffer.Enqueue(logEntry); + _currentSize += size; + } + + /// + /// Get all entries and clear the buffer + /// + public IReadOnlyCollection GetAndClear() + { + var entries = new List(); + + try + { + while (_buffer.TryDequeue(out var entry)) + { + entries.Add(entry); + } + } + catch (Exception) + { + // If dequeuing fails, just clear and return what we have + Clear(); + } + + _currentSize = 0; + return entries; + } + + /// + /// Clear the buffer without returning entries + /// + public void Clear() + { + while (_buffer.TryDequeue(out _)) { } + _currentSize = 0; + } + + /// + /// Check if the buffer has any entries + /// + public bool HasEntries => !_buffer.IsEmpty; + + /// + /// Get the current estimated size of the buffer + /// + public int CurrentSize => _currentSize; +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LogBufferManager.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LogBufferManager.cs new file mode 100644 index 00000000..48e2ad7e --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LogBufferManager.cs @@ -0,0 +1,65 @@ +/* + * 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.Concurrent; + +namespace AWS.Lambda.Powertools.Logging.Internal; + +/// +/// Singleton manager for log buffer operations +/// +internal static class LogBufferManager +{ + private static BufferingLoggerProvider _provider; + + /// + /// Register a buffering provider with the manager + /// + internal static void RegisterProvider(BufferingLoggerProvider provider) + { + _provider = provider; + } + + /// + /// Flush all buffered logs + /// + internal static void FlushAllBuffers() + { + try + { + _provider?.FlushBuffers(); + } + catch (Exception) + { + // Suppress errors + } + } + + /// + /// Clear all buffered logs + /// + internal static void ClearAllBuffers() + { + try + { + _provider?.ClearBuffers(); + } + catch (Exception) + { + // Suppress errors + } + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index 4e41fc82..4089a0ad 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -169,6 +169,7 @@ public void OnEntry( } catch (Exception exception) { + _logger.FlushBuffer(); // The purpose of ExceptionDispatchInfo.Capture is to capture a potentially mutating exception's StackTrace at a point in time: // https://learn.microsoft.com/en-us/dotnet/standard/exceptions/best-practices-for-exceptions#capture-exceptions-to-rethrow-later ExceptionDispatchInfo.Capture(exception).Throw(); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsBufferingLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsBufferingLogger.cs new file mode 100644 index 00000000..90bc2688 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsBufferingLogger.cs @@ -0,0 +1,155 @@ +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace AWS.Lambda.Powertools.Logging.Internal; + +/// + /// Logger implementation that supports buffering + /// + internal class BufferingLogger : ILogger + { + private readonly ILogger _innerLogger; + private readonly IOptionsMonitor _options; + private readonly string _categoryName; + private readonly LogBuffer _buffer = new(); + + public BufferingLogger( + ILogger innerLogger, + IOptionsMonitor options, + string categoryName) + { + _innerLogger = innerLogger; + _options = options; + _categoryName = categoryName; + } + + public IDisposable BeginScope(TState state) + { + return _innerLogger.BeginScope(state); + } + + public bool IsEnabled(LogLevel logLevel) + { + var options = _options.CurrentValue; + + // If buffering is disabled, defer to inner logger + if (!options.LogBufferingOptions.Enabled) + { + return _innerLogger.IsEnabled(logLevel); + } + + // If the log level is at or above the configured minimum log level, + // let the inner logger decide + if (logLevel >= options.MinimumLogLevel) + { + return _innerLogger.IsEnabled(logLevel); + } + + // For logs below minimum level but at or above buffer threshold, + // we should handle them (buffer them) + if (logLevel >= options.LogBufferingOptions.BufferAtLogLevel) + { + return true; + } + + // Otherwise, the log level is below our buffer threshold + return false; + } + + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception exception, + Func formatter) + { + // Skip if logger is not enabled for this level + if (!IsEnabled(logLevel)) + return; + + var options = _options.CurrentValue; + var bufferOptions = options.LogBufferingOptions; + + // Check if this log should be buffered + bool shouldBuffer = bufferOptions.Enabled && + logLevel >= bufferOptions.BufferAtLogLevel && + logLevel < options.MinimumLogLevel; + + if (shouldBuffer) + { + // Add to buffer instead of logging + try + { + if (_innerLogger is PowertoolsLogger powertoolsLogger) + { + var logEntry = powertoolsLogger.LogEntryString(logLevel, state, exception, formatter); + _buffer.Add(logEntry, bufferOptions.MaxBytes); + } + } + catch (Exception ex) + { + // If buffering fails, try to log an error about it + try + { + _innerLogger.LogError(ex, "Failed to buffer log entry"); + } + catch + { + // Last resort: if even that fails, just suppress the error + } + } + } + else + { + // If this is an error and we should flush on error + if (bufferOptions.Enabled && + bufferOptions.FlushOnErrorLog && + logLevel >= LogLevel.Error) + { + FlushBuffer(); + } + } + } + + /// + /// Flush buffered logs to the inner logger + /// + public void FlushBuffer() + { + try + { + // Get all buffered entries + var entries = _buffer.GetAndClear(); + + if (_innerLogger is PowertoolsLogger powertoolsLogger) + { + // Log each entry directly + foreach (var entry in entries) + { + powertoolsLogger.LogLine(entry); + } + } + } + catch (Exception ex) + { + // If the entire flush operation fails, try to log an error + try + { + _innerLogger.LogError(ex, "Failed to flush log buffer"); + } + catch + { + // If even that fails, just suppress the error + } + } + } + + /// + /// Clear the buffer without logging + /// + public void ClearBuffer() + { + _buffer.Clear(); + } + } \ 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 902b40af..4aac91b5 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -46,7 +46,6 @@ internal sealed class PowertoolsLogger : ILogger /// private readonly ISystemWrapper _systemWrapper; - /// /// The current scope /// @@ -127,12 +126,27 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except { return; } + + _systemWrapper.LogLine(LogEntryString(logLevel, state, exception, formatter)); + } + + internal void LogLine(string message) + { + _systemWrapper.LogLine(message); + } + + internal string LogEntryString(LogLevel logLevel, TState state, Exception exception, Func formatter) + { + var logEntry = LogEntry(logLevel, state, exception, formatter); + return PowertoolsLoggingSerializer.Serialize(logEntry, typeof(object)); + } + internal object LogEntry(LogLevel logLevel, TState state, Exception exception, Func formatter) + { + var timestamp = DateTime.UtcNow; if (formatter is null) throw new ArgumentNullException(nameof(formatter)); - var timestamp = DateTime.UtcNow; - // Extract structured parameters for template-style logging var structuredParameters = ExtractStructuredParameters(state, out string messageTemplate); @@ -146,8 +160,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except var logEntry = logFormatter is null ? GetLogEntry(logLevel, timestamp, message, exception, structuredParameters) : GetFormattedLogEntry(logLevel, timestamp, message, exception, logFormatter, structuredParameters); - - _systemWrapper.LogLine(PowertoolsLoggingSerializer.Serialize(logEntry, typeof(object))); + return logEntry; } /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LogBufferingOptions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LogBufferingOptions.cs new file mode 100644 index 00000000..93852420 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LogBufferingOptions.cs @@ -0,0 +1,44 @@ +/* + * 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 Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging; + +/// +/// Configuration options for log buffering +/// +public class LogBufferingOptions +{ + /// + /// Gets or sets whether buffering is enabled + /// + public bool Enabled { get; set; } = false; + + /// + /// Gets or sets the maximum size of the buffer in bytes + /// + public int MaxBytes { get; set; } = 20480; + + /// + /// Gets or sets the minimum log level to buffer + /// + public LogLevel BufferAtLogLevel { get; set; } = LogLevel.Debug; + + /// + /// Gets or sets whether to flush the buffer when logging an error + /// + public bool FlushOnErrorLog { get; set; } = true; +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Buffer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Buffer.cs new file mode 100644 index 00000000..52a5c94a --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Buffer.cs @@ -0,0 +1,40 @@ +/* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * .cs +/* + * 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 AWS.Lambda.Powertools.Logging.Internal; + +namespace AWS.Lambda.Powertools.Logging; + +public static partial class Logger +{ + /// + /// Flush any buffered logs + /// + public static void FlushBuffer() + { + // Use the buffer manager directly + LogBufferManager.FlushAllBuffers(); + } + + /// + /// Clear any buffered logs without writing them + /// + internal static void ClearBuffer() + { + // Use the buffer manager directly + LogBufferManager.ClearAllBuffers(); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LoggerExtensions.cs index 0c1c6de8..23302cc4 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LoggerExtensions.cs @@ -722,4 +722,24 @@ public static void RemoveKey(this ILogger logger, string key) { Logger.RemoveKey(key); } + + // Replace the buffer methods with direct calls to the manager + + /// + /// Flush any buffered logs + /// + public static void FlushBuffer(this ILogger logger) + { + // Direct call to the buffer manager to avoid any recursion + LogBufferManager.FlushAllBuffers(); + } + + /// + /// Clear any buffered logs without writing them + /// + internal static void ClearBuffer(this ILogger logger) + { + // Direct call to the buffer manager to avoid any recursion + LogBufferManager.ClearAllBuffers(); + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs index 32084e08..c583e42a 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs @@ -58,6 +58,24 @@ public PowertoolsLoggerBuilder WithFormatter(ILogFormatter formatter) _configuration.LogFormatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); return this; } + + /// + /// Enable log buffering with default options + /// + public PowertoolsLoggerBuilder WithLogBuffering(bool enabled = true) + { + _configuration.LogBufferingOptions.Enabled = enabled; + return this; + } + + /// + /// Configure log buffering options + /// + public PowertoolsLoggerBuilder WithLogBuffering(Action configure) + { + configure?.Invoke(_configuration.LogBufferingOptions); + return this; + } public ILogger Build() { diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs index 5bf25db4..b4d226ed 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs @@ -99,6 +99,11 @@ public JsonSerializerOptions? JsonOptions } } + /// + /// Options for log buffering + /// + public LogBufferingOptions LogBufferingOptions { get; set; } = new LogBufferingOptions(); + #if NET8_0_OR_GREATER /// /// Default JSON serializer context From 7a28645006947f2e9afef0966e3c6de967568ba3 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Fri, 21 Mar 2025 11:31:26 +0000 Subject: [PATCH 13/49] buffer 2 --- .../BuilderExtensions.cs | 35 ++-- .../{ => Buffer}/BufferingLoggerProvider.cs | 15 +- .../Internal/Buffer/LogBuffer.cs | 150 ++++++++++++++++++ .../Internal/{ => Buffer}/LogBufferManager.cs | 21 ++- .../{ => Internal/Buffer}/Logger.Buffer.cs | 6 +- .../{ => Buffer}/PowertoolsBufferingLogger.cs | 12 +- .../Helpers/ConfigurationExtensions.cs | 2 + .../Internal/LogBuffer.cs | 101 ------------ .../Internal/LoggingAspect.cs | 31 +++- .../LoggerExtensions.cs | 6 +- .../LoggingAttribute.cs | 6 + 11 files changed, 240 insertions(+), 145 deletions(-) rename libraries/src/AWS.Lambda.Powertools.Logging/Internal/{ => Buffer}/BufferingLoggerProvider.cs (85%) create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs rename libraries/src/AWS.Lambda.Powertools.Logging/Internal/{ => Buffer}/LogBufferManager.cs (69%) rename libraries/src/AWS.Lambda.Powertools.Logging/{ => Internal/Buffer}/Logger.Buffer.cs (90%) rename libraries/src/AWS.Lambda.Powertools.Logging/Internal/{ => Buffer}/PowertoolsBufferingLogger.cs (94%) delete mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Internal/LogBuffer.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs index 36cabbdf..5efd2103 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs @@ -79,31 +79,26 @@ private static void RegisterServices(ILoggingBuilder builder, PowertoolsLoggerCo builder.Services.TryAddSingleton(sp => new PowertoolsConfigurations(sp.GetRequiredService())); - // If buffering is enabled, register it before the standard provider + // If buffering is enabled, register buffer providers if (options?.LogBufferingOptions?.Enabled == true) { - // Add a filter for the buffer provider to capture logs at the buffer threshold + // Add a filter for the buffer provider builder.AddFilter( null, options.LogBufferingOptions.BufferAtLogLevel); - - // Register the buffering provider - builder.Services.AddSingleton(sp => - { - var optionsMonitor = sp.GetRequiredService>(); - var powertoolsConfigs = sp.GetService() ?? - new PowertoolsConfigurations(sp.GetService() ?? - new PowertoolsEnvironment()); - var output = sp.GetService() ?? new SystemWrapper(); - - // Create a dedicated provider for buffering - var powerToolsProvider = new PowertoolsLoggerProvider(optionsMonitor, powertoolsConfigs, output); - - // Return the buffering provider - return new BufferingLoggerProvider( - powerToolsProvider, - optionsMonitor); - }); + + // Register the inner provider factory + builder.Services.TryAddSingleton(sp => + new BufferingLoggerProvider( + // Create a new PowertoolsLoggerProvider specifically for buffering + new PowertoolsLoggerProvider( + sp.GetRequiredService>(), + sp.GetRequiredService(), + sp.GetRequiredService() + ), + sp.GetRequiredService>() + ) + ); } // Register the regular provider diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/BufferingLoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs similarity index 85% rename from libraries/src/AWS.Lambda.Powertools.Logging/Internal/BufferingLoggerProvider.cs rename to libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs index 3f0f6c23..3802f042 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/BufferingLoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs @@ -27,7 +27,7 @@ namespace AWS.Lambda.Powertools.Logging.Internal; internal partial class BufferingLoggerProvider : ILoggerProvider { private readonly ILoggerProvider _innerProvider; - private readonly ConcurrentDictionary _loggers = new(); + private readonly ConcurrentDictionary _loggers = new(); private readonly IOptionsMonitor _options; public BufferingLoggerProvider( @@ -45,7 +45,7 @@ public ILogger CreateLogger(string categoryName) { return _loggers.GetOrAdd( categoryName, - name => new BufferingLogger( + name => new PowertoolsBufferingLogger( _innerProvider.CreateLogger(name), _options, name)); @@ -84,4 +84,15 @@ public void ClearBuffers() logger.ClearBuffer(); } } + + /// + /// Clear buffered logs for the current invocation only + /// + public void ClearCurrentBuffer() + { + foreach (var logger in _loggers.Values) + { + logger.ClearCurrentInvocation(); + } + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs new file mode 100644 index 00000000..fd233c70 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs @@ -0,0 +1,150 @@ +/* + * 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.Concurrent; +using System.Collections.Generic; +using System.Threading; +using Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging.Internal; + +/// +/// A buffer for storing log entries, with isolation per Lambda invocation +/// +internal class LogBuffer +{ + // Use AsyncLocal for automatic context flow across async calls + private static readonly AsyncLocal _currentInvocationId = new AsyncLocal(); + + // Dictionary of buffers by invocation ID + private readonly ConcurrentDictionary _buffersByInvocation = new(); + + // Get the current invocation ID or create a fallback + private string CurrentInvocationId => _currentInvocationId.Value; + + /// + /// Set the current invocation ID (call this at the start of a Lambda invocation) + /// + public static void SetCurrentInvocationId(string invocationId) + { + _currentInvocationId.Value = invocationId; + } + + /// + /// Add a log entry to the buffer for the current invocation + /// + public void Add(string logEntry, int maxBytes) + { + var invocationId = CurrentInvocationId; + var buffer = _buffersByInvocation.GetOrAdd(invocationId, _ => new InvocationBuffer()); + buffer.Add(logEntry, maxBytes); + } + + /// + /// Get all entries for the current invocation and clear that buffer + /// + public IReadOnlyCollection GetAndClear() + { + var invocationId = CurrentInvocationId; + + // Try to get and remove the buffer for this invocation + if (_buffersByInvocation.TryRemove(invocationId, out var buffer)) + { + return buffer.GetAndClear(); + } + + return Array.Empty(); + } + + /// + /// Clear all buffers + /// + public void Clear() + { + _buffersByInvocation.Clear(); + } + + /// + /// Clear buffer for the current invocation + /// + public void ClearCurrentInvocation() + { + var invocationId = CurrentInvocationId; + if (_buffersByInvocation.TryRemove(invocationId, out _)) {} + } + + /// + /// Check if the current invocation has any buffered entries + /// + public bool HasEntries + { + get + { + var invocationId = CurrentInvocationId; + return _buffersByInvocation.TryGetValue(invocationId, out var buffer) && buffer.HasEntries; + } + } + + /// + /// Buffer for a specific invocation + /// + private class InvocationBuffer + { + private readonly ConcurrentQueue _buffer = new(); + private int _currentSize = 0; + + public void Add(string logEntry, int maxBytes) + { + // Same implementation as before + var size = 100 + (logEntry?.Length ?? 0) * 2; + + if (_currentSize + size > maxBytes) + { + while (_buffer.TryDequeue(out _) && _currentSize + size > maxBytes) + { + _currentSize -= 100; + } + + if (_currentSize < 0) _currentSize = 0; + } + + _buffer.Enqueue(logEntry); + _currentSize += size; + } + + public IReadOnlyCollection GetAndClear() + { + var entries = new List(); + + try + { + while (_buffer.TryDequeue(out var entry)) + { + entries.Add(entry); + } + } + catch (Exception) + { + _buffer.Clear(); + } + + _currentSize = 0; + return entries; + } + + public bool HasEntries => !_buffer.IsEmpty; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LogBufferManager.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBufferManager.cs similarity index 69% rename from libraries/src/AWS.Lambda.Powertools.Logging/Internal/LogBufferManager.cs rename to libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBufferManager.cs index 48e2ad7e..5eea9ec5 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LogBufferManager.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBufferManager.cs @@ -14,12 +14,11 @@ */ using System; -using System.Collections.Concurrent; namespace AWS.Lambda.Powertools.Logging.Internal; /// -/// Singleton manager for log buffer operations +/// Singleton manager for log buffer operations with invocation context awareness /// internal static class LogBufferManager { @@ -34,9 +33,17 @@ internal static void RegisterProvider(BufferingLoggerProvider provider) } /// - /// Flush all buffered logs + /// Set the current invocation ID to isolate logs between Lambda invocations /// - internal static void FlushAllBuffers() + public static void SetInvocationId(string invocationId) + { + LogBuffer.SetCurrentInvocationId(invocationId); + } + + /// + /// Flush buffered logs for the current invocation + /// + internal static void FlushCurrentBuffer() { try { @@ -49,13 +56,13 @@ internal static void FlushAllBuffers() } /// - /// Clear all buffered logs + /// Clear buffered logs for the current invocation /// - internal static void ClearAllBuffers() + internal static void ClearCurrentBuffer() { try { - _provider?.ClearBuffers(); + _provider?.ClearCurrentBuffer(); } catch (Exception) { diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Buffer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/Logger.Buffer.cs similarity index 90% rename from libraries/src/AWS.Lambda.Powertools.Logging/Logger.Buffer.cs rename to libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/Logger.Buffer.cs index 52a5c94a..9e715c55 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Buffer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/Logger.Buffer.cs @@ -26,15 +26,15 @@ public static partial class Logger public static void FlushBuffer() { // Use the buffer manager directly - LogBufferManager.FlushAllBuffers(); + LogBufferManager.FlushCurrentBuffer(); } /// /// Clear any buffered logs without writing them /// - internal static void ClearBuffer() + public static void ClearBuffer() { // Use the buffer manager directly - LogBufferManager.ClearAllBuffers(); + LogBufferManager.ClearCurrentBuffer(); } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsBufferingLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs similarity index 94% rename from libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsBufferingLogger.cs rename to libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs index 90bc2688..b2c9da5a 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsBufferingLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs @@ -7,14 +7,14 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// /// Logger implementation that supports buffering /// - internal class BufferingLogger : ILogger + internal class PowertoolsBufferingLogger : ILogger { private readonly ILogger _innerLogger; private readonly IOptionsMonitor _options; private readonly string _categoryName; private readonly LogBuffer _buffer = new(); - public BufferingLogger( + public PowertoolsBufferingLogger( ILogger innerLogger, IOptionsMonitor options, string categoryName) @@ -152,4 +152,12 @@ public void ClearBuffer() { _buffer.Clear(); } + + /// + /// Clear buffered logs only for the current invocation + /// + public void ClearCurrentInvocation() + { + _buffer.ClearCurrentInvocation(); + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/ConfigurationExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/ConfigurationExtensions.cs index 914c36cb..c8206123 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/ConfigurationExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/ConfigurationExtensions.cs @@ -24,6 +24,8 @@ public static PowertoolsLoggerConfiguration CopyFrom(this PowertoolsLoggerConfig destination.JsonOptions = source.JsonOptions; destination.TimestampFormat = source.TimestampFormat; destination.LogFormatter = source.LogFormatter; + destination.LogLevelKey = source.LogLevelKey; + destination.LogBufferingOptions = source.LogBufferingOptions; return destination; } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LogBuffer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LogBuffer.cs deleted file mode 100644 index 435d7133..00000000 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LogBuffer.cs +++ /dev/null @@ -1,101 +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.Concurrent; -using System.Collections.Generic; -using Microsoft.Extensions.Logging; - -namespace AWS.Lambda.Powertools.Logging.Internal; - -/// -/// A simplified buffer for storing log entries -/// -internal class LogBuffer -{ - // Simple storage for buffered messages - private readonly ConcurrentQueue _buffer = new(); - - // Keep track of approximate buffer size - private int _currentSize = 0; - - /// - /// Add a log entry to the buffer - /// - public void Add(string logEntry, int maxBytes) - { - // Estimate size (very roughly) - var size = 100 + (logEntry?.Length ?? 0) * 2; - - // Check if buffer is full - drop oldest entries until we have space - if (_currentSize + size > maxBytes) - { - while (_buffer.TryDequeue(out _) && _currentSize + size > maxBytes) - { - _currentSize -= 100; // Rough size per entry - } - - // Safety check - don't allow negative sizes - if (_currentSize < 0) _currentSize = 0; - } - - // Add to buffer - _buffer.Enqueue(logEntry); - _currentSize += size; - } - - /// - /// Get all entries and clear the buffer - /// - public IReadOnlyCollection GetAndClear() - { - var entries = new List(); - - try - { - while (_buffer.TryDequeue(out var entry)) - { - entries.Add(entry); - } - } - catch (Exception) - { - // If dequeuing fails, just clear and return what we have - Clear(); - } - - _currentSize = 0; - return entries; - } - - /// - /// Clear the buffer without returning entries - /// - public void Clear() - { - while (_buffer.TryDequeue(out _)) { } - _currentSize = 0; - } - - /// - /// Check if the buffer has any entries - /// - public bool HasEntries => !_buffer.IsEmpty; - - /// - /// Get the current estimated size of the buffer - /// - public int CurrentSize => _currentSize; -} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index 4089a0ad..d70f4a1c 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -14,8 +14,6 @@ */ using System; -using System.Collections.Concurrent; -using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -62,9 +60,10 @@ public class LoggingAspect private bool _clearLambdaContext; private ILogger _logger; - private readonly bool _LogEventEnv; + private readonly bool _logEventEnv; private readonly string _xRayTraceId; private bool _isDebug; + private bool _bufferingEnabled; /// @@ -73,7 +72,7 @@ public class LoggingAspect /// The Powertools configurations. public LoggingAspect(IPowertoolsConfigurations powertoolsConfigurations) { - _LogEventEnv = powertoolsConfigurations.LoggerLogEvent; + _logEventEnv = powertoolsConfigurations.LoggerLogEvent; _xRayTraceId = powertoolsConfigurations.XRayTraceId; } @@ -86,7 +85,7 @@ private void InitializeLogger(LoggingAttribute trigger) trigger.SamplingRate > 0); // Configure logger if not configured or we have explicit settings - if (!Logger.IsConfigured || hasExplicitSettings) + if (!Logger.IsConfigured ) { // Create configuration with default values when not explicitly specified var config = new PowertoolsLoggerConfiguration @@ -107,6 +106,7 @@ private void InitializeLogger(LoggingAttribute trigger) // Set debug flag based on the minimum level from Logger _isDebug = Logger.GetConfiguration().MinimumLogLevel <= LogLevel.Debug; + _bufferingEnabled = Logger.GetConfiguration().LogBufferingOptions.Enabled; } /// @@ -163,13 +163,23 @@ public void OnEntry( var eventObject = eventArgs.Args.FirstOrDefault(); CaptureXrayTraceId(); CaptureLambdaContext(eventArgs); + + if(_bufferingEnabled) + { + LogBufferManager.SetInvocationId(LoggingLambdaContext.Instance.AwsRequestId); + } + CaptureCorrelationId(eventObject, trigger.CorrelationIdPath); - if (logEvent || _LogEventEnv) + if (logEvent || _logEventEnv) LogEvent(eventObject); } catch (Exception exception) { - _logger.FlushBuffer(); + if (_bufferingEnabled && trigger.FlushBufferOnUncaughtError) + { + _logger.FlushBuffer(); + } + // The purpose of ExceptionDispatchInfo.Capture is to capture a potentially mutating exception's StackTrace at a point in time: // https://learn.microsoft.com/en-us/dotnet/standard/exceptions/best-practices-for-exceptions#capture-exceptions-to-rethrow-later ExceptionDispatchInfo.Capture(exception).Throw(); @@ -189,6 +199,12 @@ public void OnExit() if (_clearState) _logger.RemoveAllKeys(); _initializeContext = true; + + if (_bufferingEnabled) + { + // clear the buffer after the handler has finished + _logger.ClearBuffer(); + } } /// @@ -220,6 +236,7 @@ private void CaptureLambdaContext(AspectEventArgs eventArgs) /// Captures the correlation identifier. /// /// The event argument. + /// private void CaptureCorrelationId(object eventArg, string correlationIdPath) { if (string.IsNullOrWhiteSpace(correlationIdPath)) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LoggerExtensions.cs index 23302cc4..21e8dd6a 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LoggerExtensions.cs @@ -731,15 +731,15 @@ public static void RemoveKey(this ILogger logger, string key) public static void FlushBuffer(this ILogger logger) { // Direct call to the buffer manager to avoid any recursion - LogBufferManager.FlushAllBuffers(); + LogBufferManager.FlushCurrentBuffer(); } /// /// Clear any buffered logs without writing them /// - internal static void ClearBuffer(this ILogger logger) + public static void ClearBuffer(this ILogger logger) { // Direct call to the buffer manager to avoid any recursion - LogBufferManager.ClearAllBuffers(); + LogBufferManager.ClearCurrentBuffer(); } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs index 4a5da930..ba7402bd 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs @@ -171,4 +171,10 @@ public class LoggingAttribute : Attribute /// /// The log level. public LoggerOutputCase LoggerOutputCase { get; set; } = LoggerOutputCase.Default; + + /// + /// Flush buffer on uncaught error + /// When buffering is enabled, this property will flush the buffer on uncaught exceptions + /// + public bool FlushBufferOnUncaughtError { get; set; } } \ No newline at end of file From 9d48307dfedf05ecc5690a40e92b8186e09eb73f Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Fri, 21 Mar 2025 18:45:55 +0000 Subject: [PATCH 14/49] buffer 3 --- ...owertoolsLoggerConfigurationExtensions.cs} | 2 +- .../Internal/LoggingAspect.cs | 61 +++++++++++++------ .../Internal/PowertoolsLoggerProvider.cs | 2 +- .../PowertoolsLoggingSerializer.cs | 56 ++++++++++------- 4 files changed, 77 insertions(+), 44 deletions(-) rename libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/{ConfigurationExtensions.cs => PowertoolsLoggerConfigurationExtensions.cs} (95%) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/ConfigurationExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerConfigurationExtensions.cs similarity index 95% rename from libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/ConfigurationExtensions.cs rename to libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerConfigurationExtensions.cs index c8206123..2b109f15 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/ConfigurationExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerConfigurationExtensions.cs @@ -6,7 +6,7 @@ namespace AWS.Lambda.Powertools.Logging.Internal.Helpers; /// /// Extension methods for handling configuration copying between PowertoolsLogger configurations /// -internal static class ConfigurationExtensions +internal static class PowertoolsLoggerConfigurationExtensions { /// /// Copies configuration values from source to destination configuration diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index d70f4a1c..b7067074 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -74,39 +74,60 @@ public LoggingAspect(IPowertoolsConfigurations powertoolsConfigurations) { _logEventEnv = powertoolsConfigurations.LoggerLogEvent; _xRayTraceId = powertoolsConfigurations.XRayTraceId; + // Get Logger Instance + // This is a singleton, so we can reuse the same instance + _logger = Logger.GetPowertoolsLogger(); } private void InitializeLogger(LoggingAttribute trigger) - { - // Always check for explicit settings - bool hasExplicitSettings = (trigger.LogLevel != LogLevel.None || - !string.IsNullOrEmpty(trigger.Service) || - trigger.LoggerOutputCase != default || - trigger.SamplingRate > 0); + { + // Check which settings are explicitly provided in the attribute + var hasLogLevel = trigger.LogLevel != LogLevel.None; + var hasService = !string.IsNullOrEmpty(trigger.Service); + var hasOutputCase = trigger.LoggerOutputCase != default; + var hasSamplingRate = trigger.SamplingRate > 0; + + var hasExplicitSettings = hasLogLevel || hasService || hasOutputCase || hasSamplingRate; - // Configure logger if not configured or we have explicit settings - if (!Logger.IsConfigured ) + if (!Logger.IsConfigured) { - // Create configuration with default values when not explicitly specified + // First time initialization - create a new configuration with defaults for any unspecified values var config = new PowertoolsLoggerConfiguration { - // Use sensible defaults if not specified in the attribute - MinimumLogLevel = trigger.LogLevel != LogLevel.None ? trigger.LogLevel : LogLevel.Information, - Service = !string.IsNullOrEmpty(trigger.Service) ? trigger.Service : "service_undefined", - LoggerOutputCase = trigger.LoggerOutputCase != default ? trigger.LoggerOutputCase : LoggerOutputCase.SnakeCase, - SamplingRate = trigger.SamplingRate > 0 ? trigger.SamplingRate : 1.0 + MinimumLogLevel = hasLogLevel ? trigger.LogLevel : LogLevel.Information, + Service = hasService ? trigger.Service : "service_undefined", + LoggerOutputCase = hasOutputCase ? trigger.LoggerOutputCase : LoggerOutputCase.SnakeCase, + SamplingRate = hasSamplingRate ? trigger.SamplingRate : 1.0 }; - // Configure the logger with our configuration Logger.Configure(config); } + else if (hasExplicitSettings) + { + // Preserve existing configuration and only override what's explicitly specified + Logger.UpdateConfiguration(config => { + if (hasLogLevel) + config.MinimumLogLevel = trigger.LogLevel; + + if (hasService) + config.Service = trigger.Service; + + if (hasOutputCase) + config.LoggerOutputCase = trigger.LoggerOutputCase; + + if (hasSamplingRate) + config.SamplingRate = trigger.SamplingRate; + }); + } - // Get logger after configuration - _logger = Logger.GetPowertoolsLogger(); - // Set debug flag based on the minimum level from Logger - _isDebug = Logger.GetConfiguration().MinimumLogLevel <= LogLevel.Debug; - _bufferingEnabled = Logger.GetConfiguration().LogBufferingOptions.Enabled; + + // Fetch the current configuration + var currentConfig = Logger.GetConfiguration(); + + // Set operational flags based on current configuration + _isDebug = currentConfig.MinimumLogLevel <= LogLevel.Debug; + _bufferingEnabled = currentConfig.LogBufferingOptions?.Enabled ?? false; } /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs index 440b8985..e4e2c4af 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs @@ -129,7 +129,7 @@ private void ApplyPowertoolsConfig(PowertoolsLoggerConfiguration config) } // Always configure the serializer with the output case - PowertoolsLoggingSerializer.ConfigureNamingPolicy(config.LoggerOutputCase); + // PowertoolsLoggingSerializer.ConfigureNamingPolicy(config.LoggerOutputCase); // Configure the log level key based on output case config.LogLevelKey = _powertoolsConfigurations.LambdaLogLevelEnabled() && diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs index 9407dcfa..40e7d793 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -76,7 +76,7 @@ internal static void ConfigureNamingPolicy(LoggerOutputCase loggerOutputCase) // Only rebuild options if they already exist if (_jsonOptions != null) { - BuildJsonSerializerOptions(); + SetOutputCase(); } } } @@ -194,27 +194,8 @@ internal static void BuildJsonSerializerOptions(JsonSerializerOptions options = { // This should already be in a lock when called _jsonOptions = options ?? 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 -#if NET8_0_OR_GREATER - _jsonOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; - _jsonOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; -#else - _jsonOptions.PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance; - _jsonOptions.DictionaryKeyPolicy = SnakeCaseNamingPolicy.Instance; -#endif - break; - } + + SetOutputCase(); AddConverters(); @@ -239,6 +220,37 @@ internal static void BuildJsonSerializerOptions(JsonSerializerOptions options = } } + private static void SetOutputCase() + { + 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 +#if NET8_0_OR_GREATER + // If is default (Not Set) and JsonOptions provided with DictionaryKeyPolicy or PropertyNamingPolicy, use it + if (_jsonOptions.DictionaryKeyPolicy != null || _jsonOptions.PropertyNamingPolicy != null) + { + _jsonOptions.DictionaryKeyPolicy = _jsonOptions.DictionaryKeyPolicy; + _jsonOptions.PropertyNamingPolicy = _jsonOptions.PropertyNamingPolicy; + }else{ + _jsonOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; + _jsonOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; + } +#else + _jsonOptions.PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance; + _jsonOptions.DictionaryKeyPolicy = SnakeCaseNamingPolicy.Instance; +#endif + break; + } + } + private static void AddConverters() { _jsonOptions.Converters.Add(new ByteArrayConverter()); From 29690e2465196cbeab09246f2b9a9ac9f6ee793b Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Fri, 21 Mar 2025 20:03:19 +0000 Subject: [PATCH 15/49] buffer4 --- libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs index bdcdbe72..9df769ea 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs @@ -105,6 +105,17 @@ internal static PowertoolsLoggerConfiguration GetConfiguration() public static ILogger GetPowertoolsLogger() => Factory.CreatePowertoolsLogger(); + // Update configuration settings + internal static void UpdateConfiguration(Action configureAction) + { + if (configureAction == null) return; + + // Apply updates to current configuration + configureAction(_currentConfig); + + // Apply any output case changes + _currentConfig.ApplyOutputCase(); + } // For testing purposes // internal static void Reset() // { From fc7b26b32bcb2dbf266a763f55d1f4b504e23037 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Sat, 22 Mar 2025 20:33:52 +0000 Subject: [PATCH 16/49] refactor: clean up whitespace and improve logger configuration handling --- .../BuilderExtensions.cs | 11 +- .../Internal/LoggingAspect.cs | 38 ++--- .../PowertoolsLoggerConfiguration.cs | 131 +----------------- .../PowertoolsLoggingSerializer.cs | 70 ++++++++-- 4 files changed, 87 insertions(+), 163 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs index 5efd2103..d0c35c75 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs @@ -1,8 +1,6 @@ using System; -using System.Linq; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; -using AWS.Lambda.Powertools.Logging.Serializers; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; @@ -11,12 +9,17 @@ namespace AWS.Lambda.Powertools.Logging; +/// +/// Extension methods for configuring the Powertools logger +/// public static class BuilderExtensions { // Track if we're in the middle of configuration to prevent recursion private static bool _configuring = false; - // Single base method that all other overloads call + /// + /// Adds the Powertools logger to the logging builder. + /// public static ILoggingBuilder AddPowertoolsLogger( this ILoggingBuilder builder, Action? configure = null) @@ -48,7 +51,7 @@ public static ILoggingBuilder AddPowertoolsLogger( RegisterServices(builder, options); // Apply the output case configuration - PowertoolsLoggingSerializer.ConfigureNamingPolicy(options.LoggerOutputCase); + options.ApplyOutputCase(); // Configure static Logger (if not already in a configuration cycle) if (!_configuring) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index b7067074..66c2482d 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -58,7 +58,7 @@ public class LoggingAspect /// Specify to clear Lambda Context on exit /// private bool _clearLambdaContext; - + private ILogger _logger; private readonly bool _logEventEnv; private readonly string _xRayTraceId; @@ -75,10 +75,9 @@ public LoggingAspect(IPowertoolsConfigurations powertoolsConfigurations) _logEventEnv = powertoolsConfigurations.LoggerLogEvent; _xRayTraceId = powertoolsConfigurations.XRayTraceId; // Get Logger Instance - // This is a singleton, so we can reuse the same instance _logger = Logger.GetPowertoolsLogger(); } - + private void InitializeLogger(LoggingAttribute trigger) { // Check which settings are explicitly provided in the attribute @@ -86,9 +85,9 @@ private void InitializeLogger(LoggingAttribute trigger) var hasService = !string.IsNullOrEmpty(trigger.Service); var hasOutputCase = trigger.LoggerOutputCase != default; var hasSamplingRate = trigger.SamplingRate > 0; - + var hasExplicitSettings = hasLogLevel || hasService || hasOutputCase || hasSamplingRate; - + if (!Logger.IsConfigured) { // First time initialization - create a new configuration with defaults for any unspecified values @@ -99,32 +98,32 @@ private void InitializeLogger(LoggingAttribute trigger) LoggerOutputCase = hasOutputCase ? trigger.LoggerOutputCase : LoggerOutputCase.SnakeCase, SamplingRate = hasSamplingRate ? trigger.SamplingRate : 1.0 }; - + Logger.Configure(config); } else if (hasExplicitSettings) { // Preserve existing configuration and only override what's explicitly specified - Logger.UpdateConfiguration(config => { + Logger.UpdateConfiguration(config => + { if (hasLogLevel) config.MinimumLogLevel = trigger.LogLevel; - + if (hasService) config.Service = trigger.Service; - + if (hasOutputCase) config.LoggerOutputCase = trigger.LoggerOutputCase; - + if (hasSamplingRate) config.SamplingRate = trigger.SamplingRate; }); } - - - + + // Fetch the current configuration var currentConfig = Logger.GetConfiguration(); - + // Set operational flags based on current configuration _isDebug = currentConfig.MinimumLogLevel <= LogLevel.Debug; _bufferingEnabled = currentConfig.LogBufferingOptions?.Enabled ?? false; @@ -184,12 +183,12 @@ public void OnEntry( var eventObject = eventArgs.Args.FirstOrDefault(); CaptureXrayTraceId(); CaptureLambdaContext(eventArgs); - - if(_bufferingEnabled) + + if (_bufferingEnabled) { LogBufferManager.SetInvocationId(LoggingLambdaContext.Instance.AwsRequestId); } - + CaptureCorrelationId(eventObject, trigger.CorrelationIdPath); if (logEvent || _logEventEnv) LogEvent(eventObject); @@ -235,7 +234,8 @@ private void CaptureXrayTraceId() { if (string.IsNullOrWhiteSpace(_xRayTraceId)) return; - _logger.AppendKey(LoggingConstants.KeyXRayTraceId, _xRayTraceId.Split(';', StringSplitOptions.RemoveEmptyEntries)[0].Replace("Root=", "")); + _logger.AppendKey(LoggingConstants.KeyXRayTraceId, + _xRayTraceId.Split(';', StringSplitOptions.RemoveEmptyEntries)[0].Replace("Root=", "")); } /// @@ -290,7 +290,7 @@ private void CaptureCorrelationId(object eventArg, string correlationIdPath) { // For casing parsing to be removed from Logging v2 when we get rid of outputcase // without this CorrelationIdPaths.ApiGatewayRest would not work - + // TODO: fix this // var pathWithOutputCase = // _powertoolsConfigurations.ConvertToOutputCase(correlationIdPaths[i], _config.LoggerOutputCase); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs index b4d226ed..ed4903db 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs @@ -13,16 +13,10 @@ * permissions and limitations under the License. */ -using System; -using System.Collections.Generic; -using System.Collections.Concurrent; using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using AWS.Lambda.Powertools.Common; -using AWS.Lambda.Powertools.Common.Utils; using AWS.Lambda.Powertools.Logging.Serializers; namespace AWS.Lambda.Powertools.Logging; @@ -91,10 +85,7 @@ public JsonSerializerOptions? JsonOptions _jsonOptions = value; if (_jsonOptions != null) { -#if NET8_0_OR_GREATER - HandleJsonOptionsTypeResolver(_jsonOptions); -#endif - ApplyJsonOptions(); + PowertoolsLoggingSerializer.SetOptions(_jsonOptions); } } } @@ -104,125 +95,6 @@ public JsonSerializerOptions? JsonOptions /// public LogBufferingOptions LogBufferingOptions { get; set; } = new LogBufferingOptions(); -#if NET8_0_OR_GREATER - /// - /// Default JSON serializer context - /// - private JsonSerializerContext? _jsonContext = PowertoolsLoggingSerializationContext.Default; - private readonly List _additionalContexts = new(); - - /// - /// Add additional JsonSerializerContext for client types - /// - internal void AddJsonContext(JsonSerializerContext context) - { - if (context == null) - return; - - // Don't add duplicates - if (!_additionalContexts.Contains(context)) - { - _additionalContexts.Add(context); - ApplyAdditionalJsonContext(context); - - // If we have existing JSON options, update their type resolver - if (_jsonOptions != null && !RuntimeFeatureWrapper.IsDynamicCodeSupported) - { - // Reset the type resolver chain to rebuild it - _jsonOptions.TypeInfoResolver = GetCompositeResolver(); - } - } - } - - /// - /// Get all additional contexts - /// - internal IReadOnlyList GetAdditionalContexts() - { - return _additionalContexts.AsReadOnly(); - } - - private IJsonTypeInfoResolver? _customTypeInfoResolver = null; - private List? _customTypeInfoResolvers; - - /// - /// Process JSON options type resolver information - /// - internal void HandleJsonOptionsTypeResolver(JsonSerializerOptions options) - { - if (options == null) return; - - // Check for TypeInfoResolver and ensure it's not lost - if (options.TypeInfoResolver != null && - options.TypeInfoResolver != GetCompositeResolver()) - { - _customTypeInfoResolver = options.TypeInfoResolver; - - // If it's a JsonSerializerContext, also add it to our contexts - if (_customTypeInfoResolver is JsonSerializerContext jsonContext) - { - AddJsonContext(jsonContext); - } - } - } - - /// - /// Get a composite resolver that includes all configured resolvers - /// - internal IJsonTypeInfoResolver GetCompositeResolver() - { - var resolvers = new List(); - - // Add custom resolver if provided - if (_customTypeInfoResolver != null) - { - resolvers.Add(_customTypeInfoResolver); - } - - // Add additional custom resolvers if any - if (_customTypeInfoResolvers != null) - { - foreach (var resolver in _customTypeInfoResolvers) - { - resolvers.Add(resolver); - } - } - - // Add default context - if (_jsonContext != null) - { - resolvers.Add(_jsonContext); - } - - // Add additional contexts - foreach (var context in _additionalContexts) - { - resolvers.Add(context); - } - - return new CompositeJsonTypeInfoResolver(resolvers.ToArray()); - } - - /// - /// Apply additional JSON context to serializer - /// - private void ApplyAdditionalJsonContext(JsonSerializerContext context) - { - PowertoolsLoggingSerializer.AddSerializerContext(context); - } -#endif - - /// - /// Apply JSON options to the serializer - /// - private void ApplyJsonOptions() - { - if (_jsonOptions != null) - { - PowertoolsLoggingSerializer.BuildJsonSerializerOptions(_jsonOptions); - } - } - /// /// Apply output case configuration /// @@ -231,7 +103,6 @@ internal void ApplyOutputCase() PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase); } - // IOptions implementation PowertoolsLoggerConfiguration IOptions.Value => this; } \ 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 index 40e7d793..9a09180e 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -25,7 +25,6 @@ using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Common.Utils; using AWS.Lambda.Powertools.Logging.Internal.Converters; -using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging.Serializers; @@ -34,9 +33,11 @@ namespace AWS.Lambda.Powertools.Logging.Serializers; /// internal static class PowertoolsLoggingSerializer { + private static JsonSerializerOptions _currentOptions; private static LoggerOutputCase _currentOutputCase; private static JsonSerializerOptions _jsonOptions; private static readonly object _lock = new object(); + private static IJsonTypeInfoResolver? _customTypeInfoResolver = null; private static readonly ConcurrentBag AdditionalContexts = new ConcurrentBag(); @@ -53,7 +54,7 @@ internal static JsonSerializerOptions GetSerializerOptions() { if (_jsonOptions == null) { - BuildJsonSerializerOptions(); + BuildJsonSerializerOptions(_currentOptions); } } } @@ -146,9 +147,61 @@ internal static void AddSerializerContext(JsonSerializerContext context) { ArgumentNullException.ThrowIfNull(context); + // Don't add duplicates if (!AdditionalContexts.Contains(context)) { AdditionalContexts.Add(context); + + // If we have existing JSON options, update their type resolver + if (_jsonOptions != null && !RuntimeFeatureWrapper.IsDynamicCodeSupported) + { + // Reset the type resolver chain to rebuild it + _jsonOptions.TypeInfoResolver = GetCompositeResolver(); + } + } + } + + /// + /// Get a composite resolver that includes all configured resolvers + /// + internal static IJsonTypeInfoResolver GetCompositeResolver() + { + var resolvers = new List(); + + // Add custom resolver if provided + if (_customTypeInfoResolver != null) + { + resolvers.Add(_customTypeInfoResolver); + } + + // Add default context + resolvers.Add(PowertoolsLoggingSerializationContext.Default); + + // Add additional contexts + foreach (var context in AdditionalContexts) + { + resolvers.Add(context); + } + + return new CompositeJsonTypeInfoResolver(resolvers.ToArray()); + } + + /// + /// Handles the TypeInfoResolver from the JsonSerializerOptions. + /// + internal static void HandleJsonOptionsTypeResolver(JsonSerializerOptions options) + { + // Check for TypeInfoResolver and ensure it's not lost + if (options?.TypeInfoResolver != null && + options.TypeInfoResolver != GetCompositeResolver()) + { + _customTypeInfoResolver = options.TypeInfoResolver; + + // If it's a JsonSerializerContext, also add it to our contexts + if (_customTypeInfoResolver is JsonSerializerContext jsonContext) + { + AddSerializerContext(jsonContext); + } } } @@ -207,14 +260,7 @@ internal static void BuildJsonSerializerOptions(JsonSerializerOptions options = // Only add TypeInfoResolver if AOT mode if (!RuntimeFeatureWrapper.IsDynamicCodeSupported) { - // Always ensure our default context is in the chain first - _jsonOptions.TypeInfoResolverChain.Add(PowertoolsLoggingSerializationContext.Default); - - // Add all registered contexts - foreach (var context in AdditionalContexts) - { - _jsonOptions.TypeInfoResolverChain.Add(context); - } + HandleJsonOptionsTypeResolver(_jsonOptions); } #endif } @@ -279,6 +325,10 @@ internal static void ClearContext() } #endif + internal static void SetOptions(JsonSerializerOptions options) + { + _currentOptions = options; + } /// /// Clears options for tests /// From b4741c0769b82439df7a2b5a2c90e99a79362638 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Sun, 23 Mar 2025 18:31:31 +0000 Subject: [PATCH 17/49] fix net8 serializer --- .../Serializers/PowertoolsLoggingSerializer.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs index 9a09180e..f0b97ce6 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -37,8 +37,6 @@ internal static class PowertoolsLoggingSerializer private static LoggerOutputCase _currentOutputCase; private static JsonSerializerOptions _jsonOptions; private static readonly object _lock = new object(); - private static IJsonTypeInfoResolver? _customTypeInfoResolver = null; - private static readonly ConcurrentBag AdditionalContexts = new ConcurrentBag(); @@ -138,6 +136,9 @@ internal static string Serialize(object value, Type inputType) } #if NET8_0_OR_GREATER + + private static IJsonTypeInfoResolver? _customTypeInfoResolver = null; + /// /// Adds a JsonSerializerContext to the serializer options. /// From 249509868534930b7d5c30abc42f73af1364233d Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 24 Mar 2025 16:31:20 +0000 Subject: [PATCH 18/49] refactor: update log buffering options and improve serializer handling --- .../BuilderExtensions.cs | 7 +- .../Buffer/PowertoolsBufferingLogger.cs | 6 +- .../Internal/Helpers/LoggerFactoryHelper.cs | 19 +- ...PowertoolsLoggerConfigurationExtensions.cs | 64 +++---- .../Internal/LoggingAspect.cs | 102 +++++------ .../Internal/PowertoolsLogger.cs | 9 +- .../Internal/PowertoolsLoggerProvider.cs | 3 - .../Logger.Formatter.cs | 18 +- .../AWS.Lambda.Powertools.Logging/Logger.cs | 170 +++++++++++++----- .../PowertoolsLoggerBuilder.cs | 4 +- .../PowertoolsLoggerConfiguration.cs | 38 +++- ...sions.cs => PowertoolsLoggerExtensions.cs} | 4 +- .../PowertoolsLoggingSerializer.cs | 119 ++++++------ .../PowertoolsSourceGeneratorSerializer.cs | 2 +- 14 files changed, 321 insertions(+), 244 deletions(-) rename libraries/src/AWS.Lambda.Powertools.Logging/{LoggerExtensions.cs => PowertoolsLoggerExtensions.cs} (99%) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs index d0c35c75..93675828 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs @@ -50,9 +50,6 @@ public static ILoggingBuilder AddPowertoolsLogger( // Register services with the options RegisterServices(builder, options); - // Apply the output case configuration - options.ApplyOutputCase(); - // Configure static Logger (if not already in a configuration cycle) if (!_configuring) { @@ -83,12 +80,12 @@ private static void RegisterServices(ILoggingBuilder builder, PowertoolsLoggerCo new PowertoolsConfigurations(sp.GetRequiredService())); // If buffering is enabled, register buffer providers - if (options?.LogBufferingOptions?.Enabled == true) + if (options?.LogBuffering?.Enabled == true) { // Add a filter for the buffer provider builder.AddFilter( null, - options.LogBufferingOptions.BufferAtLogLevel); + options.LogBuffering.BufferAtLogLevel); // Register the inner provider factory builder.Services.TryAddSingleton(sp => diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs index b2c9da5a..190eb1dd 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs @@ -34,7 +34,7 @@ public bool IsEnabled(LogLevel logLevel) var options = _options.CurrentValue; // If buffering is disabled, defer to inner logger - if (!options.LogBufferingOptions.Enabled) + if (!options.LogBuffering.Enabled) { return _innerLogger.IsEnabled(logLevel); } @@ -48,7 +48,7 @@ public bool IsEnabled(LogLevel logLevel) // For logs below minimum level but at or above buffer threshold, // we should handle them (buffer them) - if (logLevel >= options.LogBufferingOptions.BufferAtLogLevel) + if (logLevel >= options.LogBuffering.BufferAtLogLevel) { return true; } @@ -69,7 +69,7 @@ public void Log( return; var options = _options.CurrentValue; - var bufferOptions = options.LogBufferingOptions; + var bufferOptions = options.LogBuffering; // Check if this log should be buffered bool shouldBuffer = bufferOptions.Enabled && diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs index aaa4f684..5b3b4b0b 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs @@ -18,19 +18,22 @@ public static ILoggerFactory CreateAndConfigureFactory(PowertoolsLoggerConfigura { builder.AddPowertoolsLogger(config => { - config.CopyFrom(configuration); + config.Service = configuration.Service; + config.SamplingRate = configuration.SamplingRate; + config.MinimumLogLevel = configuration.MinimumLogLevel; + config.LoggerOutputCase = configuration.LoggerOutputCase; + config.LoggerOutput = configuration.LoggerOutput; + config.JsonOptions = configuration.JsonOptions; + config.TimestampFormat = configuration.TimestampFormat; + config.LogFormatter = configuration.LogFormatter; + config.LogLevelKey = configuration.LogLevelKey; + config.LogBuffering = configuration.LogBuffering; }); }); // Configure the static logger with the factory Logger.Configure(factory); - - // Apply formatter if one is specified - if (configuration.LogFormatter != null) - { - Logger.UseFormatter(configuration.LogFormatter); - } - + return factory; } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerConfigurationExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerConfigurationExtensions.cs index 2b109f15..80b4bd93 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerConfigurationExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerConfigurationExtensions.cs @@ -1,32 +1,32 @@ -using System.Text.Json; -using Microsoft.Extensions.Logging; - -namespace AWS.Lambda.Powertools.Logging.Internal.Helpers; - -/// -/// Extension methods for handling configuration copying between PowertoolsLogger configurations -/// -internal static class PowertoolsLoggerConfigurationExtensions -{ - /// - /// Copies configuration values from source to destination configuration - /// - /// The destination configuration to copy values to - /// The source configuration to copy values from - /// The updated destination configuration - public static PowertoolsLoggerConfiguration CopyFrom(this PowertoolsLoggerConfiguration destination, PowertoolsLoggerConfiguration source) - { - destination.Service = source.Service; - destination.SamplingRate = source.SamplingRate; - destination.MinimumLogLevel = source.MinimumLogLevel; - destination.LoggerOutputCase = source.LoggerOutputCase; - destination.LoggerOutput = source.LoggerOutput; - destination.JsonOptions = source.JsonOptions; - destination.TimestampFormat = source.TimestampFormat; - destination.LogFormatter = source.LogFormatter; - destination.LogLevelKey = source.LogLevelKey; - destination.LogBufferingOptions = source.LogBufferingOptions; - - return destination; - } -} \ No newline at end of file +// using System.Text.Json; +// using Microsoft.Extensions.Logging; +// +// namespace AWS.Lambda.Powertools.Logging.Internal.Helpers; +// +// /// +// /// Extension methods for handling configuration copying between PowertoolsLogger configurations +// /// +// internal static class PowertoolsLoggerConfigurationExtensions +// { +// /// +// /// Copies configuration values from source to destination configuration +// /// +// /// The destination configuration to copy values to +// /// The source configuration to copy values from +// /// The updated destination configuration +// public static PowertoolsLoggerConfiguration CopyFrom(this PowertoolsLoggerConfiguration destination, PowertoolsLoggerConfiguration source) +// { +// destination.Service = source.Service; +// destination.SamplingRate = source.SamplingRate; +// destination.MinimumLogLevel = source.MinimumLogLevel; +// destination.LoggerOutputCase = source.LoggerOutputCase; +// destination.LoggerOutput = source.LoggerOutput; +// destination.JsonOptions = source.JsonOptions; +// destination.TimestampFormat = source.TimestampFormat; +// destination.LogFormatter = source.LogFormatter; +// destination.LogLevelKey = source.LogLevelKey; +// destination.LogBuffering = source.LogBuffering; +// +// return destination; +// } +// } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index 66c2482d..750012b7 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -75,7 +75,7 @@ public LoggingAspect(IPowertoolsConfigurations powertoolsConfigurations) _logEventEnv = powertoolsConfigurations.LoggerLogEvent; _xRayTraceId = powertoolsConfigurations.XRayTraceId; // Get Logger Instance - _logger = Logger.GetPowertoolsLogger(); + // _logger = Logger.GetPowertoolsLogger(); } private void InitializeLogger(LoggingAttribute trigger) @@ -83,50 +83,34 @@ private void InitializeLogger(LoggingAttribute trigger) // Check which settings are explicitly provided in the attribute var hasLogLevel = trigger.LogLevel != LogLevel.None; var hasService = !string.IsNullOrEmpty(trigger.Service); - var hasOutputCase = trigger.LoggerOutputCase != default; + var hasOutputCase = trigger.LoggerOutputCase != LoggerOutputCase.Default; var hasSamplingRate = trigger.SamplingRate > 0; - var hasExplicitSettings = hasLogLevel || hasService || hasOutputCase || hasSamplingRate; + // Only update configuration if any settings were provided + var needsReconfiguration = hasLogLevel || hasService || hasOutputCase || hasSamplingRate; - if (!Logger.IsConfigured) + if (needsReconfiguration) { - // First time initialization - create a new configuration with defaults for any unspecified values - var config = new PowertoolsLoggerConfiguration - { - MinimumLogLevel = hasLogLevel ? trigger.LogLevel : LogLevel.Information, - Service = hasService ? trigger.Service : "service_undefined", - LoggerOutputCase = hasOutputCase ? trigger.LoggerOutputCase : LoggerOutputCase.SnakeCase, - SamplingRate = hasSamplingRate ? trigger.SamplingRate : 1.0 - }; - - Logger.Configure(config); + // Apply each setting directly using the existing Logger static methods + if (hasLogLevel) Logger.UseMinimumLogLevel(trigger.LogLevel); + if (hasService) Logger.UseServiceName(trigger.Service); + if (hasOutputCase) Logger.UseOutputCase(trigger.LoggerOutputCase); + if (hasSamplingRate) Logger.UseSamplingRate(trigger.SamplingRate); + + // Update logger reference after configuration changes + _logger = Logger.GetPowertoolsLogger(); } - else if (hasExplicitSettings) + else if (_logger == null) { - // Preserve existing configuration and only override what's explicitly specified - Logger.UpdateConfiguration(config => - { - if (hasLogLevel) - config.MinimumLogLevel = trigger.LogLevel; - - if (hasService) - config.Service = trigger.Service; - - if (hasOutputCase) - config.LoggerOutputCase = trigger.LoggerOutputCase; - - if (hasSamplingRate) - config.SamplingRate = trigger.SamplingRate; - }); + // Only get the logger if we don't already have it + _logger = Logger.GetPowertoolsLogger(); } - - // Fetch the current configuration var currentConfig = Logger.GetConfiguration(); // Set operational flags based on current configuration _isDebug = currentConfig.MinimumLogLevel <= LogLevel.Debug; - _bufferingEnabled = currentConfig.LogBufferingOptions?.Enabled ?? false; + _bufferingEnabled = currentConfig.LogBuffering?.Enabled ?? false; } /// @@ -281,26 +265,26 @@ private void CaptureCorrelationId(object eventArg, string correlationIdPath) { var correlationId = string.Empty; - var jsonDoc = - JsonDocument.Parse(PowertoolsLoggingSerializer.Serialize(eventArg, eventArg.GetType())); - - var element = jsonDoc.RootElement; - - for (var i = 0; i < correlationIdPaths.Length; i++) - { - // For casing parsing to be removed from Logging v2 when we get rid of outputcase - // without this CorrelationIdPaths.ApiGatewayRest would not work - - // TODO: fix this - // var pathWithOutputCase = - // _powertoolsConfigurations.ConvertToOutputCase(correlationIdPaths[i], _config.LoggerOutputCase); - // if (!element.TryGetProperty(pathWithOutputCase, out var childElement)) - // break; - // - // element = childElement; - if (i == correlationIdPaths.Length - 1) - correlationId = element.ToString(); - } + // var jsonDoc = + // JsonDocument.Parse(PowertoolsLoggingSerializer.Serialize(eventArg, eventArg.GetType())); + // + // var element = jsonDoc.RootElement; + // + // for (var i = 0; i < correlationIdPaths.Length; i++) + // { + // // For casing parsing to be removed from Logging v2 when we get rid of outputcase + // // without this CorrelationIdPaths.ApiGatewayRest would not work + // + // // TODO: fix this + // // var pathWithOutputCase = + // // _powertoolsConfigurations.ConvertToOutputCase(correlationIdPaths[i], _config.LoggerOutputCase); + // // if (!element.TryGetProperty(pathWithOutputCase, out var childElement)) + // // break; + // // + // // element = childElement; + // if (i == correlationIdPaths.Length - 1) + // correlationId = element.ToString(); + // } if (!string.IsNullOrWhiteSpace(correlationId)) _logger.AppendKey(LoggingConstants.KeyCorrelationId, correlationId); @@ -322,12 +306,12 @@ private void LogEvent(object eventArg) switch (eventArg) { case null: - { - if (_isDebug) - _logger.LogDebug( - "Skipping Event Log because event parameter not found."); - break; - } + { + if (_isDebug) + _logger.LogDebug( + "Skipping Event Log because event parameter not found."); + break; + } case Stream: try { diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs index 4aac91b5..88cc32d2 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -138,8 +138,9 @@ internal void LogLine(string message) internal string LogEntryString(LogLevel logLevel, TState state, Exception exception, Func formatter) { var logEntry = LogEntry(logLevel, state, exception, formatter); - return PowertoolsLoggingSerializer.Serialize(logEntry, typeof(object)); + return _currentConfig.Serializer.Serialize(logEntry, typeof(object)); } + internal object LogEntry(LogLevel logLevel, TState state, Exception exception, Func formatter) { var timestamp = DateTime.UtcNow; @@ -156,7 +157,7 @@ internal object LogEntry(LogLevel logLevel, TState state, Exception exce : formatter(state, exception); // Get log entry - var logFormatter = Logger.GetFormatter(); + var logFormatter = _currentConfig.LogFormatter; var logEntry = logFormatter is null ? GetLogEntry(logLevel, timestamp, message, exception, structuredParameters) : GetFormattedLogEntry(logLevel, timestamp, message, exception, logFormatter, structuredParameters); @@ -176,7 +177,7 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times var logEntry = new Dictionary(); // Add Custom Keys - foreach (var (key, value) in Logger.GetAllKeys()) + foreach (var (key, value) in this.GetAllKeys()) { logEntry.TryAdd(key, value); } @@ -257,7 +258,7 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec var extraKeys = new Dictionary(); // Add Custom Keys - foreach (var (key, value) in Logger.GetAllKeys()) + foreach (var (key, value) in this.GetAllKeys()) { switch (key) { diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs index e4e2c4af..7e9ea250 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs @@ -128,9 +128,6 @@ private void ApplyPowertoolsConfig(PowertoolsLoggerConfiguration config) config.MinimumLogLevel = minLogLevel != LogLevel.None ? minLogLevel : LoggingConstants.DefaultLogLevel; } - // Always configure the serializer with the output case - // PowertoolsLoggingSerializer.ConfigureNamingPolicy(config.LoggerOutputCase); - // Configure the log level key based on output case config.LogLevelKey = _powertoolsConfigurations.LambdaLogLevelEnabled() && config.LoggerOutputCase == LoggerOutputCase.PascalCase diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Formatter.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Formatter.cs index 515ff2f0..fdeb3c97 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Formatter.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Formatter.cs @@ -13,17 +13,10 @@ * permissions and limitations under the License. */ -using System; -using AWS.Lambda.Powertools.Logging.Internal; - namespace AWS.Lambda.Powertools.Logging; public static partial class Logger { - private static ILogFormatter _logFormatter; - - #region Custom Log Formatter - /// /// Set the log formatter. /// @@ -31,7 +24,7 @@ public static partial class Logger /// WARNING: This method should not be called when using AOT. ILogFormatter should be passed to PowertoolsSourceGeneratorSerializer constructor public static void UseFormatter(ILogFormatter logFormatter) { - _logFormatter = logFormatter ?? throw new ArgumentNullException(nameof(logFormatter)); + _currentConfig.LogFormatter = logFormatter; } /// @@ -39,13 +32,6 @@ public static void UseFormatter(ILogFormatter logFormatter) /// public static void UseDefaultFormatter() { - _logFormatter = null; + _currentConfig.LogFormatter = null; } - - /// - /// Returns the log formatter. - /// - internal static ILogFormatter GetFormatter() => _logFormatter; - - #endregion } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs index 9df769ea..23f72614 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs @@ -14,11 +14,9 @@ */ using System; +using System.Text.Json; using System.Threading; using AWS.Lambda.Powertools.Common; -using AWS.Lambda.Powertools.Logging.Internal; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging; @@ -32,30 +30,41 @@ public static partial class Logger private static Lazy _factoryLazy; private static Lazy _defaultLoggerLazy; - // Add a backing field - private static bool _isConfigured = false; + // Static constructor to ensure initialization + static Logger() + { + // Initialize with default configuration (ensures we never have null fields) + InitializeWithDefaults(); + } // Properties to access the lazy-initialized instances private static ILoggerFactory Factory => _factoryLazy.Value; private static ILogger LoggerInstance => _defaultLoggerLazy.Value; - /// - /// Gets a value indicating whether the logger is configured. - /// - /// true if the logger is configured; otherwise, false. - public static bool IsConfigured => _isConfigured; - // Add this field to the Logger class private static PowertoolsLoggerConfiguration _currentConfig; + // Initialize with default settings + private static void InitializeWithDefaults() + { + _currentConfig = new PowertoolsLoggerConfiguration(); + + // Create default factory with minimal configuration + _factoryLazy = new Lazy(() => + PowertoolsLoggerFactory.Create(_currentConfig)); + + _defaultLoggerLazy = new Lazy(() => + Factory.CreatePowertoolsLogger()); + } + // Allow manual configuration using options - public static void Configure(Action configureOptions) + internal static void Configure(Action configureOptions) { var options = new PowertoolsLoggerConfiguration(); configureOptions(options); Configure(options); } - + // Configure with existing factory internal static void Configure(ILoggerFactory loggerFactory) { @@ -64,8 +73,6 @@ internal static void Configure(ILoggerFactory loggerFactory) Interlocked.Exchange(ref _defaultLoggerLazy, new Lazy(() => Factory.CreatePowertoolsLogger())); - - _isConfigured = true; } // Directly configure from a PowertoolsLoggerConfiguration @@ -73,14 +80,15 @@ internal static void Configure(PowertoolsLoggerConfiguration options) { if (options == null) throw new ArgumentNullException(nameof(options)); + // Store current config + _currentConfig = options; + // Update factory and logger Interlocked.Exchange(ref _factoryLazy, - new Lazy(() => PowertoolsLoggerFactory.Create(options))); + new Lazy(() => PowertoolsLoggerFactory.Create(_currentConfig))); Interlocked.Exchange(ref _defaultLoggerLazy, new Lazy(() => Factory.CreatePowertoolsLogger())); - - _isConfigured = true; } // Get the current configuration @@ -88,41 +96,117 @@ internal static PowertoolsLoggerConfiguration GetConfiguration() { // Ensure logger is initialized _ = LoggerInstance; - - // Create a new configuration with current settings - if (_currentConfig == null) - { - _currentConfig = new PowertoolsLoggerConfiguration(); - } - + return _currentConfig; } // Get a logger for a specific category - public static ILogger GetLogger() => GetLogger(typeof(T).Name); + internal static ILogger GetLogger() => GetLogger(typeof(T).Name); - public static ILogger GetLogger(string category) => Factory.CreateLogger(category); + internal static ILogger GetLogger(string category) => Factory.CreateLogger(category); - public static ILogger GetPowertoolsLogger() => Factory.CreatePowertoolsLogger(); + internal static ILogger GetPowertoolsLogger() => Factory.CreatePowertoolsLogger(); + + /// + /// Sets a custom output for the static logger. + /// Useful for testing to redirect logs to a test output. + /// + /// The custom output implementation + public static void UseOutput(ISystemWrapper loggerOutput) + { + if (loggerOutput == null) + throw new ArgumentNullException(nameof(loggerOutput)); + + _currentConfig.LoggerOutput = loggerOutput; + Configure(_currentConfig); + } + + /// + /// Configure logger output case (snake_case, camelCase, PascalCase) + /// + /// The case to use for the output + public static void UseOutputCase(LoggerOutputCase outputCase) + { + _currentConfig.LoggerOutputCase = outputCase; + Configure(_currentConfig); + } - // Update configuration settings - internal static void UpdateConfiguration(Action configureAction) + /// + /// Configures the minimum log level + /// + /// The minimum log level to display + public static void UseMinimumLogLevel(LogLevel logLevel) + { + _currentConfig.MinimumLogLevel = logLevel; + Configure(_currentConfig); + } + + /// + /// Configures the service name + /// + /// The service name to use in logs + public static void UseServiceName(string serviceName) { - if (configureAction == null) return; + if (string.IsNullOrEmpty(serviceName)) + throw new ArgumentException("Service name cannot be null or empty", nameof(serviceName)); - // Apply updates to current configuration - configureAction(_currentConfig); + _currentConfig.Service = serviceName; + Configure(_currentConfig); + } + + /// + /// Sets the sampling rate for logs + /// + /// The rate (0.0 to 1.0) for sampling + public static void UseSamplingRate(double samplingRate) + { + if (samplingRate < 0 || samplingRate > 1) + throw new ArgumentOutOfRangeException(nameof(samplingRate), "Sampling rate must be between 0 and 1"); - // Apply any output case changes - _currentConfig.ApplyOutputCase(); + _currentConfig.SamplingRate = samplingRate; + Configure(_currentConfig); + } + + /// + /// Log buffering options. + /// + /// Logger.UseLogBuffering(new LogBufferingOptions + /// { + /// Enabled = true, + /// BufferAtLogLevel = LogLevel.Debug + /// }); + /// + /// + public static void UseLogBuffering(LogBufferingOptions logBuffering) + { + if (logBuffering == null) + throw new ArgumentNullException(nameof(logBuffering)); + + // Update the current configuration + _currentConfig.LogBuffering = logBuffering; + + // Reconfigure to apply changes + Configure(_currentConfig); + } + +#if NET8_0_OR_GREATER + /// + /// Configure JSON serialization options + /// + /// The JSON options to use + public static void UseJsonOptions(JsonSerializerOptions jsonOptions) + { + if (jsonOptions == null) + throw new ArgumentNullException(nameof(jsonOptions)); + + // Update the current configuration + _currentConfig.JsonOptions = jsonOptions; } +#endif + // For testing purposes - // internal static void Reset() - // { - // Interlocked.Exchange(ref _factoryLazy, - // new Lazy(() => new PowertoolsLoggerFactory())); - - // Interlocked.Exchange(ref _defaultLoggerLazy, - // new Lazy(() => Factory.CreateLogger())); - // } + internal static void Reset() + { + InitializeWithDefaults(); + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs index c583e42a..e18bc22b 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs @@ -64,7 +64,7 @@ public PowertoolsLoggerBuilder WithFormatter(ILogFormatter formatter) /// public PowertoolsLoggerBuilder WithLogBuffering(bool enabled = true) { - _configuration.LogBufferingOptions.Enabled = enabled; + _configuration.LogBuffering.Enabled = enabled; return this; } @@ -73,7 +73,7 @@ public PowertoolsLoggerBuilder WithLogBuffering(bool enabled = true) /// public PowertoolsLoggerBuilder WithLogBuffering(Action configure) { - configure?.Invoke(_configuration.LogBufferingOptions); + configure?.Invoke(_configuration.LogBuffering); return this; } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs index ed4903db..b77de34b 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs @@ -83,24 +83,48 @@ public JsonSerializerOptions? JsonOptions set { _jsonOptions = value; - if (_jsonOptions != null) + if (_jsonOptions != null && _serializer != null) { - PowertoolsLoggingSerializer.SetOptions(_jsonOptions); + _serializer.SetOptions(_jsonOptions); } } } /// - /// Options for log buffering + /// Log buffering options. + /// + /// Logger.UseLogBuffering(new LogBufferingOptions + /// { + /// Enabled = true, + /// BufferAtLogLevel = LogLevel.Debug + /// }); + /// /// - public LogBufferingOptions LogBufferingOptions { get; set; } = new LogBufferingOptions(); + public LogBufferingOptions LogBuffering { get; set; } = new LogBufferingOptions(); /// - /// Apply output case configuration + /// Serializer instance for this configuration /// - internal void ApplyOutputCase() + private PowertoolsLoggingSerializer _serializer; + + /// + /// Gets the serializer instance for this configuration + /// + internal PowertoolsLoggingSerializer Serializer => _serializer ??= InitializeSerializer(); + + + /// + /// Initialize serializer with the current configuration + /// + private PowertoolsLoggingSerializer InitializeSerializer() { - PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase); + var serializer = new PowertoolsLoggingSerializer(); + if (_jsonOptions != null) + { + serializer.SetOptions(_jsonOptions); + } + serializer.ConfigureNamingPolicy(LoggerOutputCase); + return serializer; } // IOptions implementation diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerExtensions.cs similarity index 99% rename from libraries/src/AWS.Lambda.Powertools.Logging/LoggerExtensions.cs rename to libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerExtensions.cs index 21e8dd6a..e3ca6780 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggerExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerExtensions.cs @@ -23,7 +23,7 @@ namespace AWS.Lambda.Powertools.Logging; /// /// Class LoggerExtensions. /// -public static class LoggerExtensions +public static class PowertoolsLoggerExtensions { #region JSON Logger Extentions @@ -692,7 +692,7 @@ public static void AppendKey(this ILogger logger, string key, object value) /// Returns all additional keys added to the log context. /// /// IEnumerable<KeyValuePair<System.String, System.Object>>. - public static IEnumerable> GetAllKeys(this ILogger logger) + public static IEnumerable> GetAllKeys(this ILogger logger) { return Logger.GetAllKeys(); } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs index f0b97ce6..758da539 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -31,19 +31,22 @@ namespace AWS.Lambda.Powertools.Logging.Serializers; /// /// Provides serialization functionality for Powertools logging. /// -internal static class PowertoolsLoggingSerializer +internal class PowertoolsLoggingSerializer { - private static JsonSerializerOptions _currentOptions; - private static LoggerOutputCase _currentOutputCase; - private static JsonSerializerOptions _jsonOptions; - private static readonly object _lock = new object(); - private static readonly ConcurrentBag AdditionalContexts = + private JsonSerializerOptions _currentOptions; + private LoggerOutputCase _currentOutputCase; + private JsonSerializerOptions _jsonOptions; + private readonly object _lock = new object(); + + private readonly ConcurrentBag _additionalContexts = new ConcurrentBag(); + private static JsonSerializerContext _staticAdditionalContexts; + /// /// Gets the JsonSerializerOptions instance. /// - internal static JsonSerializerOptions GetSerializerOptions() + internal JsonSerializerOptions GetSerializerOptions() { // Double-checked locking pattern for thread safety while ensuring we only build once if (_jsonOptions == null) @@ -56,7 +59,7 @@ internal static JsonSerializerOptions GetSerializerOptions() } } } - + return _jsonOptions; } @@ -64,14 +67,14 @@ internal static JsonSerializerOptions GetSerializerOptions() /// Configures the naming policy for the serializer. /// /// The case to use for serialization. - internal static void ConfigureNamingPolicy(LoggerOutputCase loggerOutputCase) + internal void ConfigureNamingPolicy(LoggerOutputCase loggerOutputCase) { if (_currentOutputCase != loggerOutputCase) { lock (_lock) { _currentOutputCase = loggerOutputCase; - + // Only rebuild options if they already exist if (_jsonOptions != null) { @@ -88,7 +91,7 @@ internal static void ConfigureNamingPolicy(LoggerOutputCase loggerOutputCase) /// 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. - internal static string Serialize(object value, Type inputType) + internal string Serialize(object value, Type inputType) { #if NET6_0 var options = GetSerializerOptions(); @@ -100,11 +103,11 @@ internal static string Serialize(object value, Type inputType) #pragma warning disable return JsonSerializer.Serialize(value, jsonSerializerOptions); } - + var options = GetSerializerOptions(); - + // Try to serialize using the configured TypeInfoResolver - try + try { var typeInfo = GetTypeInfo(inputType); if (typeInfo != null) @@ -116,7 +119,7 @@ internal static string Serialize(object value, Type inputType) { // Failed to get typeinfo, will fall back to trying the serializer directly } - + // Fall back to direct serialization which may work if the resolver chain can handle it try { @@ -125,34 +128,36 @@ internal static string Serialize(object value, Type inputType) catch (JsonException ex) { throw new JsonSerializerException( - $"Type {inputType} is not known to the serializer. Ensure it's included in the JsonSerializerContext.", ex); + $"Type {inputType} is not known to the serializer. Ensure it's included in the JsonSerializerContext.", + ex); } catch (InvalidOperationException ex) { throw new JsonSerializerException( - $"Type {inputType} is not known to the serializer. Ensure it's included in the JsonSerializerContext.", ex); + $"Type {inputType} is not known to the serializer. Ensure it's included in the JsonSerializerContext.", + ex); } #endif } #if NET8_0_OR_GREATER - - private static IJsonTypeInfoResolver? _customTypeInfoResolver = null; - + + private IJsonTypeInfoResolver? _customTypeInfoResolver = null; + /// /// Adds a JsonSerializerContext to the serializer options. /// /// The JsonSerializerContext to add. /// Thrown when the context is null. - internal static void AddSerializerContext(JsonSerializerContext context) + internal void AddSerializerContext(JsonSerializerContext context) { ArgumentNullException.ThrowIfNull(context); // Don't add duplicates - if (!AdditionalContexts.Contains(context)) + if (!_additionalContexts.Contains(context)) { - AdditionalContexts.Add(context); - + _additionalContexts.Add(context); + // If we have existing JSON options, update their type resolver if (_jsonOptions != null && !RuntimeFeatureWrapper.IsDynamicCodeSupported) { @@ -162,10 +167,17 @@ internal static void AddSerializerContext(JsonSerializerContext context) } } + internal static void AddStaticSerializerContext(JsonSerializerContext context) + { + ArgumentNullException.ThrowIfNull(context); + + _staticAdditionalContexts = context; + } + /// /// Get a composite resolver that includes all configured resolvers /// - internal static IJsonTypeInfoResolver GetCompositeResolver() + private IJsonTypeInfoResolver GetCompositeResolver() { var resolvers = new List(); @@ -174,30 +186,36 @@ internal static IJsonTypeInfoResolver GetCompositeResolver() { resolvers.Add(_customTypeInfoResolver); } + + // add any static resolvers + if (_staticAdditionalContexts != null) + { + resolvers.Add(_staticAdditionalContexts); + } // Add default context resolvers.Add(PowertoolsLoggingSerializationContext.Default); // Add additional contexts - foreach (var context in AdditionalContexts) + foreach (var context in _additionalContexts) { resolvers.Add(context); } return new CompositeJsonTypeInfoResolver(resolvers.ToArray()); } - + /// /// Handles the TypeInfoResolver from the JsonSerializerOptions. /// - internal static void HandleJsonOptionsTypeResolver(JsonSerializerOptions options) + private void HandleJsonOptionsTypeResolver(JsonSerializerOptions options) { // Check for TypeInfoResolver and ensure it's not lost - if (options?.TypeInfoResolver != null && + if (options?.TypeInfoResolver != null && options.TypeInfoResolver != GetCompositeResolver()) { _customTypeInfoResolver = options.TypeInfoResolver; - + // If it's a JsonSerializerContext, also add it to our contexts if (_customTypeInfoResolver is JsonSerializerContext jsonContext) { @@ -211,7 +229,7 @@ internal static void HandleJsonOptionsTypeResolver(JsonSerializerOptions options /// /// The type to get information for. /// The JsonTypeInfo for the specified type, or null if not found. - internal static JsonTypeInfo GetTypeInfo(Type type) + private JsonTypeInfo GetTypeInfo(Type type) { var options = GetSerializerOptions(); return options.TypeInfoResolver?.GetTypeInfo(type, options); @@ -220,12 +238,12 @@ internal static JsonTypeInfo GetTypeInfo(Type type) /// /// Checks if a type is supported by any of the configured type resolvers /// - internal static bool IsTypeSupportedByAnyResolver(Type type) + private bool IsTypeSupportedByAnyResolver(Type type) { var options = GetSerializerOptions(); if (options.TypeInfoResolver == null) return false; - + try { var typeInfo = options.TypeInfoResolver.GetTypeInfo(type, options); @@ -242,13 +260,13 @@ internal static bool IsTypeSupportedByAnyResolver(Type type) /// Builds and configures the JsonSerializerOptions. /// /// A configured JsonSerializerOptions instance. - internal static void BuildJsonSerializerOptions(JsonSerializerOptions options = null) + private void BuildJsonSerializerOptions(JsonSerializerOptions options = null) { lock (_lock) { // This should already be in a lock when called _jsonOptions = options ?? new JsonSerializerOptions(); - + SetOutputCase(); AddConverters(); @@ -267,7 +285,7 @@ internal static void BuildJsonSerializerOptions(JsonSerializerOptions options = } } - private static void SetOutputCase() + internal void SetOutputCase() { switch (_currentOutputCase) { @@ -286,9 +304,11 @@ private static void SetOutputCase() { _jsonOptions.DictionaryKeyPolicy = _jsonOptions.DictionaryKeyPolicy; _jsonOptions.PropertyNamingPolicy = _jsonOptions.PropertyNamingPolicy; - }else{ + } + else + { _jsonOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; - _jsonOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; + _jsonOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; } #else _jsonOptions.PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance; @@ -298,7 +318,7 @@ private static void SetOutputCase() } } - private static void AddConverters() + private void AddConverters() { _jsonOptions.Converters.Add(new ByteArrayConverter()); _jsonOptions.Converters.Add(new ExceptionConverter()); @@ -314,27 +334,8 @@ private static void AddConverters() #endif } -#if NET8_0_OR_GREATER - internal static bool HasContext(JsonSerializerContext customContext) - { - return AdditionalContexts.Contains(customContext); - } - - internal static void ClearContext() - { - AdditionalContexts.Clear(); - } -#endif - - internal static void SetOptions(JsonSerializerOptions options) + internal void SetOptions(JsonSerializerOptions options) { _currentOptions = options; } - /// - /// Clears options for tests - /// - internal static void ClearOptions() - { - _jsonOptions = null; - } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs index dadec8da..46c83c49 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsSourceGeneratorSerializer.cs @@ -74,7 +74,7 @@ public PowertoolsSourceGeneratorSerializer( } var jsonSerializerContext = constructor.Invoke(new object[] { options }) as TSgContext; - PowertoolsLoggingSerializer.AddSerializerContext(jsonSerializerContext); + PowertoolsLoggingSerializer.AddStaticSerializerContext(jsonSerializerContext); } } From e4975634a43d91cf049248a6ec78a910bc10075c Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 25 Mar 2025 12:47:07 +0000 Subject: [PATCH 19/49] refactor configuration and create StringCaseExtensions --- .../Core/TestLoggerOutput.cs | 9 + .../Internal/LoggingAspect.cs | 67 ++--- .../Internal/LoggingAspectFactory.cs | 8 +- .../PowertoolsConfigurationsExtension.cs | 229 +++++++++--------- .../Internal/PowertoolsLoggerProvider.cs | 5 +- .../Internal/StringCaseExtensions.cs | 132 ++++++++++ .../PowertoolsLoggerConfiguration.cs | 3 + .../PowertoolsLoggerFactory.cs | 3 - 8 files changed, 293 insertions(+), 163 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Internal/StringCaseExtensions.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/TestLoggerOutput.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/TestLoggerOutput.cs index 5a70b529..9a64c881 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/TestLoggerOutput.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/TestLoggerOutput.cs @@ -48,4 +48,13 @@ public void SetOut(TextWriter writeTo) { Console.SetOut(writeTo); } + + /// + /// Overrides the ToString method to return the buffer as a string. + /// + /// + public override string ToString() + { + return Buffer.ToString(); + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index 750012b7..10be5b0f 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -31,7 +31,7 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// Scope.Global is singleton /// /// -[Aspect(Scope.Global, Factory = typeof(LoggingAspectFactory))] +[Aspect(Scope.Global)] public class LoggingAspect { /// @@ -60,24 +60,11 @@ public class LoggingAspect private bool _clearLambdaContext; private ILogger _logger; - private readonly bool _logEventEnv; - private readonly string _xRayTraceId; private bool _isDebug; private bool _bufferingEnabled; + private PowertoolsLoggerConfiguration _currentConfig; - /// - /// Initializes a new instance of the class. - /// - /// The Powertools configurations. - public LoggingAspect(IPowertoolsConfigurations powertoolsConfigurations) - { - _logEventEnv = powertoolsConfigurations.LoggerLogEvent; - _xRayTraceId = powertoolsConfigurations.XRayTraceId; - // Get Logger Instance - // _logger = Logger.GetPowertoolsLogger(); - } - private void InitializeLogger(LoggingAttribute trigger) { // Check which settings are explicitly provided in the attribute @@ -106,11 +93,11 @@ private void InitializeLogger(LoggingAttribute trigger) _logger = Logger.GetPowertoolsLogger(); } // Fetch the current configuration - var currentConfig = Logger.GetConfiguration(); + _currentConfig = Logger.GetConfiguration(); // Set operational flags based on current configuration - _isDebug = currentConfig.MinimumLogLevel <= LogLevel.Debug; - _bufferingEnabled = currentConfig.LogBuffering?.Enabled ?? false; + _isDebug = _currentConfig.MinimumLogLevel <= LogLevel.Debug; + _bufferingEnabled = _currentConfig.LogBuffering?.Enabled ?? false; } /// @@ -174,7 +161,7 @@ public void OnEntry( } CaptureCorrelationId(eventObject, trigger.CorrelationIdPath); - if (logEvent || _logEventEnv) + if (logEvent || _currentConfig.LogEvent) LogEvent(eventObject); } catch (Exception exception) @@ -216,10 +203,10 @@ public void OnExit() /// private void CaptureXrayTraceId() { - if (string.IsNullOrWhiteSpace(_xRayTraceId)) + if (string.IsNullOrWhiteSpace(_currentConfig.XRayTraceId)) return; _logger.AppendKey(LoggingConstants.KeyXRayTraceId, - _xRayTraceId.Split(';', StringSplitOptions.RemoveEmptyEntries)[0].Replace("Root=", "")); + _currentConfig.XRayTraceId.Split(';', StringSplitOptions.RemoveEmptyEntries)[0].Replace("Root=", "")); } /// @@ -265,26 +252,24 @@ private void CaptureCorrelationId(object eventArg, string correlationIdPath) { var correlationId = string.Empty; - // var jsonDoc = - // JsonDocument.Parse(PowertoolsLoggingSerializer.Serialize(eventArg, eventArg.GetType())); - // - // var element = jsonDoc.RootElement; - // - // for (var i = 0; i < correlationIdPaths.Length; i++) - // { - // // For casing parsing to be removed from Logging v2 when we get rid of outputcase - // // without this CorrelationIdPaths.ApiGatewayRest would not work - // - // // TODO: fix this - // // var pathWithOutputCase = - // // _powertoolsConfigurations.ConvertToOutputCase(correlationIdPaths[i], _config.LoggerOutputCase); - // // if (!element.TryGetProperty(pathWithOutputCase, out var childElement)) - // // break; - // // - // // element = childElement; - // if (i == correlationIdPaths.Length - 1) - // correlationId = element.ToString(); - // } + var jsonDoc = + JsonDocument.Parse(_currentConfig.Serializer.Serialize(eventArg, eventArg.GetType())); + + var element = jsonDoc.RootElement; + + for (var i = 0; i < correlationIdPaths.Length; i++) + { + // TODO: For casing parsing to be removed from Logging v2 when we get rid of outputcase without this CorrelationIdPaths.ApiGatewayRest would not work + // TODO: This will be removed and replaced by JMesPath + + var pathWithOutputCase = correlationIdPaths[i].ToCase(_currentConfig.LoggerOutputCase); + if (!element.TryGetProperty(pathWithOutputCase, out var childElement)) + break; + + element = childElement; + if (i == correlationIdPaths.Length - 1) + correlationId = element.ToString(); + } if (!string.IsNullOrWhiteSpace(correlationId)) _logger.AppendKey(LoggingConstants.KeyCorrelationId, correlationId); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs index 72ea5645..903a8850 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs @@ -28,8 +28,8 @@ internal static class LoggingAspectFactory /// /// The type of the class to be logged. /// An instance of the LoggingAspect class. - public static object GetInstance(Type type) - { - return new LoggingAspect(PowertoolsConfigurations.Instance); - } + // public static object GetInstance(Type type) + // { + // return new LoggingAspect(PowertoolsConfigurations.Instance); + // } } \ 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 index d513c185..dd36ce87 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs @@ -17,6 +17,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Serializers; using Microsoft.Extensions.Logging; @@ -193,120 +194,120 @@ internal static bool LambdaLogLevelEnabled(this IPowertoolsConfigurations powert /// /// The input string converted to the configured case (camel, pascal, or snake case). /// - internal static string ConvertToOutputCase(this IPowertoolsConfigurations powertoolsConfigurations, - string correlationIdPath, LoggerOutputCase loggerOutputCase) - { - return powertoolsConfigurations.GetLoggerOutputCase(loggerOutputCase) switch - { - LoggerOutputCase.CamelCase => ToCamelCase(correlationIdPath), - LoggerOutputCase.PascalCase => ToPascalCase(correlationIdPath), - _ => ToSnakeCase(correlationIdPath), // default snake_case - }; - } - - /// - /// Converts a string to snake_case. - /// - /// - /// The input string converted to snake_case. - private static string ToSnakeCase(string input) - { - if (string.IsNullOrEmpty(input)) - return input; - - var result = new StringBuilder(input.Length + 10); - bool lastCharWasUnderscore = false; - bool lastCharWasUpper = false; - - for (int i = 0; i < input.Length; i++) - { - char currentChar = input[i]; - - if (currentChar == '_') - { - result.Append('_'); - lastCharWasUnderscore = true; - lastCharWasUpper = false; - } - else if (char.IsUpper(currentChar)) - { - if (i > 0 && !lastCharWasUnderscore && - (!lastCharWasUpper || (i + 1 < input.Length && char.IsLower(input[i + 1])))) - { - result.Append('_'); - } - - result.Append(char.ToLowerInvariant(currentChar)); - lastCharWasUnderscore = false; - lastCharWasUpper = true; - } - else - { - result.Append(char.ToLowerInvariant(currentChar)); - lastCharWasUnderscore = false; - lastCharWasUpper = false; - } - } - - return result.ToString(); - } - - - /// - /// Converts a string to PascalCase. - /// - /// - /// The input string converted to PascalCase. - private static string ToPascalCase(string input) - { - if (string.IsNullOrEmpty(input)) - return input; - - var words = input.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries); - var result = new StringBuilder(); - - foreach (var word in words) - { - if (word.Length > 0) - { - // Capitalize the first character of each word - result.Append(char.ToUpperInvariant(word[0])); - - // Handle the rest of the characters - if (word.Length > 1) - { - // If the word is all uppercase, convert the rest to lowercase - if (word.All(char.IsUpper)) - { - result.Append(word.Substring(1).ToLowerInvariant()); - } - else - { - // Otherwise, keep the original casing - result.Append(word.Substring(1)); - } - } - } - } - - return result.ToString(); - } - - /// - /// Converts a string to camelCase. - /// - /// The string to convert. - /// The input string converted to camelCase. - private static string ToCamelCase(string input) - { - if (string.IsNullOrEmpty(input)) - return input; - - // First, convert to PascalCase - string pascalCase = ToPascalCase(input); + // internal static string ConvertToOutputCase(this IPowertoolsConfigurations powertoolsConfigurations, + // string correlationIdPath, LoggerOutputCase loggerOutputCase) + // { + // return powertoolsConfigurations.GetLoggerOutputCase(loggerOutputCase) switch + // { + // LoggerOutputCase.CamelCase => ToCamelCase(correlationIdPath), + // LoggerOutputCase.PascalCase => ToPascalCase(correlationIdPath), + // _ => ToSnakeCase(correlationIdPath), // default snake_case + // }; + // } - // Then convert the first character to lowercase - return char.ToLowerInvariant(pascalCase[0]) + pascalCase.Substring(1); - } + // /// + // /// Converts a string to snake_case. + // /// + // /// + // /// The input string converted to snake_case. + // private static string ToSnakeCase(string input) + // { + // if (string.IsNullOrEmpty(input)) + // return input; + // + // var result = new StringBuilder(input.Length + 10); + // bool lastCharWasUnderscore = false; + // bool lastCharWasUpper = false; + // + // for (int i = 0; i < input.Length; i++) + // { + // char currentChar = input[i]; + // + // if (currentChar == '_') + // { + // result.Append('_'); + // lastCharWasUnderscore = true; + // lastCharWasUpper = false; + // } + // else if (char.IsUpper(currentChar)) + // { + // if (i > 0 && !lastCharWasUnderscore && + // (!lastCharWasUpper || (i + 1 < input.Length && char.IsLower(input[i + 1])))) + // { + // result.Append('_'); + // } + // + // result.Append(char.ToLowerInvariant(currentChar)); + // lastCharWasUnderscore = false; + // lastCharWasUpper = true; + // } + // else + // { + // result.Append(char.ToLowerInvariant(currentChar)); + // lastCharWasUnderscore = false; + // lastCharWasUpper = false; + // } + // } + // + // return result.ToString(); + // } + // + // + // /// + // /// Converts a string to PascalCase. + // /// + // /// + // /// The input string converted to PascalCase. + // private static string ToPascalCase(string input) + // { + // if (string.IsNullOrEmpty(input)) + // return input; + // + // var words = input.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries); + // var result = new StringBuilder(); + // + // foreach (var word in words) + // { + // if (word.Length > 0) + // { + // // Capitalize the first character of each word + // result.Append(char.ToUpperInvariant(word[0])); + // + // // Handle the rest of the characters + // if (word.Length > 1) + // { + // // If the word is all uppercase, convert the rest to lowercase + // if (word.All(char.IsUpper)) + // { + // result.Append(word.Substring(1).ToLowerInvariant()); + // } + // else + // { + // // Otherwise, keep the original casing + // result.Append(word.Substring(1)); + // } + // } + // } + // } + // + // return result.ToString(); + // } + // + // /// + // /// Converts a string to camelCase. + // /// + // /// The string to convert. + // /// The input string converted to camelCase. + // private static string ToCamelCase(string input) + // { + // if (string.IsNullOrEmpty(input)) + // return input; + // + // // First, convert to PascalCase + // string pascalCase = ToPascalCase(input); + // + // // Then convert the first character to lowercase + // return char.ToLowerInvariant(pascalCase[0]) + pascalCase.Substring(1); + // } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs index 7e9ea250..663e3f05 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs @@ -83,7 +83,7 @@ public ILogger CreateLogger(string categoryName) _systemWrapper)); } - private PowertoolsLoggerConfiguration GetCurrentConfig() + internal PowertoolsLoggerConfiguration GetCurrentConfig() { var config = _currentConfig; @@ -128,6 +128,9 @@ private void ApplyPowertoolsConfig(PowertoolsLoggerConfiguration config) config.MinimumLogLevel = minLogLevel != LogLevel.None ? minLogLevel : LoggingConstants.DefaultLogLevel; } + config.XRayTraceId = _powertoolsConfigurations.XRayTraceId; + config.LogEvent = _powertoolsConfigurations.LoggerLogEvent; + // Configure the log level key based on output case config.LogLevelKey = _powertoolsConfigurations.LambdaLogLevelEnabled() && config.LoggerOutputCase == LoggerOutputCase.PascalCase diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/StringCaseExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/StringCaseExtensions.cs new file mode 100644 index 00000000..7e7b390a --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/StringCaseExtensions.cs @@ -0,0 +1,132 @@ +using System; +using System.Linq; +using System.Text; + +namespace AWS.Lambda.Powertools.Logging.Internal; + +/// +/// Extension methods for string case conversion. +/// +internal static class StringCaseExtensions +{ + /// + /// Converts a string to camelCase. + /// + /// The string to convert. + /// A camelCase formatted string. + public static string ToCamel(this string value) + { + if (string.IsNullOrEmpty(value)) + return value; + + // Convert to PascalCase first to handle potential snake_case or kebab-case + string pascalCase = ToPascal(value); + + // Convert first char to lowercase + return char.ToLowerInvariant(pascalCase[0]) + pascalCase.Substring(1); + } + + /// + /// Converts a string to PascalCase. + /// + /// The string to convert. + /// A PascalCase formatted string. + public static string ToPascal(this string input) + { + if (string.IsNullOrEmpty(input)) + return input; + + var words = input.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries); + var result = new StringBuilder(); + + foreach (var word in words) + { + if (word.Length > 0) + { + // Capitalize the first character of each word + result.Append(char.ToUpperInvariant(word[0])); + + // Handle the rest of the characters + if (word.Length > 1) + { + // If the word is all uppercase, convert the rest to lowercase + if (word.All(char.IsUpper)) + { + result.Append(word.Substring(1).ToLowerInvariant()); + } + else + { + // Otherwise, keep the original casing + result.Append(word.Substring(1)); + } + } + } + } + + return result.ToString(); + } + + /// + /// Converts a string to snake_case. + /// + /// The string to convert. + /// A snake_case formatted string. + public static string ToSnake(this string input) + { + if (string.IsNullOrEmpty(input)) + return input; + + var result = new StringBuilder(input.Length + 10); + bool lastCharWasUnderscore = false; + bool lastCharWasUpper = false; + + for (int i = 0; i < input.Length; i++) + { + char currentChar = input[i]; + + if (currentChar == '_') + { + result.Append('_'); + lastCharWasUnderscore = true; + lastCharWasUpper = false; + } + else if (char.IsUpper(currentChar)) + { + if (i > 0 && !lastCharWasUnderscore && + (!lastCharWasUpper || (i + 1 < input.Length && char.IsLower(input[i + 1])))) + { + result.Append('_'); + } + + result.Append(char.ToLowerInvariant(currentChar)); + lastCharWasUnderscore = false; + lastCharWasUpper = true; + } + else + { + result.Append(char.ToLowerInvariant(currentChar)); + lastCharWasUnderscore = false; + lastCharWasUpper = false; + } + } + + return result.ToString(); + } + + /// + /// Converts a string to the specified case format. + /// + /// The string to convert. + /// The target case format. + /// A formatted string in the specified case. + public static string ToCase(this string value, LoggerOutputCase outputCase) + { + return outputCase switch + { + LoggerOutputCase.CamelCase => value.ToCamel(), + LoggerOutputCase.PascalCase => value.ToPascal(), + LoggerOutputCase.SnakeCase => value.ToSnake(), + _ => value.ToSnake() // Default/unchanged + }; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs index b77de34b..e713eb0b 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs @@ -129,4 +129,7 @@ private PowertoolsLoggingSerializer InitializeSerializer() // IOptions implementation PowertoolsLoggerConfiguration IOptions.Value => this; + + internal string XRayTraceId { get; set; } + internal bool LogEvent { get; set; } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs index 18640b7b..6c5a9005 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs @@ -25,10 +25,7 @@ public static PowertoolsLoggerFactory Create(Action Date: Fri, 28 Mar 2025 15:49:13 +0000 Subject: [PATCH 20/49] checkpoint, some issues with buffer and loglevel --- .../BuilderExtensions.cs | 111 --------- .../Buffer/BufferingLoggerProvider.cs | 18 +- .../Buffer/PowertoolsBufferingLogger.cs | 10 +- .../Internal/Helpers/LoggerFactoryHelper.cs | 47 +++- .../Internal/LoggerFactoryHolder.cs | 190 +++++++++++++++ .../Internal/LoggingAspect.cs | 14 +- .../Internal/LoggingAspectFactory.cs | 8 +- .../Internal/PowertoolsLogger.cs | 41 ++-- .../Internal/PowertoolsLoggerProvider.cs | 213 ++++++++-------- .../Logger.Formatter.cs | 8 +- .../AWS.Lambda.Powertools.Logging/Logger.cs | 178 +++++++------- .../PowertoolsLoggerBuilder.cs | 6 - .../PowertoolsLoggerConfiguration.cs | 5 - .../PowertoolsLoggingBuilderExtensions.cs | 227 ++++++++++++++++++ 14 files changed, 713 insertions(+), 363 deletions(-) delete mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerFactoryHolder.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs deleted file mode 100644 index 93675828..00000000 --- a/libraries/src/AWS.Lambda.Powertools.Logging/BuilderExtensions.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using AWS.Lambda.Powertools.Common; -using AWS.Lambda.Powertools.Logging.Internal; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Configuration; -using Microsoft.Extensions.Options; - -namespace AWS.Lambda.Powertools.Logging; - -/// -/// Extension methods for configuring the Powertools logger -/// -public static class BuilderExtensions -{ - // Track if we're in the middle of configuration to prevent recursion - private static bool _configuring = false; - - /// - /// Adds the Powertools logger to the logging builder. - /// - public static ILoggingBuilder AddPowertoolsLogger( - this ILoggingBuilder builder, - Action? configure = null) - { - // Add configuration - builder.AddConfiguration(); - - // If no configuration was provided, register services with defaults - if (configure == null) - { - RegisterServices(builder); - return builder; - } - - // Create initial configuration - var options = new PowertoolsLoggerConfiguration(); - configure(options); - - // IMPORTANT: Set the minimum level directly on the builder - if (options.MinimumLogLevel != LogLevel.None) - { - builder.SetMinimumLevel(options.MinimumLogLevel); - } - - // Configure options for DI - builder.Services.Configure(configure); - - // Register services with the options - RegisterServices(builder, options); - - // Configure static Logger (if not already in a configuration cycle) - if (!_configuring) - { - try - { - _configuring = true; - Logger.Configure(options); - } - finally - { - _configuring = false; - } - } - - return builder; - } - - private static void RegisterServices(ILoggingBuilder builder, PowertoolsLoggerConfiguration options = null) - { - // Register ISystemWrapper if not already registered - builder.Services.TryAddSingleton(); - - // Register IPowertoolsEnvironment if it exists - builder.Services.TryAddSingleton(); - - // Register IPowertoolsConfigurations with all its dependencies - builder.Services.TryAddSingleton(sp => - new PowertoolsConfigurations(sp.GetRequiredService())); - - // If buffering is enabled, register buffer providers - if (options?.LogBuffering?.Enabled == true) - { - // Add a filter for the buffer provider - builder.AddFilter( - null, - options.LogBuffering.BufferAtLogLevel); - - // Register the inner provider factory - builder.Services.TryAddSingleton(sp => - new BufferingLoggerProvider( - // Create a new PowertoolsLoggerProvider specifically for buffering - new PowertoolsLoggerProvider( - sp.GetRequiredService>(), - sp.GetRequiredService(), - sp.GetRequiredService() - ), - sp.GetRequiredService>() - ) - ); - } - - // Register the regular provider - builder.Services.TryAddEnumerable( - ServiceDescriptor.Singleton()); - - LoggerProviderOptions.RegisterProviderOptions - (builder.Services); - } -} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs index 3802f042..c3b099dc 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs @@ -24,19 +24,25 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// Logger provider that supports buffering logs /// [ProviderAlias("PowertoolsBuffering")] -internal partial class BufferingLoggerProvider : ILoggerProvider +internal class BufferingLoggerProvider : ILoggerProvider { private readonly ILoggerProvider _innerProvider; private readonly ConcurrentDictionary _loggers = new(); - private readonly IOptionsMonitor _options; + private readonly IDisposable? _onChangeToken; + private PowertoolsLoggerConfiguration _currentConfig; public BufferingLoggerProvider( ILoggerProvider innerProvider, - IOptionsMonitor options) + IOptionsMonitor config) { _innerProvider = innerProvider ?? throw new ArgumentNullException(nameof(innerProvider)); - _options = options ?? throw new ArgumentNullException(nameof(options)); + _currentConfig = config.CurrentValue; + _onChangeToken = config.OnChange(updatedConfig => + { + _currentConfig = updatedConfig; + // No need to do anything else - the loggers get the config through GetCurrentConfig + }); // Register with the buffer manager LogBufferManager.RegisterProvider(this); } @@ -47,10 +53,12 @@ public ILogger CreateLogger(string categoryName) categoryName, name => new PowertoolsBufferingLogger( _innerProvider.CreateLogger(name), - _options, + GetCurrentConfig, name)); } + internal PowertoolsLoggerConfiguration GetCurrentConfig() => _currentConfig; + public void Dispose() { // Flush all buffers before disposing diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs index 190eb1dd..ef2c4f95 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs @@ -10,17 +10,17 @@ namespace AWS.Lambda.Powertools.Logging.Internal; internal class PowertoolsBufferingLogger : ILogger { private readonly ILogger _innerLogger; - private readonly IOptionsMonitor _options; + private readonly Func _getCurrentConfig; private readonly string _categoryName; private readonly LogBuffer _buffer = new(); public PowertoolsBufferingLogger( ILogger innerLogger, - IOptionsMonitor options, + Func getCurrentConfig, string categoryName) { _innerLogger = innerLogger; - _options = options; + _getCurrentConfig = getCurrentConfig; _categoryName = categoryName; } @@ -31,7 +31,7 @@ public IDisposable BeginScope(TState state) public bool IsEnabled(LogLevel logLevel) { - var options = _options.CurrentValue; + var options = _getCurrentConfig(); // If buffering is disabled, defer to inner logger if (!options.LogBuffering.Enabled) @@ -68,7 +68,7 @@ public void Log( if (!IsEnabled(logLevel)) return; - var options = _options.CurrentValue; + var options = _getCurrentConfig(); var bufferOptions = options.LogBuffering; // Check if this log should be buffered diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs index 5b3b4b0b..a4c64a65 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs @@ -1,3 +1,6 @@ +using System; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Common.Tests; using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging.Internal.Helpers; @@ -22,7 +25,6 @@ public static ILoggerFactory CreateAndConfigureFactory(PowertoolsLoggerConfigura config.SamplingRate = configuration.SamplingRate; config.MinimumLogLevel = configuration.MinimumLogLevel; config.LoggerOutputCase = configuration.LoggerOutputCase; - config.LoggerOutput = configuration.LoggerOutput; config.JsonOptions = configuration.JsonOptions; config.TimestampFormat = configuration.TimestampFormat; config.LogFormatter = configuration.LogFormatter; @@ -32,8 +34,49 @@ public static ILoggerFactory CreateAndConfigureFactory(PowertoolsLoggerConfigura }); // Configure the static logger with the factory - Logger.Configure(factory); + // Logger.Configure(factory); return factory; } +} + +// Add to a new TestHelpers.cs file +public static class PowertoolsLoggerTestHelpers +{ + private static readonly object _lock = new(); + private static ISystemWrapper _systemWrapper; + + static PowertoolsLoggerTestHelpers() + { + _systemWrapper = null; + } + + // Call this at the beginning of your test + public static TestLoggerOutput EnableTestMode() + { + var system = new TestLoggerOutput(); + _systemWrapper = system; + PowertoolsLoggingBuilderExtensions.UpdateSystemInAllProviders(system); + return system; + } + + public static void UseCustomSystem(ISystemWrapper system) + { + if (system == null) throw new ArgumentNullException(nameof(system)); + lock (_lock) + { + // Store the mock system for later use when providers are created + _systemWrapper = system; + // Update all providers to use the mock system + PowertoolsLoggingBuilderExtensions.UpdateSystemInAllProviders(system); + } + } + + internal static ISystemWrapper GetSystemWrapper() + { + lock (_lock) + { + return _systemWrapper; + } + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerFactoryHolder.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerFactoryHolder.cs new file mode 100644 index 00000000..bde8860f --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerFactoryHolder.cs @@ -0,0 +1,190 @@ +/* + * 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.Threading; +using AWS.Lambda.Powertools.Common; +using Microsoft.Extensions.Logging; + +namespace AWS.Lambda.Powertools.Logging.Internal; + +/// +/// Holds and manages the shared logger factory instance +/// +internal static class LoggerFactoryHolder +{ + private static ILoggerFactory? _factory; + private static readonly object _lock = new object(); + private static bool _isConfigured = false; + + /// + /// Gets or creates the shared logger factory + /// + public static ILoggerFactory GetOrCreateFactory() + { + lock (_lock) + { + if (_factory == null) + { + _factory = LoggerFactory.Create(builder => builder.AddPowertoolsLogger()); + } + return _factory; + } + } + + public static void SetFactory(ILoggerFactory factory) + { + if (factory == null) throw new ArgumentNullException(nameof(factory)); + lock (_lock) + { + _factory = factory; + _isConfigured = true; + } + } + + /// + /// Automatically called when GetOrCreateFactory is used + /// + public static void ConfigureFromEnvironment(IPowertoolsConfigurations configurations, ISystemWrapper systemWrapper) + { + // Only configure once + if (_isConfigured) return; + + // Create initial configuration + var config = PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); + + // Apply environment configuration if available + if (configurations != null) + { + ApplyPowertoolsConfig(config, configurations, systemWrapper); + PowertoolsLoggingBuilderExtensions.UpdateConfiguration(config); + } + + _isConfigured = true; + } + + /// + /// Apply Powertools configuration from environment variables to the logger configuration + /// + private static void ApplyPowertoolsConfig(PowertoolsLoggerConfiguration config, + IPowertoolsConfigurations configurations, ISystemWrapper systemWrapper) + { + var logLevel = configurations.GetLogLevel(LogLevel.None); + var lambdaLogLevel = configurations.GetLambdaLogLevel(); + var lambdaLogLevelEnabled = configurations.LambdaLogLevelEnabled(); + + // Check for explicit config + bool hasExplicitLevel = config.MinimumLogLevel != LogLevel.None; + + // Warn if Lambda log level doesn't match + if (lambdaLogLevelEnabled && hasExplicitLevel && config.MinimumLogLevel < lambdaLogLevel) + { + systemWrapper.LogLine( + $"Current log level ({config.MinimumLogLevel}) does not match AWS Lambda Advanced Logging Controls minimum log level ({lambdaLogLevel}). This can lead to data loss, consider adjusting them."); + } + + // Set service from environment if not explicitly set + if (string.IsNullOrEmpty(config.Service)) + { + config.Service = configurations.Service; + } + + // Set output case from environment if not explicitly set + if (config.LoggerOutputCase == LoggerOutputCase.Default) + { + var loggerOutputCase = configurations.GetLoggerOutputCase(config.LoggerOutputCase); + config.LoggerOutputCase = loggerOutputCase; + } + + // Set log level from environment ONLY if not explicitly set + if (!hasExplicitLevel) + { + var minLogLevel = lambdaLogLevelEnabled ? lambdaLogLevel : logLevel; + config.MinimumLogLevel = minLogLevel != LogLevel.None ? minLogLevel : LoggingConstants.DefaultLogLevel; + } + + config.XRayTraceId = configurations.XRayTraceId; + config.LogEvent = configurations.LoggerLogEvent; + + // Configure the log level key based on output case + config.LogLevelKey = configurations.LambdaLogLevelEnabled() && + config.LoggerOutputCase == LoggerOutputCase.PascalCase + ? "LogLevel" + : LoggingConstants.KeyLogLevel; + + ProcessSamplingRate(config, configurations, systemWrapper); + } + + /// + /// Process sampling rate configuration + /// + private static void ProcessSamplingRate(PowertoolsLoggerConfiguration config, IPowertoolsConfigurations configurations, ISystemWrapper systemWrapper) + { + var samplingRate = config.SamplingRate > 0 + ? config.SamplingRate + : configurations.LoggerSampleRate; + + samplingRate = ValidateSamplingRate(samplingRate, config.MinimumLogLevel, systemWrapper); + config.SamplingRate = samplingRate; + + // Only notify if sampling is configured + if (samplingRate > 0) + { + double sample = systemWrapper.GetRandom(); + + // Instead of changing log level, just indicate sampling status + if (sample <= samplingRate) + { + systemWrapper.LogLine( + $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate}, Sampler Value: {sample}."); + config.MinimumLogLevel = LogLevel.Debug; + } + } + } + + /// + /// Validate sampling rate + /// + private static double ValidateSamplingRate(double samplingRate, LogLevel minLogLevel, ISystemWrapper systemWrapper) + { + if (samplingRate < 0 || samplingRate > 1) + { + if (minLogLevel is LogLevel.Debug or LogLevel.Trace) + { + systemWrapper.LogLine( + $"Skipping sampling rate configuration because of invalid value. Sampling rate: {samplingRate}"); + } + + return 0; + } + + return samplingRate; + } + + + + /// + /// Resets the factory holder for testing + /// + internal static void Reset() + { + lock (_lock) + { + var oldFactory = Interlocked.Exchange(ref _factory, null); + oldFactory?.Dispose(); + _isConfigured = false; + } + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index 10be5b0f..0f301f9e 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -31,9 +31,11 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// Scope.Global is singleton /// /// -[Aspect(Scope.Global)] +[Aspect(Scope.Global, Factory = typeof(LoggingAspectFactory))] public class LoggingAspect { + private readonly ILoggerFactory _loggerFactory; + /// /// The is cold start /// @@ -64,6 +66,10 @@ public class LoggingAspect private bool _bufferingEnabled; private PowertoolsLoggerConfiguration _currentConfig; + public LoggingAspect(ILogger logger) + { + _logger = logger ?? LoggerFactoryHolder.GetOrCreateFactory().CreatePowertoolsLogger(); + } private void InitializeLogger(LoggingAttribute trigger) { @@ -85,15 +91,15 @@ private void InitializeLogger(LoggingAttribute trigger) if (hasSamplingRate) Logger.UseSamplingRate(trigger.SamplingRate); // Update logger reference after configuration changes - _logger = Logger.GetPowertoolsLogger(); + // _logger = Logger.GetPowertoolsLogger(); } else if (_logger == null) { // Only get the logger if we don't already have it - _logger = Logger.GetPowertoolsLogger(); + // _logger = Logger.GetPowertoolsLogger(); } // Fetch the current configuration - _currentConfig = Logger.GetConfiguration(); + _currentConfig = Logger.GetCurrentConfiguration(); // Set operational flags based on current configuration _isDebug = _currentConfig.MinimumLogLevel <= LogLevel.Debug; diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs index 903a8850..ada06cb1 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs @@ -28,8 +28,8 @@ internal static class LoggingAspectFactory /// /// The type of the class to be logged. /// An instance of the LoggingAspect class. - // public static object GetInstance(Type type) - // { - // return new LoggingAspect(PowertoolsConfigurations.Instance); - // } + public static object GetInstance(Type type) + { + return new LoggingAspect(LoggerFactoryHolder.GetOrCreateFactory().CreatePowertoolsLogger()); + } } \ 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 88cc32d2..f625bae6 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -39,12 +39,12 @@ internal sealed class PowertoolsLogger : ILogger /// /// The current configuration /// - private readonly PowertoolsLoggerConfiguration _currentConfig; + private readonly Func _currentConfig; /// /// The system wrapper /// - private readonly ISystemWrapper _systemWrapper; + private readonly Func _getSystemWrapper; /// /// The current scope @@ -56,15 +56,15 @@ internal sealed class PowertoolsLogger : ILogger /// /// The name. /// - /// The system wrapper. + /// The system wrapper. public PowertoolsLogger( string categoryName, Func getCurrentConfig, - ISystemWrapper systemWrapper) + Func getSystemWrapper) { _categoryName = categoryName; - _currentConfig = getCurrentConfig(); - _systemWrapper = systemWrapper; + _currentConfig = getCurrentConfig; + _getSystemWrapper = getSystemWrapper; } /// @@ -95,9 +95,10 @@ internal void EndScope() [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsEnabled(LogLevel logLevel) { + var config = _currentConfig(); // If we have no explicit minimum level, use the default - var effectiveMinLevel = _currentConfig.MinimumLogLevel != LogLevel.None - ? _currentConfig.MinimumLogLevel + var effectiveMinLevel = config.MinimumLogLevel != LogLevel.None + ? config.MinimumLogLevel : LoggingConstants.DefaultLogLevel; // Log diagnostic info for Debug/Trace levels @@ -127,18 +128,18 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except return; } - _systemWrapper.LogLine(LogEntryString(logLevel, state, exception, formatter)); + _getSystemWrapper().LogLine(LogEntryString(logLevel, state, exception, formatter)); } internal void LogLine(string message) { - _systemWrapper.LogLine(message); + _getSystemWrapper().LogLine(message); } internal string LogEntryString(LogLevel logLevel, TState state, Exception exception, Func formatter) { var logEntry = LogEntry(logLevel, state, exception, formatter); - return _currentConfig.Serializer.Serialize(logEntry, typeof(object)); + return _currentConfig().Serializer.Serialize(logEntry, typeof(object)); } internal object LogEntry(LogLevel logLevel, TState state, Exception exception, Func formatter) @@ -157,7 +158,7 @@ internal object LogEntry(LogLevel logLevel, TState state, Exception exce : formatter(state, exception); // Get log entry - var logFormatter = _currentConfig.LogFormatter; + var logFormatter = _currentConfig().LogFormatter; var logEntry = logFormatter is null ? GetLogEntry(logLevel, timestamp, message, exception, structuredParameters) : GetFormattedLogEntry(logLevel, timestamp, message, exception, logFormatter, structuredParameters); @@ -212,13 +213,14 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times } } - logEntry.TryAdd(LoggingConstants.KeyTimestamp, timestamp.ToString( _currentConfig.TimestampFormat ?? "o")); - logEntry.TryAdd(_currentConfig.LogLevelKey, logLevel.ToString()); - logEntry.TryAdd(LoggingConstants.KeyService, _currentConfig.Service); + var config = _currentConfig(); + logEntry.TryAdd(LoggingConstants.KeyTimestamp, timestamp.ToString( config.TimestampFormat ?? "o")); + logEntry.TryAdd(config.LogLevelKey, logLevel.ToString()); + logEntry.TryAdd(LoggingConstants.KeyService, config.Service); logEntry.TryAdd(LoggingConstants.KeyLoggerName, _categoryName); logEntry.TryAdd(LoggingConstants.KeyMessage, message); - if (_currentConfig.SamplingRate > 0) - logEntry.TryAdd(LoggingConstants.KeySamplingRate, _currentConfig.SamplingRate); + if (config.SamplingRate > 0) + logEntry.TryAdd(LoggingConstants.KeySamplingRate, config.SamplingRate); // Use the AddExceptionDetails method instead of adding exception directly if (exception != null) @@ -244,15 +246,16 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec if (logFormatter is null) return null; + var config = _currentConfig(); var logEntry = new LogEntry { Timestamp = timestamp, Level = logLevel, - Service = _currentConfig.Service, + Service = config.Service, Name = _categoryName, Message = message, Exception = exception, // Keep this to maintain compatibility - SamplingRate = _currentConfig.SamplingRate, + SamplingRate = config.SamplingRate, }; var extraKeys = new Dictionary(); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs index 663e3f05..986d6851 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs @@ -38,7 +38,7 @@ internal sealed class PowertoolsLoggerProvider : ILoggerProvider /// /// The system wrapper /// - private readonly ISystemWrapper _systemWrapper; + private ISystemWrapper _systemWrapper; /// /// The loggers @@ -56,17 +56,17 @@ internal sealed class PowertoolsLoggerProvider : ILoggerProvider /// public PowertoolsLoggerProvider(IOptionsMonitor config, IPowertoolsConfigurations powertoolsConfigurations, - ISystemWrapper? systemWrapper = null) + ISystemWrapper systemWrapper = null) { - // Use custom system wrapper if provided through config - var currentConfig = config.CurrentValue; - _systemWrapper = currentConfig.LoggerOutput ?? systemWrapper ?? new SystemWrapper(); - + _currentConfig = config.CurrentValue; + _systemWrapper = systemWrapper; _powertoolsConfigurations = powertoolsConfigurations; - _currentConfig = currentConfig; - - _onChangeToken = config.OnChange(updatedConfig => _currentConfig = updatedConfig); - ApplyPowertoolsConfig(_currentConfig); + + _onChangeToken = config.OnChange(updatedConfig => + { + _currentConfig = updatedConfig; + // No need to do anything else - the loggers get the config through GetCurrentConfig + }); } /// @@ -79,107 +79,112 @@ public ILogger CreateLogger(string categoryName) _powertoolsConfigurations.SetExecutionEnvironment(typeof(PowertoolsLogger)); return _loggers.GetOrAdd(categoryName, name => new PowertoolsLogger(name, - () => _currentConfig, - _systemWrapper)); - } - - internal PowertoolsLoggerConfiguration GetCurrentConfig() - { - var config = _currentConfig; - - ApplyPowertoolsConfig(config); - - return config; + GetCurrentConfig, + () => _systemWrapper)); } - private void ApplyPowertoolsConfig(PowertoolsLoggerConfiguration config) + internal PowertoolsLoggerConfiguration GetCurrentConfig() => _currentConfig; + + public void UpdateConfiguration(PowertoolsLoggerConfiguration config) { - var logLevel = _powertoolsConfigurations.GetLogLevel(LogLevel.None); - var lambdaLogLevel = _powertoolsConfigurations.GetLambdaLogLevel(); - var lambdaLogLevelEnabled = _powertoolsConfigurations.LambdaLogLevelEnabled(); - - // Check for explicit config - bool hasExplicitLevel = config.MinimumLogLevel != LogLevel.None; - - // Warn if Lambda log level doesn't match - if (lambdaLogLevelEnabled && hasExplicitLevel && config.MinimumLogLevel < lambdaLogLevel) - { - _systemWrapper.LogLine( - $"Current log level ({config.MinimumLogLevel}) does not match AWS Lambda Advanced Logging Controls minimum log level ({lambdaLogLevel}). This can lead to data loss, consider adjusting them."); - } - - // Set service from environment if not explicitly set - if (string.IsNullOrEmpty(config.Service)) - { - config.Service = _powertoolsConfigurations.Service; - } - - // Set output case from environment if not explicitly set - if (config.LoggerOutputCase == LoggerOutputCase.Default) - { - var loggerOutputCase = _powertoolsConfigurations.GetLoggerOutputCase(config.LoggerOutputCase); - config.LoggerOutputCase = loggerOutputCase; - } - - // Set log level from environment ONLY if not explicitly set - if (!hasExplicitLevel) - { - var minLogLevel = lambdaLogLevelEnabled ? lambdaLogLevel : logLevel; - config.MinimumLogLevel = minLogLevel != LogLevel.None ? minLogLevel : LoggingConstants.DefaultLogLevel; - } - - config.XRayTraceId = _powertoolsConfigurations.XRayTraceId; - config.LogEvent = _powertoolsConfigurations.LoggerLogEvent; - - // Configure the log level key based on output case - config.LogLevelKey = _powertoolsConfigurations.LambdaLogLevelEnabled() && - config.LoggerOutputCase == LoggerOutputCase.PascalCase - ? "LogLevel" - : LoggingConstants.KeyLogLevel; - - // Handle sampling rate - BUT DON'T MODIFY MINIMUM LEVEL - ProcessSamplingRate(config); + _currentConfig = config; } - - private void ProcessSamplingRate(PowertoolsLoggerConfiguration config) + + public void UpdateSystem(ISystemWrapper system) { - var samplingRate = config.SamplingRate > 0 - ? config.SamplingRate - : _powertoolsConfigurations.LoggerSampleRate; - - samplingRate = ValidateSamplingRate(samplingRate, config.MinimumLogLevel, _systemWrapper); - config.SamplingRate = samplingRate; - - // Only notify if sampling is configured - if (samplingRate > 0) - { - double sample = _systemWrapper.GetRandom(); - - // Instead of changing log level, just indicate sampling status - if (sample <= samplingRate) - { - _systemWrapper.LogLine( - $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate}, Sampler Value: {sample}."); - config.MinimumLogLevel = LogLevel.Debug; - } - } + if (system == null) return; + + _systemWrapper = system; } - private static double ValidateSamplingRate(double samplingRate, LogLevel minLogLevel, ISystemWrapper systemWrapper) - { - if (samplingRate < 0 || samplingRate > 1) - { - if (minLogLevel is LogLevel.Debug or LogLevel.Trace) - { - systemWrapper.LogLine( - $"Skipping sampling rate configuration because of invalid value. Sampling rate: {samplingRate}"); - } - - return 0; - } - - return samplingRate; - } + // private void ApplyPowertoolsConfig(PowertoolsLoggerConfiguration config) + // { + // var logLevel = _powertoolsConfigurations.GetLogLevel(LogLevel.None); + // var lambdaLogLevel = _powertoolsConfigurations.GetLambdaLogLevel(); + // var lambdaLogLevelEnabled = _powertoolsConfigurations.LambdaLogLevelEnabled(); + // + // // Check for explicit config + // bool hasExplicitLevel = config.MinimumLogLevel != LogLevel.None; + // + // // Warn if Lambda log level doesn't match + // if (lambdaLogLevelEnabled && hasExplicitLevel && config.MinimumLogLevel < lambdaLogLevel) + // { + // _systemWrapper.LogLine( + // $"Current log level ({config.MinimumLogLevel}) does not match AWS Lambda Advanced Logging Controls minimum log level ({lambdaLogLevel}). This can lead to data loss, consider adjusting them."); + // } + // + // // Set service from environment if not explicitly set + // if (string.IsNullOrEmpty(config.Service)) + // { + // config.Service = _powertoolsConfigurations.Service; + // } + // + // // Set output case from environment if not explicitly set + // if (config.LoggerOutputCase == LoggerOutputCase.Default) + // { + // var loggerOutputCase = _powertoolsConfigurations.GetLoggerOutputCase(config.LoggerOutputCase); + // config.LoggerOutputCase = loggerOutputCase; + // } + // + // // Set log level from environment ONLY if not explicitly set + // if (!hasExplicitLevel) + // { + // var minLogLevel = lambdaLogLevelEnabled ? lambdaLogLevel : logLevel; + // config.MinimumLogLevel = minLogLevel != LogLevel.None ? minLogLevel : LoggingConstants.DefaultLogLevel; + // } + // + // config.XRayTraceId = _powertoolsConfigurations.XRayTraceId; + // config.LogEvent = _powertoolsConfigurations.LoggerLogEvent; + // + // // Configure the log level key based on output case + // config.LogLevelKey = _powertoolsConfigurations.LambdaLogLevelEnabled() && + // config.LoggerOutputCase == LoggerOutputCase.PascalCase + // ? "LogLevel" + // : LoggingConstants.KeyLogLevel; + // + // // Handle sampling rate - BUT DON'T MODIFY MINIMUM LEVEL + // ProcessSamplingRate(config); + // } + // + // private void ProcessSamplingRate(PowertoolsLoggerConfiguration config) + // { + // var samplingRate = config.SamplingRate > 0 + // ? config.SamplingRate + // : _powertoolsConfigurations.LoggerSampleRate; + // + // samplingRate = ValidateSamplingRate(samplingRate, config.MinimumLogLevel, _systemWrapper); + // config.SamplingRate = samplingRate; + // + // // Only notify if sampling is configured + // if (samplingRate > 0) + // { + // double sample = _systemWrapper.GetRandom(); + // + // // Instead of changing log level, just indicate sampling status + // if (sample <= samplingRate) + // { + // _systemWrapper.LogLine( + // $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate}, Sampler Value: {sample}."); + // config.MinimumLogLevel = LogLevel.Debug; + // } + // } + // } + // + // private static double ValidateSamplingRate(double samplingRate, LogLevel minLogLevel, ISystemWrapper systemWrapper) + // { + // if (samplingRate < 0 || samplingRate > 1) + // { + // if (minLogLevel is LogLevel.Debug or LogLevel.Trace) + // { + // systemWrapper.LogLine( + // $"Skipping sampling rate configuration because of invalid value. Sampling rate: {samplingRate}"); + // } + // + // return 0; + // } + // + // return samplingRate; + // } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Formatter.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Formatter.cs index fdeb3c97..d5f5b5ca 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Formatter.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Formatter.cs @@ -24,7 +24,9 @@ public static partial class Logger /// WARNING: This method should not be called when using AOT. ILogFormatter should be passed to PowertoolsSourceGeneratorSerializer constructor public static void UseFormatter(ILogFormatter logFormatter) { - _currentConfig.LogFormatter = logFormatter; + Configure(config => { + config.LogFormatter = logFormatter; + }); } /// @@ -32,6 +34,8 @@ public static void UseFormatter(ILogFormatter logFormatter) /// public static void UseDefaultFormatter() { - _currentConfig.LogFormatter = null; + Configure(config => { + config.LogFormatter = null; + }); } } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs index 23f72614..d6398a08 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs @@ -15,8 +15,9 @@ using System; using System.Text.Json; -using System.Threading; using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Internal.Helpers; using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging; @@ -26,99 +27,72 @@ namespace AWS.Lambda.Powertools.Logging; /// public static partial class Logger { - // Use Lazy for thread-safe initialization - private static Lazy _factoryLazy; - private static Lazy _defaultLoggerLazy; + private static readonly object _lock = new object(); - // Static constructor to ensure initialization + // Static constructor to ensure initialization happens before first use static Logger() { - // Initialize with default configuration (ensures we never have null fields) - InitializeWithDefaults(); + LoggerInstance = GetPowertoolsLogger(); + // Factory and logger will be lazily initialized when first accessed } - // Properties to access the lazy-initialized instances - private static ILoggerFactory Factory => _factoryLazy.Value; - private static ILogger LoggerInstance => _defaultLoggerLazy.Value; + // Get the current logger instance + private static ILogger LoggerInstance; - // Add this field to the Logger class - private static PowertoolsLoggerConfiguration _currentConfig; - - // Initialize with default settings - private static void InitializeWithDefaults() - { - _currentConfig = new PowertoolsLoggerConfiguration(); - - // Create default factory with minimal configuration - _factoryLazy = new Lazy(() => - PowertoolsLoggerFactory.Create(_currentConfig)); - - _defaultLoggerLazy = new Lazy(() => - Factory.CreatePowertoolsLogger()); - } - - // Allow manual configuration using options - internal static void Configure(Action configureOptions) - { - var options = new PowertoolsLoggerConfiguration(); - configureOptions(options); - Configure(options); - } - - // Configure with existing factory + /// + /// Configure with an existing logger factory + /// + /// The factory to use internal static void Configure(ILoggerFactory loggerFactory) { - Interlocked.Exchange(ref _factoryLazy, - new Lazy(() => loggerFactory)); - - Interlocked.Exchange(ref _defaultLoggerLazy, - new Lazy(() => Factory.CreatePowertoolsLogger())); + if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); + LoggerFactoryHolder.SetFactory(loggerFactory); } - // Directly configure from a PowertoolsLoggerConfiguration - internal static void Configure(PowertoolsLoggerConfiguration options) + /// + /// Configure using a configuration action + /// + /// + internal static void Configure(Action configure) { - if (options == null) throw new ArgumentNullException(nameof(options)); - - // Store current config - _currentConfig = options; - - // Update factory and logger - Interlocked.Exchange(ref _factoryLazy, - new Lazy(() => PowertoolsLoggerFactory.Create(_currentConfig))); - - Interlocked.Exchange(ref _defaultLoggerLazy, - new Lazy(() => Factory.CreatePowertoolsLogger())); + lock (_lock) + { + var config = GetCurrentConfiguration(); + configure(config); + PowertoolsLoggingBuilderExtensions.UpdateConfiguration(config); + } } - - // Get the current configuration - internal static PowertoolsLoggerConfiguration GetConfiguration() + + public static PowertoolsLoggerConfiguration GetCurrentConfiguration() { - // Ensure logger is initialized - _ = LoggerInstance; - - return _currentConfig; + return PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); } - // Get a logger for a specific category - internal static ILogger GetLogger() => GetLogger(typeof(T).Name); - - internal static ILogger GetLogger(string category) => Factory.CreateLogger(category); - - internal static ILogger GetPowertoolsLogger() => Factory.CreatePowertoolsLogger(); + // /// + // /// Get a logger for a specific type + // /// + // /// The type to create logger for + // /// A configured logger + // internal static ILogger GetLogger() => GetLogger(typeof(T).Name); + // + // /// + // /// Get a logger for a specific category + // /// + // /// The category name + // /// A configured logger + // internal static ILogger GetLogger(string category) + // { + // return LoggerFactoryHolder.GetOrCreateFactory().CreateLogger(category); + // } + // /// - /// Sets a custom output for the static logger. - /// Useful for testing to redirect logs to a test output. + /// Get the Powertools logger instance /// - /// The custom output implementation - public static void UseOutput(ISystemWrapper loggerOutput) + /// The configured Powertools logger + internal static ILogger GetPowertoolsLogger() { - if (loggerOutput == null) - throw new ArgumentNullException(nameof(loggerOutput)); - - _currentConfig.LoggerOutput = loggerOutput; - Configure(_currentConfig); + return LoggerFactoryHolder.GetOrCreateFactory().CreatePowertoolsLogger(); } /// @@ -127,18 +101,20 @@ public static void UseOutput(ISystemWrapper loggerOutput) /// The case to use for the output public static void UseOutputCase(LoggerOutputCase outputCase) { - _currentConfig.LoggerOutputCase = outputCase; - Configure(_currentConfig); + Configure(config => { + config.LoggerOutputCase = outputCase; + }); } - + /// /// Configures the minimum log level /// /// The minimum log level to display public static void UseMinimumLogLevel(LogLevel logLevel) { - _currentConfig.MinimumLogLevel = logLevel; - Configure(_currentConfig); + Configure(config => { + config.MinimumLogLevel = logLevel; + }); } /// @@ -149,9 +125,10 @@ public static void UseServiceName(string serviceName) { if (string.IsNullOrEmpty(serviceName)) throw new ArgumentException("Service name cannot be null or empty", nameof(serviceName)); - - _currentConfig.Service = serviceName; - Configure(_currentConfig); + + Configure(config => { + config.Service = serviceName; + }); } /// @@ -162,9 +139,10 @@ public static void UseSamplingRate(double samplingRate) { if (samplingRate < 0 || samplingRate > 1) throw new ArgumentOutOfRangeException(nameof(samplingRate), "Sampling rate must be between 0 and 1"); - - _currentConfig.SamplingRate = samplingRate; - Configure(_currentConfig); + + Configure(config => { + config.SamplingRate = samplingRate; + }); } /// @@ -182,11 +160,9 @@ public static void UseLogBuffering(LogBufferingOptions logBuffering) if (logBuffering == null) throw new ArgumentNullException(nameof(logBuffering)); - // Update the current configuration - _currentConfig.LogBuffering = logBuffering; - - // Reconfigure to apply changes - Configure(_currentConfig); + Configure(config => { + config.LogBuffering = logBuffering; + }); } #if NET8_0_OR_GREATER @@ -198,15 +174,25 @@ public static void UseJsonOptions(JsonSerializerOptions jsonOptions) { if (jsonOptions == null) throw new ArgumentNullException(nameof(jsonOptions)); - - // Update the current configuration - _currentConfig.JsonOptions = jsonOptions; + + Configure(config => { + config.JsonOptions = jsonOptions; + }); } #endif - // For testing purposes + // Add to Logger.cs + public static ISystemWrapper UseStringWriter() + { + return PowertoolsLoggerTestHelpers.EnableTestMode(); + + } + + /// + /// Reset the logger for testing + /// internal static void Reset() { - InitializeWithDefaults(); + LoggerFactoryHolder.Reset(); } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs index e18bc22b..06f6657b 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs @@ -46,12 +46,6 @@ public PowertoolsLoggerBuilder WithOutputCase(LoggerOutputCase outputCase) _configuration.LoggerOutputCase = outputCase; return this; } - - public PowertoolsLoggerBuilder WithOutput(ISystemWrapper output) - { - _configuration.LoggerOutput = output; - return this; - } public PowertoolsLoggerBuilder WithFormatter(ILogFormatter formatter) { diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs index e713eb0b..f6a2ea5b 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs @@ -63,11 +63,6 @@ public class PowertoolsLoggerConfiguration : IOptions internal string LogLevelKey { get; set; } = "level"; - /// - /// Custom output logger to use instead of Console - /// - public ISystemWrapper? LoggerOutput { get; set; } - /// /// Custom log formatter to use for formatting log entries /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs new file mode 100644 index 00000000..70e2f135 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Internal.Helpers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Configuration; +using Microsoft.Extensions.Options; + +namespace AWS.Lambda.Powertools.Logging; + +/// +/// Extension methods for configuring the Powertools logger +/// +public static class PowertoolsLoggingBuilderExtensions +{ + private static readonly ConcurrentBag AllProviders = new(); + private static readonly object _lock = new(); + private static PowertoolsLoggerConfiguration _currentConfig = new(); + + public static void UpdateConfiguration(PowertoolsLoggerConfiguration config) + { + lock (_lock) + { + // Update the shared configuration + _currentConfig = config; + + // Notify all providers about the change + foreach (var provider in AllProviders) + { + provider.UpdateConfiguration(config); + } + } + } + + public static PowertoolsLoggerConfiguration GetCurrentConfiguration() + { + lock (_lock) + { + // Return a copy to prevent external modification + return new PowertoolsLoggerConfiguration + { + Service = _currentConfig.Service, + SamplingRate = _currentConfig.SamplingRate, + MinimumLogLevel = _currentConfig.MinimumLogLevel, + LoggerOutputCase = _currentConfig.LoggerOutputCase, + JsonOptions = _currentConfig.JsonOptions, + TimestampFormat = _currentConfig.TimestampFormat, + LogFormatter = _currentConfig.LogFormatter, + LogLevelKey = _currentConfig.LogLevelKey, + LogBuffering = _currentConfig.LogBuffering + + }; + } + } + + public static ILoggingBuilder AddPowertoolsLogger( + this ILoggingBuilder builder) + { + builder.AddConfiguration(); + + // Register ISystemWrapper if not already registered + builder.Services.TryAddSingleton(provider => + { + // Check if there's a pending mock system first + var mockSystem = PowertoolsLoggerTestHelpers.GetSystemWrapper(); + return mockSystem ?? new SystemWrapper(); + }); + + builder.Services.TryAddSingleton(); + // Register IPowertoolsConfigurations with all its dependencies + builder.Services.TryAddSingleton(sp => + new PowertoolsConfigurations(sp.GetRequiredService())); + + // Register the regular provider + builder.Services.TryAddEnumerable( + ServiceDescriptor.Singleton(provider => + { + var powertoolsConfigurations = provider.GetRequiredService(); + var systemWrapper = provider.GetRequiredService(); + + var loggerProvider = new PowertoolsLoggerProvider( + new TrackedOptionsMonitor(_currentConfig, UpdateConfiguration), powertoolsConfigurations, + systemWrapper); + lock (_lock) + { + AllProviders.Add(loggerProvider); + } + + return loggerProvider; + })); + + builder.Services.ConfigureOptions(); + + LoggerProviderOptions.RegisterProviderOptions + (builder.Services); + + return builder; + } + + /// + /// Adds the Powertools logger to the logging builder. + /// + public static ILoggingBuilder AddPowertoolsLogger( + this ILoggingBuilder builder, + Action configure) + { + // Add configuration + builder.AddPowertoolsLogger(); + + // Create initial configuration + var options = new PowertoolsLoggerConfiguration(); + configure(options); + + // IMPORTANT: Set the minimum level directly on the builder + if (options.MinimumLogLevel != LogLevel.None) + { + builder.SetMinimumLevel(options.MinimumLogLevel); + } + + builder.Services.Configure(configure); + + UpdateConfiguration(options); + + // If buffering is enabled, register buffer providers + if (options?.LogBuffering?.Enabled == true) + { + // Add a filter for the buffer provider + builder.AddFilter( + null, + options.LogBuffering.BufferAtLogLevel); + + // Register the inner provider factory + builder.Services.AddSingleton(sp => + new BufferingLoggerProvider( + // We need to create a PowertoolsLoggerProvider here + new PowertoolsLoggerProvider( + new TrackedOptionsMonitor(_currentConfig, UpdateConfiguration), + sp.GetRequiredService(), + sp.GetRequiredService() + ), + new TrackedOptionsMonitor(_currentConfig, UpdateConfiguration) + ) + ); + } + + return builder; + } + + internal static void UpdateSystemInAllProviders(ISystemWrapper system) + { + if (system == null) return; + + lock (_lock) + { + foreach (var provider in AllProviders) + { + provider.UpdateSystem(system); + } + } + } + + private class ConfigureLoggingOptions : IConfigureOptions + { + private readonly IPowertoolsConfigurations _configurations; + private readonly ISystemWrapper _systemWrapper; + + public ConfigureLoggingOptions(IPowertoolsConfigurations configurations, ISystemWrapper systemWrapper) + { + _configurations = configurations; + _systemWrapper = systemWrapper; + } + + public void Configure(LoggerFilterOptions options) + { + // This runs when IOptions is resolved + LoggerFactoryHolder.ConfigureFromEnvironment(_configurations,_systemWrapper); + } + } + + private class TrackedOptionsMonitor : IOptionsMonitor + { + private PowertoolsLoggerConfiguration _config; + private readonly Action _updateCallback; + private readonly List> _listeners = new(); + + public TrackedOptionsMonitor( + PowertoolsLoggerConfiguration config, + Action updateCallback) + { + _config = config; + _updateCallback = updateCallback; + } + + public PowertoolsLoggerConfiguration CurrentValue => _config; + + public IDisposable OnChange(Action listener) + { + _listeners.Add(listener); + return new ListenerDisposable(_listeners, listener); + } + + public PowertoolsLoggerConfiguration Get(string? name) => _config; + + private class ListenerDisposable : IDisposable + { + private readonly List> _listeners; + private readonly Action _listener; + + public ListenerDisposable( + List> listeners, + Action listener) + { + _listeners = listeners; + _listener = listener; + } + + public void Dispose() + { + _listeners.Remove(_listener); + } + } + } +} \ No newline at end of file From e7dc7049a2b1922c9c185e42071f8a7ee53cb30a Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Sat, 29 Mar 2025 10:09:01 +0000 Subject: [PATCH 21/49] fix buffering --- .../Buffer/BufferingLoggerProvider.cs | 50 ++++++-------- .../Internal/Helpers/LoggerFactoryHelper.cs | 44 ------------- .../Internal/PowertoolsLoggerProvider.cs | 6 +- .../AWS.Lambda.Powertools.Logging/Logger.cs | 7 -- .../PowertoolsLoggingBuilderExtensions.cs | 65 +++++++++++++------ 5 files changed, 69 insertions(+), 103 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs index c3b099dc..3f5e89e2 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs @@ -15,6 +15,7 @@ using System; using System.Collections.Concurrent; +using AWS.Lambda.Powertools.Common; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -24,53 +25,30 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// Logger provider that supports buffering logs /// [ProviderAlias("PowertoolsBuffering")] -internal class BufferingLoggerProvider : ILoggerProvider +internal class BufferingLoggerProvider : PowertoolsLoggerProvider { - private readonly ILoggerProvider _innerProvider; private readonly ConcurrentDictionary _loggers = new(); - private readonly IDisposable? _onChangeToken; - private PowertoolsLoggerConfiguration _currentConfig; public BufferingLoggerProvider( - ILoggerProvider innerProvider, - IOptionsMonitor config) + IOptionsMonitor config, + IPowertoolsConfigurations powertoolsConfigurations, + ISystemWrapper systemWrapper) + : base(config, powertoolsConfigurations, systemWrapper) { - _innerProvider = innerProvider ?? throw new ArgumentNullException(nameof(innerProvider)); - _currentConfig = config.CurrentValue; - - _onChangeToken = config.OnChange(updatedConfig => - { - _currentConfig = updatedConfig; - // No need to do anything else - the loggers get the config through GetCurrentConfig - }); // Register with the buffer manager LogBufferManager.RegisterProvider(this); } - public ILogger CreateLogger(string categoryName) + public override ILogger CreateLogger(string categoryName) { return _loggers.GetOrAdd( categoryName, name => new PowertoolsBufferingLogger( - _innerProvider.CreateLogger(name), + base.CreateLogger(name), // Use the parent's logger creation GetCurrentConfig, name)); } - internal PowertoolsLoggerConfiguration GetCurrentConfig() => _currentConfig; - - public void Dispose() - { - // Flush all buffers before disposing - foreach (var logger in _loggers.Values) - { - logger.FlushBuffer(); - } - - _innerProvider.Dispose(); - _loggers.Clear(); - } - /// /// Flush all buffered logs /// @@ -103,4 +81,16 @@ public void ClearCurrentBuffer() logger.ClearCurrentInvocation(); } } + + public override void Dispose() + { + // Flush all buffers before disposing + foreach (var logger in _loggers.Values) + { + logger.FlushBuffer(); + } + + _loggers.Clear(); + base.Dispose(); + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs index a4c64a65..e08cfdd1 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs @@ -1,6 +1,3 @@ -using System; -using AWS.Lambda.Powertools.Common; -using AWS.Lambda.Powertools.Common.Tests; using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging.Internal.Helpers; @@ -38,45 +35,4 @@ public static ILoggerFactory CreateAndConfigureFactory(PowertoolsLoggerConfigura return factory; } -} - -// Add to a new TestHelpers.cs file -public static class PowertoolsLoggerTestHelpers -{ - private static readonly object _lock = new(); - private static ISystemWrapper _systemWrapper; - - static PowertoolsLoggerTestHelpers() - { - _systemWrapper = null; - } - - // Call this at the beginning of your test - public static TestLoggerOutput EnableTestMode() - { - var system = new TestLoggerOutput(); - _systemWrapper = system; - PowertoolsLoggingBuilderExtensions.UpdateSystemInAllProviders(system); - return system; - } - - public static void UseCustomSystem(ISystemWrapper system) - { - if (system == null) throw new ArgumentNullException(nameof(system)); - lock (_lock) - { - // Store the mock system for later use when providers are created - _systemWrapper = system; - // Update all providers to use the mock system - PowertoolsLoggingBuilderExtensions.UpdateSystemInAllProviders(system); - } - } - - internal static ISystemWrapper GetSystemWrapper() - { - lock (_lock) - { - return _systemWrapper; - } - } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs index 986d6851..e0caecb2 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs @@ -28,7 +28,7 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// /// [ProviderAlias("PowertoolsLogger")] -internal sealed class PowertoolsLoggerProvider : ILoggerProvider +internal class PowertoolsLoggerProvider : ILoggerProvider { /// /// The powertools configurations @@ -74,7 +74,7 @@ public PowertoolsLoggerProvider(IOptionsMonitor c /// /// The category name for messages produced by the logger. /// The instance of that was created. - public ILogger CreateLogger(string categoryName) + public virtual ILogger CreateLogger(string categoryName) { _powertoolsConfigurations.SetExecutionEnvironment(typeof(PowertoolsLogger)); @@ -189,7 +189,7 @@ public void UpdateSystem(ISystemWrapper system) /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// - public void Dispose() + public virtual void Dispose() { _loggers.Clear(); _onChangeToken?.Dispose(); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs index d6398a08..ff9ae0fa 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs @@ -180,13 +180,6 @@ public static void UseJsonOptions(JsonSerializerOptions jsonOptions) }); } #endif - - // Add to Logger.cs - public static ISystemWrapper UseStringWriter() - { - return PowertoolsLoggerTestHelpers.EnableTestMode(); - - } /// /// Reset the logger for testing diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs index 70e2f135..15d480dc 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs @@ -63,12 +63,14 @@ public static ILoggingBuilder AddPowertoolsLogger( builder.AddConfiguration(); // Register ISystemWrapper if not already registered - builder.Services.TryAddSingleton(provider => - { - // Check if there's a pending mock system first - var mockSystem = PowertoolsLoggerTestHelpers.GetSystemWrapper(); - return mockSystem ?? new SystemWrapper(); - }); + // builder.Services.TryAddSingleton(provider => + // { + // // Check if there's a pending mock system first + // var mockSystem = PowertoolsLoggerTestFixture.GetSystemWrapper(); + // return mockSystem ?? new SystemWrapper(); + // }); + + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); // Register IPowertoolsConfigurations with all its dependencies @@ -120,7 +122,7 @@ public static ILoggingBuilder AddPowertoolsLogger( { builder.SetMinimumLevel(options.MinimumLogLevel); } - + builder.Services.Configure(configure); UpdateConfiguration(options); @@ -133,18 +135,27 @@ public static ILoggingBuilder AddPowertoolsLogger( null, options.LogBuffering.BufferAtLogLevel); - // Register the inner provider factory - builder.Services.AddSingleton(sp => - new BufferingLoggerProvider( - // We need to create a PowertoolsLoggerProvider here - new PowertoolsLoggerProvider( - new TrackedOptionsMonitor(_currentConfig, UpdateConfiguration), - sp.GetRequiredService(), - sp.GetRequiredService() - ), - new TrackedOptionsMonitor(_currentConfig, UpdateConfiguration) - ) - ); + // Register the buffer provider as an enumerable service + // Using singleton to ensure it's properly tracked + builder.Services.TryAddEnumerable( + ServiceDescriptor.Singleton(provider => + { + var powertoolsConfigurations = provider.GetRequiredService(); + var systemWrapper = provider.GetRequiredService(); + + var bufferingProvider = new BufferingLoggerProvider( + new TrackedOptionsMonitor(_currentConfig, UpdateConfiguration), + powertoolsConfigurations, + systemWrapper + ); + + lock (_lock) + { + AllProviders.Add(bufferingProvider); + } + + return bufferingProvider; + })); } return builder; @@ -163,6 +174,22 @@ internal static void UpdateSystemInAllProviders(ISystemWrapper system) } } + /// + /// Resets all providers and clears the configuration. + /// This is useful for testing purposes to ensure a clean state. + /// + internal static void ResetAllProviders() + { + lock (_lock) + { + // Clear the provider collection + AllProviders.Clear(); + + // Reset the current configuration to default + _currentConfig = new PowertoolsLoggerConfiguration(); + } + } + private class ConfigureLoggingOptions : IConfigureOptions { private readonly IPowertoolsConfigurations _configurations; From c43c165e03dc7f3c32ea3e0fb54dd51277ca259d Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 31 Mar 2025 23:37:12 +0100 Subject: [PATCH 22/49] refactor: enhance logger configuration and output handling. Fix tests --- .../Core/TestLoggerOutput.cs | 22 +- .../Buffer/BufferingLoggerProvider.cs | 30 +- .../Internal/Buffer/Logger.Buffer.cs | 1 + .../Internal/Converters/ByteArrayConverter.cs | 27 +- .../Internal/Helpers/LoggerFactoryHelper.cs | 3 + .../Internal/LoggerFactoryHolder.cs | 178 ++---- .../Internal/LoggingAspect.cs | 19 +- .../Internal/LoggingAspectFactory.cs | 1 + .../Internal/PowertoolsLogger.cs | 39 +- .../Internal/PowertoolsLoggerProvider.cs | 240 ++++---- .../AWS.Lambda.Powertools.Logging/Logger.cs | 62 +- .../LoggingAttribute.cs | 14 +- .../PowertoolsLoggerConfiguration.cs | 46 +- .../PowertoolsLoggingBuilderExtensions.cs | 120 +--- .../PowertoolsLoggingSerializer.cs | 37 +- .../Attributes/LoggerAspectTests.cs | 278 +++++---- .../Attributes/LoggingAttributeTest.cs | 296 +++++----- .../Attributes/ServiceTests.cs | 50 ++ .../Formatter/LogFormatterTest.cs | 83 +-- .../Handlers/ExceptionFunctionHandlerTests.cs | 2 +- .../Handlers/TestHandlers.cs | 11 - .../PowertoolsLoggerTest.cs | 558 +++++++++--------- .../PowertoolsLambdaSerializerTests.cs | 34 +- .../PowertoolsLoggingSerializerTests.cs | 268 +++++++-- .../PowertoolsConfigurationExtensionsTests.cs | 16 +- .../Utilities/PowertoolsLoggerHelpersTests.cs | 21 +- .../Utilities/SystemWrapperMock.cs | 68 --- 27 files changed, 1289 insertions(+), 1235 deletions(-) create mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/ServiceTests.cs delete mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/SystemWrapperMock.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/TestLoggerOutput.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/TestLoggerOutput.cs index 9a64c881..96974d69 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/TestLoggerOutput.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/TestLoggerOutput.cs @@ -12,7 +12,7 @@ public class TestLoggerOutput : ISystemWrapper /// /// Buffer for all the log messages written to the logger. /// - public StringBuilder Buffer { get; } = new StringBuilder(); + private readonly StringBuilder _outputBuffer = new StringBuilder(); /// /// Logs the specified value. @@ -20,8 +20,7 @@ public class TestLoggerOutput : ISystemWrapper /// public void Log(string value) { - Buffer.Append(value); - Console.Write(value); + _outputBuffer.Append(value); } /// @@ -29,8 +28,7 @@ public void Log(string value) /// public void LogLine(string value) { - Buffer.AppendLine(value); - Console.WriteLine(value); + _outputBuffer.AppendLine(value); } /// @@ -46,15 +44,15 @@ public double GetRandom() /// public void SetOut(TextWriter writeTo) { - Console.SetOut(writeTo); } - - /// - /// Overrides the ToString method to return the buffer as a string. - /// - /// + + public void Clear() + { + _outputBuffer.Clear(); + } + public override string ToString() { - return Buffer.ToString(); + return _outputBuffer.ToString(); } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs index 3f5e89e2..2e0ce5fa 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.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 @@ -28,27 +28,27 @@ namespace AWS.Lambda.Powertools.Logging.Internal; internal class BufferingLoggerProvider : PowertoolsLoggerProvider { private readonly ConcurrentDictionary _loggers = new(); - + private readonly IPowertoolsConfigurations _powertoolsConfigurations; + public BufferingLoggerProvider( - IOptionsMonitor config, - IPowertoolsConfigurations powertoolsConfigurations, - ISystemWrapper systemWrapper) - : base(config, powertoolsConfigurations, systemWrapper) + PowertoolsLoggerConfiguration config, + IPowertoolsConfigurations powertoolsConfigurations) + : base(config, powertoolsConfigurations) { // Register with the buffer manager LogBufferManager.RegisterProvider(this); } - + public override ILogger CreateLogger(string categoryName) { return _loggers.GetOrAdd( - categoryName, + categoryName, name => new PowertoolsBufferingLogger( base.CreateLogger(name), // Use the parent's logger creation GetCurrentConfig, name)); } - + /// /// Flush all buffered logs /// @@ -59,7 +59,7 @@ public void FlushBuffers() logger.FlushBuffer(); } } - + /// /// Clear all buffered logs /// @@ -70,7 +70,7 @@ public void ClearBuffers() logger.ClearBuffer(); } } - + /// /// Clear buffered logs for the current invocation only /// @@ -81,7 +81,7 @@ public void ClearCurrentBuffer() logger.ClearCurrentInvocation(); } } - + public override void Dispose() { // Flush all buffers before disposing @@ -89,7 +89,7 @@ public override void Dispose() { logger.FlushBuffer(); } - + _loggers.Clear(); base.Dispose(); } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/Logger.Buffer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/Logger.Buffer.cs index 9e715c55..f3739651 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/Logger.Buffer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/Logger.Buffer.cs @@ -14,6 +14,7 @@ * permissions and limitations under the License. */ +using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; namespace AWS.Lambda.Powertools.Logging; diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ByteArrayConverter.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ByteArrayConverter.cs index b6d7120d..ab709bf9 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ByteArrayConverter.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ByteArrayConverter.cs @@ -34,7 +34,13 @@ internal class ByteArrayConverter : JsonConverter /// public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - throw new NotSupportedException("Deserializing ByteArray is not allowed"); + if (reader.TokenType == JsonTokenType.Null) + return null; + + if (reader.TokenType == JsonTokenType.String) + return Convert.FromBase64String(reader.GetString()!); + + throw new JsonException("Expected string value for byte array"); } /// @@ -43,22 +49,15 @@ public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS /// The unicode JsonWriter. /// The byte array. /// The JsonSerializer options. - public override void Write(Utf8JsonWriter writer, byte[] values, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options) { - if (values == null) + if (value == null) { writer.WriteNullValue(); + return; } - else - { - writer.WriteStartArray(); - - foreach (var value in values) - { - writer.WriteNumberValue(value); - } - - writer.WriteEndArray(); - } + + string base64 = Convert.ToBase64String(value); + writer.WriteStringValue(base64); } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs index e08cfdd1..e8dacf4e 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs @@ -27,6 +27,9 @@ public static ILoggerFactory CreateAndConfigureFactory(PowertoolsLoggerConfigura config.LogFormatter = configuration.LogFormatter; config.LogLevelKey = configuration.LogLevelKey; config.LogBuffering = configuration.LogBuffering; + config.LogEvent = configuration.LogEvent; + config.LogOutput = configuration.LogOutput; + config.XRayTraceId = configuration.XRayTraceId; }); }); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerFactoryHolder.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerFactoryHolder.cs index bde8860f..9299b44b 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerFactoryHolder.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerFactoryHolder.cs @@ -29,152 +29,68 @@ internal static class LoggerFactoryHolder private static readonly object _lock = new object(); private static bool _isConfigured = false; + private static LogLevel _currentFilterLevel = LogLevel.Information; + /// - /// Gets or creates the shared logger factory + /// Updates the filter log level at runtime /// - public static ILoggerFactory GetOrCreateFactory() + /// The new minimum log level + public static void UpdateFilterLogLevel(LogLevel logLevel) { lock (_lock) { - if (_factory == null) + // Only reset if level actually changes + if (_currentFilterLevel != logLevel) { - _factory = LoggerFactory.Create(builder => builder.AddPowertoolsLogger()); + _currentFilterLevel = logLevel; + + if (_factory != null) + { + try { _factory.Dispose(); } catch { /* Ignore */ } + _factory = null; + } } - return _factory; } } - public static void SetFactory(ILoggerFactory factory) - { - if (factory == null) throw new ArgumentNullException(nameof(factory)); - lock (_lock) - { - _factory = factory; - _isConfigured = true; - } - } - /// - /// Automatically called when GetOrCreateFactory is used - /// - public static void ConfigureFromEnvironment(IPowertoolsConfigurations configurations, ISystemWrapper systemWrapper) - { - // Only configure once - if (_isConfigured) return; - - // Create initial configuration - var config = PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); - - // Apply environment configuration if available - if (configurations != null) - { - ApplyPowertoolsConfig(config, configurations, systemWrapper); - PowertoolsLoggingBuilderExtensions.UpdateConfiguration(config); - } - - _isConfigured = true; - } - - /// - /// Apply Powertools configuration from environment variables to the logger configuration - /// - private static void ApplyPowertoolsConfig(PowertoolsLoggerConfiguration config, - IPowertoolsConfigurations configurations, ISystemWrapper systemWrapper) - { - var logLevel = configurations.GetLogLevel(LogLevel.None); - var lambdaLogLevel = configurations.GetLambdaLogLevel(); - var lambdaLogLevelEnabled = configurations.LambdaLogLevelEnabled(); - - // Check for explicit config - bool hasExplicitLevel = config.MinimumLogLevel != LogLevel.None; - - // Warn if Lambda log level doesn't match - if (lambdaLogLevelEnabled && hasExplicitLevel && config.MinimumLogLevel < lambdaLogLevel) - { - systemWrapper.LogLine( - $"Current log level ({config.MinimumLogLevel}) does not match AWS Lambda Advanced Logging Controls minimum log level ({lambdaLogLevel}). This can lead to data loss, consider adjusting them."); - } - - // Set service from environment if not explicitly set - if (string.IsNullOrEmpty(config.Service)) - { - config.Service = configurations.Service; - } - - // Set output case from environment if not explicitly set - if (config.LoggerOutputCase == LoggerOutputCase.Default) - { - var loggerOutputCase = configurations.GetLoggerOutputCase(config.LoggerOutputCase); - config.LoggerOutputCase = loggerOutputCase; - } - - // Set log level from environment ONLY if not explicitly set - if (!hasExplicitLevel) - { - var minLogLevel = lambdaLogLevelEnabled ? lambdaLogLevel : logLevel; - config.MinimumLogLevel = minLogLevel != LogLevel.None ? minLogLevel : LoggingConstants.DefaultLogLevel; - } - - config.XRayTraceId = configurations.XRayTraceId; - config.LogEvent = configurations.LoggerLogEvent; - - // Configure the log level key based on output case - config.LogLevelKey = configurations.LambdaLogLevelEnabled() && - config.LoggerOutputCase == LoggerOutputCase.PascalCase - ? "LogLevel" - : LoggingConstants.KeyLogLevel; - - ProcessSamplingRate(config, configurations, systemWrapper); - } - - /// - /// Process sampling rate configuration + /// Gets or creates the shared logger factory /// - private static void ProcessSamplingRate(PowertoolsLoggerConfiguration config, IPowertoolsConfigurations configurations, ISystemWrapper systemWrapper) + public static ILoggerFactory GetOrCreateFactory() { - var samplingRate = config.SamplingRate > 0 - ? config.SamplingRate - : configurations.LoggerSampleRate; - - samplingRate = ValidateSamplingRate(samplingRate, config.MinimumLogLevel, systemWrapper); - config.SamplingRate = samplingRate; - - // Only notify if sampling is configured - if (samplingRate > 0) + lock (_lock) { - double sample = systemWrapper.GetRandom(); - - // Instead of changing log level, just indicate sampling status - if (sample <= samplingRate) + if (_factory == null) { - systemWrapper.LogLine( - $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate}, Sampler Value: {sample}."); - config.MinimumLogLevel = LogLevel.Debug; + var config = PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); + + // Use current filter level or level from config + _currentFilterLevel = config.MinimumLogLevel != LogLevel.None + ? config.MinimumLogLevel + : _currentFilterLevel; + + _factory = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(); + + // Correctly configure the filter + builder.AddFilter(null, _currentFilterLevel); + }); } + return _factory; } } - /// - /// Validate sampling rate - /// - private static double ValidateSamplingRate(double samplingRate, LogLevel minLogLevel, ISystemWrapper systemWrapper) + public static void SetFactory(ILoggerFactory factory) { - if (samplingRate < 0 || samplingRate > 1) + if (factory == null) throw new ArgumentNullException(nameof(factory)); + lock (_lock) { - if (minLogLevel is LogLevel.Debug or LogLevel.Trace) - { - systemWrapper.LogLine( - $"Skipping sampling rate configuration because of invalid value. Sampling rate: {samplingRate}"); - } - - return 0; + _factory = factory; + _isConfigured = true; } - - return samplingRate; } - - /// /// Resets the factory holder for testing /// @@ -182,8 +98,22 @@ internal static void Reset() { lock (_lock) { - var oldFactory = Interlocked.Exchange(ref _factory, null); - oldFactory?.Dispose(); + // Dispose the old factory if it exists + if (_factory != null) + { + try + { + _factory.Dispose(); + } + catch + { + // Ignore disposal errors + } + + _factory = null; + } + + _currentFilterLevel = LogLevel.None; _isConfigured = false; } } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index 0f301f9e..415547fd 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -90,14 +90,11 @@ private void InitializeLogger(LoggingAttribute trigger) if (hasOutputCase) Logger.UseOutputCase(trigger.LoggerOutputCase); if (hasSamplingRate) Logger.UseSamplingRate(trigger.SamplingRate); - // Update logger reference after configuration changes - // _logger = Logger.GetPowertoolsLogger(); - } - else if (_logger == null) - { - // Only get the logger if we don't already have it + // Need to refresh the logger after configuration changes // _logger = Logger.GetPowertoolsLogger(); + _logger = LoggerFactoryHolder.GetOrCreateFactory().CreatePowertoolsLogger(); } + // Fetch the current configuration _currentConfig = Logger.GetCurrentConfiguration(); @@ -167,8 +164,15 @@ public void OnEntry( } CaptureCorrelationId(eventObject, trigger.CorrelationIdPath); - if (logEvent || _currentConfig.LogEvent) + + if(trigger.IsLogEventSet && trigger.LogEvent) + { LogEvent(eventObject); + } + else if (!trigger.IsLogEventSet && _currentConfig.LogEvent) + { + LogEvent(eventObject); + } } catch (Exception exception) { @@ -334,6 +338,5 @@ private void LogEvent(object eventArg) internal static void ResetForTest() { LoggingLambdaContext.Clear(); - // _logger.RemoveAllKeys(); } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs index ada06cb1..a3fce9d3 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs @@ -30,6 +30,7 @@ internal static class LoggingAspectFactory /// An instance of the LoggingAspect class. public static object GetInstance(Type type) { + // Use Logger.GetPowertoolsLogger() to ensure it's consistent with current config return new LoggingAspect(LoggerFactoryHolder.GetOrCreateFactory().CreatePowertoolsLogger()); } } \ 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 f625bae6..9f278a12 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -41,11 +41,6 @@ internal sealed class PowertoolsLogger : ILogger /// private readonly Func _currentConfig; - /// - /// The system wrapper - /// - private readonly Func _getSystemWrapper; - /// /// The current scope /// @@ -56,15 +51,12 @@ internal sealed class PowertoolsLogger : ILogger /// /// The name. /// - /// The system wrapper. public PowertoolsLogger( string categoryName, - Func getCurrentConfig, - Func getSystemWrapper) + Func getCurrentConfig) { _categoryName = categoryName; _currentConfig = getCurrentConfig; - _getSystemWrapper = getSystemWrapper; } /// @@ -128,12 +120,12 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except return; } - _getSystemWrapper().LogLine(LogEntryString(logLevel, state, exception, formatter)); + _currentConfig().LogOutput.LogLine(LogEntryString(logLevel, state, exception, formatter)); } internal void LogLine(string message) { - _getSystemWrapper().LogLine(message); + _currentConfig().LogOutput.LogLine(message); } internal string LogEntryString(LogLevel logLevel, TState state, Exception exception, Func formatter) @@ -225,7 +217,7 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times // Use the AddExceptionDetails method instead of adding exception directly if (exception != null) { - AddExceptionDetails(logEntry, exception); + logEntry.TryAdd(LoggingConstants.KeyException, exception); } return logEntry; @@ -308,7 +300,7 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec if (exception != null) { var exceptionDetails = new Dictionary(); - AddExceptionDetails(exceptionDetails, exception); + exceptionDetails.TryAdd(LoggingConstants.KeyException, exception); // Add exception details to extra keys foreach (var (key, value) in exceptionDetails) @@ -564,25 +556,4 @@ private string ExtractParameterName(string key) ? nameWithPossibleFormat.Substring(0, colonIndex) : nameWithPossibleFormat; } - - private void AddExceptionDetails(Dictionary logEntry, Exception exception) - { - if (exception == null) - return; - - logEntry.TryAdd("errorType", exception.GetType().FullName); - logEntry.TryAdd("errorMessage", exception.Message); - - // Add stack trace as array of strings for better readability - var stackFrames = exception.StackTrace?.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); - if (stackFrames?.Length > 0) - { - var cleanedStackTrace = new List - { - $"{exception.GetType().FullName}: {exception.Message}" - }; - cleanedStackTrace.AddRange(stackFrames); - logEntry.TryAdd("stackTrace", cleanedStackTrace); - } - } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs index e0caecb2..bea51da3 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs @@ -30,57 +30,120 @@ namespace AWS.Lambda.Powertools.Logging.Internal; [ProviderAlias("PowertoolsLogger")] internal class PowertoolsLoggerProvider : ILoggerProvider { - /// - /// The powertools configurations - /// + private readonly ConcurrentDictionary _loggers = new(StringComparer.OrdinalIgnoreCase); + private readonly IDisposable? _onChangeToken; + private PowertoolsLoggerConfiguration _currentConfig; private readonly IPowertoolsConfigurations _powertoolsConfigurations; - /// - /// The system wrapper - /// - private ISystemWrapper _systemWrapper; + public PowertoolsLoggerProvider( + PowertoolsLoggerConfiguration config, + IPowertoolsConfigurations powertoolsConfigurations) + { + _powertoolsConfigurations = powertoolsConfigurations; + _currentConfig = config; + + // Set execution environment + _powertoolsConfigurations.SetExecutionEnvironment(this); + + // Apply environment configurations if available + ConfigureFromEnvironment(); + } - /// - /// The loggers - /// - private readonly ConcurrentDictionary _loggers = new(StringComparer.OrdinalIgnoreCase); + public void ConfigureFromEnvironment() + { + var logLevel = _powertoolsConfigurations.GetLogLevel(_currentConfig.MinimumLogLevel); + var lambdaLogLevel = _powertoolsConfigurations.GetLambdaLogLevel(); + var lambdaLogLevelEnabled = _powertoolsConfigurations.LambdaLogLevelEnabled(); - private readonly IDisposable? _onChangeToken; - private PowertoolsLoggerConfiguration _currentConfig; + // Warn if Lambda log level doesn't match + if (lambdaLogLevelEnabled && logLevel < lambdaLogLevel) + { + _currentConfig.LogOutput.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."); + } + + // Set service from environment if not explicitly set + if (string.IsNullOrEmpty(_currentConfig.Service)) + { + _currentConfig.Service = _powertoolsConfigurations.Service; + } + + // Set output case from environment if not explicitly set + if (_currentConfig.LoggerOutputCase == LoggerOutputCase.Default) + { + var loggerOutputCase = _powertoolsConfigurations.GetLoggerOutputCase(_currentConfig.LoggerOutputCase); + _currentConfig.LoggerOutputCase = loggerOutputCase; + } + // Set log level from environment ONLY if not explicitly set + var minLogLevel = lambdaLogLevelEnabled ? lambdaLogLevel : logLevel; + _currentConfig.MinimumLogLevel = minLogLevel != LogLevel.None ? minLogLevel : LoggingConstants.DefaultLogLevel; + + // LoggerFactoryHolder.UpdateFilterLogLevel(minLogLevel); + + _currentConfig.XRayTraceId = _powertoolsConfigurations.XRayTraceId; + _currentConfig.LogEvent = _powertoolsConfigurations.LoggerLogEvent; + + // Configure the log level key based on output case + _currentConfig.LogLevelKey = _powertoolsConfigurations.LambdaLogLevelEnabled() && + _currentConfig.LoggerOutputCase == LoggerOutputCase.PascalCase + ? "LogLevel" + : LoggingConstants.KeyLogLevel; + + ProcessSamplingRate(_currentConfig, _powertoolsConfigurations); + } + /// - /// Initializes a new instance of the class. + /// Process sampling rate configuration /// - /// The configuration. - /// - /// - public PowertoolsLoggerProvider(IOptionsMonitor config, - IPowertoolsConfigurations powertoolsConfigurations, - ISystemWrapper systemWrapper = null) + private void ProcessSamplingRate(PowertoolsLoggerConfiguration config, IPowertoolsConfigurations configurations) { - _currentConfig = config.CurrentValue; - _systemWrapper = systemWrapper; - _powertoolsConfigurations = powertoolsConfigurations; - - _onChangeToken = config.OnChange(updatedConfig => + var samplingRate = config.SamplingRate > 0 + ? config.SamplingRate + : configurations.LoggerSampleRate; + + samplingRate = ValidateSamplingRate(samplingRate, config); + config.SamplingRate = samplingRate; + + // Only notify if sampling is configured + if (samplingRate > 0) { - _currentConfig = updatedConfig; - // No need to do anything else - the loggers get the config through GetCurrentConfig - }); + double sample = config.GetRandom(); + + // Instead of changing log level, just indicate sampling status + if (sample <= samplingRate) + { + config.LogOutput.LogLine( + $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate}, Sampler Value: {sample}."); + config.MinimumLogLevel = LogLevel.Debug; + } + } } /// - /// Creates a new instance. + /// Validate sampling rate /// - /// The category name for messages produced by the logger. - /// The instance of that was created. + private double ValidateSamplingRate(double samplingRate, PowertoolsLoggerConfiguration config) + { + if (samplingRate < 0 || samplingRate > 1) + { + if (config.MinimumLogLevel is LogLevel.Debug or LogLevel.Trace) + { + config.LogOutput.LogLine( + $"Skipping sampling rate configuration because of invalid value. Sampling rate: {samplingRate}"); + } + + return 0; + } + + return samplingRate; + } + public virtual ILogger CreateLogger(string categoryName) { - _powertoolsConfigurations.SetExecutionEnvironment(typeof(PowertoolsLogger)); - - return _loggers.GetOrAdd(categoryName, name => new PowertoolsLogger(name, - GetCurrentConfig, - () => _systemWrapper)); + return _loggers.GetOrAdd(categoryName, name => new PowertoolsLogger( + name, + GetCurrentConfig)); } internal PowertoolsLoggerConfiguration GetCurrentConfig() => _currentConfig; @@ -88,107 +151,14 @@ public virtual ILogger CreateLogger(string categoryName) public void UpdateConfiguration(PowertoolsLoggerConfiguration config) { _currentConfig = config; - } - - public void UpdateSystem(ISystemWrapper system) - { - if (system == null) return; - - _systemWrapper = system; + + // Apply environment configurations if available + if (_powertoolsConfigurations != null) + { + ConfigureFromEnvironment(); + } } - // private void ApplyPowertoolsConfig(PowertoolsLoggerConfiguration config) - // { - // var logLevel = _powertoolsConfigurations.GetLogLevel(LogLevel.None); - // var lambdaLogLevel = _powertoolsConfigurations.GetLambdaLogLevel(); - // var lambdaLogLevelEnabled = _powertoolsConfigurations.LambdaLogLevelEnabled(); - // - // // Check for explicit config - // bool hasExplicitLevel = config.MinimumLogLevel != LogLevel.None; - // - // // Warn if Lambda log level doesn't match - // if (lambdaLogLevelEnabled && hasExplicitLevel && config.MinimumLogLevel < lambdaLogLevel) - // { - // _systemWrapper.LogLine( - // $"Current log level ({config.MinimumLogLevel}) does not match AWS Lambda Advanced Logging Controls minimum log level ({lambdaLogLevel}). This can lead to data loss, consider adjusting them."); - // } - // - // // Set service from environment if not explicitly set - // if (string.IsNullOrEmpty(config.Service)) - // { - // config.Service = _powertoolsConfigurations.Service; - // } - // - // // Set output case from environment if not explicitly set - // if (config.LoggerOutputCase == LoggerOutputCase.Default) - // { - // var loggerOutputCase = _powertoolsConfigurations.GetLoggerOutputCase(config.LoggerOutputCase); - // config.LoggerOutputCase = loggerOutputCase; - // } - // - // // Set log level from environment ONLY if not explicitly set - // if (!hasExplicitLevel) - // { - // var minLogLevel = lambdaLogLevelEnabled ? lambdaLogLevel : logLevel; - // config.MinimumLogLevel = minLogLevel != LogLevel.None ? minLogLevel : LoggingConstants.DefaultLogLevel; - // } - // - // config.XRayTraceId = _powertoolsConfigurations.XRayTraceId; - // config.LogEvent = _powertoolsConfigurations.LoggerLogEvent; - // - // // Configure the log level key based on output case - // config.LogLevelKey = _powertoolsConfigurations.LambdaLogLevelEnabled() && - // config.LoggerOutputCase == LoggerOutputCase.PascalCase - // ? "LogLevel" - // : LoggingConstants.KeyLogLevel; - // - // // Handle sampling rate - BUT DON'T MODIFY MINIMUM LEVEL - // ProcessSamplingRate(config); - // } - // - // private void ProcessSamplingRate(PowertoolsLoggerConfiguration config) - // { - // var samplingRate = config.SamplingRate > 0 - // ? config.SamplingRate - // : _powertoolsConfigurations.LoggerSampleRate; - // - // samplingRate = ValidateSamplingRate(samplingRate, config.MinimumLogLevel, _systemWrapper); - // config.SamplingRate = samplingRate; - // - // // Only notify if sampling is configured - // if (samplingRate > 0) - // { - // double sample = _systemWrapper.GetRandom(); - // - // // Instead of changing log level, just indicate sampling status - // if (sample <= samplingRate) - // { - // _systemWrapper.LogLine( - // $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate}, Sampler Value: {sample}."); - // config.MinimumLogLevel = LogLevel.Debug; - // } - // } - // } - // - // private static double ValidateSamplingRate(double samplingRate, LogLevel minLogLevel, ISystemWrapper systemWrapper) - // { - // if (samplingRate < 0 || samplingRate > 1) - // { - // if (minLogLevel is LogLevel.Debug or LogLevel.Trace) - // { - // systemWrapper.LogLine( - // $"Skipping sampling rate configuration because of invalid value. Sampling rate: {samplingRate}"); - // } - // - // return 0; - // } - // - // return samplingRate; - // } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// public virtual void Dispose() { _loggers.Clear(); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs index ff9ae0fa..63619b2f 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs @@ -27,18 +27,29 @@ namespace AWS.Lambda.Powertools.Logging; /// public static partial class Logger { + private static ILogger _loggerInstance; private static readonly object _lock = new object(); - // Static constructor to ensure initialization happens before first use - static Logger() + // Change this to a property with getter that recreates if needed + private static ILogger LoggerInstance { - LoggerInstance = GetPowertoolsLogger(); - // Factory and logger will be lazily initialized when first accessed + get + { + // If we have no instance or configuration has changed, get a new logger + if (_loggerInstance == null) + { + lock (_lock) + { + if (_loggerInstance == null) + { + _loggerInstance = GetPowertoolsLogger(); + } + } + } + return _loggerInstance; + } } - // Get the current logger instance - private static ILogger LoggerInstance; - /// /// Configure with an existing logger factory /// @@ -68,24 +79,6 @@ public static PowertoolsLoggerConfiguration GetCurrentConfiguration() return PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); } - - // /// - // /// Get a logger for a specific type - // /// - // /// The type to create logger for - // /// A configured logger - // internal static ILogger GetLogger() => GetLogger(typeof(T).Name); - // - // /// - // /// Get a logger for a specific category - // /// - // /// The category name - // /// A configured logger - // internal static ILogger GetLogger(string category) - // { - // return LoggerFactoryHolder.GetOrCreateFactory().CreateLogger(category); - // } - // /// /// Get the Powertools logger instance /// @@ -115,6 +108,10 @@ public static void UseMinimumLogLevel(LogLevel logLevel) Configure(config => { config.MinimumLogLevel = logLevel; }); + + // Also directly update the log filter level to ensure it takes effect immediately + LoggerFactoryHolder.UpdateFilterLogLevel(logLevel); + _loggerInstance = null; } /// @@ -137,9 +134,6 @@ public static void UseServiceName(string serviceName) /// The rate (0.0 to 1.0) for sampling public static void UseSamplingRate(double samplingRate) { - if (samplingRate < 0 || samplingRate > 1) - throw new ArgumentOutOfRangeException(nameof(samplingRate), "Sampling rate must be between 0 and 1"); - Configure(config => { config.SamplingRate = samplingRate; }); @@ -187,5 +181,17 @@ public static void UseJsonOptions(JsonSerializerOptions jsonOptions) internal static void Reset() { LoggerFactoryHolder.Reset(); + _loggerInstance = null; + RemoveAllKeys(); + } + + public static void SetOutput(ISystemWrapper consoleOut) + { + if (consoleOut == null) + throw new ArgumentNullException(nameof(consoleOut)); + + Configure(config => { + config.LogOutput = consoleOut; + }); } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs index ba7402bd..b52b12f1 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs @@ -146,7 +146,19 @@ public class LoggingAttribute : Attribute /// such as a string or any custom data object. /// /// true if [log event]; otherwise, false. - public bool LogEvent { get; set; } + public bool LogEvent + { + get => _logEvent; + set + { + _logEvent = value; + _logEventSet = true; + } + } + + private bool _logEventSet; + private bool _logEvent; + internal bool IsLogEventSet => _logEventSet; /// /// Pointer path to extract correlation id from input parameter. diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs index f6a2ea5b..67f7f3a2 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs @@ -13,6 +13,7 @@ * permissions and limitations under the License. */ +using System; using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -107,7 +108,12 @@ public JsonSerializerOptions? JsonOptions /// internal PowertoolsLoggingSerializer Serializer => _serializer ??= InitializeSerializer(); - + /// + /// The system wrapper used for output operations. Defaults to SystemWrapper instance. + /// Primarily useful for testing to capture and verify output. + /// + public ISystemWrapper LogOutput { get; set; } = new SystemWrapper(); + /// /// Initialize serializer with the current configuration /// @@ -122,9 +128,47 @@ private PowertoolsLoggingSerializer InitializeSerializer() return serializer; } + /// + /// Creates a deep clone of the configuration + /// + public PowertoolsLoggerConfiguration Clone() + { + return new PowertoolsLoggerConfiguration + { + Service = Service, + TimestampFormat = TimestampFormat, + MinimumLogLevel = MinimumLogLevel, + SamplingRate = SamplingRate, + LoggerOutputCase = LoggerOutputCase, + LogLevelKey = LogLevelKey, + LogFormatter = LogFormatter, + JsonOptions = JsonOptions, + LogBuffering = new LogBufferingOptions + { + Enabled = LogBuffering.Enabled, + BufferAtLogLevel = LogBuffering.BufferAtLogLevel, + FlushOnErrorLog = LogBuffering.FlushOnErrorLog, + }, + LogOutput = LogOutput, // Reference the same output for now + XRayTraceId = XRayTraceId, + LogEvent = LogEvent + }; + } + // IOptions implementation PowertoolsLoggerConfiguration IOptions.Value => this; internal string XRayTraceId { get; set; } internal bool LogEvent { get; set; } + + internal double Random { get; set; } = new Random().NextDouble(); + + /// + /// Gets random number + /// + /// System.Double. + internal virtual double GetRandom() + { + return Random; + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs index 15d480dc..322e2389 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs @@ -27,6 +27,10 @@ public static void UpdateConfiguration(PowertoolsLoggerConfiguration config) { // Update the shared configuration _currentConfig = config; + + // Uncomment this line to update the filter level + if(config.MinimumLogLevel != LogLevel.None) + LoggerFactoryHolder.UpdateFilterLogLevel(config.MinimumLogLevel); // Notify all providers about the change foreach (var provider in AllProviders) @@ -41,19 +45,7 @@ public static PowertoolsLoggerConfiguration GetCurrentConfiguration() lock (_lock) { // Return a copy to prevent external modification - return new PowertoolsLoggerConfiguration - { - Service = _currentConfig.Service, - SamplingRate = _currentConfig.SamplingRate, - MinimumLogLevel = _currentConfig.MinimumLogLevel, - LoggerOutputCase = _currentConfig.LoggerOutputCase, - JsonOptions = _currentConfig.JsonOptions, - TimestampFormat = _currentConfig.TimestampFormat, - LogFormatter = _currentConfig.LogFormatter, - LogLevelKey = _currentConfig.LogLevelKey, - LogBuffering = _currentConfig.LogBuffering - - }; + return _currentConfig.Clone(); } } @@ -62,31 +54,19 @@ public static ILoggingBuilder AddPowertoolsLogger( { builder.AddConfiguration(); - // Register ISystemWrapper if not already registered - // builder.Services.TryAddSingleton(provider => - // { - // // Check if there's a pending mock system first - // var mockSystem = PowertoolsLoggerTestFixture.GetSystemWrapper(); - // return mockSystem ?? new SystemWrapper(); - // }); - - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - // Register IPowertoolsConfigurations with all its dependencies builder.Services.TryAddSingleton(sp => new PowertoolsConfigurations(sp.GetRequiredService())); - // Register the regular provider builder.Services.TryAddEnumerable( ServiceDescriptor.Singleton(provider => { var powertoolsConfigurations = provider.GetRequiredService(); - var systemWrapper = provider.GetRequiredService(); var loggerProvider = new PowertoolsLoggerProvider( - new TrackedOptionsMonitor(_currentConfig, UpdateConfiguration), powertoolsConfigurations, - systemWrapper); + _currentConfig, + powertoolsConfigurations); + lock (_lock) { AllProviders.Add(loggerProvider); @@ -94,9 +74,7 @@ public static ILoggingBuilder AddPowertoolsLogger( return loggerProvider; })); - - builder.Services.ConfigureOptions(); - + LoggerProviderOptions.RegisterProviderOptions (builder.Services); @@ -141,12 +119,9 @@ public static ILoggingBuilder AddPowertoolsLogger( ServiceDescriptor.Singleton(provider => { var powertoolsConfigurations = provider.GetRequiredService(); - var systemWrapper = provider.GetRequiredService(); var bufferingProvider = new BufferingLoggerProvider( - new TrackedOptionsMonitor(_currentConfig, UpdateConfiguration), - powertoolsConfigurations, - systemWrapper + _currentConfig, powertoolsConfigurations ); lock (_lock) @@ -161,19 +136,6 @@ public static ILoggingBuilder AddPowertoolsLogger( return builder; } - internal static void UpdateSystemInAllProviders(ISystemWrapper system) - { - if (system == null) return; - - lock (_lock) - { - foreach (var provider in AllProviders) - { - provider.UpdateSystem(system); - } - } - } - /// /// Resets all providers and clears the configuration. /// This is useful for testing purposes to ensure a clean state. @@ -189,66 +151,4 @@ internal static void ResetAllProviders() _currentConfig = new PowertoolsLoggerConfiguration(); } } - - private class ConfigureLoggingOptions : IConfigureOptions - { - private readonly IPowertoolsConfigurations _configurations; - private readonly ISystemWrapper _systemWrapper; - - public ConfigureLoggingOptions(IPowertoolsConfigurations configurations, ISystemWrapper systemWrapper) - { - _configurations = configurations; - _systemWrapper = systemWrapper; - } - - public void Configure(LoggerFilterOptions options) - { - // This runs when IOptions is resolved - LoggerFactoryHolder.ConfigureFromEnvironment(_configurations,_systemWrapper); - } - } - - private class TrackedOptionsMonitor : IOptionsMonitor - { - private PowertoolsLoggerConfiguration _config; - private readonly Action _updateCallback; - private readonly List> _listeners = new(); - - public TrackedOptionsMonitor( - PowertoolsLoggerConfiguration config, - Action updateCallback) - { - _config = config; - _updateCallback = updateCallback; - } - - public PowertoolsLoggerConfiguration CurrentValue => _config; - - public IDisposable OnChange(Action listener) - { - _listeners.Add(listener); - return new ListenerDisposable(_listeners, listener); - } - - public PowertoolsLoggerConfiguration Get(string? name) => _config; - - private class ListenerDisposable : IDisposable - { - private readonly List> _listeners; - private readonly Action _listener; - - public ListenerDisposable( - List> listeners, - Action listener) - { - _listeners = listeners; - _listener = listener; - } - - public void Dispose() - { - _listeners.Remove(_listener); - } - } - } } \ 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 index 758da539..20b3a4c0 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -104,39 +104,15 @@ internal string Serialize(object value, Type inputType) return JsonSerializer.Serialize(value, jsonSerializerOptions); } - var options = GetSerializerOptions(); - // Try to serialize using the configured TypeInfoResolver - try - { - var typeInfo = GetTypeInfo(inputType); - if (typeInfo != null) - { - return JsonSerializer.Serialize(value, typeInfo); - } - } - catch (InvalidOperationException) - { - // Failed to get typeinfo, will fall back to trying the serializer directly - } - - // Fall back to direct serialization which may work if the resolver chain can handle it - try - { - return JsonSerializer.Serialize(value, inputType, options); - } - catch (JsonException ex) + var typeInfo = GetTypeInfo(inputType); + if (typeInfo == null) { throw new JsonSerializerException( - $"Type {inputType} is not known to the serializer. Ensure it's included in the JsonSerializerContext.", - ex); - } - catch (InvalidOperationException ex) - { - throw new JsonSerializerException( - $"Type {inputType} is not known to the serializer. Ensure it's included in the JsonSerializerContext.", - ex); + $"Type {inputType} is not known to the serializer. Ensure it's included in the JsonSerializerContext."); } + return JsonSerializer.Serialize(value, typeInfo); + #endif } @@ -280,6 +256,9 @@ private void BuildJsonSerializerOptions(JsonSerializerOptions options = null) if (!RuntimeFeatureWrapper.IsDynamicCodeSupported) { HandleJsonOptionsTypeResolver(_jsonOptions); + + // Ensure the TypeInfoResolver is set + _jsonOptions.TypeInfoResolver = GetCompositeResolver(); } #endif } diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs index ba08453f..840fa13a 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs @@ -16,7 +16,6 @@ using System; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; -using AWS.Lambda.Powertools.Logging.Serializers; using AWS.Lambda.Powertools.Logging.Tests.Handlers; using AWS.Lambda.Powertools.Logging.Tests.Serializers; using Microsoft.Extensions.Logging; @@ -28,23 +27,20 @@ namespace AWS.Lambda.Powertools.Logging.Tests.Attributes; [Collection("Sequential")] public class LoggerAspectTests : IDisposable { - private ISystemWrapper _mockSystemWrapper; - private readonly IPowertoolsConfigurations _mockPowertoolsConfigurations; - - public LoggerAspectTests() - { - _mockSystemWrapper = Substitute.For(); - _mockPowertoolsConfigurations = Substitute.For(); - } - [Fact] public void OnEntry_ShouldInitializeLogger_WhenCalledWithValidArguments() { // Arrange -#if NET8_0_OR_GREATER - // Add seriolization context for AOT - PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); -#endif + var consoleOut = Substitute.For(); + + var config = new PowertoolsLoggerConfiguration + { + Service = "TestService", + MinimumLogLevel = LogLevel.Information, + LogOutput = consoleOut + }; + + var logger = PowertoolsLoggerFactory.Create(config).CreatePowertoolsLogger(); var instance = new object(); var name = "TestMethod"; @@ -66,14 +62,12 @@ public void OnEntry_ShouldInitializeLogger_WhenCalledWithValidArguments() } }; - _mockSystemWrapper.GetRandom().Returns(0.7); - // Act - var loggingAspect = new LoggingAspect(_mockPowertoolsConfigurations, _mockSystemWrapper); + var loggingAspect = new LoggingAspect(logger); loggingAspect.OnEntry(instance, name, args, hostType, method, returnType, triggers); // Assert - _mockSystemWrapper.Received().LogLine(Arg.Is(s => + consoleOut.Received().LogLine(Arg.Is(s => s.Contains( "\"Level\":\"Information\",\"Service\":\"TestService\",\"Name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":null},\"SamplingRate\":0.5}") && s.Contains("\"CorrelationId\":\"20\"") @@ -84,12 +78,19 @@ public void OnEntry_ShouldInitializeLogger_WhenCalledWithValidArguments() public void OnEntry_ShouldLog_Event_When_EnvironmentVariable_Set() { // Arrange -#if NET8_0_OR_GREATER - - // Add seriolization context for AOT - PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); -#endif - + Environment.SetEnvironmentVariable(Constants.LoggerLogEventNameEnv, "true"); + var consoleOut = Substitute.For(); + + var config = new PowertoolsLoggerConfiguration + { + Service = "TestService", + MinimumLogLevel = LogLevel.Information, + LogEvent = true, + LogOutput = consoleOut + }; + + var logger = PowertoolsLoggerFactory.Create(config).CreatePowertoolsLogger(); + var instance = new object(); var name = "TestMethod"; var args = new object[] { new TestObject { FullName = "Powertools", Age = 20 } }; @@ -103,43 +104,97 @@ public void OnEntry_ShouldLog_Event_When_EnvironmentVariable_Set() Service = "TestService", LoggerOutputCase = LoggerOutputCase.PascalCase, LogLevel = LogLevel.Information, - LogEvent = false, CorrelationIdPath = "/Age", ClearState = true } }; - - // Env returns true - _mockPowertoolsConfigurations.LoggerLogEvent.Returns(true); - + // Act - var loggingAspect = new LoggingAspect(_mockPowertoolsConfigurations, _mockSystemWrapper); + var loggingAspect = new LoggingAspect(logger); loggingAspect.OnEntry(instance, name, args, hostType, method, returnType, triggers); - + + var updatedConfig = PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); + // Assert - var config = _mockPowertoolsConfigurations.CurrentConfig(); - Assert.NotNull(Logger.LoggerProvider); - Assert.Equal("TestService", config.Service); - Assert.Equal(LoggerOutputCase.PascalCase, config.LoggerOutputCase); - Assert.Equal(0, config.SamplingRate); - - _mockSystemWrapper.Received().LogLine(Arg.Is(s => + Assert.Equal("TestService", updatedConfig.Service); + Assert.Equal(LoggerOutputCase.PascalCase, updatedConfig.LoggerOutputCase); + Assert.Equal(0, updatedConfig.SamplingRate); + Assert.True(updatedConfig.LogEvent); + + consoleOut.Received().LogLine(Arg.Is(s => s.Contains( "\"Level\":\"Information\",\"Service\":\"TestService\",\"Name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":null}}") && s.Contains("\"CorrelationId\":\"20\"") )); } + + [Fact] + public void OnEntry_Should_NOT_Log_Event_When_EnvironmentVariable_Set_But_Attribute_False() + { + // Arrange + Environment.SetEnvironmentVariable(Constants.LoggerLogEventNameEnv, "true"); + var consoleOut = Substitute.For(); + + var config = new PowertoolsLoggerConfiguration + { + Service = "TestService", + MinimumLogLevel = LogLevel.Information, + LogEvent = true, + LogOutput = consoleOut + }; + + var logger = PowertoolsLoggerFactory.Create(config).CreatePowertoolsLogger(); + + var instance = new object(); + var name = "TestMethod"; + var args = new object[] { new TestObject { FullName = "Powertools", Age = 20 } }; + var hostType = typeof(string); + var method = typeof(TestHandlers).GetMethod("TestMethod"); + var returnType = typeof(string); + var triggers = new Attribute[] + { + new LoggingAttribute + { + Service = "TestService", + LoggerOutputCase = LoggerOutputCase.PascalCase, + LogLevel = LogLevel.Information, + LogEvent = false, + CorrelationIdPath = "/Age", + ClearState = true + } + }; + + // Act + var loggingAspect = new LoggingAspect(logger); + loggingAspect.OnEntry(instance, name, args, hostType, method, returnType, triggers); + + var updatedConfig = PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); + + // Assert + Assert.Equal("TestService", updatedConfig.Service); + Assert.Equal(LoggerOutputCase.PascalCase, updatedConfig.LoggerOutputCase); + Assert.Equal(0, updatedConfig.SamplingRate); + Assert.True(updatedConfig.LogEvent); + consoleOut.DidNotReceive().LogLine(Arg.Any()); + } + [Fact] public void OnEntry_ShouldLog_SamplingRate_When_EnvironmentVariable_Set() { // Arrange -#if NET8_0_OR_GREATER - - // Add seriolization context for AOT - PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); -#endif - + var consoleOut = Substitute.For(); + + var config = new PowertoolsLoggerConfiguration + { + Service = "TestService", + MinimumLogLevel = LogLevel.Information, + SamplingRate = 0.5, + LogOutput = consoleOut + }; + + var logger = PowertoolsLoggerFactory.Create(config).CreatePowertoolsLogger(); + var instance = new object(); var name = "TestMethod"; var args = new object[] { new TestObject { FullName = "Powertools", Age = 20 } }; @@ -159,31 +214,39 @@ public void OnEntry_ShouldLog_SamplingRate_When_EnvironmentVariable_Set() } }; - // Env returns true - _mockPowertoolsConfigurations.LoggerSampleRate.Returns(0.5); - // Act - var loggingAspect = new LoggingAspect(_mockPowertoolsConfigurations, _mockSystemWrapper); + var loggingAspect = new LoggingAspect(logger); loggingAspect.OnEntry(instance, name, args, hostType, method, returnType, triggers); - + // Assert - var config = _mockPowertoolsConfigurations.CurrentConfig(); - Assert.NotNull(Logger.LoggerProvider); - Assert.Equal("TestService", config.Service); - Assert.Equal(LoggerOutputCase.PascalCase, config.LoggerOutputCase); - Assert.Equal(0.5, config.SamplingRate); - - _mockSystemWrapper.Received().LogLine(Arg.Is(s => + var updatedConfig = PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); + + Assert.Equal("TestService", updatedConfig.Service); + Assert.Equal(LoggerOutputCase.PascalCase, updatedConfig.LoggerOutputCase); + Assert.Equal(0.5, updatedConfig.SamplingRate); + + consoleOut.Received().LogLine(Arg.Is(s => s.Contains( "\"Level\":\"Information\",\"Service\":\"TestService\",\"Name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":null},\"SamplingRate\":0.5}") && s.Contains("\"CorrelationId\":\"20\"") )); } - + [Fact] public void OnEntry_ShouldLogEvent_WhenLogEventIsTrue() { // Arrange + var consoleOut = Substitute.For(); + + var config = new PowertoolsLoggerConfiguration + { + Service = "TestService", + MinimumLogLevel = LogLevel.Information, + LogOutput = consoleOut, + }; + + var logger = PowertoolsLoggerFactory.Create(config).CreatePowertoolsLogger(); + var eventObject = new { testData = "test-data" }; var triggers = new Attribute[] { @@ -192,29 +255,34 @@ public void OnEntry_ShouldLogEvent_WhenLogEventIsTrue() LogEvent = true } }; - + // Act - - var loggingAspect = new LoggingAspect(_mockPowertoolsConfigurations, _mockSystemWrapper); + + var loggingAspect = new LoggingAspect(logger); loggingAspect.OnEntry(null, null, new object[] { eventObject }, null, null, null, triggers); - + // Assert - _mockSystemWrapper.Received().LogLine(Arg.Is(s => + consoleOut.Received().LogLine(Arg.Is(s => s.Contains( "\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":{\"test_data\":\"test-data\"}}") )); } - + [Fact] public void OnEntry_ShouldNot_Log_Info_When_LogLevel_Higher_EnvironmentVariable() { // Arrange -#if NET8_0_OR_GREATER - - // Add seriolization context for AOT - PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); -#endif - + var consoleOut = Substitute.For(); + + var config = new PowertoolsLoggerConfiguration + { + Service = "TestService", + MinimumLogLevel = LogLevel.Error, + LogOutput = consoleOut + }; + + var logger = PowertoolsLoggerFactory.Create(config).CreatePowertoolsLogger(); + var instance = new object(); var name = "TestMethod"; var args = new object[] { new TestObject { FullName = "Powertools", Age = 20 } }; @@ -227,38 +295,37 @@ public void OnEntry_ShouldNot_Log_Info_When_LogLevel_Higher_EnvironmentVariable( { Service = "TestService", LoggerOutputCase = LoggerOutputCase.PascalCase, - + LogEvent = true, CorrelationIdPath = "/age" } }; - - // Env returns true - _mockPowertoolsConfigurations.LogLevel.Returns(LogLevel.Error.ToString()); - + // Act - var loggingAspect = new LoggingAspect(_mockPowertoolsConfigurations, _mockSystemWrapper); + var loggingAspect = new LoggingAspect(logger); loggingAspect.OnEntry(instance, name, args, hostType, method, returnType, triggers); - + + var updatedConfig = PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); + // Assert - var config = _mockPowertoolsConfigurations.CurrentConfig(); - Assert.NotNull(Logger.LoggerProvider); - Assert.Equal("TestService", config.Service); - Assert.Equal(LoggerOutputCase.PascalCase, config.LoggerOutputCase); - - _mockSystemWrapper.DidNotReceive().LogLine(Arg.Any()); + Assert.Equal("TestService", updatedConfig.Service); + Assert.Equal(LoggerOutputCase.PascalCase, updatedConfig.LoggerOutputCase); + + consoleOut.DidNotReceive().LogLine(Arg.Any()); } - + [Fact] public void OnEntry_Should_LogDebug_WhenSet_EnvironmentVariable() { // Arrange -#if NET8_0_OR_GREATER - - // Add seriolization context for AOT - PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); -#endif - + Environment.SetEnvironmentVariable("POWERTOOLS_LOG_LEVEL", "Debug"); + + var consoleOut = Substitute.For(); + var config = new PowertoolsLoggerConfiguration + { + LogOutput = consoleOut + }; + var instance = new object(); var name = "TestMethod"; var args = new object[] @@ -278,25 +345,26 @@ public void OnEntry_Should_LogDebug_WhenSet_EnvironmentVariable() CorrelationIdPath = "/Headers/MyRequestIdHeader" } }; + + var logger = PowertoolsLoggerFactory.Create(config).CreatePowertoolsLogger(); - // Env returns true - _mockPowertoolsConfigurations.LogLevel.Returns(LogLevel.Debug.ToString()); - + // Act - var loggingAspect = new LoggingAspect(_mockPowertoolsConfigurations, _mockSystemWrapper); + var loggingAspect = new LoggingAspect(logger); loggingAspect.OnEntry(instance, name, args, hostType, method, returnType, triggers); - + // Assert - var config = _mockPowertoolsConfigurations.CurrentConfig(); - Assert.NotNull(Logger.LoggerProvider); - Assert.Equal("TestService", config.Service); - Assert.Equal(LoggerOutputCase.PascalCase, config.LoggerOutputCase); - Assert.Equal(LogLevel.Debug, config.MinimumLevel); - - _mockSystemWrapper.Received(1).LogLine(Arg.Is(s => - s == "Skipping Lambda Context injection because ILambdaContext context parameter not found.")); - - _mockSystemWrapper.Received(1).LogLine(Arg.Is(s => + var updatedConfig = PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); + + Assert.Equal("TestService", updatedConfig.Service); + Assert.Equal(LoggerOutputCase.PascalCase, updatedConfig.LoggerOutputCase); + Assert.Equal(LogLevel.Debug, updatedConfig.MinimumLogLevel); + + consoleOut.Received(1).LogLine(Arg.Is(s => + s.Contains( + "\"Level\":\"Debug\",\"Service\":\"TestService\",\"Name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"Message\":\"Skipping Lambda Context injection because ILambdaContext context parameter not found.\"}"))); + + consoleOut.Received(1).LogLine(Arg.Is(s => s.Contains("\"CorrelationId\":\"test\"") && s.Contains( "\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":{\"MyRequestIdHeader\":\"test\"}") @@ -306,6 +374,6 @@ public void OnEntry_Should_LogDebug_WhenSet_EnvironmentVariable() public void Dispose() { LoggingAspect.ResetForTest(); - PowertoolsLoggingSerializer.ClearOptions(); + Logger.Reset(); } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs index 892a2baf..27141662 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs @@ -15,17 +15,17 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.ApplicationLoadBalancerEvents; using Amazon.Lambda.CloudWatchEvents.S3Events; using Amazon.Lambda.TestUtilities; using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Common.Tests; using AWS.Lambda.Powertools.Logging.Internal; -using AWS.Lambda.Powertools.Logging.Serializers; using AWS.Lambda.Powertools.Logging.Tests.Handlers; using AWS.Lambda.Powertools.Logging.Tests.Serializers; +using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -35,27 +35,26 @@ namespace AWS.Lambda.Powertools.Logging.Tests.Attributes public class LoggingAttributeTests : IDisposable { private TestHandlers _testHandlers; - + public LoggingAttributeTests() { _testHandlers = new TestHandlers(); } - + [Fact] public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContext() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); - + var consoleOut = Substitute.For(); + Logger.SetOutput(consoleOut); + // Act _testHandlers.TestMethod(); - + // Assert var allKeys = Logger.GetAllKeys() .ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value); - - Assert.NotNull(Logger.LoggerProvider); + Assert.True(allKeys.ContainsKey(LoggingConstants.KeyColdStart)); //Assert.True((bool)allKeys[LoggingConstants.KeyColdStart]); Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionName)); @@ -63,25 +62,24 @@ public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContext() Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionMemorySize)); Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionArn)); Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionRequestId)); - - consoleOut.DidNotReceive().WriteLine(Arg.Any()); + + consoleOut.DidNotReceive().LogLine(Arg.Any()); } - + [Fact] public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContextAndLogDebug() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); - + var consoleOut = Substitute.For(); + Logger.SetOutput(consoleOut); + // Act _testHandlers.TestMethodDebug(); - + // Assert var allKeys = Logger.GetAllKeys() .ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value); - - Assert.NotNull(Logger.LoggerProvider); + Assert.True(allKeys.ContainsKey(LoggingConstants.KeyColdStart)); //Assert.True((bool)allKeys[LoggingConstants.KeyColdStart]); Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionName)); @@ -89,24 +87,24 @@ public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContextAndLogDebu Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionMemorySize)); Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionArn)); Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionRequestId)); - - consoleOut.Received(1).WriteLine( + + consoleOut.Received(1).LogLine( Arg.Is(i => - i == $"Skipping Lambda Context injection because ILambdaContext context parameter not found.") + i.Contains("\"level\":\"Debug\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Skipping Lambda Context injection because ILambdaContext context parameter not found.\"}")) ); } - + [Fact] public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArg() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); - + var consoleOut = Substitute.For(); + Logger.SetOutput(consoleOut); + // Act _testHandlers.LogEventNoArgs(); - - consoleOut.DidNotReceive().WriteLine( + + consoleOut.DidNotReceive().LogLine( Arg.Any() ); } @@ -115,20 +113,15 @@ public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArg() public void OnEntry_WhenEventArgExist_LogEvent() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + var consoleOut = Substitute.For(); + Logger.SetOutput(consoleOut); var correlationId = Guid.NewGuid().ToString(); -#if NET8_0_OR_GREATER - - // Add seriolization context for AOT - PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); -#endif var context = new TestLambdaContext() { FunctionName = "PowertoolsLoggingSample-HelloWorldFunction-Gg8rhPwO7Wa1" }; - + var testObj = new TestObject { Headers = new Header @@ -139,8 +132,8 @@ public void OnEntry_WhenEventArgExist_LogEvent() // Act _testHandlers.LogEvent(testObj, context); - - consoleOut.Received(1).WriteLine( + + consoleOut.Received(1).LogLine( Arg.Is(i => i.Contains("FunctionName\":\"PowertoolsLoggingSample-HelloWorldFunction-Gg8rhPwO7Wa1")) ); } @@ -149,14 +142,9 @@ public void OnEntry_WhenEventArgExist_LogEvent() public void OnEntry_WhenEventArgExist_LogEvent_False_Should_Not_Log() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); - -#if NET8_0_OR_GREATER - - // Add seriolization context for AOT - PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); -#endif + var consoleOut = Substitute.For(); + Logger.SetOutput(consoleOut); + var context = new TestLambdaContext() { FunctionName = "PowertoolsLoggingSample-HelloWorldFunction-Gg8rhPwO7Wa1" @@ -164,41 +152,44 @@ public void OnEntry_WhenEventArgExist_LogEvent_False_Should_Not_Log() // Act _testHandlers.LogEventFalse(context); - - consoleOut.DidNotReceive().WriteLine( + + consoleOut.DidNotReceive().LogLine( Arg.Any() ); } - + [Fact] public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArgAndLogDebug() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); - + var consoleOut = Substitute.For(); + Logger.SetOutput(consoleOut); + // Act _testHandlers.LogEventDebug(); - - consoleOut.Received(1).WriteLine( - Arg.Is(i => i == "Skipping Event Log because event parameter not found.") + + consoleOut.Received(1).LogLine( + Arg.Is(i => i.Contains("\"level\":\"Debug\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Skipping Event Log because event parameter not found.\"}")) + ); + + consoleOut.Received(1).LogLine( + Arg.Is(i => i.Contains("\"level\":\"Debug\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Skipping Lambda Context injection because ILambdaContext context parameter not found.\"}")) ); } - + [Fact] public void OnExit_WhenHandler_ClearState_Enabled_ClearKeys() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); - + var consoleOut = Substitute.For(); + Logger.SetOutput(consoleOut); + // Act _testHandlers.ClearState(); - - Assert.NotNull(Logger.LoggerProvider); + Assert.False(Logger.GetAllKeys().Any()); } - + [Theory] [InlineData(CorrelationIdPaths.ApiGatewayRest)] [InlineData(CorrelationIdPaths.ApplicationLoadBalancer)] @@ -208,13 +199,8 @@ public void OnEntry_WhenEventArgExists_CapturesCorrelationId(string correlationI { // Arrange var correlationId = Guid.NewGuid().ToString(); - -#if NET8_0_OR_GREATER - - // Add seriolization context for AOT - PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); -#endif - + + // Act switch (correlationIdPath) { @@ -252,15 +238,15 @@ public void OnEntry_WhenEventArgExists_CapturesCorrelationId(string correlationI }); break; } - + // Assert var allKeys = Logger.GetAllKeys() .ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value); - + Assert.True(allKeys.ContainsKey(LoggingConstants.KeyCorrelationId)); Assert.Equal((string)allKeys[LoggingConstants.KeyCorrelationId], correlationId); } - + [Theory] [InlineData(LoggerOutputCase.SnakeCase)] [InlineData(LoggerOutputCase.PascalCase)] @@ -269,13 +255,8 @@ public void When_Capturing_CorrelationId_Converts_To_Case(LoggerOutputCase outpu { // Arrange var correlationId = Guid.NewGuid().ToString(); - -#if NET8_0_OR_GREATER - - // Add seriolization context for AOT - PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); -#endif - + + // Act switch (outputCase) { @@ -307,11 +288,11 @@ public void When_Capturing_CorrelationId_Converts_To_Case(LoggerOutputCase outpu }); break; } - + // Assert var allKeys = Logger.GetAllKeys() .ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value); - + Assert.True(allKeys.ContainsKey(LoggingConstants.KeyCorrelationId)); Assert.Equal((string)allKeys[LoggingConstants.KeyCorrelationId], correlationId); } @@ -324,13 +305,8 @@ public void When_Capturing_CorrelationId_Converts_To_Case_From_Environment_Var(L { // Arrange var correlationId = Guid.NewGuid().ToString(); - -#if NET8_0_OR_GREATER - - // Add seriolization context for AOT - PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default); -#endif - + + // Act switch (outputCase) { @@ -364,11 +340,11 @@ public void When_Capturing_CorrelationId_Converts_To_Case_From_Environment_Var(L }); break; } - + // Assert var allKeys = Logger.GetAllKeys() .ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value); - + Assert.True(allKeys.ContainsKey(LoggingConstants.KeyCorrelationId)); Assert.Equal((string)allKeys[LoggingConstants.KeyCorrelationId], correlationId); } @@ -377,15 +353,15 @@ public void When_Capturing_CorrelationId_Converts_To_Case_From_Environment_Var(L public void When_Setting_SamplingRate_Should_Add_Key() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + var consoleOut = Substitute.For(); + Logger.SetOutput(consoleOut); // Act _testHandlers.HandlerSamplingRate(); // Assert - consoleOut.Received().WriteLine( + consoleOut.Received().LogLine( Arg.Is(i => i.Contains("\"message\":\"test\",\"samplingRate\":0.5")) ); } @@ -394,8 +370,8 @@ public void When_Setting_SamplingRate_Should_Add_Key() public void When_Setting_Service_Should_Update_Key() { // Arrange - var consoleOut = new StringWriter(); - SystemWrapper.Instance.SetOut(consoleOut); + var consoleOut = new TestLoggerOutput(); + Logger.SetOutput(consoleOut); // Act _testHandlers.HandlerService(); @@ -410,9 +386,9 @@ public void When_Setting_Service_Should_Update_Key() public void When_Setting_LogLevel_Should_Update_LogLevel() { // Arrange - var consoleOut = new StringWriter(); - SystemWrapper.Instance.SetOut(consoleOut); - + var consoleOut = new TestLoggerOutput(); + Logger.SetOutput(consoleOut); + // Act _testHandlers.TestLogLevelCritical(); @@ -426,8 +402,8 @@ public void When_Setting_LogLevel_Should_Update_LogLevel() public void When_Setting_LogLevel_HigherThanInformation_Should_Not_LogEvent() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + var consoleOut = Substitute.For(); + Logger.SetOutput(consoleOut); var context = new TestLambdaContext() { FunctionName = "PowertoolsLoggingSample-HelloWorldFunction-Gg8rhPwO7Wa1" @@ -437,108 +413,126 @@ public void When_Setting_LogLevel_HigherThanInformation_Should_Not_LogEvent() _testHandlers.TestLogLevelCriticalLogEvent(context); // Assert - consoleOut.DidNotReceive().WriteLine(Arg.Any()); + consoleOut.DidNotReceive().LogLine(Arg.Any()); } [Fact] public void When_LogLevel_Debug_Should_Log_Message_When_No_Context_And_LogEvent_True() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + var consoleOut = Substitute.For(); + Logger.SetOutput(consoleOut); // Act _testHandlers.TestLogEventWithoutContext(); // Assert - consoleOut.Received(1).WriteLine(Arg.Is(s => s == "Skipping Event Log because event parameter not found.")); + consoleOut.Received(1).LogLine(Arg.Is(s => + s.Contains("\"level\":\"Debug\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Skipping Event Log because event parameter not found.\"}"))); + + consoleOut.Received(1).LogLine(Arg.Is(s => + s.Contains("\"level\":\"Debug\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Skipping Lambda Context injection because ILambdaContext context parameter not found.\"}"))); } [Fact] public void Should_Log_When_Not_Using_Decorator() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + var consoleOut = Substitute.For(); + Logger.SetOutput(consoleOut); var test = new TestHandlers(); // Act test.TestLogNoDecorator(); - + // Assert - consoleOut.Received().WriteLine( + consoleOut.Received().LogLine( Arg.Is(i => i.Contains("\"level\":\"Information\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"test\"}")) ); } - public void Dispose() + [Fact] + public void LoggingAspect_ShouldRespectDynamicLogLevelChanges() { - Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", ""); - Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", ""); - LoggingAspect.ResetForTest(); - PowertoolsLoggingSerializer.ClearOptions(); - } - } + // Arrange + var consoleOut = Substitute.For(); + Logger.SetOutput(consoleOut); + Logger.UseMinimumLogLevel(LogLevel.Warning); // Start with Warning level - [Collection("A Sequential")] - public class ServiceTests : IDisposable - { - private readonly TestServiceHandler _testHandler; + // Act + _testHandlers.TestMethodDebug(); // Uses LogLevel.Debug attribute + + // Assert + consoleOut.Received(1).LogLine(Arg.Is(s => + s.Contains("\"level\":\"Debug\"") && + s.Contains("Skipping Lambda Context injection"))); + } - public ServiceTests() + [Fact] + public void LoggingAspect_ShouldCorrectlyResetLogLevelAfterExecution() { - _testHandler = new TestServiceHandler(); + // Arrange + var consoleOut = Substitute.For(); + Logger.SetOutput(consoleOut); + Logger.UseMinimumLogLevel(LogLevel.Warning); + + // Act - First call with Debug level attribute + _testHandlers.TestMethodDebug(); + consoleOut.ClearReceivedCalls(); + + // Act - Then log directly at Debug level (should still work) + Logger.LogDebug("This should be logged"); + + // Assert + consoleOut.Received(1).LogLine(Arg.Is(s => + s.Contains("\"level\":\"Debug\"") && + s.Contains("\"message\":\"This should be logged\""))); } [Fact] - public void When_Setting_Service_Should_Override_Env() + public void LoggingAspect_ShouldRespectAttributePrecedenceOverEnvironment() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); - + Environment.SetEnvironmentVariable("POWERTOOLS_LOG_LEVEL", "Error"); + var consoleOut = Substitute.For(); + Logger.SetOutput(consoleOut); + // Act - _testHandler.LogWithEnv(); - _testHandler.Handler(); - + _testHandlers.TestMethodDebug(); // Uses LogLevel.Debug attribute + // Assert - - consoleOut.Received(1).WriteLine( - Arg.Is(i => i.Contains("\"level\":\"Information\",\"service\":\"Environment Service\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Service: Environment Service\"")) - ); - consoleOut.Received(1).WriteLine( - Arg.Is(i => i.Contains("\"level\":\"Information\",\"service\":\"Attribute Service\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Service: Attribute Service\"")) - ); + consoleOut.Received(1).LogLine(Arg.Is(s => + s.Contains("\"level\":\"Debug\""))); } [Fact] - public void When_Setting_Service_Should_Override_Env_And_Empty() + public void LoggingAspect_ShouldImmediatelyApplyFilterLevelChanges() { // Arrange - var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); - + var consoleOut = Substitute.For(); + Logger.SetOutput(consoleOut); + Logger.UseMinimumLogLevel(LogLevel.Error); + // Act - _testHandler.LogWithAndWithoutEnv(); - _testHandler.Handler(); - + Logger.LogInformation("This should NOT be logged"); + _testHandlers.TestMethodDebug(); // Should change level to Debug + Logger.LogInformation("This should be logged"); + // Assert - - consoleOut.Received(2).WriteLine( - Arg.Is(i => i.Contains("\"level\":\"Information\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Service: service_undefined\"")) - ); - consoleOut.Received(1).WriteLine( - Arg.Is(i => i.Contains("\"level\":\"Information\",\"service\":\"Attribute Service\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Service: Attribute Service\"")) - ); + consoleOut.Received(1).LogLine(Arg.Is(s => + s.Contains("\"message\":\"This should be logged\""))); + consoleOut.DidNotReceive().LogLine(Arg.Is(s => + s.Contains("\"message\":\"This should NOT be logged\""))); } - + public void Dispose() { Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", ""); Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", ""); LoggingAspect.ResetForTest(); - PowertoolsLoggingSerializer.ClearOptions(); + Logger.Reset(); + PowertoolsLoggingBuilderExtensions.ResetAllProviders(); } } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/ServiceTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/ServiceTests.cs new file mode 100644 index 00000000..3e3d4ac6 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/ServiceTests.cs @@ -0,0 +1,50 @@ +using System; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Tests.Handlers; +using NSubstitute; +using Xunit; + +namespace AWS.Lambda.Powertools.Logging.Tests.Attributes; + +[Collection("A Sequential")] +public class ServiceTests : IDisposable +{ + private readonly TestServiceHandler _testHandler; + + public ServiceTests() + { + _testHandler = new TestServiceHandler(); + } + + [Fact] + public void When_Setting_Service_Should_Override_Env() + { + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "Environment Service"); + + var consoleOut = Substitute.For(); + Logger.SetOutput(consoleOut); + + // Act + _testHandler.LogWithEnv(); + _testHandler.Handler(); + + // Assert + + consoleOut.Received(1).LogLine( + Arg.Is(i => i.Contains("\"level\":\"Information\",\"service\":\"Environment Service\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Service: Environment Service\"")) + ); + consoleOut.Received(1).LogLine( + Arg.Is(i => i.Contains("\"level\":\"Information\",\"service\":\"Attribute Service\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Service: Attribute Service\"")) + ); + } + + public void Dispose() + { + Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", ""); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", ""); + LoggingAspect.ResetForTest(); + Logger.Reset(); + PowertoolsLoggingBuilderExtensions.ResetAllProviders(); + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs index b9bc8708..89ca07d5 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs @@ -15,7 +15,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Reflection; using System.Text.Json; @@ -26,6 +25,7 @@ using AWS.Lambda.Powertools.Logging.Internal; using AWS.Lambda.Powertools.Logging.Serializers; using AWS.Lambda.Powertools.Logging.Tests.Handlers; +using Microsoft.Extensions.Options; using NSubstitute; using NSubstitute.ExceptionExtensions; using NSubstitute.ReturnsExtensions; @@ -47,8 +47,9 @@ public LogFormatterTest() [Fact] public void Serialize_ShouldHandleEnumValues() { - var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + var consoleOut = Substitute.For(); + Logger.SetOutput(consoleOut); + var lambdaContext = new TestLambdaContext { FunctionName = "funtionName", @@ -61,14 +62,14 @@ public void Serialize_ShouldHandleEnumValues() var handler = new TestHandlers(); handler.TestEnums("fake", lambdaContext); - consoleOut.Received(1).WriteLine(Arg.Is(i => + consoleOut.Received(1).LogLine(Arg.Is(i => i.Contains("\"message\":5") )); - consoleOut.Received(1).WriteLine(Arg.Is(i => + consoleOut.Received(1).LogLine(Arg.Is(i => i.Contains("\"message\":\"Dog\"") )); - var json = JsonSerializer.Serialize(Pet.Dog, PowertoolsLoggingSerializer.GetSerializerOptions()); + var json = JsonSerializer.Serialize(Pet.Dog, new PowertoolsLoggingSerializer().GetSerializerOptions()); Assert.Contains("Dog", json); } @@ -107,13 +108,6 @@ 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() }, @@ -173,12 +167,20 @@ public void Log_WhenCustomFormatter_LogsCustomFormat() } }; + var systemWrapper = Substitute.For(); logFormatter.FormatLogEntry(new LogEntry()).ReturnsForAnyArgs(formattedLogEntry); - Logger.UseFormatter(logFormatter); + + var config = new PowertoolsLoggerConfiguration + { + Service = service, + MinimumLogLevel = minimumLevel, + LoggerOutputCase = LoggerOutputCase.PascalCase, + LogFormatter = logFormatter, + LogOutput = systemWrapper + }; - var systemWrapper = Substitute.For(); - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var provider = new PowertoolsLoggerProvider(config, configurations); var logger = provider.CreateLogger(loggerName); var scopeExtraKeys = new Dictionary @@ -227,8 +229,9 @@ public void Log_WhenCustomFormatter_LogsCustomFormat() [Fact] public void Should_Log_CustomFormatter_When_Decorated() { - var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + var consoleOut = Substitute.For(); + Logger.SetOutput(consoleOut); + var lambdaContext = new TestLambdaContext { FunctionName = "funtionName", @@ -245,13 +248,13 @@ public void Should_Log_CustomFormatter_When_Decorated() // in .net 8 it removes null properties #if NET8_0_OR_GREATER - consoleOut.Received(1).WriteLine( + consoleOut.Received(1).LogLine( Arg.Is(i => i.Contains( "\"correlation_ids\":{\"aws_request_id\":\"requestId\"},\"lambda_function\":{\"name\":\"funtionName\",\"arn\":\"function::arn\",\"memory_limit_in_mb\":128,\"version\":\"version\",\"cold_start\":true},\"level\":\"Information\"")) ); #else - consoleOut.Received(1).WriteLine( + consoleOut.Received(1).LogLine( Arg.Is(i => i.Contains( "{\"message\":\"test\",\"service\":\"my_service\",\"correlation_ids\":{\"aws_request_id\":\"requestId\",\"x_ray_trace_id\":null,\"correlation_id\":null},\"lambda_function\":{\"name\":\"funtionName\",\"arn\":\"function::arn\",\"memory_limit_in_m_b\":128,\"version\":\"version\",\"cold_start\":true},\"level\":\"Information\",\"timestamp\":\"2024-01-01T00:00:00.0000000\",\"logger\":{\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"sample_rate\"")) @@ -262,8 +265,9 @@ public void Should_Log_CustomFormatter_When_Decorated() [Fact] public void Should_Log_CustomFormatter_When_No_Decorated_Just_Log() { - var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + var consoleOut = Substitute.For(); + Logger.SetOutput(consoleOut); + var lambdaContext = new TestLambdaContext { FunctionName = "funtionName", @@ -281,13 +285,13 @@ public void Should_Log_CustomFormatter_When_No_Decorated_Just_Log() // in .net 8 it removes null properties #if NET8_0_OR_GREATER - consoleOut.Received(1).WriteLine( + consoleOut.Received(1).LogLine( Arg.Is(i => i == "{\"message\":\"test\",\"service\":\"service_undefined\",\"correlation_ids\":{},\"lambda_function\":{\"cold_start\":true},\"level\":\"Information\",\"timestamp\":\"2024-01-01T00:00:00.0000000\",\"logger\":{\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"sample_rate\":0}}") ); #else - consoleOut.Received(1).WriteLine( + consoleOut.Received(1).LogLine( Arg.Is(i => i == "{\"message\":\"test\",\"service\":\"service_undefined\",\"correlation_ids\":{\"aws_request_id\":null,\"x_ray_trace_id\":null,\"correlation_id\":null},\"lambda_function\":{\"name\":null,\"arn\":null,\"memory_limit_in_m_b\":null,\"version\":null,\"cold_start\":true},\"level\":\"Information\",\"timestamp\":\"2024-01-01T00:00:00.0000000\",\"logger\":{\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"sample_rate\":0}}") @@ -298,21 +302,21 @@ public void Should_Log_CustomFormatter_When_No_Decorated_Just_Log() [Fact] public void Should_Log_CustomFormatter_When_Decorated_No_Context() { - var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); - + var consoleOut = Substitute.For(); + Logger.SetOutput(consoleOut); + Logger.UseFormatter(new CustomLogFormatter()); _testHandler.TestCustomFormatterWithDecoratorNoContext("test"); #if NET8_0_OR_GREATER - consoleOut.Received(1).WriteLine( + consoleOut.Received(1).LogLine( Arg.Is(i => i == "{\"message\":\"test\",\"service\":\"my_service\",\"correlation_ids\":{},\"lambda_function\":{\"cold_start\":true},\"level\":\"Information\",\"timestamp\":\"2024-01-01T00:00:00.0000000\",\"logger\":{\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"sample_rate\":0.2}}") ); #else - consoleOut.Received(1).WriteLine( + consoleOut.Received(1).LogLine( Arg.Is(i => i == "{\"message\":\"test\",\"service\":\"my_service\",\"correlation_ids\":{\"aws_request_id\":null,\"x_ray_trace_id\":null,\"correlation_id\":null},\"lambda_function\":{\"name\":null,\"arn\":null,\"memory_limit_in_m_b\":null,\"version\":null,\"cold_start\":true},\"level\":\"Information\",\"timestamp\":\"2024-01-01T00:00:00.0000000\",\"logger\":{\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"sample_rate\":0.2}}") @@ -326,7 +330,7 @@ public void Dispose() Logger.RemoveAllKeys(); LoggingLambdaContext.Clear(); LoggingAspect.ResetForTest(); - PowertoolsLoggingSerializer.ClearOptions(); + Logger.Reset(); } } @@ -351,14 +355,15 @@ public void Log_WhenCustomFormatterReturnNull_ThrowsLogFormatException() Logger.UseFormatter(logFormatter); var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var config = new PowertoolsLoggerConfiguration { Service = service, - MinimumLevel = LogLevel.Information, - LoggerOutputCase = LoggerOutputCase.PascalCase + MinimumLogLevel = LogLevel.Information, + LoggerOutputCase = LoggerOutputCase.PascalCase, + LogFormatter = logFormatter }; - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var provider = new PowertoolsLoggerProvider(config, configurations); var logger = provider.CreateLogger(loggerName); // Act @@ -393,17 +398,17 @@ public void Log_WhenCustomFormatterRaisesException_ThrowsLogFormatException() var logFormatter = Substitute.For(); logFormatter.FormatLogEntry(new LogEntry()).ThrowsForAnyArgs(new Exception(errorMessage)); - Logger.UseFormatter(logFormatter); var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var config = new PowertoolsLoggerConfiguration { Service = service, - MinimumLevel = LogLevel.Information, - LoggerOutputCase = LoggerOutputCase.PascalCase + MinimumLogLevel = LogLevel.Information, + LoggerOutputCase = LoggerOutputCase.PascalCase, + LogFormatter = logFormatter }; - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var provider = new PowertoolsLoggerProvider(config, configurations); var logger = provider.CreateLogger(loggerName); // Act diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/ExceptionFunctionHandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/ExceptionFunctionHandlerTests.cs index f9ffd5eb..1bed72bf 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/ExceptionFunctionHandlerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/ExceptionFunctionHandlerTests.cs @@ -42,6 +42,6 @@ public void Utility_Should_Not_Throw_Exceptions_To_Client() public void Dispose() { LoggingAspect.ResetForTest(); - PowertoolsLoggingSerializer.ClearOptions(); + Logger.Reset(); } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs index 08fe54d4..00d4fbba 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs @@ -189,19 +189,8 @@ public class TestServiceHandler { public void LogWithEnv() { - Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "Environment Service"); - Logger.LogInformation("Service: Environment Service"); } - - public void LogWithAndWithoutEnv() - { - Logger.LogInformation("Service: service_undefined"); - - Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "Environment Service"); - - Logger.LogInformation("Service: service_undefined"); - } [Logging(Service = "Attribute Service")] public void Handler() diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs index e034ce33..d5252f10 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs @@ -20,6 +20,7 @@ using System.Linq; using System.Text; using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Common.Tests; using AWS.Lambda.Powertools.Logging.Internal; using AWS.Lambda.Powertools.Logging.Serializers; using AWS.Lambda.Powertools.Logging.Tests.Utilities; @@ -34,10 +35,10 @@ public class PowertoolsLoggerTest : IDisposable { public PowertoolsLoggerTest() { - Logger.UseDefaultFormatter(); + // Logger.UseDefaultFormatter(); } - private static void Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel logLevel, LogLevel minimumLevel) + private static void Log_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel logLevel, LogLevel MinimumLogLevel) { // Arrange var loggerName = Guid.NewGuid().ToString(); @@ -49,15 +50,17 @@ private static void Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel logLevel, // Configure the substitute for IPowertoolsConfigurations configurations.Service.Returns(service); configurations.LoggerOutputCase.Returns(LoggerOutputCase.PascalCase.ToString()); - configurations.LogLevel.Returns(minimumLevel.ToString()); + configurations.LogLevel.Returns(MinimumLogLevel.ToString()); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { - Service = null, - MinimumLevel = LogLevel.None + Service = service, + LoggerOutputCase = LoggerOutputCase.PascalCase, + MinimumLogLevel = MinimumLogLevel, + LogOutput = systemWrapper // Set the output directly on configuration }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); switch (logLevel) @@ -93,7 +96,8 @@ private static void Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel logLevel, ); } - private static void Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel logLevel, LogLevel minimumLevel) + private static void Log_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel logLevel, + LogLevel MinimumLogLevel) { // Arrange var loggerName = Guid.NewGuid().ToString(); @@ -105,15 +109,16 @@ private static void Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel logL // Configure the substitute for IPowertoolsConfigurations configurations.Service.Returns(service); configurations.LoggerOutputCase.Returns(LoggerOutputCase.PascalCase.ToString()); - configurations.LogLevel.Returns(minimumLevel.ToString()); + configurations.LogLevel.Returns(MinimumLogLevel.ToString()); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = service, - MinimumLevel = minimumLevel + MinimumLogLevel = MinimumLogLevel, + LogOutput = systemWrapper // Set the output directly on configuration }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); switch (logLevel) @@ -151,26 +156,26 @@ private static void Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel logL [Theory] [InlineData(LogLevel.Trace)] - public void LogTrace_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLevel) + public void LogTrace_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel.Trace, minimumLevel); + Log_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel.Trace, MinimumLogLevel); } [Theory] [InlineData(LogLevel.Trace)] [InlineData(LogLevel.Debug)] - public void LogDebug_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLevel) + public void LogDebug_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel.Debug, minimumLevel); + Log_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel.Debug, MinimumLogLevel); } [Theory] [InlineData(LogLevel.Trace)] [InlineData(LogLevel.Debug)] [InlineData(LogLevel.Information)] - public void LogInformation_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLevel) + public void LogInformation_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel.Information, minimumLevel); + Log_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel.Information, MinimumLogLevel); } [Theory] @@ -178,9 +183,9 @@ public void LogInformation_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimum [InlineData(LogLevel.Debug)] [InlineData(LogLevel.Information)] [InlineData(LogLevel.Warning)] - public void LogWarning_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLevel) + public void LogWarning_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel.Warning, minimumLevel); + Log_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel.Warning, MinimumLogLevel); } [Theory] @@ -189,9 +194,9 @@ public void LogWarning_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLeve [InlineData(LogLevel.Information)] [InlineData(LogLevel.Warning)] [InlineData(LogLevel.Error)] - public void LogError_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLevel) + public void LogError_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel.Error, minimumLevel); + Log_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel.Error, MinimumLogLevel); } [Theory] @@ -201,9 +206,9 @@ public void LogError_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLevel) [InlineData(LogLevel.Warning)] [InlineData(LogLevel.Error)] [InlineData(LogLevel.Critical)] - public void LogCritical_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLevel) + public void LogCritical_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel.Critical, minimumLevel); + Log_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel.Critical, MinimumLogLevel); } [Theory] @@ -212,9 +217,9 @@ public void LogCritical_WhenMinimumLevelIsBelowLogLevel_Logs(LogLevel minimumLev [InlineData(LogLevel.Warning)] [InlineData(LogLevel.Error)] [InlineData(LogLevel.Critical)] - public void LogTrace_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel minimumLevel) + public void LogTrace_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel.Trace, minimumLevel); + Log_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel.Trace, MinimumLogLevel); } [Theory] @@ -222,33 +227,33 @@ public void LogTrace_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel minimum [InlineData(LogLevel.Warning)] [InlineData(LogLevel.Error)] [InlineData(LogLevel.Critical)] - public void LogDebug_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel minimumLevel) + public void LogDebug_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel.Debug, minimumLevel); + Log_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel.Debug, MinimumLogLevel); } [Theory] [InlineData(LogLevel.Warning)] [InlineData(LogLevel.Error)] [InlineData(LogLevel.Critical)] - public void LogInformation_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel minimumLevel) + public void LogInformation_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel.Information, minimumLevel); + Log_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel.Information, MinimumLogLevel); } [Theory] [InlineData(LogLevel.Error)] [InlineData(LogLevel.Critical)] - public void LogWarning_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel minimumLevel) + public void LogWarning_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel.Warning, minimumLevel); + Log_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel.Warning, MinimumLogLevel); } [Theory] [InlineData(LogLevel.Critical)] - public void LogError_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel minimumLevel) + public void LogError_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel.Error, minimumLevel); + Log_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel.Error, MinimumLogLevel); } [Theory] @@ -258,9 +263,9 @@ public void LogError_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel minimum [InlineData(LogLevel.Warning)] [InlineData(LogLevel.Error)] [InlineData(LogLevel.Critical)] - public void LogNone_WithAnyMinimumLevel_DoesNotLog(LogLevel minimumLevel) + public void LogNone_WithAnyMinimumLogLevel_DoesNotLog(LogLevel MinimumLogLevel) { - Log_WhenMinimumLevelIsAboveLogLevel_DoesNotLog(LogLevel.None, minimumLevel); + Log_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel.None, MinimumLogLevel); } [Fact] @@ -280,16 +285,15 @@ public void Log_ConfigurationIsNotProvided_ReadsFromEnvironmentVariables() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { - Service = null, - MinimumLevel = LogLevel.None + Service = service, + MinimumLogLevel = logLevel, + LogOutput = systemWrapper, + SamplingRate = loggerSampleRate }; - // Act - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); - + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger("test"); logger.LogInformation("Test"); @@ -318,20 +322,21 @@ public void Log_SamplingRateGreaterThanRandom_ChangedLogLevelToDebug() configurations.LoggerSampleRate.Returns(loggerSampleRate); var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); - - var loggerConfiguration = new LoggerConfiguration + + var loggerConfiguration = new PowertoolsLoggerConfiguration { - Service = null, - MinimumLevel = LogLevel.None + Service = service, + MinimumLogLevel = logLevel, + LogOutput = systemWrapper, + SamplingRate = loggerSampleRate, + Random = randomSampleRate }; - + // Act - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger("test"); - + logger.LogInformation("Test"); // Assert @@ -359,14 +364,15 @@ public void Log_SamplingRateGreaterThanOne_SkipsSamplingRateConfiguration() var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { - Service = null, - MinimumLevel = LogLevel.None - }; - - // Act - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + Service = service, + MinimumLogLevel = logLevel, + LogOutput = systemWrapper, + SamplingRate = loggerSampleRate + }; + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); logger.LogInformation("Test"); @@ -387,7 +393,6 @@ public void Log_EnvVarSetsCaseToCamelCase_OutputsCamelCaseLog() var loggerName = Guid.NewGuid().ToString(); var service = Guid.NewGuid().ToString(); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); @@ -395,16 +400,16 @@ public void Log_EnvVarSetsCaseToCamelCase_OutputsCamelCaseLog() configurations.LoggerOutputCase.Returns(LoggerOutputCase.CamelCase.ToString()); var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None + MinimumLogLevel = LogLevel.None, + LogOutput = systemWrapper }; // Act - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -430,24 +435,23 @@ public void Log_AttributeSetsCaseToCamelCase_OutputsCamelCaseLog() var loggerName = Guid.NewGuid().ToString(); var service = Guid.NewGuid().ToString(); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None, - LoggerOutputCase = LoggerOutputCase.CamelCase + MinimumLogLevel = LogLevel.None, + LoggerOutputCase = LoggerOutputCase.CamelCase, + LogOutput = systemWrapper }; - + // Act - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -483,13 +487,14 @@ public void Log_EnvVarSetsCaseToPascalCase_OutputsPascalCaseLog() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None + MinimumLogLevel = LogLevel.None, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -515,23 +520,22 @@ public void Log_AttributeSetsCaseToPascalCase_OutputsPascalCaseLog() var loggerName = Guid.NewGuid().ToString(); var service = Guid.NewGuid().ToString(); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None, - LoggerOutputCase = LoggerOutputCase.PascalCase + MinimumLogLevel = LogLevel.None, + LoggerOutputCase = LoggerOutputCase.PascalCase, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -565,13 +569,14 @@ public void Log_EnvVarSetsCaseToSnakeCase_OutputsSnakeCaseLog() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None + MinimumLogLevel = LogLevel.None, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -595,23 +600,22 @@ public void Log_AttributeSetsCaseToSnakeCase_OutputsSnakeCaseLog() var loggerName = Guid.NewGuid().ToString(); var service = Guid.NewGuid().ToString(); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None, - LoggerOutputCase = LoggerOutputCase.SnakeCase + MinimumLogLevel = LogLevel.None, + LoggerOutputCase = LoggerOutputCase.SnakeCase, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -644,13 +648,14 @@ public void Log_NoOutputCaseSet_OutputDefaultsToSnakeCaseLog() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None - }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + MinimumLogLevel = LogLevel.None, + LogOutput = systemWrapper + }; + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -679,13 +684,13 @@ public void BeginScope_WhenScopeIsObject_ExtractScopeKeys() configurations.LogLevel.Returns(logLevel.ToString()); var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = service, - MinimumLevel = logLevel + MinimumLogLevel = logLevel }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = (PowertoolsLogger)provider.CreateLogger(loggerName); var scopeKeys = new @@ -722,13 +727,13 @@ public void BeginScope_WhenScopeIsObjectDictionary_ExtractScopeKeys() configurations.LogLevel.Returns(logLevel.ToString()); var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = service, - MinimumLevel = logLevel + MinimumLogLevel = logLevel }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = (PowertoolsLogger)provider.CreateLogger(loggerName); var scopeKeys = new Dictionary @@ -765,13 +770,13 @@ public void BeginScope_WhenScopeIsStringDictionary_ExtractScopeKeys() configurations.LogLevel.Returns(logLevel.ToString()); var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = service, - MinimumLevel = logLevel + MinimumLogLevel = logLevel }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = (PowertoolsLogger)provider.CreateLogger(loggerName); var scopeKeys = new Dictionary @@ -821,12 +826,13 @@ public void Log_WhenExtraKeysIsObjectDictionary_AppendExtraKeys(LogLevel logLeve configurations.LoggerOutputCase.Returns(LoggerOutputCase.PascalCase.ToString()); var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = service, - MinimumLevel = LogLevel.Trace, + MinimumLogLevel = LogLevel.Trace, + LogOutput = systemWrapper }; - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = (PowertoolsLogger)provider.CreateLogger(loggerName); var scopeKeys = new Dictionary @@ -904,13 +910,14 @@ public void Log_WhenExtraKeysIsStringDictionary_AppendExtraKeys(LogLevel logLeve configurations.LoggerOutputCase.Returns(LoggerOutputCase.PascalCase.ToString()); var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = service, - MinimumLevel = LogLevel.Trace, + MinimumLogLevel = LogLevel.Trace, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = (PowertoolsLogger)provider.CreateLogger(loggerName); var scopeKeys = new Dictionary @@ -988,13 +995,14 @@ public void Log_WhenExtraKeysAsObject_AppendExtraKeys(LogLevel logLevel, bool lo configurations.LoggerOutputCase.Returns(LoggerOutputCase.PascalCase.ToString()); var systemWrapper = Substitute.For(); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = service, - MinimumLevel = LogLevel.Trace, + MinimumLogLevel = LogLevel.Trace, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = (PowertoolsLogger)provider.CreateLogger(loggerName); var scopeKeys = new @@ -1054,22 +1062,21 @@ public void Log_WhenException_LogsExceptionDetails() var service = Guid.NewGuid().ToString(); var error = new InvalidOperationException("TestError"); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None + MinimumLogLevel = LogLevel.None, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); try @@ -1087,10 +1094,11 @@ public void Log_WhenException_LogsExceptionDetails() error.Message + "\"") )); systemWrapper.Received(1).LogLine(Arg.Is(s => - s.Contains("\"exception\":{\"type\":\"System.InvalidOperationException\",\"message\":\"TestError\",\"source\":\"AWS.Lambda.Powertools.Logging.Tests\",\"stack_trace\":\" at AWS.Lambda.Powertools.Logging.Tests.PowertoolsLoggerTest.Log_WhenException_LogsExceptionDetails()") + s.Contains( + "\"exception\":{\"type\":\"System.InvalidOperationException\",\"message\":\"TestError\",\"source\":\"AWS.Lambda.Powertools.Logging.Tests\",\"stack_trace\":\" at AWS.Lambda.Powertools.Logging.Tests.PowertoolsLoggerTest.Log_WhenException_LogsExceptionDetails()") )); } - + [Fact] public void Log_Inner_Exception() { @@ -1109,17 +1117,18 @@ public void Log_Inner_Exception() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None + MinimumLogLevel = LogLevel.None, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); logger.LogError( - error, + error, "Something went wrong and we logged an exception itself with an inner exception. This is a param {arg}", 12345); @@ -1128,12 +1137,13 @@ public void Log_Inner_Exception() s.Contains("\"exception\":{\"type\":\"" + error.GetType().FullName + "\",\"message\":\"" + error.Message + "\"") )); - + systemWrapper.Received(1).LogLine(Arg.Is(s => - s.Contains("\"level\":\"Error\",\"service\":\"" + service+ "\",\"name\":\"" + loggerName + "\",\"message\":\"Something went wrong and we logged an exception itself with an inner exception. This is a param 12345\",\"exception\":{\"type\":\"System.InvalidOperationException\",\"message\":\"Parent exception message\",\"inner_exception\":{\"type\":\"System.ArgumentNullException\",\"message\":\"Very important inner exception message (Parameter 'service')\"}}}") + s.Contains("\"level\":\"Error\",\"service\":\"" + service + "\",\"name\":\"" + loggerName + + "\",\"message\":\"Something went wrong and we logged an exception itself with an inner exception. This is a param 12345\",\"exception\":{\"type\":\"System.InvalidOperationException\",\"message\":\"Parent exception message\",\"inner_exception\":{\"type\":\"System.ArgumentNullException\",\"message\":\"Very important inner exception message (Parameter 'service')\"}}}") )); } - + [Fact] public void Log_Nested_Inner_Exception() { @@ -1143,7 +1153,7 @@ public void Log_Nested_Inner_Exception() var error = new InvalidOperationException("Parent exception message", new ArgumentNullException(nameof(service), new Exception("Very important nested inner exception message"))); - + var logLevel = LogLevel.Information; var randomSampleRate = 0.5; @@ -1154,24 +1164,26 @@ public void Log_Nested_Inner_Exception() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None + MinimumLogLevel = LogLevel.None, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); - + logger.LogError( - error, + error, "Something went wrong and we logged an exception itself with an inner exception. This is a param {arg}", 12345); // Assert systemWrapper.Received(1).LogLine(Arg.Is(s => - s.Contains("\"message\":\"Something went wrong and we logged an exception itself with an inner exception. This is a param 12345\",\"exception\":{\"type\":\"System.InvalidOperationException\",\"message\":\"Parent exception message\",\"inner_exception\":{\"type\":\"System.ArgumentNullException\",\"message\":\"service\",\"inner_exception\":{\"type\":\"System.Exception\",\"message\":\"Very important nested inner exception message\"}}}}") + s.Contains( + "\"message\":\"Something went wrong and we logged an exception itself with an inner exception. This is a param 12345\",\"exception\":{\"type\":\"System.InvalidOperationException\",\"message\":\"Parent exception message\",\"inner_exception\":{\"type\":\"System.ArgumentNullException\",\"message\":\"service\",\"inner_exception\":{\"type\":\"System.Exception\",\"message\":\"Very important nested inner exception message\"}}}}") )); } @@ -1183,22 +1195,21 @@ public void Log_WhenNestedException_LogsExceptionDetails() var service = Guid.NewGuid().ToString(); var error = new InvalidOperationException("TestError"); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None + MinimumLogLevel = LogLevel.None, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); try @@ -1218,7 +1229,7 @@ public void Log_WhenNestedException_LogsExceptionDetails() } [Fact] - public void Log_WhenByteArray_LogsByteArrayNumbers() + public void Log_WhenByteArray_LogsBase64EncodedString() { // Arrange var loggerName = Guid.NewGuid().ToString(); @@ -1226,29 +1237,29 @@ public void Log_WhenByteArray_LogsByteArrayNumbers() var bytes = new byte[10]; new Random().NextBytes(bytes); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None + MinimumLogLevel = LogLevel.None, + LogOutput = systemWrapper }; - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); // Act logger.LogInformation(new { Name = "Test Object", Bytes = bytes }); // Assert + var base64String = Convert.ToBase64String(bytes); systemWrapper.Received(1).LogLine(Arg.Is(s => - s.Contains("\"bytes\":[" + string.Join(",", bytes) + "]") + s.Contains($"\"bytes\":\"{base64String}\"") )); } @@ -1265,22 +1276,21 @@ public void Log_WhenMemoryStream_LogsBase64String() Position = 0 }; var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None + MinimumLogLevel = LogLevel.None, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); // Act @@ -1307,22 +1317,21 @@ public void Log_WhenMemoryStream_LogsBase64String_UnsafeRelaxedJsonEscaping() Position = 0 }; var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None + MinimumLogLevel = LogLevel.None, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); // Act @@ -1337,35 +1346,55 @@ public void Log_WhenMemoryStream_LogsBase64String_UnsafeRelaxedJsonEscaping() [Fact] public void Log_Set_Execution_Environment_Context() { - var _originalValue = Environment.GetEnvironmentVariable("POWERTOOLS_SERVICE_NAME"); - // Arrange var loggerName = Guid.NewGuid().ToString(); - var assemblyName = "AWS.Lambda.Powertools.Logger"; - var assemblyVersion = "1.0.0"; - var env = Substitute.For(); - env.GetAssemblyName(Arg.Any()).Returns(assemblyName); - env.GetAssemblyVersion(Arg.Any()).Returns(assemblyVersion); + var env = new PowertoolsEnvironment(); + // Act + var configurations = new PowertoolsConfigurations(env); + + var loggerConfiguration = new PowertoolsLoggerConfiguration + { + Service = null, + MinimumLogLevel = LogLevel.None + }; + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); + var logger = provider.CreateLogger(loggerName); + logger.LogInformation("Test"); + + // Assert + Assert.Equal($"{Constants.FeatureContextIdentifier}/Logging/0.0.1", + env.GetEnvironmentVariable("AWS_EXECUTION_ENV")); + } + + [Fact] + public void Log_Skip_If_Exists_Execution_Environment_Context() + { + // Arrange + var loggerName = Guid.NewGuid().ToString(); + + var env = new PowertoolsEnvironment(); + env.SetEnvironmentVariable("AWS_EXECUTION_ENV", + $"{Constants.FeatureContextIdentifier}/Logging/AlreadyThere"); // Act - var systemWrapper = new SystemWrapper(env); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(env); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None + MinimumLogLevel = LogLevel.None }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); logger.LogInformation("Test"); // Assert - env.Received(1).SetEnvironmentVariable("AWS_EXECUTION_ENV", - $"{Constants.FeatureContextIdentifier}/Logger/{assemblyVersion}"); - env.Received(1).GetEnvironmentVariable("AWS_EXECUTION_ENV"); + Assert.Equal($"{Constants.FeatureContextIdentifier}/Logging/AlreadyThere", + env.GetEnvironmentVariable("AWS_EXECUTION_ENV")); + env.SetEnvironmentVariable("AWS_EXECUTION_ENV", null); } [Fact] @@ -1384,14 +1413,15 @@ public void Log_Should_Serialize_DateOnly() var systemWrapper = Substitute.For(); systemWrapper.GetRandom().Returns(randomSampleRate); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None, - LoggerOutputCase = LoggerOutputCase.CamelCase + MinimumLogLevel = LogLevel.None, + LoggerOutputCase = LoggerOutputCase.CamelCase, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -1410,7 +1440,8 @@ public void Log_Should_Serialize_DateOnly() // Assert systemWrapper.Received(1).LogLine( Arg.Is(s => - s.Contains("\"message\":{\"propOne\":\"Value 1\",\"propTwo\":\"Value 2\",\"propThree\":{\"propFour\":1},\"date\":\"2022-01-01\"}}") + s.Contains( + "\"message\":{\"propOne\":\"Value 1\",\"propTwo\":\"Value 2\",\"propThree\":{\"propFour\":1},\"date\":\"2022-01-01\"}}") ) ); } @@ -1429,16 +1460,17 @@ public void Log_Should_Serialize_TimeOnly() configurations.LogLevel.Returns(logLevel.ToString()); var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = null, - MinimumLevel = LogLevel.None, - LoggerOutputCase = LoggerOutputCase.CamelCase + MinimumLogLevel = LogLevel.None, + LoggerOutputCase = LoggerOutputCase.CamelCase, + LogOutput = systemWrapper, + Random = randomSampleRate }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -1458,13 +1490,14 @@ public void Log_Should_Serialize_TimeOnly() ); } - + [Theory] [InlineData(true, "WARN", LogLevel.Warning)] [InlineData(false, "Fatal", LogLevel.Critical)] [InlineData(false, "NotValid", LogLevel.Critical)] [InlineData(true, "NotValid", LogLevel.Warning)] - public void Log_Should_Use_Powertools_Log_Level_When_Lambda_Log_Level_Enabled(bool willLog, string awsLogLevel, LogLevel logLevel) + public void Log_Should_Use_Powertools_Log_Level_When_Lambda_Log_Level_Enabled(bool willLog, string awsLogLevel, + LogLevel logLevel) { // Arrange var loggerName = Guid.NewGuid().ToString(); @@ -1474,15 +1507,14 @@ public void Log_Should_Use_Powertools_Log_Level_When_Lambda_Log_Level_Enabled(bo environment.GetEnvironmentVariable("POWERTOOLS_LOG_LEVEL").Returns(logLevel.ToString()); environment.GetEnvironmentVariable("AWS_LAMBDA_LOG_LEVEL").Returns(awsLogLevel); - var systemWrapper = new SystemWrapperMock(environment); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { LoggerOutputCase = LoggerOutputCase.CamelCase }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -1494,17 +1526,17 @@ public void Log_Should_Use_Powertools_Log_Level_When_Lambda_Log_Level_Enabled(bo // Act logger.LogWarning(message); - + // Assert Assert.True(logger.IsEnabled(logLevel)); Assert.Equal(logLevel, configurations.GetLogLevel()); - Assert.Equal(willLog, systemWrapper.LogMethodCalled); } - + [Theory] [InlineData(true, "WARN", LogLevel.Warning)] [InlineData(true, "Fatal", LogLevel.Critical)] - public void Log_Should_Use_AWS_Lambda_Log_Level_When_Enabled(bool willLog, string awsLogLevel, LogLevel logLevel) + public void Log_Should_Use_AWS_Lambda_Log_Level_When_Enabled(bool willLog, string awsLogLevel, + LogLevel logLevel) { // Arrange var loggerName = Guid.NewGuid().ToString(); @@ -1514,15 +1546,14 @@ public void Log_Should_Use_AWS_Lambda_Log_Level_When_Enabled(bool willLog, strin environment.GetEnvironmentVariable("POWERTOOLS_LOG_LEVEL").Returns(string.Empty); environment.GetEnvironmentVariable("AWS_LAMBDA_LOG_LEVEL").Returns(awsLogLevel); - var systemWrapper = new SystemWrapperMock(environment); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { LoggerOutputCase = LoggerOutputCase.CamelCase, }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -1534,14 +1565,13 @@ public void Log_Should_Use_AWS_Lambda_Log_Level_When_Enabled(bool willLog, strin // Act logger.LogWarning(message); - + // Assert Assert.True(logger.IsEnabled(logLevel)); Assert.Equal(LogLevel.Information, configurations.GetLogLevel()); //default Assert.Equal(logLevel, configurations.GetLambdaLogLevel()); - Assert.Equal(willLog, systemWrapper.LogMethodCalled); } - + [Fact] public void Log_Should_Show_Warning_When_AWS_Lambda_Log_Level_Enabled() { @@ -1552,49 +1582,53 @@ public void Log_Should_Show_Warning_When_AWS_Lambda_Log_Level_Enabled() environment.GetEnvironmentVariable("POWERTOOLS_LOG_LEVEL").Returns("Debug"); environment.GetEnvironmentVariable("AWS_LAMBDA_LOG_LEVEL").Returns("Warn"); - var systemWrapper = new SystemWrapperMock(environment); - var configurations = new PowertoolsConfigurations(systemWrapper); + var systemWrapper = new TestLoggerOutput(); + var configurations = new PowertoolsConfigurations(environment); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { - LoggerOutputCase = LoggerOutputCase.CamelCase + LoggerOutputCase = LoggerOutputCase.CamelCase, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var logLevel = configurations.GetLogLevel(); var lambdaLogLevel = configurations.GetLambdaLogLevel(); - + // Assert Assert.True(logger.IsEnabled(LogLevel.Warning)); Assert.Equal(LogLevel.Debug, logLevel); Assert.Equal(LogLevel.Warning, lambdaLogLevel); - Assert.Contains($"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.", - systemWrapper.LogMethodCalledWithArgument); + Assert.Contains( + $"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.", + systemWrapper.ToString()); } - + [Theory] - [InlineData(true,"LogLevel")] - [InlineData(false,"Level")] - public void Log_PascalCase_Outputs_Correct_Level_Property_When_AWS_Lambda_Log_Level_Enabled_Or_Disabled(bool alcEnabled, string levelProp) + [InlineData(true, "LogLevel")] + [InlineData(false, "Level")] + public void Log_PascalCase_Outputs_Correct_Level_Property_When_AWS_Lambda_Log_Level_Enabled_Or_Disabled( + bool alcEnabled, string levelProp) { // Arrange var loggerName = Guid.NewGuid().ToString(); - + var environment = Substitute.For(); environment.GetEnvironmentVariable("POWERTOOLS_LOG_LEVEL").Returns("Information"); - if(alcEnabled) + if (alcEnabled) environment.GetEnvironmentVariable("AWS_LAMBDA_LOG_LEVEL").Returns("Info"); - var systemWrapper = new SystemWrapperMock(environment); - var configurations = new PowertoolsConfigurations(systemWrapper); - var loggerConfiguration = new LoggerConfiguration + var systemWrapper = new TestLoggerOutput(); + var configurations = new PowertoolsConfigurations(environment); + var loggerConfiguration = new PowertoolsLoggerConfiguration { - LoggerOutputCase = LoggerOutputCase.PascalCase + LoggerOutputCase = LoggerOutputCase.PascalCase, + LogOutput = systemWrapper }; - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -1605,10 +1639,9 @@ public void Log_PascalCase_Outputs_Correct_Level_Property_When_AWS_Lambda_Log_Le logger.LogInformation(message); // Assert - Assert.True(systemWrapper.LogMethodCalled); - Assert.Contains($"\"{levelProp}\":\"Information\"",systemWrapper.LogMethodCalledWithArgument); + Assert.Contains($"\"{levelProp}\":\"Information\"", systemWrapper.ToString()); } - + [Theory] [InlineData(LoggerOutputCase.CamelCase)] [InlineData(LoggerOutputCase.SnakeCase)] @@ -1616,21 +1649,22 @@ public void Log_CamelCase_Outputs_Level_When_AWS_Lambda_Log_Level_Enabled(Logger { // Arrange var loggerName = Guid.NewGuid().ToString(); - + var environment = Substitute.For(); environment.GetEnvironmentVariable("POWERTOOLS_LOG_LEVEL").Returns(string.Empty); environment.GetEnvironmentVariable("AWS_LAMBDA_LOG_LEVEL").Returns("Info"); - var systemWrapper = new SystemWrapperMock(environment); - var configurations = new PowertoolsConfigurations(systemWrapper); + var systemWrapper = new TestLoggerOutput(); + var configurations = new PowertoolsConfigurations(environment); configurations.LoggerOutputCase.Returns(casing.ToString()); - - var loggerConfiguration = new LoggerConfiguration + + var loggerConfiguration = new PowertoolsLoggerConfiguration { - LoggerOutputCase = casing + LoggerOutputCase = casing, + LogOutput = systemWrapper }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -1641,10 +1675,9 @@ public void Log_CamelCase_Outputs_Level_When_AWS_Lambda_Log_Level_Enabled(Logger logger.LogInformation(message); // Assert - Assert.True(systemWrapper.LogMethodCalled); - Assert.Contains("\"level\":\"Information\"",systemWrapper.LogMethodCalledWithArgument); + Assert.Contains("\"level\":\"Information\"", systemWrapper.ToString()); } - + [Theory] [InlineData("TRACE", LogLevel.Trace)] [InlineData("debug", LogLevel.Debug)] @@ -1659,12 +1692,11 @@ public void Should_Map_AWS_Log_Level_And_Default_To_Information(string awsLogLev var environment = Substitute.For(); environment.GetEnvironmentVariable("AWS_LAMBDA_LOG_LEVEL").Returns(awsLogLevel); - var systemWrapper = new SystemWrapperMock(environment); - var configuration = new PowertoolsConfigurations(systemWrapper); + var configuration = new PowertoolsConfigurations(environment); // Act var logLvl = configuration.GetLambdaLogLevel(); - + // Assert Assert.Equal(logLevel, logLvl); } @@ -1679,15 +1711,14 @@ public void Log_Should_Use_Powertools_Log_Level_When_Set(bool willLog, LogLevel var environment = Substitute.For(); environment.GetEnvironmentVariable("POWERTOOLS_LOG_LEVEL").Returns(logLevel.ToString()); - var systemWrapper = new SystemWrapperMock(environment); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); - var loggerConfiguration = new LoggerConfiguration + var loggerConfiguration = new PowertoolsLoggerConfiguration { LoggerOutputCase = LoggerOutputCase.CamelCase }; - - var provider = new LoggerProvider(loggerConfiguration, configurations, systemWrapper); + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); var logger = provider.CreateLogger(loggerName); var message = new @@ -1703,13 +1734,12 @@ public void Log_Should_Use_Powertools_Log_Level_When_Set(bool willLog, LogLevel // Assert Assert.True(logger.IsEnabled(logLevel)); Assert.Equal(logLevel.ToString(), configurations.LogLevel); - Assert.Equal(willLog, systemWrapper.LogMethodCalled); } public void Dispose() { - PowertoolsLoggingSerializer.ClearOptions(); - LoggingAspect.ResetForTest(); + // PowertoolsLoggingSerializer.ClearOptions(); + // LoggingAspect.ResetForTest(); } } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs index b522963f..b8df5d4f 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLambdaSerializerTests.cs @@ -30,30 +30,23 @@ namespace AWS.Lambda.Powertools.Logging.Tests.Serializers; public class PowertoolsLambdaSerializerTests : IDisposable { + private readonly PowertoolsLoggingSerializer _serializer; + + public PowertoolsLambdaSerializerTests() + { + _serializer = new PowertoolsLoggingSerializer(); + } + #if NET8_0_OR_GREATER [Fact] public void Constructor_ShouldNotThrowException() { // Arrange & Act & Assert var exception = - Record.Exception(() => PowertoolsLoggingSerializer.AddSerializerContext(TestJsonContext.Default)); + Record.Exception(() => _serializer.AddSerializerContext(TestJsonContext.Default)); Assert.Null(exception); } - [Fact] - public void Constructor_ShouldAddCustomerContext() - { - // Arrange - var customerContext = new TestJsonContext(); - - // Act - PowertoolsLoggingSerializer.AddSerializerContext(customerContext); - ; - - // Assert - Assert.True(PowertoolsLoggingSerializer.HasContext(customerContext)); - } - [Theory] [InlineData(LoggerOutputCase.CamelCase, "{\"fullName\":\"John\",\"age\":30}", "John", 30)] [InlineData(LoggerOutputCase.PascalCase, "{\"FullName\":\"Jane\",\"Age\":25}", "Jane", 25)] @@ -81,7 +74,7 @@ public void Deserialize_InvalidType_ShouldThrowInvalidOperationException() var serializer = new PowertoolsSourceGeneratorSerializer(); ; - PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase.PascalCase); + _serializer.ConfigureNamingPolicy(LoggerOutputCase.PascalCase); var json = "{\"FullName\":\"John\",\"Age\":30}"; var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); @@ -209,7 +202,7 @@ public void Should_Serialize_Unknown_Type_When_Including_Outside_Context() stream.Position = 0; var outputExternalSerializer = new StreamReader(stream).ReadToEnd(); - var outptuMySerializer = PowertoolsLoggingSerializer.Serialize(log, typeof(LogEntry)); + var outptuMySerializer = _serializer.Serialize(log, typeof(LogEntry)); // Assert Assert.Equal( @@ -224,8 +217,7 @@ public void Should_Serialize_Unknown_Type_When_Including_Outside_Context() #endif public void Dispose() { - PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggingConstants.DefaultLoggerOutputCase); - PowertoolsLoggingSerializer.ClearOptions(); + } #if NET6_0 @@ -234,7 +226,7 @@ public void Dispose() public void Should_Serialize_Net6() { // Arrange - PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggingConstants.DefaultLoggerOutputCase); + _serializer.ConfigureNamingPolicy(LoggingConstants.DefaultLoggerOutputCase); var testObject = new APIGatewayProxyRequest { Path = "asda", @@ -250,7 +242,7 @@ public void Should_Serialize_Net6() Message = testObject }; - var outptuMySerializer = PowertoolsLoggingSerializer.Serialize(log, null); + var outptuMySerializer = _serializer.Serialize(log, null); // Assert Assert.Equal( diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs index f8e1cd48..fb2397d0 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs @@ -1,24 +1,10 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; +using System.IO; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using Amazon.Lambda.Serialization.SystemTextJson; using AWS.Lambda.Powertools.Common.Utils; using AWS.Lambda.Powertools.Logging.Internal; @@ -31,19 +17,21 @@ namespace AWS.Lambda.Powertools.Logging.Tests.Serializers; public class PowertoolsLoggingSerializerTests : IDisposable { + private readonly PowertoolsLoggingSerializer _serializer; public PowertoolsLoggingSerializerTests() { - PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggingConstants.DefaultLoggerOutputCase); + _serializer = new PowertoolsLoggingSerializer(); + _serializer.ConfigureNamingPolicy(LoggingConstants.DefaultLoggerOutputCase); #if NET8_0_OR_GREATER - PowertoolsLoggingSerializer.ClearContext(); + ClearContext(); #endif } - + [Fact] public void SerializerOptions_ShouldNotBeNull() { - var options = PowertoolsLoggingSerializer.GetSerializerOptions(); + var options = _serializer.GetSerializerOptions(); Assert.NotNull(options); } @@ -51,9 +39,9 @@ public void SerializerOptions_ShouldNotBeNull() public void SerializerOptions_ShouldHaveCorrectDefaultSettings() { RuntimeFeatureWrapper.SetIsDynamicCodeSupported(false); - - var options = PowertoolsLoggingSerializer.GetSerializerOptions(); - + + var options = _serializer.GetSerializerOptions(); + Assert.Collection(options.Converters, converter => Assert.IsType(converter), converter => Assert.IsType(converter), @@ -71,17 +59,17 @@ public void SerializerOptions_ShouldHaveCorrectDefaultSettings() #if NET8_0_OR_GREATER Assert.Collection(options.TypeInfoResolverChain, - resolver => Assert.IsType(resolver)); + resolver => Assert.IsType(resolver)); #endif } - + [Fact] public void SerializerOptions_ShouldHaveCorrectDefaultSettings_WhenDynamic() { RuntimeFeatureWrapper.SetIsDynamicCodeSupported(true); - - var options = PowertoolsLoggingSerializer.GetSerializerOptions(); - + + var options = _serializer.GetSerializerOptions(); + Assert.Collection(options.Converters, converter => Assert.IsType(converter), converter => Assert.IsType(converter), @@ -132,7 +120,7 @@ public void ConfigureNamingPolicy_ShouldNotChangeWhenPassedNull() public void ConfigureNamingPolicy_ShouldNotChangeWhenPassedSameCase() { var originalJson = SerializeTestObject(LoggerOutputCase.SnakeCase); - PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase.SnakeCase); + _serializer.ConfigureNamingPolicy(LoggerOutputCase.SnakeCase); var newJson = SerializeTestObject(LoggerOutputCase.SnakeCase); Assert.Equal(originalJson, newJson); } @@ -140,7 +128,7 @@ public void ConfigureNamingPolicy_ShouldNotChangeWhenPassedSameCase() [Fact] public void Serialize_ShouldHandleNestedObjects() { - PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase.SnakeCase); + _serializer.ConfigureNamingPolicy(LoggerOutputCase.SnakeCase); var testObject = new LogEntry { @@ -151,7 +139,7 @@ public void Serialize_ShouldHandleNestedObjects() } }; - var json = JsonSerializer.Serialize(testObject, PowertoolsLoggingSerializer.GetSerializerOptions()); + var json = JsonSerializer.Serialize(testObject, _serializer.GetSerializerOptions()); Assert.Contains("\"cold_start\":true", json); Assert.Contains("\"nested_object\":{\"property_name\":\"Value\"}", json); } @@ -163,7 +151,7 @@ public void Serialize_ShouldHandleEnumValues() { Level = LogLevel.Error }; - var json = JsonSerializer.Serialize(testObject, PowertoolsLoggingSerializer.GetSerializerOptions()); + var json = JsonSerializer.Serialize(testObject, _serializer.GetSerializerOptions()); Assert.Contains("\"level\":\"Error\"", json); } @@ -177,50 +165,248 @@ public void Serialize_UnknownType_ThrowsInvalidOperationException() RuntimeFeatureWrapper.SetIsDynamicCodeSupported(false); // Act & Assert var exception = Assert.Throws(() => - PowertoolsLoggingSerializer.Serialize(unknownObject, typeof(UnknownType))); + _serializer.Serialize(unknownObject, typeof(UnknownType))); Assert.Contains("is not known to the serializer", exception.Message); Assert.Contains(typeof(UnknownType).ToString(), exception.Message); } - + [Fact] public void Serialize_UnknownType_Should_Not_Throw_InvalidOperationException_When_Dynamic() { // Arrange - var unknownObject = new UnknownType{ SomeProperty = "Hello"}; + var unknownObject = new UnknownType { SomeProperty = "Hello" }; RuntimeFeatureWrapper.SetIsDynamicCodeSupported(true); // Act & Assert var expected = - PowertoolsLoggingSerializer.Serialize(unknownObject, typeof(UnknownType)); + _serializer.Serialize(unknownObject, typeof(UnknownType)); Assert.Equal("{\"some_property\":\"Hello\"}", expected); } + [Fact] + public void AddSerializerContext_ShouldUpdateTypeInfoResolver() + { + // Arrange + RuntimeFeatureWrapper.SetIsDynamicCodeSupported(false); + var testContext = new TestSerializerContext(new JsonSerializerOptions()); + + // Get the initial resolver + var beforeOptions = _serializer.GetSerializerOptions(); + var beforeResolver = beforeOptions.TypeInfoResolver; + + // Act + _serializer.AddSerializerContext(testContext); + + // Get the updated resolver + var afterOptions = _serializer.GetSerializerOptions(); + var afterResolver = afterOptions.TypeInfoResolver; + + // Assert - adding a context should create a new resolver + Assert.NotSame(beforeResolver, afterResolver); + Assert.IsType(afterResolver); + } + private class UnknownType { public string SomeProperty { get; set; } } + + private class TestSerializerContext : JsonSerializerContext + { + private readonly JsonSerializerOptions _options; + + public TestSerializerContext(JsonSerializerOptions options) : base(options) + { + _options = options; + } + + public override JsonTypeInfo? GetTypeInfo(Type type) + { + return null; // For testing purposes only + } + + protected override JsonSerializerOptions? GeneratedSerializerOptions => _options; + } + + private void ClearContext() + { + // Create a new serializer to clear any existing contexts + _serializer.SetOptions(new JsonSerializerOptions()); + } #endif private string SerializeTestObject(LoggerOutputCase? outputCase) { if (outputCase.HasValue) { - PowertoolsLoggingSerializer.ConfigureNamingPolicy(outputCase.Value); + _serializer.ConfigureNamingPolicy(outputCase.Value); } LogEntry testObject = new LogEntry { ColdStart = true }; - return JsonSerializer.Serialize(testObject, PowertoolsLoggingSerializer.GetSerializerOptions()); + return JsonSerializer.Serialize(testObject, _serializer.GetSerializerOptions()); + } + + [Fact] + public void ByteArrayConverter_ShouldProduceBase64EncodedString() + { + // Arrange + var testObject = new { BinaryData = new byte[] { 1, 2, 3, 4, 5 } }; + + // Act + var json = JsonSerializer.Serialize(testObject, _serializer.GetSerializerOptions()); + + // Assert + Assert.Contains("\"binary_data\":\"AQIDBAU=\"", json); + } + + [Fact] + public void ExceptionConverter_ShouldSerializeExceptionDetails() + { + // Arrange + var exception = new InvalidOperationException("Test error message", new Exception("Inner exception")); + var testObject = new { Error = exception }; + + // Act + var json = JsonSerializer.Serialize(testObject, _serializer.GetSerializerOptions()); + + // Assert + Assert.Equal("{\"error\":{\"type\":\"System.InvalidOperationException\",\"message\":\"Test error message\",\"inner_exception\":{\"type\":\"System.Exception\",\"message\":\"Inner exception\"}}}", json); + } + + [Fact] + public void MemoryStreamConverter_ShouldConvertToBase64() + { + // Arrange + var bytes = new byte[] { 10, 20, 30, 40, 50 }; + var memoryStream = new MemoryStream(bytes); + var testObject = new { Stream = memoryStream }; + + // Act + var json = JsonSerializer.Serialize(testObject, _serializer.GetSerializerOptions()); + + // Assert + Assert.Contains("\"stream\":\"ChQeKDI=\"", json); + } + + [Fact] + public void ConstantClassConverter_ShouldSerializeToString() + { + // Arrange + var testObject = new { Level = LogLevel.Warning }; + + // Act + var json = JsonSerializer.Serialize(testObject, _serializer.GetSerializerOptions()); + + // Assert + Assert.Contains("\"level\":\"Warning\"", json); + } + +#if NET6_0_OR_GREATER + [Fact] + public void DateOnlyConverter_ShouldSerializeToIsoDate() + { + // Arrange + var date = new DateOnly(2023, 10, 15); + var testObject = new { Date = date }; + + // Act + var json = JsonSerializer.Serialize(testObject, _serializer.GetSerializerOptions()); + + // Assert + Assert.Contains("\"date\":\"2023-10-15\"", json); + } + + [Fact] + public void TimeOnlyConverter_ShouldSerializeToIsoTime() + { + // Arrange + var time = new TimeOnly(13, 45, 30); + var testObject = new { Time = time }; + + // Act + var json = JsonSerializer.Serialize(testObject, _serializer.GetSerializerOptions()); + + // Assert + Assert.Contains("\"time\":\"13:45:30\"", json); + } +#endif + + [Fact] + public void LogLevelJsonConverter_ShouldSerializeAllLogLevels() + { + // Arrange + var levels = new Dictionary + { + { "trace", LogLevel.Trace }, + { "debug", LogLevel.Debug }, + { "info", LogLevel.Information }, + { "warning", LogLevel.Warning }, + { "error", LogLevel.Error }, + { "critical", LogLevel.Critical } + }; + + // Act + var json = JsonSerializer.Serialize(levels, _serializer.GetSerializerOptions()); + + // Assert + Assert.Contains("\"trace\":\"Trace\"", json); + Assert.Contains("\"debug\":\"Debug\"", json); + Assert.Contains("\"info\":\"Information\"", json); + Assert.Contains("\"warning\":\"Warning\"", json); + Assert.Contains("\"error\":\"Error\"", json); + Assert.Contains("\"critical\":\"Critical\"", json); + } + + [Fact] + public void Serialize_ComplexObjectWithMultipleConverters_ShouldConvertAllProperties() + { + // Arrange + var testObject = new ComplexTestObject + { + BinaryData = new byte[] { 1, 2, 3 }, + Exception = new ArgumentException("Test argument"), + Stream = new MemoryStream(new byte[] { 4, 5, 6 }), + Level = LogLevel.Information, +#if NET6_0_OR_GREATER + Date = new DateOnly(2023, 1, 15), + Time = new TimeOnly(14, 30, 0), +#endif + }; + + // Act + var json = JsonSerializer.Serialize(testObject, _serializer.GetSerializerOptions()); + + // Assert + Assert.Contains("\"binary_data\":\"AQID\"", json); + Assert.Contains("\"exception\":{\"type\":\"System.ArgumentException\"", json); + Assert.Contains("\"stream\":\"BAUG\"", json); + Assert.Contains("\"level\":\"Information\"", json); +#if NET6_0_OR_GREATER + Assert.Contains("\"date\":\"2023-01-15\"", json); + Assert.Contains("\"time\":\"14:30:00\"", json); +#endif + } + + private class ComplexTestObject + { + public byte[] BinaryData { get; set; } + public Exception Exception { get; set; } + public MemoryStream Stream { get; set; } + public LogLevel Level { get; set; } +#if NET6_0_OR_GREATER + public DateOnly Date { get; set; } + public TimeOnly Time { get; set; } +#endif } public void Dispose() { - PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggingConstants.DefaultLoggerOutputCase); #if NET8_0_OR_GREATER - PowertoolsLoggingSerializer.ClearContext(); + ClearContext(); #endif - PowertoolsLoggingSerializer.ClearOptions(); + _serializer.SetOptions(null); RuntimeFeatureWrapper.Reset(); } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsConfigurationExtensionsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsConfigurationExtensionsTests.cs index 6a719d1b..d10c4a0e 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsConfigurationExtensionsTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsConfigurationExtensionsTests.cs @@ -15,10 +15,7 @@ using System; using Xunit; -using NSubstitute; -using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; -using AWS.Lambda.Powertools.Logging.Serializers; namespace AWS.Lambda.Powertools.Logging.Tests.Utilities; @@ -31,12 +28,8 @@ public class PowertoolsConfigurationExtensionsTests : IDisposable [InlineData(LoggerOutputCase.SnakeCase, "testString", "test_string")] // Default case public void ConvertToOutputCase_ShouldConvertCorrectly(LoggerOutputCase outputCase, string input, string expected) { - // Arrange - var systemWrapper = Substitute.For(); - var configurations = new PowertoolsConfigurations(systemWrapper); - // Act - var result = configurations.ConvertToOutputCase(input, outputCase); + var result = input.ToCase(outputCase); // Assert Assert.Equal(expected, result); @@ -66,7 +59,7 @@ public void ConvertToOutputCase_ShouldConvertCorrectly(LoggerOutputCase outputCa public void ToSnakeCase_ShouldConvertCorrectly(string input, string expected) { // Act - var result = PrivateMethod.InvokeStatic(typeof(PowertoolsConfigurationsExtension), "ToSnakeCase", input); + var result = input.ToSnake(); // Assert Assert.Equal(expected, result); @@ -97,7 +90,7 @@ public void ToSnakeCase_ShouldConvertCorrectly(string input, string expected) public void ToPascalCase_ShouldConvertCorrectly(string input, string expected) { // Act - var result = PrivateMethod.InvokeStatic(typeof(PowertoolsConfigurationsExtension), "ToPascalCase", input); + var result = input.ToPascal(); // Assert Assert.Equal(expected, result); @@ -135,7 +128,7 @@ public void ToPascalCase_ShouldConvertCorrectly(string input, string expected) public void ToCamelCase_ShouldConvertCorrectly(string input, string expected) { // Act - var result = PrivateMethod.InvokeStatic(typeof(PowertoolsConfigurationsExtension), "ToCamelCase", input); + var result = input.ToCamel(); // Assert Assert.Equal(expected, result); @@ -144,7 +137,6 @@ public void ToCamelCase_ShouldConvertCorrectly(string input, string expected) public void Dispose() { LoggingAspect.ResetForTest(); - PowertoolsLoggingSerializer.ClearOptions(); } } diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs index 399792a5..7818e13e 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs @@ -12,7 +12,7 @@ namespace AWS.Lambda.Powertools.Logging.Tests.Utilities; public class PowertoolsLoggerHelpersTests : IDisposable -{ +{ [Fact] public void ObjectToDictionary_AnonymousObjectWithSimpleProperties_ReturnsDictionary() { @@ -73,9 +73,9 @@ public void ObjectToDictionary_NullObject_Return_New_Dictionary() [Fact] public void Should_Log_With_Anonymous() { - var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); - + var consoleOut = Substitute.For(); + Logger.SetOutput(consoleOut); + // Act & Assert Logger.AppendKey("newKey", new { @@ -84,7 +84,7 @@ public void Should_Log_With_Anonymous() Logger.LogInformation("test"); - consoleOut.Received(1).WriteLine( + consoleOut.Received(1).LogLine( Arg.Is(i => i.Contains("\"new_key\":{\"name\":\"my name\"}")) ); @@ -93,9 +93,9 @@ public void Should_Log_With_Anonymous() [Fact] public void Should_Log_With_Complex_Anonymous() { - var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); - + var consoleOut = Substitute.For(); + Logger.SetOutput(consoleOut); + // Act & Assert Logger.AppendKey("newKey", new { @@ -115,7 +115,7 @@ public void Should_Log_With_Complex_Anonymous() Logger.LogInformation("test"); - consoleOut.Received(1).WriteLine( + consoleOut.Received(1).LogLine( Arg.Is(i => i.Contains( "\"new_key\":{\"id\":1,\"name\":\"my name\",\"adresses\":{\"street\":\"street 1\",\"number\":1,\"city\":{\"name\":\"city 1\",\"state\":\"state 1\"}")) @@ -201,8 +201,7 @@ public void ObjectToDictionary_ObjectWithAllNullProperties_ReturnsEmptyDictionar public void Dispose() { - PowertoolsLoggingSerializer.ConfigureNamingPolicy(LoggerOutputCase.Default); - PowertoolsLoggingSerializer.ClearOptions(); + // Logger.Reset(); } } diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/SystemWrapperMock.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/SystemWrapperMock.cs deleted file mode 100644 index 1ab2b94e..00000000 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/SystemWrapperMock.cs +++ /dev/null @@ -1,68 +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.IO; -using AWS.Lambda.Powertools.Common; - -namespace AWS.Lambda.Powertools.Logging.Tests.Utilities; - -public class SystemWrapperMock : ISystemWrapper -{ - private readonly IPowertoolsEnvironment _powertoolsEnvironment; - public bool LogMethodCalled { get; private set; } - public string LogMethodCalledWithArgument { get; private set; } - - public SystemWrapperMock(IPowertoolsEnvironment powertoolsEnvironment) - { - _powertoolsEnvironment = powertoolsEnvironment; - } - - public string GetEnvironmentVariable(string variable) - { - return _powertoolsEnvironment.GetEnvironmentVariable(variable); - } - - public void Log(string value) - { - LogMethodCalledWithArgument = value; - LogMethodCalled = true; - } - - public void LogLine(string value) - { - LogMethodCalledWithArgument = value; - LogMethodCalled = true; - } - - - public double GetRandom() - { - return 0.7; - } - - public void SetEnvironmentVariable(string variable, string value) - { - throw new System.NotImplementedException(); - } - - public void SetExecutionEnvironment(T type) - { - } - - public void SetOut(TextWriter writeTo) - { - - } -} \ No newline at end of file From 2a8da60e779f97368b51ea0d2f4b79af765d97ed Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 1 Apr 2025 10:44:17 +0100 Subject: [PATCH 23/49] tests green --- .../Attributes/LoggerAspectTests.cs | 32 +++++++ .../Attributes/LoggingAttributeTest.cs | 91 ++++++++++--------- .../Formatter/LogFormatterTest.cs | 26 +++++- .../Utilities/PowertoolsLoggerHelpersTests.cs | 23 ++++- 4 files changed, 127 insertions(+), 45 deletions(-) diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs index 840fa13a..e21a648a 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs @@ -27,6 +27,17 @@ namespace AWS.Lambda.Powertools.Logging.Tests.Attributes; [Collection("Sequential")] public class LoggerAspectTests : IDisposable { + static LoggerAspectTests() + { + ResetAllState(); + } + + public LoggerAspectTests() + { + // Start each test with clean state + ResetAllState(); + } + [Fact] public void OnEntry_ShouldInitializeLogger_WhenCalledWithValidArguments() { @@ -373,7 +384,28 @@ public void OnEntry_Should_LogDebug_WhenSet_EnvironmentVariable() public void Dispose() { + ResetAllState(); + } + + private static void ResetAllState() + { + // Clear environment variables + Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", null); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", null); + Environment.SetEnvironmentVariable("POWERTOOLS_LOG_LEVEL", null); + + // Reset all logging components LoggingAspect.ResetForTest(); Logger.Reset(); + PowertoolsLoggingBuilderExtensions.ResetAllProviders(); + LoggerFactoryHolder.Reset(); + + // Force default configuration + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LoggerOutputCase = LoggerOutputCase.SnakeCase + }; + PowertoolsLoggingBuilderExtensions.UpdateConfiguration(config); } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs index 27141662..4c9b0fd3 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs @@ -45,8 +45,7 @@ public LoggingAttributeTests() public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContext() { // Arrange - var consoleOut = Substitute.For(); - Logger.SetOutput(consoleOut); + var consoleOut = GetConsoleOutput(); // Act _testHandlers.TestMethod(); @@ -70,8 +69,7 @@ public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContext() public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContextAndLogDebug() { // Arrange - var consoleOut = Substitute.For(); - Logger.SetOutput(consoleOut); + var consoleOut = GetConsoleOutput(); // Act _testHandlers.TestMethodDebug(); @@ -98,8 +96,7 @@ public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContextAndLogDebu public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArg() { // Arrange - var consoleOut = Substitute.For(); - Logger.SetOutput(consoleOut); + var consoleOut = GetConsoleOutput(); // Act _testHandlers.LogEventNoArgs(); @@ -113,8 +110,7 @@ public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArg() public void OnEntry_WhenEventArgExist_LogEvent() { // Arrange - var consoleOut = Substitute.For(); - Logger.SetOutput(consoleOut); + var consoleOut = GetConsoleOutput(); var correlationId = Guid.NewGuid().ToString(); var context = new TestLambdaContext() @@ -142,8 +138,7 @@ public void OnEntry_WhenEventArgExist_LogEvent() public void OnEntry_WhenEventArgExist_LogEvent_False_Should_Not_Log() { // Arrange - var consoleOut = Substitute.For(); - Logger.SetOutput(consoleOut); + var consoleOut = GetConsoleOutput(); var context = new TestLambdaContext() { @@ -162,8 +157,7 @@ public void OnEntry_WhenEventArgExist_LogEvent_False_Should_Not_Log() public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArgAndLogDebug() { // Arrange - var consoleOut = Substitute.For(); - Logger.SetOutput(consoleOut); + var consoleOut = GetConsoleOutput(); // Act _testHandlers.LogEventDebug(); @@ -180,10 +174,6 @@ public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArgAndLogDebug() [Fact] public void OnExit_WhenHandler_ClearState_Enabled_ClearKeys() { - // Arrange - var consoleOut = Substitute.For(); - Logger.SetOutput(consoleOut); - // Act _testHandlers.ClearState(); @@ -200,7 +190,6 @@ public void OnEntry_WhenEventArgExists_CapturesCorrelationId(string correlationI // Arrange var correlationId = Guid.NewGuid().ToString(); - // Act switch (correlationIdPath) { @@ -256,7 +245,6 @@ public void When_Capturing_CorrelationId_Converts_To_Case(LoggerOutputCase outpu // Arrange var correlationId = Guid.NewGuid().ToString(); - // Act switch (outputCase) { @@ -306,7 +294,6 @@ public void When_Capturing_CorrelationId_Converts_To_Case_From_Environment_Var(L // Arrange var correlationId = Guid.NewGuid().ToString(); - // Act switch (outputCase) { @@ -353,8 +340,7 @@ public void When_Capturing_CorrelationId_Converts_To_Case_From_Environment_Var(L public void When_Setting_SamplingRate_Should_Add_Key() { // Arrange - var consoleOut = Substitute.For(); - Logger.SetOutput(consoleOut); + var consoleOut = GetConsoleOutput(); // Act _testHandlers.HandlerSamplingRate(); @@ -372,7 +358,7 @@ public void When_Setting_Service_Should_Update_Key() // Arrange var consoleOut = new TestLoggerOutput(); Logger.SetOutput(consoleOut); - + // Act _testHandlers.HandlerService(); @@ -386,9 +372,9 @@ public void When_Setting_Service_Should_Update_Key() public void When_Setting_LogLevel_Should_Update_LogLevel() { // Arrange - var consoleOut = new TestLoggerOutput(); + var consoleOut = new TestLoggerOutput();; Logger.SetOutput(consoleOut); - + // Act _testHandlers.TestLogLevelCritical(); @@ -402,8 +388,8 @@ public void When_Setting_LogLevel_Should_Update_LogLevel() public void When_Setting_LogLevel_HigherThanInformation_Should_Not_LogEvent() { // Arrange - var consoleOut = Substitute.For(); - Logger.SetOutput(consoleOut); + var consoleOut = GetConsoleOutput(); + var context = new TestLambdaContext() { FunctionName = "PowertoolsLoggingSample-HelloWorldFunction-Gg8rhPwO7Wa1" @@ -420,8 +406,7 @@ public void When_Setting_LogLevel_HigherThanInformation_Should_Not_LogEvent() public void When_LogLevel_Debug_Should_Log_Message_When_No_Context_And_LogEvent_True() { // Arrange - var consoleOut = Substitute.For(); - Logger.SetOutput(consoleOut); + var consoleOut = GetConsoleOutput(); // Act _testHandlers.TestLogEventWithoutContext(); @@ -438,8 +423,7 @@ public void When_LogLevel_Debug_Should_Log_Message_When_No_Context_And_LogEvent_ public void Should_Log_When_Not_Using_Decorator() { // Arrange - var consoleOut = Substitute.For(); - Logger.SetOutput(consoleOut); + var consoleOut = GetConsoleOutput(); var test = new TestHandlers(); @@ -456,8 +440,8 @@ public void Should_Log_When_Not_Using_Decorator() public void LoggingAspect_ShouldRespectDynamicLogLevelChanges() { // Arrange - var consoleOut = Substitute.For(); - Logger.SetOutput(consoleOut); + var consoleOut = GetConsoleOutput(); + Logger.UseMinimumLogLevel(LogLevel.Warning); // Start with Warning level // Act @@ -473,8 +457,8 @@ public void LoggingAspect_ShouldRespectDynamicLogLevelChanges() public void LoggingAspect_ShouldCorrectlyResetLogLevelAfterExecution() { // Arrange - var consoleOut = Substitute.For(); - Logger.SetOutput(consoleOut); + var consoleOut = GetConsoleOutput(); + Logger.UseMinimumLogLevel(LogLevel.Warning); // Act - First call with Debug level attribute @@ -495,14 +479,13 @@ public void LoggingAspect_ShouldRespectAttributePrecedenceOverEnvironment() { // Arrange Environment.SetEnvironmentVariable("POWERTOOLS_LOG_LEVEL", "Error"); - var consoleOut = Substitute.For(); - Logger.SetOutput(consoleOut); + var consoleOut = GetConsoleOutput(); // Act _testHandlers.TestMethodDebug(); // Uses LogLevel.Debug attribute // Assert - consoleOut.Received(1).LogLine(Arg.Is(s => + consoleOut.Received().LogLine(Arg.Is(s => s.Contains("\"level\":\"Debug\""))); } @@ -510,8 +493,8 @@ public void LoggingAspect_ShouldRespectAttributePrecedenceOverEnvironment() public void LoggingAspect_ShouldImmediatelyApplyFilterLevelChanges() { // Arrange - var consoleOut = Substitute.For(); - Logger.SetOutput(consoleOut); + var consoleOut = GetConsoleOutput(); + Logger.UseMinimumLogLevel(LogLevel.Error); // Act @@ -528,11 +511,37 @@ public void LoggingAspect_ShouldImmediatelyApplyFilterLevelChanges() public void Dispose() { - Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", ""); - Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", ""); + ResetAllState(); + } + + private ISystemWrapper GetConsoleOutput() + { + // Create a new mock each time + var output = Substitute.For(); + Logger.SetOutput(output); + return output; + } + + private void ResetAllState() + { + // Clear environment variables + Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", null); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", null); + Environment.SetEnvironmentVariable("POWERTOOLS_LOG_LEVEL", null); + + // Reset all logging components LoggingAspect.ResetForTest(); Logger.Reset(); PowertoolsLoggingBuilderExtensions.ResetAllProviders(); + LoggerFactoryHolder.Reset(); + + // Force default configuration + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LoggerOutputCase = LoggerOutputCase.SnakeCase + }; + PowertoolsLoggingBuilderExtensions.UpdateConfiguration(config); } } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs index 89ca07d5..320f79bc 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs @@ -229,6 +229,7 @@ public void Log_WhenCustomFormatter_LogsCustomFormat() [Fact] public void Should_Log_CustomFormatter_When_Decorated() { + ResetAllState(); var consoleOut = Substitute.For(); Logger.SetOutput(consoleOut); @@ -265,6 +266,7 @@ public void Should_Log_CustomFormatter_When_Decorated() [Fact] public void Should_Log_CustomFormatter_When_No_Decorated_Just_Log() { + ResetAllState(); var consoleOut = Substitute.For(); Logger.SetOutput(consoleOut); @@ -326,11 +328,29 @@ public void Should_Log_CustomFormatter_When_Decorated_No_Context() public void Dispose() { - Logger.UseDefaultFormatter(); - Logger.RemoveAllKeys(); - LoggingLambdaContext.Clear(); + ResetAllState(); + } + + private static void ResetAllState() + { + // Clear environment variables + Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", null); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", null); + Environment.SetEnvironmentVariable("POWERTOOLS_LOG_LEVEL", null); + + // Reset all logging components LoggingAspect.ResetForTest(); Logger.Reset(); + PowertoolsLoggingBuilderExtensions.ResetAllProviders(); + LoggerFactoryHolder.Reset(); + + // Force default configuration + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LoggerOutputCase = LoggerOutputCase.SnakeCase + }; + PowertoolsLoggingBuilderExtensions.UpdateConfiguration(config); } } diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs index 7818e13e..ceaffded 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs @@ -6,6 +6,7 @@ using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal.Helpers; using AWS.Lambda.Powertools.Logging.Serializers; +using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -201,7 +202,27 @@ public void ObjectToDictionary_ObjectWithAllNullProperties_ReturnsEmptyDictionar public void Dispose() { - // Logger.Reset(); + ResetAllState(); + } + + private static void ResetAllState() + { + // Clear environment variables + Environment.SetEnvironmentVariable("POWERTOOLS_LOGGER_CASE", null); + Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", null); + Environment.SetEnvironmentVariable("POWERTOOLS_LOG_LEVEL", null); + + // Reset all logging components + Logger.Reset(); + PowertoolsLoggingBuilderExtensions.ResetAllProviders(); + + // Force default configuration + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LoggerOutputCase = LoggerOutputCase.SnakeCase + }; + PowertoolsLoggingBuilderExtensions.UpdateConfiguration(config); } } From bb9166d9663092c477f4139ccd122db60ecca5af Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 1 Apr 2025 12:21:22 +0100 Subject: [PATCH 24/49] refactor: replace SystemWrapper with ConsoleWrapper in tests and update logging methods. revert systemwrapper, revert lambda.core to 2.5.0 --- .../{Core => Tests}/TestLoggerOutput.cs | 47 ++++++++----------- 1 file changed, 19 insertions(+), 28 deletions(-) rename libraries/src/AWS.Lambda.Powertools.Common/{Core => Tests}/TestLoggerOutput.cs (50%) diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/TestLoggerOutput.cs b/libraries/src/AWS.Lambda.Powertools.Common/Tests/TestLoggerOutput.cs similarity index 50% rename from libraries/src/AWS.Lambda.Powertools.Common/Core/TestLoggerOutput.cs rename to libraries/src/AWS.Lambda.Powertools.Common/Tests/TestLoggerOutput.cs index 96974d69..19c55683 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/TestLoggerOutput.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Tests/TestLoggerOutput.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using System.Text; namespace AWS.Lambda.Powertools.Common.Tests; @@ -7,52 +5,45 @@ namespace AWS.Lambda.Powertools.Common.Tests; /// /// Test logger output /// -public class TestLoggerOutput : ISystemWrapper +public class TestLoggerOutput : IConsoleWrapper { /// /// Buffer for all the log messages written to the logger. /// private readonly StringBuilder _outputBuffer = new StringBuilder(); - + /// - /// Logs the specified value. + /// Cleasr the output buffer. /// - /// - public void Log(string value) + public void Clear() { - _outputBuffer.Append(value); + _outputBuffer.Clear(); } /// - /// Logs the line. + /// Output the contents of the buffer. /// - public void LogLine(string value) + /// + public override string ToString() { - _outputBuffer.AppendLine(value); + return _outputBuffer.ToString(); } - /// - /// Gets random number - /// - public double GetRandom() + /// + public void WriteLine(string message) { - return 0.7; + _outputBuffer.AppendLine(message); } - /// - /// Sets console output - /// - public void SetOut(TextWriter writeTo) + /// + public void Debug(string message) { + _outputBuffer.AppendLine(message); } - - public void Clear() - { - _outputBuffer.Clear(); - } - - public override string ToString() + + /// + public void Error(string message) { - return _outputBuffer.ToString(); + _outputBuffer.AppendLine(message); } } \ No newline at end of file From 6b8f843b8b19b3e3beda7f2f10a9acae7bfc5379 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 1 Apr 2025 12:22:07 +0100 Subject: [PATCH 25/49] refactor: replace SystemWrapper with ConsoleWrapper in tests and update logging methods. revert systemwrapper, revert lambda.core to 2.5.0 --- .../Core/ConsoleWrapper.cs | 29 ++- .../Core/IConsoleWrapper.cs | 6 - .../Core/ISystemWrapper.cs | 20 ++ .../Core/SystemWrapper.cs | 114 ++++++++-- .../Internal/LoggingAspectFactory.cs | 3 +- .../Internal/PowertoolsLogger.cs | 4 +- .../Internal/PowertoolsLoggerProvider.cs | 6 +- .../AWS.Lambda.Powertools.Logging/Logger.cs | 16 +- .../PowertoolsLoggerConfiguration.cs | 4 +- libraries/src/Directory.Packages.props | 2 +- .../Internal/BatchProcessingInternalTests.cs | 54 ++--- .../ConsoleWrapperTests.cs | 15 -- .../Core/PowertoolsConfigurationsTest.cs | 200 +++++++++--------- .../Core/PowertoolsEnvironmentTest.cs | 5 + .../Internal/IdempotentAspectTests.cs | 17 +- .../Attributes/LoggerAspectTests.cs | 30 +-- .../Attributes/LoggingAttributeTest.cs | 40 ++-- .../Attributes/ServiceTests.cs | 6 +- .../Formatter/LogFormatterTest.cs | 36 ++-- .../PowertoolsLoggerTest.cs | 122 +++++------ .../Utilities/PowertoolsLoggerHelpersTests.cs | 8 +- .../ClearDimensionsTests.cs | 2 +- .../EMFValidationTests.cs | 2 +- .../Handlers/FunctionHandlerTests.cs | 2 +- .../MetricsTests.cs | 16 +- .../XRayRecorderTests.cs | 18 +- libraries/tests/Directory.Packages.props | 4 +- .../src/AOT-Function/AOT-Function.csproj | 2 +- .../src/AOT-Function/AOT-Function.csproj | 2 +- 29 files changed, 417 insertions(+), 368 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs index 87321140..75e43a67 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs @@ -14,18 +14,43 @@ */ using System; +using System.IO; namespace AWS.Lambda.Powertools.Common; /// public class ConsoleWrapper : IConsoleWrapper { + private static bool _redirected; + + /// + /// Initializes a new instance of the class. + /// + public ConsoleWrapper() + { + if(_redirected) + { + _redirected = false; + return; + } + + var standardOutput = new StreamWriter(Console.OpenStandardOutput()); + standardOutput.AutoFlush = true; + Console.SetOut(standardOutput); + var errordOutput = new StreamWriter(Console.OpenStandardError()); + errordOutput.AutoFlush = true; + Console.SetError(errordOutput); + } /// public void WriteLine(string message) => Console.WriteLine(message); /// public void Debug(string message) => System.Diagnostics.Debug.WriteLine(message); /// public void Error(string message) => Console.Error.WriteLine(message); - /// - public string ReadLine() => Console.ReadLine(); + + internal static void SetOut(StringWriter consoleOut) + { + _redirected = true; + Console.SetOut(consoleOut); + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/IConsoleWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/IConsoleWrapper.cs index de75020e..a311507f 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/IConsoleWrapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/IConsoleWrapper.cs @@ -37,10 +37,4 @@ public interface IConsoleWrapper /// /// The error message to write. void Error(string message); - - /// - /// Reads the next line of characters from the standard input stream. - /// - /// The next line of characters from the input stream, or null if no more lines are available. - string ReadLine(); } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/ISystemWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/ISystemWrapper.cs index 745118d7..8a035984 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/ISystemWrapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/ISystemWrapper.cs @@ -22,6 +22,13 @@ namespace AWS.Lambda.Powertools.Common; /// public interface ISystemWrapper { + /// + /// Gets the environment variable. + /// + /// The variable. + /// System.String. + string GetEnvironmentVariable(string variable); + /// /// Logs the specified value. /// @@ -40,6 +47,19 @@ public interface ISystemWrapper /// System.Double. double GetRandom(); + /// + /// Sets the environment variable. + /// + /// The variable. + /// + void SetEnvironmentVariable(string variable, string value); + + /// + /// Sets the execution Environment Variable (AWS_EXECUTION_ENV) + /// + /// + void SetExecutionEnvironment(T type); + /// /// Sets console output /// Useful for testing and checking the console output diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs index cc7da82d..2c5fc9c1 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs @@ -15,6 +15,7 @@ using System; using System.IO; +using System.Text; namespace AWS.Lambda.Powertools.Common; @@ -23,43 +24,132 @@ namespace AWS.Lambda.Powertools.Common; /// Implements the /// /// -internal class SystemWrapper : ISystemWrapper +public class SystemWrapper : ISystemWrapper { + private static IPowertoolsEnvironment _powertoolsEnvironment; + + /// + /// The instance + /// + private static ISystemWrapper _instance; + /// /// Prevents a default instance of the class from being created. /// - public SystemWrapper() + public SystemWrapper(IPowertoolsEnvironment powertoolsEnvironment) { - // Clear AWS SDK Console injected parameters StdOut and StdErr - var standardOutput = new StreamWriter(Console.OpenStandardOutput()); - standardOutput.AutoFlush = true; - Console.SetOut(standardOutput); - var errordOutput = new StreamWriter(Console.OpenStandardError()); - errordOutput.AutoFlush = true; - Console.SetError(errordOutput); + _powertoolsEnvironment = powertoolsEnvironment; + _instance ??= this; + + // // Clear AWS SDK Console injected parameters StdOut and StdErr + // var standardOutput = new StreamWriter(Console.OpenStandardOutput()); + // standardOutput.AutoFlush = true; + // Console.SetOut(standardOutput); + // var errordOutput = new StreamWriter(Console.OpenStandardError()); + // errordOutput.AutoFlush = true; + // Console.SetError(errordOutput); } - /// + /// + /// Gets the instance. + /// + /// The instance. + public static ISystemWrapper Instance => _instance ??= new SystemWrapper(PowertoolsEnvironment.Instance); + + /// + /// Gets the environment variable. + /// + /// The variable. + /// System.String. + public string GetEnvironmentVariable(string variable) + { + return _powertoolsEnvironment.GetEnvironmentVariable(variable); + } + + /// + /// Logs the specified value. + /// + /// The value. public void Log(string value) { Console.Write(value); } - /// + /// + /// Logs the line. + /// + /// The value. public void LogLine(string value) { Console.WriteLine(value); } - /// + /// + /// Gets random number + /// + /// System.Double. public double GetRandom() { return new Random().NextDouble(); } + /// + public void SetEnvironmentVariable(string variable, string value) + { + _powertoolsEnvironment.SetEnvironmentVariable(variable, value); + } + + /// + public void SetExecutionEnvironment(T type) + { + const string envName = Constants.AwsExecutionEnvironmentVariableName; + var envValue = new StringBuilder(); + var currentEnvValue = GetEnvironmentVariable(envName); + var assemblyName = ParseAssemblyName(_powertoolsEnvironment.GetAssemblyName(type)); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if (!string.IsNullOrEmpty(currentEnvValue)) + { + // Avoid duplication - should not happen since the calling Instances are Singletons - defensive purposes + if (currentEnvValue.Contains(assemblyName)) + { + return; + } + + envValue.Append($"{currentEnvValue} "); + } + + var assemblyVersion = _powertoolsEnvironment.GetAssemblyVersion(type); + + envValue.Append($"{assemblyName}/{assemblyVersion}"); + + SetEnvironmentVariable(envName, envValue.ToString()); + } + /// public void SetOut(TextWriter writeTo) { Console.SetOut(writeTo); } + + /// + /// Parsing the name to conform with the required naming convention for the UserAgent header (PTFeature/Name/Version) + /// Fallback to Assembly Name on exception + /// + /// + /// + private string ParseAssemblyName(string assemblyName) + { + try + { + var parsedName = assemblyName.Substring(assemblyName.LastIndexOf(".", StringComparison.Ordinal) + 1); + return $"{Constants.FeatureContextIdentifier}/{parsedName}"; + } + catch + { + //NOOP + } + + return $"{Constants.FeatureContextIdentifier}/{assemblyName}"; + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs index a3fce9d3..c0a49da8 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspectFactory.cs @@ -19,7 +19,7 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// -/// Class LoggingAspectFactory. For "dependency inject" Configuration and SystemWrapper to Aspect +/// Class LoggingAspectFactory. For "dependency inject" Aspect /// internal static class LoggingAspectFactory { @@ -30,7 +30,6 @@ internal static class LoggingAspectFactory /// An instance of the LoggingAspect class. public static object GetInstance(Type type) { - // Use Logger.GetPowertoolsLogger() to ensure it's consistent with current config return new LoggingAspect(LoggerFactoryHolder.GetOrCreateFactory().CreatePowertoolsLogger()); } } \ 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 9f278a12..d4bae34a 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -120,12 +120,12 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except return; } - _currentConfig().LogOutput.LogLine(LogEntryString(logLevel, state, exception, formatter)); + _currentConfig().LogOutput.WriteLine(LogEntryString(logLevel, state, exception, formatter)); } internal void LogLine(string message) { - _currentConfig().LogOutput.LogLine(message); + _currentConfig().LogOutput.WriteLine(message); } internal string LogEntryString(LogLevel logLevel, TState state, Exception exception, Func formatter) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs index bea51da3..c0fd479b 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs @@ -58,7 +58,7 @@ public void ConfigureFromEnvironment() // Warn if Lambda log level doesn't match if (lambdaLogLevelEnabled && logLevel < lambdaLogLevel) { - _currentConfig.LogOutput.LogLine( + _currentConfig.LogOutput.WriteLine( $"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."); } @@ -113,7 +113,7 @@ private void ProcessSamplingRate(PowertoolsLoggerConfiguration config, IPowertoo // Instead of changing log level, just indicate sampling status if (sample <= samplingRate) { - config.LogOutput.LogLine( + config.LogOutput.WriteLine( $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate}, Sampler Value: {sample}."); config.MinimumLogLevel = LogLevel.Debug; } @@ -129,7 +129,7 @@ private double ValidateSamplingRate(double samplingRate, PowertoolsLoggerConfigu { if (config.MinimumLogLevel is LogLevel.Debug or LogLevel.Trace) { - config.LogOutput.LogLine( + config.LogOutput.WriteLine( $"Skipping sampling rate configuration because of invalid value. Sampling rate: {samplingRate}"); } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs index 63619b2f..0bbcd174 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs @@ -17,7 +17,6 @@ using System.Text.Json; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; -using AWS.Lambda.Powertools.Logging.Internal.Helpers; using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging; @@ -28,7 +27,7 @@ namespace AWS.Lambda.Powertools.Logging; public static partial class Logger { private static ILogger _loggerInstance; - private static readonly object _lock = new object(); + private static readonly object Lock = new object(); // Change this to a property with getter that recreates if needed private static ILogger LoggerInstance @@ -38,7 +37,7 @@ private static ILogger LoggerInstance // If we have no instance or configuration has changed, get a new logger if (_loggerInstance == null) { - lock (_lock) + lock (Lock) { if (_loggerInstance == null) { @@ -66,7 +65,7 @@ internal static void Configure(ILoggerFactory loggerFactory) /// internal static void Configure(Action configure) { - lock (_lock) + lock (Lock) { var config = GetCurrentConfiguration(); configure(config); @@ -74,7 +73,7 @@ internal static void Configure(Action configure) } } - public static PowertoolsLoggerConfiguration GetCurrentConfiguration() + internal static PowertoolsLoggerConfiguration GetCurrentConfiguration() { return PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); } @@ -185,7 +184,12 @@ internal static void Reset() RemoveAllKeys(); } - public static void SetOutput(ISystemWrapper consoleOut) + /// + /// Set the output for the logger + /// + /// + /// + public static void SetOutput(IConsoleWrapper consoleOut) { if (consoleOut == null) throw new ArgumentNullException(nameof(consoleOut)); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs index 67f7f3a2..5e70db70 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs @@ -109,10 +109,10 @@ public JsonSerializerOptions? JsonOptions internal PowertoolsLoggingSerializer Serializer => _serializer ??= InitializeSerializer(); /// - /// The system wrapper used for output operations. Defaults to SystemWrapper instance. + /// The console wrapper used for output operations. Defaults to ConsoleWrapper instance. /// Primarily useful for testing to capture and verify output. /// - public ISystemWrapper LogOutput { get; set; } = new SystemWrapper(); + public IConsoleWrapper LogOutput { get; set; } = new ConsoleWrapper(); /// /// Initialize serializer with the current configuration diff --git a/libraries/src/Directory.Packages.props b/libraries/src/Directory.Packages.props index a28320f2..db4a6a7f 100644 --- a/libraries/src/Directory.Packages.props +++ b/libraries/src/Directory.Packages.props @@ -6,7 +6,7 @@ - + diff --git a/libraries/tests/AWS.Lambda.Powertools.BatchProcessing.Tests/Internal/BatchProcessingInternalTests.cs b/libraries/tests/AWS.Lambda.Powertools.BatchProcessing.Tests/Internal/BatchProcessingInternalTests.cs index 299956ec..1356df69 100644 --- a/libraries/tests/AWS.Lambda.Powertools.BatchProcessing.Tests/Internal/BatchProcessingInternalTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.BatchProcessing.Tests/Internal/BatchProcessingInternalTests.cs @@ -28,25 +28,15 @@ public class BatchProcessingInternalTests public void BatchProcessing_Set_Execution_Environment_Context_SQS() { // Arrange - var assemblyName = "AWS.Lambda.Powertools.BatchProcessing"; - var assemblyVersion = "1.0.0"; - - var env = Substitute.For(); - env.GetAssemblyName(Arg.Any()).Returns(assemblyName); - env.GetAssemblyVersion(Arg.Any()).ReturnsForAnyArgs(assemblyVersion); - - var conf = new PowertoolsConfigurations(new SystemWrapper(env)); + var env = new PowertoolsEnvironment(); + var conf = new PowertoolsConfigurations(env); // Act var sqsBatchProcessor = new SqsBatchProcessor(conf); // Assert - env.Received(1).SetEnvironmentVariable( - "AWS_EXECUTION_ENV", - $"{Constants.FeatureContextIdentifier}/BatchProcessing/{assemblyVersion}" - ); - - env.Received(1).GetEnvironmentVariable("AWS_EXECUTION_ENV"); + Assert.Equal($"{Constants.FeatureContextIdentifier}/BatchProcessing/0.0.1", + env.GetEnvironmentVariable("AWS_EXECUTION_ENV")); Assert.NotNull(sqsBatchProcessor); } @@ -55,25 +45,15 @@ public void BatchProcessing_Set_Execution_Environment_Context_SQS() public void BatchProcessing_Set_Execution_Environment_Context_Kinesis() { // Arrange - var assemblyName = "AWS.Lambda.Powertools.BatchProcessing"; - var assemblyVersion = "1.0.0"; - - var env = Substitute.For(); - env.GetAssemblyName(Arg.Any()).Returns(assemblyName); - env.GetAssemblyVersion(Arg.Any()).ReturnsForAnyArgs(assemblyVersion); - - var conf = new PowertoolsConfigurations(new SystemWrapper(env)); + var env = new PowertoolsEnvironment(); + var conf = new PowertoolsConfigurations(env); // Act var KinesisEventBatchProcessor = new KinesisEventBatchProcessor(conf); // Assert - env.Received(1).SetEnvironmentVariable( - "AWS_EXECUTION_ENV", - $"{Constants.FeatureContextIdentifier}/BatchProcessing/{assemblyVersion}" - ); - - env.Received(1).GetEnvironmentVariable("AWS_EXECUTION_ENV"); + Assert.Equal($"{Constants.FeatureContextIdentifier}/BatchProcessing/0.0.1", + env.GetEnvironmentVariable("AWS_EXECUTION_ENV")); Assert.NotNull(KinesisEventBatchProcessor); } @@ -82,25 +62,15 @@ public void BatchProcessing_Set_Execution_Environment_Context_Kinesis() public void BatchProcessing_Set_Execution_Environment_Context_DynamoDB() { // Arrange - var assemblyName = "AWS.Lambda.Powertools.BatchProcessing"; - var assemblyVersion = "1.0.0"; - - var env = Substitute.For(); - env.GetAssemblyName(Arg.Any()).Returns(assemblyName); - env.GetAssemblyVersion(Arg.Any()).ReturnsForAnyArgs(assemblyVersion); - - var conf = new PowertoolsConfigurations(new SystemWrapper(env)); + var env = new PowertoolsEnvironment(); + var conf = new PowertoolsConfigurations(env); // Act var dynamoDbStreamBatchProcessor = new DynamoDbStreamBatchProcessor(conf); // Assert - env.Received(1).SetEnvironmentVariable( - "AWS_EXECUTION_ENV", - $"{Constants.FeatureContextIdentifier}/BatchProcessing/{assemblyVersion}" - ); - - env.Received(1).GetEnvironmentVariable("AWS_EXECUTION_ENV"); + Assert.Equal($"{Constants.FeatureContextIdentifier}/BatchProcessing/0.0.1", + env.GetEnvironmentVariable("AWS_EXECUTION_ENV")); Assert.NotNull(dynamoDbStreamBatchProcessor); } diff --git a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs index 4da57dc0..c70c0aa0 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs @@ -36,19 +36,4 @@ public void Error_Should_Write_To_Error_Console() // Assert Assert.Equal($"error message{Environment.NewLine}", writer.ToString()); } - - [Fact] - public void ReadLine_Should_Read_From_Console() - { - // Arrange - var consoleWrapper = new ConsoleWrapper(); - var reader = new StringReader("input text"); - Console.SetIn(reader); - - // Act - var result = consoleWrapper.ReadLine(); - - // Assert - Assert.Equal("input text", result); - } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsConfigurationsTest.cs b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsConfigurationsTest.cs index e154acd9..934a162c 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsConfigurationsTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsConfigurationsTest.cs @@ -29,17 +29,17 @@ public void GetEnvironmentVariableOrDefault_WhenEnvironmentVariableIsNull_Return // Arrange var key = Guid.NewGuid().ToString(); var defaultValue = Guid.NewGuid().ToString(); - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(key).Returns(string.Empty); + environment.GetEnvironmentVariable(key).Returns(string.Empty); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.GetEnvironmentVariableOrDefault(key, defaultValue); // Assert - systemWrapper.Received(1).GetEnvironmentVariable(key); + environment.Received(1).GetEnvironmentVariable(key); Assert.Equal(result, defaultValue); } @@ -49,17 +49,17 @@ public void GetEnvironmentVariableOrDefault_WhenEnvironmentVariableIsNull_Return { // Arrange var key = Guid.NewGuid().ToString(); - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(key).Returns(string.Empty); + environment.GetEnvironmentVariable(key).Returns(string.Empty); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.GetEnvironmentVariableOrDefault(key, false); // Assert - systemWrapper.Received(1).GetEnvironmentVariable(key); + environment.Received(1).GetEnvironmentVariable(key); Assert.False(result); } @@ -69,17 +69,17 @@ public void GetEnvironmentVariableOrDefault_WhenEnvironmentVariableIsNull_Return { // Arrange var key = Guid.NewGuid().ToString(); - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(key).Returns(string.Empty); + environment.GetEnvironmentVariable(key).Returns(string.Empty); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.GetEnvironmentVariableOrDefault(key, true); // Assert - systemWrapper.Received(1).GetEnvironmentVariable(Arg.Is(i => i == key)); + environment.Received(1).GetEnvironmentVariable(Arg.Is(i => i == key)); Assert.True(result); } @@ -91,17 +91,17 @@ public void GetEnvironmentVariableOrDefault_WhenEnvironmentVariableHasValue_Retu var key = Guid.NewGuid().ToString(); var defaultValue = Guid.NewGuid().ToString(); var value = Guid.NewGuid().ToString(); - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(key).Returns(value); + environment.GetEnvironmentVariable(key).Returns(value); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.GetEnvironmentVariableOrDefault(key, defaultValue); // Assert - systemWrapper.Received(1).GetEnvironmentVariable(Arg.Is(i => i == key)); + environment.Received(1).GetEnvironmentVariable(Arg.Is(i => i == key)); Assert.Equal(result, value); } @@ -111,17 +111,17 @@ public void GetEnvironmentVariableOrDefault_WhenEnvironmentVariableHasValue_Retu { // Arrange var key = Guid.NewGuid().ToString(); - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(key).Returns("true"); + environment.GetEnvironmentVariable(key).Returns("true"); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.GetEnvironmentVariableOrDefault(key, false); // Assert - systemWrapper.Received(1).GetEnvironmentVariable(Arg.Is(i => i == key)); + environment.Received(1).GetEnvironmentVariable(Arg.Is(i => i == key)); Assert.True(result); } @@ -131,17 +131,17 @@ public void GetEnvironmentVariableOrDefault_WhenEnvironmentVariableHasValue_Retu { // Arrange var key = Guid.NewGuid().ToString(); - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(key).Returns("false"); + environment.GetEnvironmentVariable(key).Returns("false"); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.GetEnvironmentVariableOrDefault(key, true); // Assert - systemWrapper.Received(1).GetEnvironmentVariable(Arg.Is(i => i == key)); + environment.Received(1).GetEnvironmentVariable(Arg.Is(i => i == key)); Assert.False(result); } @@ -155,17 +155,17 @@ public void Service_WhenEnvironmentIsNull_ReturnsDefaultValue() { // Arrange var defaultService = "service_undefined"; - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.ServiceNameEnv).Returns(string.Empty); + environment.GetEnvironmentVariable(Constants.ServiceNameEnv).Returns(string.Empty); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.Service; // Assert - systemWrapper.Received(1).GetEnvironmentVariable(Arg.Is(i => i == Constants.ServiceNameEnv)); + environment.Received(1).GetEnvironmentVariable(Arg.Is(i => i == Constants.ServiceNameEnv)); Assert.Equal(result, defaultService); } @@ -175,17 +175,17 @@ public void Service_WhenEnvironmentHasValue_ReturnsValue() { // Arrange var service = Guid.NewGuid().ToString(); - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.ServiceNameEnv).Returns(service); + environment.GetEnvironmentVariable(Constants.ServiceNameEnv).Returns(service); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.Service; // Assert - systemWrapper.Received(1).GetEnvironmentVariable(Arg.Is(i => i == Constants.ServiceNameEnv)); + environment.Received(1).GetEnvironmentVariable(Arg.Is(i => i == Constants.ServiceNameEnv)); Assert.Equal(result, service); } @@ -199,17 +199,17 @@ public void IsServiceDefined_WhenEnvironmentHasValue_ReturnsTrue() { // Arrange var service = Guid.NewGuid().ToString(); - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.ServiceNameEnv).Returns(service); + environment.GetEnvironmentVariable(Constants.ServiceNameEnv).Returns(service); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.IsServiceDefined; // Assert - systemWrapper.Received(1).GetEnvironmentVariable(Arg.Is(i => i == Constants.ServiceNameEnv)); + environment.Received(1).GetEnvironmentVariable(Arg.Is(i => i == Constants.ServiceNameEnv)); Assert.True(result); } @@ -218,17 +218,17 @@ public void IsServiceDefined_WhenEnvironmentHasValue_ReturnsTrue() public void IsServiceDefined_WhenEnvironmentDoesNotHaveValue_ReturnsFalse() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.ServiceNameEnv).Returns(string.Empty); + environment.GetEnvironmentVariable(Constants.ServiceNameEnv).Returns(string.Empty); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.IsServiceDefined; // Assert - systemWrapper.Received(1).GetEnvironmentVariable(Arg.Is(i => i == Constants.ServiceNameEnv)); + environment.Received(1).GetEnvironmentVariable(Arg.Is(i => i == Constants.ServiceNameEnv)); Assert.False(result); } @@ -241,17 +241,17 @@ public void IsServiceDefined_WhenEnvironmentDoesNotHaveValue_ReturnsFalse() public void TracerCaptureResponse_WhenEnvironmentIsNull_ReturnsDefaultValue() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.TracerCaptureResponseEnv).Returns(string.Empty); + environment.GetEnvironmentVariable(Constants.TracerCaptureResponseEnv).Returns(string.Empty); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.TracerCaptureResponse; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.TracerCaptureResponseEnv)); Assert.True(result); @@ -261,17 +261,17 @@ public void TracerCaptureResponse_WhenEnvironmentIsNull_ReturnsDefaultValue() public void TracerCaptureResponse_WhenEnvironmentHasValue_ReturnsValueFalse() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.TracerCaptureResponseEnv).Returns("false"); + environment.GetEnvironmentVariable(Constants.TracerCaptureResponseEnv).Returns("false"); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.TracerCaptureResponse; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.TracerCaptureResponseEnv)); Assert.False(result); @@ -281,17 +281,17 @@ public void TracerCaptureResponse_WhenEnvironmentHasValue_ReturnsValueFalse() public void TracerCaptureResponse_WhenEnvironmentHasValue_ReturnsValueTrue() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.TracerCaptureResponseEnv).Returns("true"); + environment.GetEnvironmentVariable(Constants.TracerCaptureResponseEnv).Returns("true"); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.TracerCaptureResponse; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.TracerCaptureResponseEnv)); Assert.True(result); @@ -305,17 +305,17 @@ public void TracerCaptureResponse_WhenEnvironmentHasValue_ReturnsValueTrue() public void TracerCaptureError_WhenEnvironmentIsNull_ReturnsDefaultValue() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.TracerCaptureErrorEnv).Returns(string.Empty); + environment.GetEnvironmentVariable(Constants.TracerCaptureErrorEnv).Returns(string.Empty); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.TracerCaptureError; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.TracerCaptureErrorEnv)); Assert.True(result); @@ -325,17 +325,17 @@ public void TracerCaptureError_WhenEnvironmentIsNull_ReturnsDefaultValue() public void TracerCaptureError_WhenEnvironmentHasValue_ReturnsValueFalse() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.TracerCaptureErrorEnv).Returns("false"); + environment.GetEnvironmentVariable(Constants.TracerCaptureErrorEnv).Returns("false"); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.TracerCaptureError; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.TracerCaptureErrorEnv)); Assert.False(result); @@ -345,17 +345,17 @@ public void TracerCaptureError_WhenEnvironmentHasValue_ReturnsValueFalse() public void TracerCaptureError_WhenEnvironmentHasValue_ReturnsValueTrue() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.TracerCaptureErrorEnv).Returns("true"); + environment.GetEnvironmentVariable(Constants.TracerCaptureErrorEnv).Returns("true"); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.TracerCaptureError; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.TracerCaptureErrorEnv)); Assert.True(result); @@ -369,17 +369,17 @@ public void TracerCaptureError_WhenEnvironmentHasValue_ReturnsValueTrue() public void IsSamLocal_WhenEnvironmentIsNull_ReturnsDefaultValue() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.SamLocalEnv).Returns(string.Empty); + environment.GetEnvironmentVariable(Constants.SamLocalEnv).Returns(string.Empty); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.IsSamLocal; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.SamLocalEnv)); Assert.False(result); @@ -389,17 +389,17 @@ public void IsSamLocal_WhenEnvironmentIsNull_ReturnsDefaultValue() public void IsSamLocal_WhenEnvironmentHasValue_ReturnsValueFalse() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.SamLocalEnv).Returns("false"); + environment.GetEnvironmentVariable(Constants.SamLocalEnv).Returns("false"); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.IsSamLocal; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.SamLocalEnv)); Assert.False(result); @@ -409,17 +409,17 @@ public void IsSamLocal_WhenEnvironmentHasValue_ReturnsValueFalse() public void IsSamLocal_WhenEnvironmentHasValue_ReturnsValueTrue() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.SamLocalEnv).Returns("true"); + environment.GetEnvironmentVariable(Constants.SamLocalEnv).Returns("true"); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.IsSamLocal; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.SamLocalEnv)); Assert.True(result); @@ -433,17 +433,17 @@ public void IsSamLocal_WhenEnvironmentHasValue_ReturnsValueTrue() public void TracingDisabled_WhenEnvironmentIsNull_ReturnsDefaultValue() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.TracingDisabledEnv).Returns(string.Empty); + environment.GetEnvironmentVariable(Constants.TracingDisabledEnv).Returns(string.Empty); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.TracingDisabled; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.TracingDisabledEnv)); Assert.False(result); @@ -453,17 +453,17 @@ public void TracingDisabled_WhenEnvironmentIsNull_ReturnsDefaultValue() public void TracingDisabled_WhenEnvironmentHasValue_ReturnsValueFalse() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.TracingDisabledEnv).Returns("false"); + environment.GetEnvironmentVariable(Constants.TracingDisabledEnv).Returns("false"); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.TracingDisabled; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.TracingDisabledEnv)); Assert.False(result); @@ -473,17 +473,17 @@ public void TracingDisabled_WhenEnvironmentHasValue_ReturnsValueFalse() public void TracingDisabled_WhenEnvironmentHasValue_ReturnsValueTrue() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.TracingDisabledEnv).Returns("true"); + environment.GetEnvironmentVariable(Constants.TracingDisabledEnv).Returns("true"); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.TracingDisabled; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.TracingDisabledEnv)); Assert.True(result); @@ -497,17 +497,17 @@ public void TracingDisabled_WhenEnvironmentHasValue_ReturnsValueTrue() public void IsLambdaEnvironment_WhenEnvironmentIsNull_ReturnsFalse() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.LambdaTaskRoot).Returns((string)null); + environment.GetEnvironmentVariable(Constants.LambdaTaskRoot).Returns((string)null); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.IsLambdaEnvironment; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.LambdaTaskRoot)); Assert.False(result); @@ -517,17 +517,17 @@ public void IsLambdaEnvironment_WhenEnvironmentIsNull_ReturnsFalse() public void IsLambdaEnvironment_WhenEnvironmentHasValue_ReturnsTrue() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - systemWrapper.GetEnvironmentVariable(Constants.TracingDisabledEnv).Returns(Guid.NewGuid().ToString()); + environment.GetEnvironmentVariable(Constants.TracingDisabledEnv).Returns(Guid.NewGuid().ToString()); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act var result = configurations.IsLambdaEnvironment; // Assert - systemWrapper.Received(1) + environment.Received(1) .GetEnvironmentVariable(Arg.Is(i => i == Constants.LambdaTaskRoot)); Assert.True(result); @@ -537,20 +537,20 @@ public void IsLambdaEnvironment_WhenEnvironmentHasValue_ReturnsTrue() public void Set_Lambda_Execution_Context() { // Arrange - var systemWrapper = Substitute.For(); + var environment = Substitute.For(); - // systemWrapper.Setup(c => + // environment.Setup(c => // c.SetExecutionEnvironment(GetType()) // ); - var configurations = new PowertoolsConfigurations(systemWrapper); + var configurations = new PowertoolsConfigurations(environment); // Act configurations.SetExecutionEnvironment(typeof(PowertoolsConfigurations)); // Assert // method with correct type was called - systemWrapper.Received(1) + environment.Received(1) .SetExecutionEnvironment(Arg.Is(i => i == typeof(PowertoolsConfigurations))); } diff --git a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsEnvironmentTest.cs b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsEnvironmentTest.cs index df41e253..936432ac 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsEnvironmentTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/PowertoolsEnvironmentTest.cs @@ -118,4 +118,9 @@ public string GetAssemblyVersion(T type) { return "1.0.0"; } + + public void SetExecutionEnvironment(T type) + { + throw new NotImplementedException(); + } } diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs index f83cfe34..218b6c2d 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs @@ -264,25 +264,16 @@ public async Task Handle_WhenIdempotencyDisabled_ShouldJustRunTheFunction(Type t public void Idempotency_Set_Execution_Environment_Context() { // Arrange - var assemblyName = "AWS.Lambda.Powertools.Idempotency"; - var assemblyVersion = "1.0.0"; - var env = Substitute.For(); - env.GetAssemblyName(Arg.Any()).Returns(assemblyName); - env.GetAssemblyVersion(Arg.Any()).Returns(assemblyVersion); - - var conf = new PowertoolsConfigurations(new SystemWrapper(env)); + var env = new PowertoolsEnvironment(); + var conf = new PowertoolsConfigurations(env); // Act var xRayRecorder = new Idempotency(conf); // Assert - env.Received(1).SetEnvironmentVariable( - "AWS_EXECUTION_ENV", - $"{Constants.FeatureContextIdentifier}/Idempotency/{assemblyVersion}" - ); - - env.Received(1).GetEnvironmentVariable("AWS_EXECUTION_ENV"); + Assert.Equal($"{Constants.FeatureContextIdentifier}/Idempotency/0.0.1", + env.GetEnvironmentVariable("AWS_EXECUTION_ENV")); Assert.NotNull(xRayRecorder); } diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs index e21a648a..8c498288 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs @@ -42,7 +42,7 @@ public LoggerAspectTests() public void OnEntry_ShouldInitializeLogger_WhenCalledWithValidArguments() { // Arrange - var consoleOut = Substitute.For(); + var consoleOut = Substitute.For(); var config = new PowertoolsLoggerConfiguration { @@ -78,7 +78,7 @@ public void OnEntry_ShouldInitializeLogger_WhenCalledWithValidArguments() loggingAspect.OnEntry(instance, name, args, hostType, method, returnType, triggers); // Assert - consoleOut.Received().LogLine(Arg.Is(s => + consoleOut.Received().WriteLine(Arg.Is(s => s.Contains( "\"Level\":\"Information\",\"Service\":\"TestService\",\"Name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":null},\"SamplingRate\":0.5}") && s.Contains("\"CorrelationId\":\"20\"") @@ -90,7 +90,7 @@ public void OnEntry_ShouldLog_Event_When_EnvironmentVariable_Set() { // Arrange Environment.SetEnvironmentVariable(Constants.LoggerLogEventNameEnv, "true"); - var consoleOut = Substitute.For(); + var consoleOut = Substitute.For(); var config = new PowertoolsLoggerConfiguration { @@ -132,7 +132,7 @@ public void OnEntry_ShouldLog_Event_When_EnvironmentVariable_Set() Assert.Equal(0, updatedConfig.SamplingRate); Assert.True(updatedConfig.LogEvent); - consoleOut.Received().LogLine(Arg.Is(s => + consoleOut.Received().WriteLine(Arg.Is(s => s.Contains( "\"Level\":\"Information\",\"Service\":\"TestService\",\"Name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":null}}") && s.Contains("\"CorrelationId\":\"20\"") @@ -144,7 +144,7 @@ public void OnEntry_Should_NOT_Log_Event_When_EnvironmentVariable_Set_But_Attrib { // Arrange Environment.SetEnvironmentVariable(Constants.LoggerLogEventNameEnv, "true"); - var consoleOut = Substitute.For(); + var consoleOut = Substitute.For(); var config = new PowertoolsLoggerConfiguration { @@ -187,14 +187,14 @@ public void OnEntry_Should_NOT_Log_Event_When_EnvironmentVariable_Set_But_Attrib Assert.Equal(0, updatedConfig.SamplingRate); Assert.True(updatedConfig.LogEvent); - consoleOut.DidNotReceive().LogLine(Arg.Any()); + consoleOut.DidNotReceive().WriteLine(Arg.Any()); } [Fact] public void OnEntry_ShouldLog_SamplingRate_When_EnvironmentVariable_Set() { // Arrange - var consoleOut = Substitute.For(); + var consoleOut = Substitute.For(); var config = new PowertoolsLoggerConfiguration { @@ -236,7 +236,7 @@ public void OnEntry_ShouldLog_SamplingRate_When_EnvironmentVariable_Set() Assert.Equal(LoggerOutputCase.PascalCase, updatedConfig.LoggerOutputCase); Assert.Equal(0.5, updatedConfig.SamplingRate); - consoleOut.Received().LogLine(Arg.Is(s => + consoleOut.Received().WriteLine(Arg.Is(s => s.Contains( "\"Level\":\"Information\",\"Service\":\"TestService\",\"Name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":null},\"SamplingRate\":0.5}") && s.Contains("\"CorrelationId\":\"20\"") @@ -247,7 +247,7 @@ public void OnEntry_ShouldLog_SamplingRate_When_EnvironmentVariable_Set() public void OnEntry_ShouldLogEvent_WhenLogEventIsTrue() { // Arrange - var consoleOut = Substitute.For(); + var consoleOut = Substitute.For(); var config = new PowertoolsLoggerConfiguration { @@ -273,7 +273,7 @@ public void OnEntry_ShouldLogEvent_WhenLogEventIsTrue() loggingAspect.OnEntry(null, null, new object[] { eventObject }, null, null, null, triggers); // Assert - consoleOut.Received().LogLine(Arg.Is(s => + consoleOut.Received().WriteLine(Arg.Is(s => s.Contains( "\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":{\"test_data\":\"test-data\"}}") )); @@ -283,7 +283,7 @@ public void OnEntry_ShouldLogEvent_WhenLogEventIsTrue() public void OnEntry_ShouldNot_Log_Info_When_LogLevel_Higher_EnvironmentVariable() { // Arrange - var consoleOut = Substitute.For(); + var consoleOut = Substitute.For(); var config = new PowertoolsLoggerConfiguration { @@ -322,7 +322,7 @@ public void OnEntry_ShouldNot_Log_Info_When_LogLevel_Higher_EnvironmentVariable( Assert.Equal("TestService", updatedConfig.Service); Assert.Equal(LoggerOutputCase.PascalCase, updatedConfig.LoggerOutputCase); - consoleOut.DidNotReceive().LogLine(Arg.Any()); + consoleOut.DidNotReceive().WriteLine(Arg.Any()); } [Fact] @@ -331,7 +331,7 @@ public void OnEntry_Should_LogDebug_WhenSet_EnvironmentVariable() // Arrange Environment.SetEnvironmentVariable("POWERTOOLS_LOG_LEVEL", "Debug"); - var consoleOut = Substitute.For(); + var consoleOut = Substitute.For(); var config = new PowertoolsLoggerConfiguration { LogOutput = consoleOut @@ -371,11 +371,11 @@ public void OnEntry_Should_LogDebug_WhenSet_EnvironmentVariable() Assert.Equal(LoggerOutputCase.PascalCase, updatedConfig.LoggerOutputCase); Assert.Equal(LogLevel.Debug, updatedConfig.MinimumLogLevel); - consoleOut.Received(1).LogLine(Arg.Is(s => + consoleOut.Received(1).WriteLine(Arg.Is(s => s.Contains( "\"Level\":\"Debug\",\"Service\":\"TestService\",\"Name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"Message\":\"Skipping Lambda Context injection because ILambdaContext context parameter not found.\"}"))); - consoleOut.Received(1).LogLine(Arg.Is(s => + consoleOut.Received(1).WriteLine(Arg.Is(s => s.Contains("\"CorrelationId\":\"test\"") && s.Contains( "\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":{\"MyRequestIdHeader\":\"test\"}") diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs index 4c9b0fd3..c327d16d 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs @@ -55,14 +55,13 @@ public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContext() .ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value); Assert.True(allKeys.ContainsKey(LoggingConstants.KeyColdStart)); - //Assert.True((bool)allKeys[LoggingConstants.KeyColdStart]); Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionName)); Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionVersion)); Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionMemorySize)); Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionArn)); Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionRequestId)); - consoleOut.DidNotReceive().LogLine(Arg.Any()); + consoleOut.DidNotReceive().WriteLine(Arg.Any()); } [Fact] @@ -79,14 +78,13 @@ public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContextAndLogDebu .ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value); Assert.True(allKeys.ContainsKey(LoggingConstants.KeyColdStart)); - //Assert.True((bool)allKeys[LoggingConstants.KeyColdStart]); Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionName)); Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionVersion)); Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionMemorySize)); Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionArn)); Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionRequestId)); - consoleOut.Received(1).LogLine( + consoleOut.Received(1).WriteLine( Arg.Is(i => i.Contains("\"level\":\"Debug\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Skipping Lambda Context injection because ILambdaContext context parameter not found.\"}")) ); @@ -101,7 +99,7 @@ public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArg() // Act _testHandlers.LogEventNoArgs(); - consoleOut.DidNotReceive().LogLine( + consoleOut.DidNotReceive().WriteLine( Arg.Any() ); } @@ -129,7 +127,7 @@ public void OnEntry_WhenEventArgExist_LogEvent() // Act _testHandlers.LogEvent(testObj, context); - consoleOut.Received(1).LogLine( + consoleOut.Received(1).WriteLine( Arg.Is(i => i.Contains("FunctionName\":\"PowertoolsLoggingSample-HelloWorldFunction-Gg8rhPwO7Wa1")) ); } @@ -148,7 +146,7 @@ public void OnEntry_WhenEventArgExist_LogEvent_False_Should_Not_Log() // Act _testHandlers.LogEventFalse(context); - consoleOut.DidNotReceive().LogLine( + consoleOut.DidNotReceive().WriteLine( Arg.Any() ); } @@ -162,11 +160,11 @@ public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArgAndLogDebug() // Act _testHandlers.LogEventDebug(); - consoleOut.Received(1).LogLine( + consoleOut.Received(1).WriteLine( Arg.Is(i => i.Contains("\"level\":\"Debug\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Skipping Event Log because event parameter not found.\"}")) ); - consoleOut.Received(1).LogLine( + consoleOut.Received(1).WriteLine( Arg.Is(i => i.Contains("\"level\":\"Debug\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Skipping Lambda Context injection because ILambdaContext context parameter not found.\"}")) ); } @@ -347,7 +345,7 @@ public void When_Setting_SamplingRate_Should_Add_Key() // Assert - consoleOut.Received().LogLine( + consoleOut.Received().WriteLine( Arg.Is(i => i.Contains("\"message\":\"test\",\"samplingRate\":0.5")) ); } @@ -399,7 +397,7 @@ public void When_Setting_LogLevel_HigherThanInformation_Should_Not_LogEvent() _testHandlers.TestLogLevelCriticalLogEvent(context); // Assert - consoleOut.DidNotReceive().LogLine(Arg.Any()); + consoleOut.DidNotReceive().WriteLine(Arg.Any()); } [Fact] @@ -412,10 +410,10 @@ public void When_LogLevel_Debug_Should_Log_Message_When_No_Context_And_LogEvent_ _testHandlers.TestLogEventWithoutContext(); // Assert - consoleOut.Received(1).LogLine(Arg.Is(s => + consoleOut.Received(1).WriteLine(Arg.Is(s => s.Contains("\"level\":\"Debug\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Skipping Event Log because event parameter not found.\"}"))); - consoleOut.Received(1).LogLine(Arg.Is(s => + consoleOut.Received(1).WriteLine(Arg.Is(s => s.Contains("\"level\":\"Debug\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Skipping Lambda Context injection because ILambdaContext context parameter not found.\"}"))); } @@ -431,7 +429,7 @@ public void Should_Log_When_Not_Using_Decorator() test.TestLogNoDecorator(); // Assert - consoleOut.Received().LogLine( + consoleOut.Received().WriteLine( Arg.Is(i => i.Contains("\"level\":\"Information\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"test\"}")) ); } @@ -448,7 +446,7 @@ public void LoggingAspect_ShouldRespectDynamicLogLevelChanges() _testHandlers.TestMethodDebug(); // Uses LogLevel.Debug attribute // Assert - consoleOut.Received(1).LogLine(Arg.Is(s => + consoleOut.Received(1).WriteLine(Arg.Is(s => s.Contains("\"level\":\"Debug\"") && s.Contains("Skipping Lambda Context injection"))); } @@ -469,7 +467,7 @@ public void LoggingAspect_ShouldCorrectlyResetLogLevelAfterExecution() Logger.LogDebug("This should be logged"); // Assert - consoleOut.Received(1).LogLine(Arg.Is(s => + consoleOut.Received(1).WriteLine(Arg.Is(s => s.Contains("\"level\":\"Debug\"") && s.Contains("\"message\":\"This should be logged\""))); } @@ -485,7 +483,7 @@ public void LoggingAspect_ShouldRespectAttributePrecedenceOverEnvironment() _testHandlers.TestMethodDebug(); // Uses LogLevel.Debug attribute // Assert - consoleOut.Received().LogLine(Arg.Is(s => + consoleOut.Received().WriteLine(Arg.Is(s => s.Contains("\"level\":\"Debug\""))); } @@ -503,9 +501,9 @@ public void LoggingAspect_ShouldImmediatelyApplyFilterLevelChanges() Logger.LogInformation("This should be logged"); // Assert - consoleOut.Received(1).LogLine(Arg.Is(s => + consoleOut.Received(1).WriteLine(Arg.Is(s => s.Contains("\"message\":\"This should be logged\""))); - consoleOut.DidNotReceive().LogLine(Arg.Is(s => + consoleOut.DidNotReceive().WriteLine(Arg.Is(s => s.Contains("\"message\":\"This should NOT be logged\""))); } @@ -514,10 +512,10 @@ public void Dispose() ResetAllState(); } - private ISystemWrapper GetConsoleOutput() + private IConsoleWrapper GetConsoleOutput() { // Create a new mock each time - var output = Substitute.For(); + var output = Substitute.For(); Logger.SetOutput(output); return output; } diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/ServiceTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/ServiceTests.cs index 3e3d4ac6..0dab6499 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/ServiceTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/ServiceTests.cs @@ -22,7 +22,7 @@ public void When_Setting_Service_Should_Override_Env() { Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "Environment Service"); - var consoleOut = Substitute.For(); + var consoleOut = Substitute.For(); Logger.SetOutput(consoleOut); // Act @@ -31,10 +31,10 @@ public void When_Setting_Service_Should_Override_Env() // Assert - consoleOut.Received(1).LogLine( + consoleOut.Received(1).WriteLine( Arg.Is(i => i.Contains("\"level\":\"Information\",\"service\":\"Environment Service\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Service: Environment Service\"")) ); - consoleOut.Received(1).LogLine( + consoleOut.Received(1).WriteLine( Arg.Is(i => i.Contains("\"level\":\"Information\",\"service\":\"Attribute Service\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Service: Attribute Service\"")) ); } diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs index 320f79bc..0acf12cd 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs @@ -47,7 +47,7 @@ public LogFormatterTest() [Fact] public void Serialize_ShouldHandleEnumValues() { - var consoleOut = Substitute.For(); + var consoleOut = Substitute.For(); Logger.SetOutput(consoleOut); var lambdaContext = new TestLambdaContext @@ -62,10 +62,10 @@ public void Serialize_ShouldHandleEnumValues() var handler = new TestHandlers(); handler.TestEnums("fake", lambdaContext); - consoleOut.Received(1).LogLine(Arg.Is(i => + consoleOut.Received(1).WriteLine(Arg.Is(i => i.Contains("\"message\":5") )); - consoleOut.Received(1).LogLine(Arg.Is(i => + consoleOut.Received(1).WriteLine(Arg.Is(i => i.Contains("\"message\":\"Dog\"") )); @@ -167,7 +167,7 @@ public void Log_WhenCustomFormatter_LogsCustomFormat() } }; - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); logFormatter.FormatLogEntry(new LogEntry()).ReturnsForAnyArgs(formattedLogEntry); var config = new PowertoolsLoggerConfiguration @@ -223,14 +223,14 @@ public void Log_WhenCustomFormatter_LogsCustomFormat() x.LambdaContext.AwsRequestId == lambdaContext.AwsRequestId )); - systemWrapper.Received(1).LogLine(JsonSerializer.Serialize(formattedLogEntry)); + systemWrapper.Received(1).WriteLine(JsonSerializer.Serialize(formattedLogEntry)); } [Fact] public void Should_Log_CustomFormatter_When_Decorated() { ResetAllState(); - var consoleOut = Substitute.For(); + var consoleOut = Substitute.For(); Logger.SetOutput(consoleOut); var lambdaContext = new TestLambdaContext @@ -249,13 +249,13 @@ public void Should_Log_CustomFormatter_When_Decorated() // in .net 8 it removes null properties #if NET8_0_OR_GREATER - consoleOut.Received(1).LogLine( + consoleOut.Received(1).WriteLine( Arg.Is(i => i.Contains( "\"correlation_ids\":{\"aws_request_id\":\"requestId\"},\"lambda_function\":{\"name\":\"funtionName\",\"arn\":\"function::arn\",\"memory_limit_in_mb\":128,\"version\":\"version\",\"cold_start\":true},\"level\":\"Information\"")) ); #else - consoleOut.Received(1).LogLine( + consoleOut.Received(1).WriteLine( Arg.Is(i => i.Contains( "{\"message\":\"test\",\"service\":\"my_service\",\"correlation_ids\":{\"aws_request_id\":\"requestId\",\"x_ray_trace_id\":null,\"correlation_id\":null},\"lambda_function\":{\"name\":\"funtionName\",\"arn\":\"function::arn\",\"memory_limit_in_m_b\":128,\"version\":\"version\",\"cold_start\":true},\"level\":\"Information\",\"timestamp\":\"2024-01-01T00:00:00.0000000\",\"logger\":{\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"sample_rate\"")) @@ -267,7 +267,7 @@ public void Should_Log_CustomFormatter_When_Decorated() public void Should_Log_CustomFormatter_When_No_Decorated_Just_Log() { ResetAllState(); - var consoleOut = Substitute.For(); + var consoleOut = Substitute.For(); Logger.SetOutput(consoleOut); var lambdaContext = new TestLambdaContext @@ -287,13 +287,13 @@ public void Should_Log_CustomFormatter_When_No_Decorated_Just_Log() // in .net 8 it removes null properties #if NET8_0_OR_GREATER - consoleOut.Received(1).LogLine( + consoleOut.Received(1).WriteLine( Arg.Is(i => i == "{\"message\":\"test\",\"service\":\"service_undefined\",\"correlation_ids\":{},\"lambda_function\":{\"cold_start\":true},\"level\":\"Information\",\"timestamp\":\"2024-01-01T00:00:00.0000000\",\"logger\":{\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"sample_rate\":0}}") ); #else - consoleOut.Received(1).LogLine( + consoleOut.Received(1).WriteLine( Arg.Is(i => i == "{\"message\":\"test\",\"service\":\"service_undefined\",\"correlation_ids\":{\"aws_request_id\":null,\"x_ray_trace_id\":null,\"correlation_id\":null},\"lambda_function\":{\"name\":null,\"arn\":null,\"memory_limit_in_m_b\":null,\"version\":null,\"cold_start\":true},\"level\":\"Information\",\"timestamp\":\"2024-01-01T00:00:00.0000000\",\"logger\":{\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"sample_rate\":0}}") @@ -304,7 +304,7 @@ public void Should_Log_CustomFormatter_When_No_Decorated_Just_Log() [Fact] public void Should_Log_CustomFormatter_When_Decorated_No_Context() { - var consoleOut = Substitute.For(); + var consoleOut = Substitute.For(); Logger.SetOutput(consoleOut); Logger.UseFormatter(new CustomLogFormatter()); @@ -312,13 +312,13 @@ public void Should_Log_CustomFormatter_When_Decorated_No_Context() _testHandler.TestCustomFormatterWithDecoratorNoContext("test"); #if NET8_0_OR_GREATER - consoleOut.Received(1).LogLine( + consoleOut.Received(1).WriteLine( Arg.Is(i => i == "{\"message\":\"test\",\"service\":\"my_service\",\"correlation_ids\":{},\"lambda_function\":{\"cold_start\":true},\"level\":\"Information\",\"timestamp\":\"2024-01-01T00:00:00.0000000\",\"logger\":{\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"sample_rate\":0.2}}") ); #else - consoleOut.Received(1).LogLine( + consoleOut.Received(1).WriteLine( Arg.Is(i => i == "{\"message\":\"test\",\"service\":\"my_service\",\"correlation_ids\":{\"aws_request_id\":null,\"x_ray_trace_id\":null,\"correlation_id\":null},\"lambda_function\":{\"name\":null,\"arn\":null,\"memory_limit_in_m_b\":null,\"version\":null,\"cold_start\":true},\"level\":\"Information\",\"timestamp\":\"2024-01-01T00:00:00.0000000\",\"logger\":{\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"sample_rate\":0.2}}") @@ -374,7 +374,7 @@ public void Log_WhenCustomFormatterReturnNull_ThrowsLogFormatException() logFormatter.FormatLogEntry(new LogEntry()).ReturnsNullForAnyArgs(); Logger.UseFormatter(logFormatter); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); var config = new PowertoolsLoggerConfiguration { Service = service, @@ -392,7 +392,7 @@ public void Log_WhenCustomFormatterReturnNull_ThrowsLogFormatException() // Assert Assert.Throws(Act); logFormatter.Received(1).FormatLogEntry(Arg.Any()); - systemWrapper.DidNotReceiveWithAnyArgs().LogLine(Arg.Any()); + systemWrapper.DidNotReceiveWithAnyArgs().WriteLine(Arg.Any()); //Clean up Logger.UseDefaultFormatter(); @@ -419,7 +419,7 @@ public void Log_WhenCustomFormatterRaisesException_ThrowsLogFormatException() var logFormatter = Substitute.For(); logFormatter.FormatLogEntry(new LogEntry()).ThrowsForAnyArgs(new Exception(errorMessage)); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); var config = new PowertoolsLoggerConfiguration { Service = service, @@ -437,7 +437,7 @@ public void Log_WhenCustomFormatterRaisesException_ThrowsLogFormatException() // Assert Assert.Throws(Act); logFormatter.Received(1).FormatLogEntry(Arg.Any()); - systemWrapper.DidNotReceiveWithAnyArgs().LogLine(Arg.Any()); + systemWrapper.DidNotReceiveWithAnyArgs().WriteLine(Arg.Any()); //Clean up Logger.UseDefaultFormatter(); diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs index d5252f10..278c64cb 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs @@ -45,7 +45,7 @@ private static void Log_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel logLeve var service = Guid.NewGuid().ToString(); var configurations = Substitute.For(); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); // Configure the substitute for IPowertoolsConfigurations configurations.Service.Returns(service); @@ -91,7 +91,7 @@ private static void Log_WhenMinimumLogLevelIsBelowLogLevel_Logs(LogLevel logLeve } // Assert - systemWrapper.Received(1).LogLine( + systemWrapper.Received(1).WriteLine( Arg.Is(s => s.Contains(service)) ); } @@ -104,7 +104,7 @@ private static void Log_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel l var service = Guid.NewGuid().ToString(); var configurations = Substitute.For(); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); // Configure the substitute for IPowertoolsConfigurations configurations.Service.Returns(service); @@ -149,7 +149,7 @@ private static void Log_WhenMinimumLogLevelIsAboveLogLevel_DoesNotLog(LogLevel l } // Assert - systemWrapper.DidNotReceive().LogLine( + systemWrapper.DidNotReceive().WriteLine( Arg.Any() ); } @@ -275,16 +275,14 @@ public void Log_ConfigurationIsNotProvided_ReadsFromEnvironmentVariables() var service = Guid.NewGuid().ToString(); var logLevel = LogLevel.Trace; var loggerSampleRate = 0.7; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); configurations.LoggerSampleRate.Returns(loggerSampleRate); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); - + var systemWrapper = Substitute.For(); + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = service, @@ -299,7 +297,7 @@ public void Log_ConfigurationIsNotProvided_ReadsFromEnvironmentVariables() logger.LogInformation("Test"); // Assert - systemWrapper.Received(1).LogLine( + systemWrapper.Received(1).WriteLine( Arg.Is(s => s.Contains(service) && s.Contains(loggerSampleRate.ToString(CultureInfo.InvariantCulture)) @@ -321,7 +319,7 @@ public void Log_SamplingRateGreaterThanRandom_ChangedLogLevelToDebug() configurations.LogLevel.Returns(logLevel.ToString()); configurations.LoggerSampleRate.Returns(loggerSampleRate); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -340,7 +338,7 @@ public void Log_SamplingRateGreaterThanRandom_ChangedLogLevelToDebug() logger.LogInformation("Test"); // Assert - systemWrapper.Received(1).LogLine( + systemWrapper.Received(1).WriteLine( Arg.Is(s => s == $"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {loggerSampleRate}, Sampler Value: {randomSampleRate}." @@ -362,7 +360,7 @@ public void Log_SamplingRateGreaterThanOne_SkipsSamplingRateConfiguration() configurations.LogLevel.Returns(logLevel.ToString()); configurations.LoggerSampleRate.Returns(loggerSampleRate); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -378,7 +376,7 @@ public void Log_SamplingRateGreaterThanOne_SkipsSamplingRateConfiguration() logger.LogInformation("Test"); // Assert - systemWrapper.Received(1).LogLine( + systemWrapper.Received(1).WriteLine( Arg.Is(s => s == $"Skipping sampling rate configuration because of invalid value. Sampling rate: {loggerSampleRate}" @@ -399,7 +397,7 @@ public void Log_EnvVarSetsCaseToCamelCase_OutputsCamelCaseLog() configurations.LogLevel.Returns(logLevel.ToString()); configurations.LoggerOutputCase.Returns(LoggerOutputCase.CamelCase.ToString()); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -421,7 +419,7 @@ public void Log_EnvVarSetsCaseToCamelCase_OutputsCamelCaseLog() logger.LogInformation(message); // Assert - systemWrapper.Received(1).LogLine( + systemWrapper.Received(1).WriteLine( Arg.Is(s => s.Contains("\"message\":{\"propOne\":\"Value 1\",\"propTwo\":\"Value 2\"}") ) @@ -440,7 +438,7 @@ public void Log_AttributeSetsCaseToCamelCase_OutputsCamelCaseLog() configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -463,7 +461,7 @@ public void Log_AttributeSetsCaseToCamelCase_OutputsCamelCaseLog() logger.LogInformation(message); // Assert - systemWrapper.Received(1).LogLine( + systemWrapper.Received(1).WriteLine( Arg.Is(s => s.Contains("\"message\":{\"propOne\":\"Value 1\",\"propTwo\":\"Value 2\"}") ) @@ -477,15 +475,13 @@ public void Log_EnvVarSetsCaseToPascalCase_OutputsPascalCaseLog() var loggerName = Guid.NewGuid().ToString(); var service = Guid.NewGuid().ToString(); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); configurations.LoggerOutputCase.Returns(LoggerOutputCase.PascalCase.ToString()); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -506,7 +502,7 @@ public void Log_EnvVarSetsCaseToPascalCase_OutputsPascalCaseLog() logger.LogInformation(message); // Assert - systemWrapper.Received(1).LogLine( + systemWrapper.Received(1).WriteLine( Arg.Is(s => s.Contains("\"Message\":{\"PropOne\":\"Value 1\",\"PropTwo\":\"Value 2\"}") ) @@ -525,7 +521,7 @@ public void Log_AttributeSetsCaseToPascalCase_OutputsPascalCaseLog() configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -547,7 +543,7 @@ public void Log_AttributeSetsCaseToPascalCase_OutputsPascalCaseLog() logger.LogInformation(message); // Assert - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains("\"Message\":{\"PropOne\":\"Value 1\",\"PropTwo\":\"Value 2\"}") )); } @@ -559,15 +555,13 @@ public void Log_EnvVarSetsCaseToSnakeCase_OutputsSnakeCaseLog() var loggerName = Guid.NewGuid().ToString(); var service = Guid.NewGuid().ToString(); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); configurations.LoggerOutputCase.Returns(LoggerOutputCase.SnakeCase.ToString()); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -588,7 +582,7 @@ public void Log_EnvVarSetsCaseToSnakeCase_OutputsSnakeCaseLog() logger.LogInformation(message); // Assert - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains("\"message\":{\"prop_one\":\"Value 1\",\"prop_two\":\"Value 2\"}") )); } @@ -605,7 +599,7 @@ public void Log_AttributeSetsCaseToSnakeCase_OutputsSnakeCaseLog() configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -627,7 +621,7 @@ public void Log_AttributeSetsCaseToSnakeCase_OutputsSnakeCaseLog() logger.LogInformation(message); // Assert - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains("\"message\":{\"prop_one\":\"Value 1\",\"prop_two\":\"Value 2\"}") )); } @@ -639,14 +633,12 @@ public void Log_NoOutputCaseSet_OutputDefaultsToSnakeCaseLog() var loggerName = Guid.NewGuid().ToString(); var service = Guid.NewGuid().ToString(); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -667,7 +659,7 @@ public void Log_NoOutputCaseSet_OutputDefaultsToSnakeCaseLog() logger.LogInformation(message); // Assert - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains("\"message\":{\"prop_one\":\"Value 1\",\"prop_two\":\"Value 2\"}"))); } @@ -682,7 +674,7 @@ public void BeginScope_WhenScopeIsObject_ExtractScopeKeys() var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -725,7 +717,7 @@ public void BeginScope_WhenScopeIsObjectDictionary_ExtractScopeKeys() var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -768,7 +760,7 @@ public void BeginScope_WhenScopeIsStringDictionary_ExtractScopeKeys() var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -824,7 +816,7 @@ public void Log_WhenExtraKeysIsObjectDictionary_AppendExtraKeys(LogLevel logLeve configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); configurations.LoggerOutputCase.Returns(LoggerOutputCase.PascalCase.ToString()); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -874,7 +866,7 @@ public void Log_WhenExtraKeysIsObjectDictionary_AppendExtraKeys(LogLevel logLeve } } - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains(scopeKeys.Keys.First()) && s.Contains(scopeKeys.Keys.Last()) && s.Contains(scopeKeys.Values.First().ToString()) && @@ -908,7 +900,7 @@ public void Log_WhenExtraKeysIsStringDictionary_AppendExtraKeys(LogLevel logLeve configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); configurations.LoggerOutputCase.Returns(LoggerOutputCase.PascalCase.ToString()); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -959,7 +951,7 @@ public void Log_WhenExtraKeysIsStringDictionary_AppendExtraKeys(LogLevel logLeve } } - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains(scopeKeys.Keys.First()) && s.Contains(scopeKeys.Keys.Last()) && s.Contains(scopeKeys.Values.First()) && @@ -993,7 +985,7 @@ public void Log_WhenExtraKeysAsObject_AppendExtraKeys(LogLevel logLevel, bool lo configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); configurations.LoggerOutputCase.Returns(LoggerOutputCase.PascalCase.ToString()); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -1044,7 +1036,7 @@ public void Log_WhenExtraKeysAsObject_AppendExtraKeys(LogLevel logLevel, bool lo } } - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains("PropOne") && s.Contains("PropTwo") && s.Contains(scopeKeys.PropOne) && @@ -1067,7 +1059,7 @@ public void Log_WhenException_LogsExceptionDetails() configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -1089,11 +1081,11 @@ public void Log_WhenException_LogsExceptionDetails() } // Assert - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains("\"exception\":{\"type\":\"" + error.GetType().FullName + "\",\"message\":\"" + error.Message + "\"") )); - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains( "\"exception\":{\"type\":\"System.InvalidOperationException\",\"message\":\"TestError\",\"source\":\"AWS.Lambda.Powertools.Logging.Tests\",\"stack_trace\":\" at AWS.Lambda.Powertools.Logging.Tests.PowertoolsLoggerTest.Log_WhenException_LogsExceptionDetails()") )); @@ -1108,14 +1100,12 @@ public void Log_Inner_Exception() var error = new InvalidOperationException("Parent exception message", new ArgumentNullException(nameof(service), "Very important inner exception message")); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -1133,12 +1123,12 @@ public void Log_Inner_Exception() 12345); // Assert - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains("\"exception\":{\"type\":\"" + error.GetType().FullName + "\",\"message\":\"" + error.Message + "\"") )); - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains("\"level\":\"Error\",\"service\":\"" + service + "\",\"name\":\"" + loggerName + "\",\"message\":\"Something went wrong and we logged an exception itself with an inner exception. This is a param 12345\",\"exception\":{\"type\":\"System.InvalidOperationException\",\"message\":\"Parent exception message\",\"inner_exception\":{\"type\":\"System.ArgumentNullException\",\"message\":\"Very important inner exception message (Parameter 'service')\"}}}") )); @@ -1155,14 +1145,12 @@ public void Log_Nested_Inner_Exception() new Exception("Very important nested inner exception message"))); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -1181,7 +1169,7 @@ public void Log_Nested_Inner_Exception() 12345); // Assert - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains( "\"message\":\"Something went wrong and we logged an exception itself with an inner exception. This is a param 12345\",\"exception\":{\"type\":\"System.InvalidOperationException\",\"message\":\"Parent exception message\",\"inner_exception\":{\"type\":\"System.ArgumentNullException\",\"message\":\"service\",\"inner_exception\":{\"type\":\"System.Exception\",\"message\":\"Very important nested inner exception message\"}}}}") )); @@ -1200,7 +1188,7 @@ public void Log_WhenNestedException_LogsExceptionDetails() configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -1222,7 +1210,7 @@ public void Log_WhenNestedException_LogsExceptionDetails() } // Assert - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains("\"error\":{\"type\":\"" + error.GetType().FullName + "\",\"message\":\"" + error.Message + "\"") )); @@ -1242,7 +1230,7 @@ public void Log_WhenByteArray_LogsBase64EncodedString() configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -1258,7 +1246,7 @@ public void Log_WhenByteArray_LogsBase64EncodedString() // Assert var base64String = Convert.ToBase64String(bytes); - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains($"\"bytes\":\"{base64String}\"") )); } @@ -1281,7 +1269,7 @@ public void Log_WhenMemoryStream_LogsBase64String() configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -1297,7 +1285,7 @@ public void Log_WhenMemoryStream_LogsBase64String() logger.LogInformation(new { Name = "Test Object", Stream = memoryStream }); // Assert - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains("\"stream\":\"" + Convert.ToBase64String(bytes) + "\"") )); } @@ -1322,7 +1310,7 @@ public void Log_WhenMemoryStream_LogsBase64String_UnsafeRelaxedJsonEscaping() configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -1338,7 +1326,7 @@ public void Log_WhenMemoryStream_LogsBase64String_UnsafeRelaxedJsonEscaping() logger.LogInformation(new { Name = "Test Object", Stream = memoryStream }); // Assert - systemWrapper.Received(1).LogLine(Arg.Is(s => + systemWrapper.Received(1).WriteLine(Arg.Is(s => s.Contains("\"stream\":\"" + Convert.ToBase64String(bytes) + "\"") )); } @@ -1404,14 +1392,12 @@ public void Log_Should_Serialize_DateOnly() var loggerName = Guid.NewGuid().ToString(); var service = Guid.NewGuid().ToString(); var logLevel = LogLevel.Information; - var randomSampleRate = 0.5; var configurations = Substitute.For(); configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); - systemWrapper.GetRandom().Returns(randomSampleRate); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -1438,7 +1424,7 @@ public void Log_Should_Serialize_DateOnly() logger.LogInformation(message); // Assert - systemWrapper.Received(1).LogLine( + systemWrapper.Received(1).WriteLine( Arg.Is(s => s.Contains( "\"message\":{\"propOne\":\"Value 1\",\"propTwo\":\"Value 2\",\"propThree\":{\"propFour\":1},\"date\":\"2022-01-01\"}}") @@ -1459,7 +1445,7 @@ public void Log_Should_Serialize_TimeOnly() configurations.Service.Returns(service); configurations.LogLevel.Returns(logLevel.ToString()); - var systemWrapper = Substitute.For(); + var systemWrapper = Substitute.For(); var loggerConfiguration = new PowertoolsLoggerConfiguration { @@ -1483,7 +1469,7 @@ public void Log_Should_Serialize_TimeOnly() logger.LogInformation(message); // Assert - systemWrapper.Received(1).LogLine( + systemWrapper.Received(1).WriteLine( Arg.Is(s => s.Contains("\"message\":{\"propOne\":\"Value 1\",\"propTwo\":\"Value 2\",\"time\":\"12:00:00\"}") ) diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs index ceaffded..f5d707ca 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs @@ -74,7 +74,7 @@ public void ObjectToDictionary_NullObject_Return_New_Dictionary() [Fact] public void Should_Log_With_Anonymous() { - var consoleOut = Substitute.For(); + var consoleOut = Substitute.For(); Logger.SetOutput(consoleOut); // Act & Assert @@ -85,7 +85,7 @@ public void Should_Log_With_Anonymous() Logger.LogInformation("test"); - consoleOut.Received(1).LogLine( + consoleOut.Received(1).WriteLine( Arg.Is(i => i.Contains("\"new_key\":{\"name\":\"my name\"}")) ); @@ -94,7 +94,7 @@ public void Should_Log_With_Anonymous() [Fact] public void Should_Log_With_Complex_Anonymous() { - var consoleOut = Substitute.For(); + var consoleOut = Substitute.For(); Logger.SetOutput(consoleOut); // Act & Assert @@ -116,7 +116,7 @@ public void Should_Log_With_Complex_Anonymous() Logger.LogInformation("test"); - consoleOut.Received(1).LogLine( + consoleOut.Received(1).WriteLine( Arg.Is(i => i.Contains( "\"new_key\":{\"id\":1,\"name\":\"my name\",\"adresses\":{\"street\":\"street 1\",\"number\":1,\"city\":{\"name\":\"city 1\",\"state\":\"state 1\"}")) diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/ClearDimensionsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/ClearDimensionsTests.cs index 0a46d6fd..90a3547a 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/ClearDimensionsTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/ClearDimensionsTests.cs @@ -13,7 +13,7 @@ public void WhenClearAllDimensions_NoDimensionsInOutput() { // Arrange var consoleOut = new StringWriter(); - SystemWrapper.Instance.SetOut(consoleOut); + ConsoleWrapper.SetOut(consoleOut); // Act var handler = new FunctionHandler(); diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs index 687ba17e..12451cfb 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs @@ -35,7 +35,7 @@ public EmfValidationTests() { _handler = new FunctionHandler(); _consoleOut = new CustomConsoleWriter(); - SystemWrapper.Instance.SetOut(_consoleOut); + ConsoleWrapper.SetOut(_consoleOut); } [Trait("Category", value: "SchemaValidation")] diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs index 254de9e9..462b183c 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs @@ -34,7 +34,7 @@ public FunctionHandlerTests() { _handler = new FunctionHandler(); _consoleOut = new CustomConsoleWriter(); - SystemWrapper.Instance.SetOut(_consoleOut); + ConsoleWrapper.SetOut(_consoleOut); } [Fact] diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs index 8db246a1..ffb90af5 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs @@ -17,23 +17,15 @@ public void Metrics_Set_Execution_Environment_Context() { // Arrange Metrics.ResetForTest(); - var assemblyName = "AWS.Lambda.Powertools.Metrics"; - var assemblyVersion = "1.0.0"; + var env = new PowertoolsEnvironment(); - var env = Substitute.For(); - env.GetAssemblyName(Arg.Any()).Returns(assemblyName); - env.GetAssemblyVersion(Arg.Any()).Returns(assemblyVersion); - - var conf = new PowertoolsConfigurations(new SystemWrapper(env)); + var conf = new PowertoolsConfigurations(env); _ = new Metrics(conf); // Assert - env.Received(1).SetEnvironmentVariable( - "AWS_EXECUTION_ENV", $"{Constants.FeatureContextIdentifier}/Metrics/{assemblyVersion}" - ); - - env.Received(1).GetEnvironmentVariable("AWS_EXECUTION_ENV"); + Assert.Equal($"{Constants.FeatureContextIdentifier}/Metrics/0.0.1", + env.GetEnvironmentVariable("AWS_EXECUTION_ENV")); } [Fact] diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/XRayRecorderTests.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/XRayRecorderTests.cs index 6a024334..9c02bdab 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/XRayRecorderTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/XRayRecorderTests.cs @@ -31,27 +31,17 @@ public class XRayRecorderTests public void Tracing_Set_Execution_Environment_Context() { // Arrange - var assemblyName = "AWS.Lambda.Powertools.Tracing"; - var assemblyVersion = "1.0.0"; + var env = new PowertoolsEnvironment(); - var env = Substitute.For(); - env.GetAssemblyName(Arg.Any()).Returns(assemblyName); - env.GetAssemblyVersion(Arg.Any()).Returns(assemblyVersion); - - var conf = new PowertoolsConfigurations(new SystemWrapper(env)); + var conf = new PowertoolsConfigurations(env); var awsXray = Substitute.For(); // Act var xRayRecorder = new XRayRecorder(awsXray, conf); // Assert - env.Received(1).SetEnvironmentVariable( - "AWS_EXECUTION_ENV", $"{Constants.FeatureContextIdentifier}/Tracing/{assemblyVersion}" - ); - - env.Received(1).GetEnvironmentVariable( - "AWS_EXECUTION_ENV" - ); + Assert.Equal($"{Constants.FeatureContextIdentifier}/Tracing/0.0.1", + env.GetEnvironmentVariable("AWS_EXECUTION_ENV")); Assert.NotNull(xRayRecorder); } diff --git a/libraries/tests/Directory.Packages.props b/libraries/tests/Directory.Packages.props index c6868210..88751caf 100644 --- a/libraries/tests/Directory.Packages.props +++ b/libraries/tests/Directory.Packages.props @@ -4,7 +4,7 @@ - + @@ -13,7 +13,7 @@ - + diff --git a/libraries/tests/e2e/functions/core/logging/AOT-Function/src/AOT-Function/AOT-Function.csproj b/libraries/tests/e2e/functions/core/logging/AOT-Function/src/AOT-Function/AOT-Function.csproj index b2636d6b..8655735e 100644 --- a/libraries/tests/e2e/functions/core/logging/AOT-Function/src/AOT-Function/AOT-Function.csproj +++ b/libraries/tests/e2e/functions/core/logging/AOT-Function/src/AOT-Function/AOT-Function.csproj @@ -17,7 +17,7 @@ partial - + diff --git a/libraries/tests/e2e/functions/core/tracing/AOT-Function/src/AOT-Function/AOT-Function.csproj b/libraries/tests/e2e/functions/core/tracing/AOT-Function/src/AOT-Function/AOT-Function.csproj index 85b41ba2..111b59c2 100644 --- a/libraries/tests/e2e/functions/core/tracing/AOT-Function/src/AOT-Function/AOT-Function.csproj +++ b/libraries/tests/e2e/functions/core/tracing/AOT-Function/src/AOT-Function/AOT-Function.csproj @@ -17,7 +17,7 @@ partial - + From 34f2946cb883fecc5ff3c16a94c346187f47f36f Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 1 Apr 2025 12:22:53 +0100 Subject: [PATCH 26/49] loggeroutput --- .../src/AWS.Lambda.Powertools.Common/Tests/TestLoggerOutput.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Tests/TestLoggerOutput.cs b/libraries/src/AWS.Lambda.Powertools.Common/Tests/TestLoggerOutput.cs index 19c55683..b5dded35 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Tests/TestLoggerOutput.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Tests/TestLoggerOutput.cs @@ -10,7 +10,7 @@ public class TestLoggerOutput : IConsoleWrapper /// /// Buffer for all the log messages written to the logger. /// - private readonly StringBuilder _outputBuffer = new StringBuilder(); + private readonly StringBuilder _outputBuffer = new(); /// /// Cleasr the output buffer. From 49ed3015c20dae6731d8037221a5be6895b3ce55 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 2 Apr 2025 12:57:43 +0100 Subject: [PATCH 27/49] refactor: improve logging buffer management and configuration handling --- .../Buffer/BufferingLoggerProvider.cs | 3 + .../Internal/Buffer/LogBuffer.cs | 11 + .../Internal/Buffer/LogBufferManager.cs | 38 +- .../Buffer/PowertoolsBufferingLogger.cs | 257 ++++---- .../Internal/Helpers/LoggerFactoryHelper.cs | 12 +- .../Internal/LoggerFactoryHolder.cs | 61 +- .../Internal/LoggingAspect.cs | 17 +- .../Internal/PowertoolsLoggerProvider.cs | 4 +- .../AWS.Lambda.Powertools.Logging/Logger.cs | 7 +- .../PowertoolsLoggingBuilderExtensions.cs | 9 +- .../Attributes/LoggingAttributeTest.cs | 20 +- .../Buffering/LambdaContextBufferingTests.cs | 519 ++++++++++++++++ .../Buffering/LogBufferingHandlerTests.cs | 361 +++++++++++ .../Buffering/LogBufferingTests.cs | 585 ++++++++++++++++++ 14 files changed, 1715 insertions(+), 189 deletions(-) create mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LambdaContextBufferingTests.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingHandlerTests.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingTests.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs index 2e0ce5fa..a6e1a6bb 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs @@ -89,6 +89,9 @@ public override void Dispose() { logger.FlushBuffer(); } + + // Unregister from buffer manager + LogBufferManager.UnregisterProvider(this); _loggers.Clear(); base.Dispose(); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs index fd233c70..9cb74ad3 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs @@ -49,6 +49,11 @@ public static void SetCurrentInvocationId(string invocationId) public void Add(string logEntry, int maxBytes) { var invocationId = CurrentInvocationId; + if (string.IsNullOrEmpty(invocationId)) + { + // No invocation ID set, do not buffer + return; + } var buffer = _buffersByInvocation.GetOrAdd(invocationId, _ => new InvocationBuffer()); buffer.Add(logEntry, maxBytes); } @@ -60,6 +65,12 @@ public IReadOnlyCollection GetAndClear() { var invocationId = CurrentInvocationId; + if (string.IsNullOrEmpty(invocationId)) + { + // No invocation ID set, return empty + return Array.Empty(); + } + // Try to get and remove the buffer for this invocation if (_buffersByInvocation.TryRemove(invocationId, out var buffer)) { diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBufferManager.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBufferManager.cs index 5eea9ec5..b7f47f43 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBufferManager.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBufferManager.cs @@ -14,6 +14,7 @@ */ using System; +using System.Collections.Generic; namespace AWS.Lambda.Powertools.Logging.Internal; @@ -22,20 +23,21 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// internal static class LogBufferManager { - private static BufferingLoggerProvider _provider; + private static readonly List Providers = new(); /// /// Register a buffering provider with the manager /// internal static void RegisterProvider(BufferingLoggerProvider provider) { - _provider = provider; + if (!Providers.Contains(provider)) + Providers.Add(provider); } /// - /// Set the current invocation ID to isolate logs between Lambda invocations + /// Set the current invocation ID to isolate logs between invocations /// - public static void SetInvocationId(string invocationId) + internal static void SetInvocationId(string invocationId) { LogBuffer.SetCurrentInvocationId(invocationId); } @@ -47,7 +49,10 @@ internal static void FlushCurrentBuffer() { try { - _provider?.FlushBuffers(); + foreach (var provider in Providers) + { + provider?.FlushBuffers(); + } } catch (Exception) { @@ -62,11 +67,32 @@ internal static void ClearCurrentBuffer() { try { - _provider?.ClearCurrentBuffer(); + foreach (var provider in Providers) + { + provider?.ClearCurrentBuffer(); + } } catch (Exception) { // Suppress errors } } + + /// + /// Unregister a buffering provider from the manager + /// + /// + internal static void UnregisterProvider(BufferingLoggerProvider provider) + { + Providers.Remove(provider); + } + + /// + /// Reset the manager state (for testing purposes) + /// + internal static void ResetForTesting() + { + Providers.Clear(); + LogBuffer.SetCurrentInvocationId(null); + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs index ef2c4f95..f8147418 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs @@ -5,159 +5,162 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// - /// Logger implementation that supports buffering - /// - internal class PowertoolsBufferingLogger : ILogger +/// Logger implementation that supports buffering +/// +internal class PowertoolsBufferingLogger : ILogger +{ + private readonly ILogger _innerLogger; + private readonly Func _getCurrentConfig; + private readonly string _categoryName; + private readonly LogBuffer _buffer = new(); + + public PowertoolsBufferingLogger( + ILogger innerLogger, + Func getCurrentConfig, + string categoryName) + { + _innerLogger = innerLogger; + _getCurrentConfig = getCurrentConfig; + _categoryName = categoryName; + } + + public IDisposable BeginScope(TState state) { - private readonly ILogger _innerLogger; - private readonly Func _getCurrentConfig; - private readonly string _categoryName; - private readonly LogBuffer _buffer = new(); - - public PowertoolsBufferingLogger( - ILogger innerLogger, - Func getCurrentConfig, - string categoryName) + return _innerLogger.BeginScope(state); + } + + public bool IsEnabled(LogLevel logLevel) + { + var options = _getCurrentConfig(); + + // If buffering is disabled, defer to inner logger + if (!options.LogBuffering.Enabled) { - _innerLogger = innerLogger; - _getCurrentConfig = getCurrentConfig; - _categoryName = categoryName; + return _innerLogger.IsEnabled(logLevel); } - - public IDisposable BeginScope(TState state) + + // If the log level is at or above the configured minimum log level, + // let the inner logger decide + if (logLevel >= options.MinimumLogLevel) { - return _innerLogger.BeginScope(state); + return _innerLogger.IsEnabled(logLevel); } - - public bool IsEnabled(LogLevel logLevel) - { - var options = _getCurrentConfig(); - - // If buffering is disabled, defer to inner logger - if (!options.LogBuffering.Enabled) - { - return _innerLogger.IsEnabled(logLevel); - } - - // If the log level is at or above the configured minimum log level, - // let the inner logger decide - if (logLevel >= options.MinimumLogLevel) - { - return _innerLogger.IsEnabled(logLevel); - } - - // For logs below minimum level but at or above buffer threshold, - // we should handle them (buffer them) - if (logLevel >= options.LogBuffering.BufferAtLogLevel) - { - return true; - } - - // Otherwise, the log level is below our buffer threshold - return false; - } - - public void Log( - LogLevel logLevel, - EventId eventId, - TState state, - Exception exception, - Func formatter) + + // For logs below minimum level but at or above buffer threshold, + // we should handle them (buffer them) + if (logLevel >= options.LogBuffering.BufferAtLogLevel) { - // Skip if logger is not enabled for this level - if (!IsEnabled(logLevel)) - return; - - var options = _getCurrentConfig(); - var bufferOptions = options.LogBuffering; - - // Check if this log should be buffered - bool shouldBuffer = bufferOptions.Enabled && - logLevel >= bufferOptions.BufferAtLogLevel && - logLevel < options.MinimumLogLevel; - - if (shouldBuffer) - { - // Add to buffer instead of logging - try - { - if (_innerLogger is PowertoolsLogger powertoolsLogger) - { - var logEntry = powertoolsLogger.LogEntryString(logLevel, state, exception, formatter); - _buffer.Add(logEntry, bufferOptions.MaxBytes); - } - } - catch (Exception ex) - { - // If buffering fails, try to log an error about it - try - { - _innerLogger.LogError(ex, "Failed to buffer log entry"); - } - catch - { - // Last resort: if even that fails, just suppress the error - } - } - } - else - { - // If this is an error and we should flush on error - if (bufferOptions.Enabled && - bufferOptions.FlushOnErrorLog && - logLevel >= LogLevel.Error) - { - FlushBuffer(); - } - } + return true; } - - /// - /// Flush buffered logs to the inner logger - /// - public void FlushBuffer() + + // Otherwise, the log level is below our buffer threshold + return false; + } + + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception exception, + Func formatter) + { + // Skip if logger is not enabled for this level + if (!IsEnabled(logLevel)) + return; + + var options = _getCurrentConfig(); + var bufferOptions = options.LogBuffering; + + // Check if this log should be buffered + bool shouldBuffer = bufferOptions.Enabled && + logLevel >= bufferOptions.BufferAtLogLevel && + logLevel < options.MinimumLogLevel; + + if (shouldBuffer) { + // Add to buffer instead of logging try { - // Get all buffered entries - var entries = _buffer.GetAndClear(); - if (_innerLogger is PowertoolsLogger powertoolsLogger) { - // Log each entry directly - foreach (var entry in entries) - { - powertoolsLogger.LogLine(entry); - } + var logEntry = powertoolsLogger.LogEntryString(logLevel, state, exception, formatter); + _buffer.Add(logEntry, bufferOptions.MaxBytes); } } catch (Exception ex) { - // If the entire flush operation fails, try to log an error + // If buffering fails, try to log an error about it try { - _innerLogger.LogError(ex, "Failed to flush log buffer"); + _innerLogger.LogError(ex, "Failed to buffer log entry"); } catch { - // If even that fails, just suppress the error + // Last resort: if even that fails, just suppress the error } } } - - /// - /// Clear the buffer without logging - /// - public void ClearBuffer() + else { - _buffer.Clear(); + // If this is an error and we should flush on error + if (bufferOptions.Enabled && + bufferOptions.FlushOnErrorLog && + logLevel >= LogLevel.Error) + { + FlushBuffer(); + } + + // When not buffering, forward to the inner logger + _innerLogger.Log(logLevel, eventId, state, exception, formatter); } + } - /// - /// Clear buffered logs only for the current invocation - /// - public void ClearCurrentInvocation() + /// + /// Flush buffered logs to the inner logger + /// + public void FlushBuffer() + { + try + { + // Get all buffered entries + var entries = _buffer.GetAndClear(); + + if (_innerLogger is PowertoolsLogger powertoolsLogger) + { + // Log each entry directly + foreach (var entry in entries) + { + powertoolsLogger.LogLine(entry); + } + } + } + catch (Exception ex) { - _buffer.ClearCurrentInvocation(); + // If the entire flush operation fails, try to log an error + try + { + _innerLogger.LogError(ex, "Failed to flush log buffer"); + } + catch + { + // If even that fails, just suppress the error + } } - } \ No newline at end of file + } + + /// + /// Clear the buffer without logging + /// + public void ClearBuffer() + { + _buffer.Clear(); + } + + /// + /// Clear buffered logs only for the current invocation + /// + public void ClearCurrentInvocation() + { + _buffer.ClearCurrentInvocation(); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs index e8dacf4e..c68ba0bf 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs @@ -31,10 +31,16 @@ public static ILoggerFactory CreateAndConfigureFactory(PowertoolsLoggerConfigura config.LogOutput = configuration.LogOutput; config.XRayTraceId = configuration.XRayTraceId; }); + + // Use current filter level or level from config + if (configuration.MinimumLogLevel != LogLevel.None) + { + builder.AddFilter(null, configuration.MinimumLogLevel); + builder.SetMinimumLevel(configuration.MinimumLogLevel); + } }); - - // Configure the static logger with the factory - // Logger.Configure(factory); + + LoggerFactoryHolder.SetFactory(factory); return factory; } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerFactoryHolder.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerFactoryHolder.cs index 9299b44b..74e31371 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerFactoryHolder.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerFactoryHolder.cs @@ -16,6 +16,7 @@ using System; using System.Threading; using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging.Internal.Helpers; using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging.Internal; @@ -29,29 +30,29 @@ internal static class LoggerFactoryHolder private static readonly object _lock = new object(); private static bool _isConfigured = false; - private static LogLevel _currentFilterLevel = LogLevel.Information; + // private static LogLevel _currentFilterLevel = LogLevel.Information; - /// - /// Updates the filter log level at runtime - /// - /// The new minimum log level - public static void UpdateFilterLogLevel(LogLevel logLevel) - { - lock (_lock) - { - // Only reset if level actually changes - if (_currentFilterLevel != logLevel) - { - _currentFilterLevel = logLevel; - - if (_factory != null) - { - try { _factory.Dispose(); } catch { /* Ignore */ } - _factory = null; - } - } - } - } + // /// + // /// Updates the filter log level at runtime + // /// + // /// The new minimum log level + // public static void UpdateFilterLogLevel(LogLevel logLevel) + // { + // lock (_lock) + // { + // // Only reset if level actually changes + // if (_currentFilterLevel != logLevel) + // { + // _currentFilterLevel = logLevel; + // + // if (_factory != null) + // { + // try { _factory.Dispose(); } catch { /* Ignore */ } + // _factory = null; + // } + // } + // } + // } /// /// Gets or creates the shared logger factory @@ -64,18 +65,7 @@ public static ILoggerFactory GetOrCreateFactory() { var config = PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); - // Use current filter level or level from config - _currentFilterLevel = config.MinimumLogLevel != LogLevel.None - ? config.MinimumLogLevel - : _currentFilterLevel; - - _factory = LoggerFactory.Create(builder => - { - builder.AddPowertoolsLogger(); - - // Correctly configure the filter - builder.AddFilter(null, _currentFilterLevel); - }); + _factory = LoggerFactoryHelper.CreateAndConfigureFactory(config); } return _factory; } @@ -88,6 +78,7 @@ public static void SetFactory(ILoggerFactory factory) { _factory = factory; _isConfigured = true; + Logger.ClearInstance(); } } @@ -113,7 +104,7 @@ internal static void Reset() _factory = null; } - _currentFilterLevel = LogLevel.None; + // _currentFilterLevel = LogLevel.None; _isConfigured = false; } } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index 415547fd..a141dc64 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -21,6 +21,7 @@ using System.Text.Json; using AspectInjector.Broker; using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Logging.Internal.Helpers; using AWS.Lambda.Powertools.Logging.Serializers; using Microsoft.Extensions.Logging; @@ -81,22 +82,24 @@ private void InitializeLogger(LoggingAttribute trigger) // Only update configuration if any settings were provided var needsReconfiguration = hasLogLevel || hasService || hasOutputCase || hasSamplingRate; - + _currentConfig = PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); + if (needsReconfiguration) { // Apply each setting directly using the existing Logger static methods - if (hasLogLevel) Logger.UseMinimumLogLevel(trigger.LogLevel); - if (hasService) Logger.UseServiceName(trigger.Service); - if (hasOutputCase) Logger.UseOutputCase(trigger.LoggerOutputCase); - if (hasSamplingRate) Logger.UseSamplingRate(trigger.SamplingRate); + if (hasLogLevel) _currentConfig.MinimumLogLevel = trigger.LogLevel; + if (hasService) _currentConfig.Service = trigger.Service; + if (hasOutputCase) _currentConfig.LoggerOutputCase = trigger.LoggerOutputCase; + if (hasSamplingRate) _currentConfig.SamplingRate = trigger.SamplingRate; // Need to refresh the logger after configuration changes // _logger = Logger.GetPowertoolsLogger(); - _logger = LoggerFactoryHolder.GetOrCreateFactory().CreatePowertoolsLogger(); + _logger = LoggerFactoryHelper.CreateAndConfigureFactory(_currentConfig).CreatePowertoolsLogger(); + // Logger.ClearInstance(); } // Fetch the current configuration - _currentConfig = Logger.GetCurrentConfiguration(); + // Set operational flags based on current configuration _isDebug = _currentConfig.MinimumLogLevel <= LogLevel.Debug; diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs index c0fd479b..4822204f 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs @@ -34,6 +34,7 @@ internal class PowertoolsLoggerProvider : ILoggerProvider private readonly IDisposable? _onChangeToken; private PowertoolsLoggerConfiguration _currentConfig; private readonly IPowertoolsConfigurations _powertoolsConfigurations; + private bool _environmentConfigured; public PowertoolsLoggerProvider( PowertoolsLoggerConfiguration config, @@ -91,6 +92,7 @@ public void ConfigureFromEnvironment() : LoggingConstants.KeyLogLevel; ProcessSamplingRate(_currentConfig, _powertoolsConfigurations); + _environmentConfigured = true; } /// @@ -153,7 +155,7 @@ public void UpdateConfiguration(PowertoolsLoggerConfiguration config) _currentConfig = config; // Apply environment configurations if available - if (_powertoolsConfigurations != null) + if (_powertoolsConfigurations != null && !_environmentConfigured) { ConfigureFromEnvironment(); } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs index 0bbcd174..b4f686dd 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs @@ -108,8 +108,6 @@ public static void UseMinimumLogLevel(LogLevel logLevel) config.MinimumLogLevel = logLevel; }); - // Also directly update the log filter level to ensure it takes effect immediately - LoggerFactoryHolder.UpdateFilterLogLevel(logLevel); _loggerInstance = null; } @@ -183,6 +181,11 @@ internal static void Reset() _loggerInstance = null; RemoveAllKeys(); } + + internal static void ClearInstance() + { + _loggerInstance = null; + } /// /// Set the output for the logger diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs index 322e2389..6b56ee02 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs @@ -21,16 +21,12 @@ public static class PowertoolsLoggingBuilderExtensions private static readonly object _lock = new(); private static PowertoolsLoggerConfiguration _currentConfig = new(); - public static void UpdateConfiguration(PowertoolsLoggerConfiguration config) + internal static void UpdateConfiguration(PowertoolsLoggerConfiguration config) { lock (_lock) { // Update the shared configuration _currentConfig = config; - - // Uncomment this line to update the filter level - if(config.MinimumLogLevel != LogLevel.None) - LoggerFactoryHolder.UpdateFilterLogLevel(config.MinimumLogLevel); // Notify all providers about the change foreach (var provider in AllProviders) @@ -40,7 +36,7 @@ public static void UpdateConfiguration(PowertoolsLoggerConfiguration config) } } - public static PowertoolsLoggerConfiguration GetCurrentConfiguration() + internal static PowertoolsLoggerConfiguration GetCurrentConfiguration() { lock (_lock) { @@ -132,6 +128,7 @@ public static ILoggingBuilder AddPowertoolsLogger( return bufferingProvider; })); } + return builder; } diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs index c327d16d..0162ef97 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs @@ -492,19 +492,35 @@ public void LoggingAspect_ShouldImmediatelyApplyFilterLevelChanges() { // Arrange var consoleOut = GetConsoleOutput(); - + Logger.UseMinimumLogLevel(LogLevel.Error); // Act Logger.LogInformation("This should NOT be logged"); _testHandlers.TestMethodDebug(); // Should change level to Debug Logger.LogInformation("This should be logged"); - + // Assert + consoleOut.Received(1).WriteLine(Arg.Is(s => s.Contains("\"message\":\"This should be logged\""))); consoleOut.DidNotReceive().WriteLine(Arg.Is(s => s.Contains("\"message\":\"This should NOT be logged\""))); + + Logger.UseMinimumLogLevel(LogLevel.Warning); + + Logger.LogInformation("Information should not be logged"); + + Logger.UseMinimumLogLevel(LogLevel.Information); + Logger.LogDebug("Debug should not be logged"); + Logger.LogInformation("Information should be logged"); + + consoleOut.Received(1).WriteLine(Arg.Is(s => + s.Contains("\"message\":\"Information should be logged\""))); + consoleOut.DidNotReceive().WriteLine(Arg.Is(s => + s.Contains("\"message\":\"Information should not be logged\""))); + consoleOut.DidNotReceive().WriteLine(Arg.Is(s => + s.Contains("\"message\":\"Debug should not be logged\""))); } public void Dispose() diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LambdaContextBufferingTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LambdaContextBufferingTests.cs new file mode 100644 index 00000000..8dd41ac1 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LambdaContextBufferingTests.cs @@ -0,0 +1,519 @@ +using System; +using System.Threading.Tasks; +using Amazon.Lambda.Core; +using Amazon.Lambda.TestUtilities; +using AWS.Lambda.Powertools.Common.Tests; +using AWS.Lambda.Powertools.Logging.Internal; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace AWS.Lambda.Powertools.Logging.Tests.Buffering +{ + [Collection("Sequential")] + public class LambdaContextBufferingTests : IDisposable + { + private readonly ITestOutputHelper _output; + private readonly TestLoggerOutput _consoleOut; + + public LambdaContextBufferingTests(ITestOutputHelper output) + { + _output = output; + _consoleOut = new TestLoggerOutput(); + LogBufferManager.ResetForTesting(); + } + + [Fact] + public void DisabledBuffering_LogsAllLevelsDirectly() + { + // Arrange + var logger = CreateLogger(LogLevel.Debug, false, LogLevel.Debug); + var handler = new LambdaHandler(logger); + var context = CreateTestContext("test-request-2"); + + // Act + handler.TestMethod("Event", context); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Information message", output); + Assert.Contains("Debug message", output); + Assert.Contains("Error message", output); + } + + [Fact] + public void FlushOnErrorEnabled_AutomaticallyFlushesBuffer() + { + // Arrange + var logger = CreateLoggerWithFlushOnError(true); + var handler = new ErrorOnlyHandler(logger); + var context = CreateTestContext("test-request-3"); + + // Act + handler.TestMethod("Event", context); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Debug message", output); + Assert.Contains("Error triggering flush", output); + } + + [Fact] + public async Task AsyncOperations_MaintainBufferContext() + { + // Arrange + var logger = CreateLogger(LogLevel.Information, true, LogLevel.Debug); + var handler = new AsyncLambdaHandler(logger); + var context = CreateTestContext("async-test"); + + // Act + await handler.TestMethodAsync("Event", context); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Async info message", output); + Assert.Contains("Debug from task 1", output); + Assert.Contains("Debug from task 2", output); + } + + private TestLambdaContext CreateTestContext(string requestId) + { + return new TestLambdaContext + { + FunctionName = "test-function", + FunctionVersion = "1", + AwsRequestId = requestId, + InvokedFunctionArn = "arn:aws:lambda:us-east-1:123456789012:function:test-function" + }; + } + + private int CountOccurrences(string text, string pattern) + { + int count = 0; + int i = 0; + while ((i = text.IndexOf(pattern, i)) != -1) + { + i += pattern.Length; + count++; + } + + return count; + } + + private ILogger CreateLogger(LogLevel minimumLevel, bool enableBuffering, LogLevel bufferAtLevel) + { + return LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "test-service"; + config.MinimumLogLevel = minimumLevel; + config.LogOutput = _consoleOut; + config.LogBuffering = new LogBufferingOptions + { + Enabled = enableBuffering, + BufferAtLogLevel = bufferAtLevel + }; + }); + }).CreatePowertoolsLogger(); + } + + private ILogger CreateLoggerWithFlushOnError(bool flushOnError) + { + return LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "test-service"; + config.MinimumLogLevel = LogLevel.Information; + config.LogOutput = _consoleOut; + config.LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug, + FlushOnErrorLog = flushOnError + }; + }); + }).CreatePowertoolsLogger(); + } + + public void Dispose() + { + Logger.ClearBuffer(); + LogBufferManager.ResetForTesting(); + } + } + + + [Collection("Sequential")] + public class StaticLoggerBufferingTests : IDisposable + { + private readonly TestLoggerOutput _consoleOut; + private readonly ITestOutputHelper _output; + + public StaticLoggerBufferingTests(ITestOutputHelper output) + { + _output = output; + _consoleOut = new TestLoggerOutput(); + + // Configure static Logger with our test output + Logger.SetOutput(_consoleOut); + } + + [Fact] + public void StaticLogger_BasicBufferingBehavior() + { + // Arrange - explicitly configure Logger for this test + // First reset any existing configuration + Logger.Reset(); + + // Configure the logger with the test output + Logger.SetOutput(_consoleOut); + + // Now configure buffering options + Logger.UseMinimumLogLevel(LogLevel.Information); // Set to Debug to capture all logs + Logger.UseLogBuffering(new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug, + FlushOnErrorLog = false // Disable auto-flush to test manual flush + }); + + // Set invocation ID manually + LogBufferManager.SetInvocationId("test-static-request-1"); + + // Act - log messages + Logger.AppendKey("custom-key", "custom-value"); + Logger.LogInformation("Information message"); + Logger.LogDebug("Debug message"); // Should be buffered + + // Check the internal state before flush + var outputBeforeFlush = _consoleOut.ToString(); + _output.WriteLine($"Before flush: {outputBeforeFlush}"); + Assert.DoesNotContain("Debug message", outputBeforeFlush); + + // Flush the buffer + Logger.FlushBuffer(); + + // Assert after flush + var outputAfterFlush = _consoleOut.ToString(); + _output.WriteLine($"After flush: {outputAfterFlush}"); + Assert.Contains("Debug message", outputAfterFlush); + } + + [Fact] + public void StaticLogger_WithLoggingDecoratedHandler() + { + // Arrange + Logger.UseMinimumLogLevel(LogLevel.Information); + var handler = new StaticLambdaHandler(); + var context = new TestLambdaContext + { + AwsRequestId = "test-static-request-2", + FunctionName = "test-function" + }; + + // Act + handler.TestMethod("test-event", context); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Information message", output); + Assert.Contains("Debug message", output); + Assert.Contains("Error message", output); + Assert.Contains("custom-key", output); + Assert.Contains("custom-value", output); + } + + [Fact] + public void StaticLogger_ClearBufferRemovesLogs() + { + // Arrange + Logger.UseMinimumLogLevel(LogLevel.Information); + Logger.UseLogBuffering(new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug + }); + + // Set invocation ID + LogBufferManager.SetInvocationId("test-static-request-3"); + + // Act - log message and clear buffer + Logger.LogDebug("Debug message before clear"); + Logger.ClearBuffer(); + Logger.LogDebug("Debug message after clear"); + Logger.FlushBuffer(); + + // Assert + var output = _consoleOut.ToString(); + Assert.DoesNotContain("Debug message before clear", output); + Assert.Contains("Debug message after clear", output); + } + + [Fact] + public void StaticLogger_FlushOnErrorLogEnabled() + { + // Arrange + Logger.UseMinimumLogLevel(LogLevel.Information); + Logger.UseLogBuffering(new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug, + FlushOnErrorLog = true + }); + + // Set invocation ID + LogBufferManager.SetInvocationId("test-static-request-4"); + + // Act - log debug then error + Logger.LogDebug("Debug message"); + Logger.LogError("Error message"); + + // Assert - error should trigger flush + var output = _consoleOut.ToString(); + Assert.Contains("Debug message", output); + Assert.Contains("Error message", output); + } + + [Fact] + public void StaticLogger_MultipleInvocationsIsolated() + { + // Arrange + Logger.UseMinimumLogLevel(LogLevel.Information); + Logger.UseLogBuffering(new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug + }); + + // Act - first invocation + LogBufferManager.SetInvocationId("test-static-request-5A"); + Logger.LogDebug("Debug from invocation A"); + + // Switch to second invocation + LogBufferManager.SetInvocationId("test-static-request-5B"); + Logger.LogDebug("Debug from invocation B"); + Logger.FlushBuffer(); // Only flush B + + // Assert - after first flush + var outputAfterFirstFlush = _consoleOut.ToString(); + Assert.Contains("Debug from invocation B", outputAfterFirstFlush); + Assert.DoesNotContain("Debug from invocation A", outputAfterFirstFlush); + + // Switch back to first invocation and flush + LogBufferManager.SetInvocationId("test-static-request-5A"); + Logger.FlushBuffer(); + + // Assert - after second flush + var outputAfterSecondFlush = _consoleOut.ToString(); + Assert.Contains("Debug from invocation A", outputAfterSecondFlush); + } + + [Fact] + public void StaticLogger_FlushOnErrorDisabled() + { + // Arrange + Logger.Reset(); + Logger.SetOutput(_consoleOut); + Logger.UseMinimumLogLevel(LogLevel.Information); + Logger.UseLogBuffering(new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug, + FlushOnErrorLog = false // Disable auto-flush + }); + + LogBufferManager.SetInvocationId("test-static-request-6"); + + // Act - log debug then error + Logger.LogDebug("Debug message with auto-flush disabled"); + Logger.LogError("Error message that should not trigger flush"); + + // Assert - debug message should remain buffered + var output = _consoleOut.ToString(); + Assert.DoesNotContain("Debug message with auto-flush disabled", output); + Assert.Contains("Error message that should not trigger flush", output); + + // Now manually flush and verify debug message appears + Logger.FlushBuffer(); + output = _consoleOut.ToString(); + Assert.Contains("Debug message with auto-flush disabled", output); + } + + [Fact] + public void StaticLogger_WithCustomHandlerThatDoesntFlush() + { + // Arrange + Logger.Reset(); + Logger.SetOutput(_consoleOut); + Logger.UseMinimumLogLevel(LogLevel.Information); + Logger.UseLogBuffering(new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug, + FlushOnErrorLog = false // Disable auto-flush + }); + + var handler = new StaticHandlerWithoutFlush(); + var context = new TestLambdaContext + { + AwsRequestId = "test-static-request-7", + FunctionName = "test-function" + }; + + // Act + handler.TestMethod("test-event", context); + + // Assert - debug logs should be buffered + var output = _consoleOut.ToString(); + Assert.Contains("Information message", output); + Assert.Contains("Error message", output); + Assert.DoesNotContain("Debug message", output); // Should still be buffered + + // Manually flush and check again + Logger.FlushBuffer(); + output = _consoleOut.ToString(); + Assert.Contains("Debug message", output); // Now appears + } + + [Fact] + public void StaticLogger_AsyncOperationsMaintainContext() + { + // Arrange + Logger.Reset(); + Logger.SetOutput(_consoleOut); + Logger.UseMinimumLogLevel(LogLevel.Information); + Logger.UseLogBuffering(new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug, + FlushOnErrorLog = false + }); + + LogBufferManager.SetInvocationId("test-static-request-8"); + + // Act - simulate async operations + Task.Run(() => { Logger.LogDebug("Debug from task 1"); }).Wait(); + + Task.Run(() => { Logger.LogDebug("Debug from task 2"); }).Wait(); + + Logger.LogInformation("Main thread info message"); + + // Flush buffers + Logger.FlushBuffer(); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Debug from task 1", output); + Assert.Contains("Debug from task 2", output); + Assert.Contains("Main thread info message", output); + } + + public void Dispose() + { + // Clean up all state between tests + Logger.ClearBuffer(); + LogBufferManager.ResetForTesting(); + LoggerFactoryHolder.Reset(); + _consoleOut.Clear(); + } + } + + public class StaticHandlerWithoutFlush + { + [Logging(LogEvent = true)] + public void TestMethod(string message, ILambdaContext lambdaContext) + { + // Configure logging but don't manually flush + Logger.AppendKey("custom-key", "custom-value"); + Logger.LogInformation("Information message"); + Logger.LogDebug("Debug message"); + Logger.LogError("Error message"); + // No FlushBuffer call + } + } + + public class StaticLambdaHandler + { + [Logging(LogEvent = true)] + public void TestMethod(string message, ILambdaContext lambdaContext) + { + // The handler will configure buffering internally + Logger.UseLogBuffering(new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug + }); + + Logger.AppendKey("custom-key", "custom-value"); + Logger.LogInformation("Information message"); + Logger.LogDebug("Debug message"); + Logger.LogError("Error message"); + Logger.FlushBuffer(); + } + } + + // Lambda handlers for testing + public class LambdaHandler + { + private readonly ILogger _logger; + + public LambdaHandler(ILogger logger) + { + _logger = logger; + } + + [Logging(LogEvent = true)] + public void TestMethod(string message, ILambdaContext lambdaContext) + { + _logger.AppendKey("custom-key", "custom-value"); + _logger.LogInformation("Information message"); + _logger.LogDebug("Debug message"); + _logger.LogError("Error message"); + _logger.FlushBuffer(); + } + } + + public class ErrorOnlyHandler + { + private readonly ILogger _logger; + + public ErrorOnlyHandler(ILogger logger) + { + _logger = logger; + } + + [Logging(LogEvent = true)] + public void TestMethod(string message, ILambdaContext lambdaContext) + { + _logger.LogDebug("Debug message"); + _logger.LogError("Error triggering flush"); + } + } + + public class AsyncLambdaHandler + { + private readonly ILogger _logger; + + public AsyncLambdaHandler(ILogger logger) + { + _logger = logger; + } + + [Logging(LogEvent = true)] + public async Task TestMethodAsync(string message, ILambdaContext lambdaContext) + { + _logger.LogInformation("Async info message"); + _logger.LogDebug("Async debug message"); + + var task1 = Task.Run(() => { _logger.LogDebug("Debug from task 1"); }); + + var task2 = Task.Run(() => { _logger.LogDebug("Debug from task 2"); }); + + await Task.WhenAll(task1, task2); + _logger.FlushBuffer(); + } + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingHandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingHandlerTests.cs new file mode 100644 index 00000000..b8406db2 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingHandlerTests.cs @@ -0,0 +1,361 @@ +using System; +using System.Threading.Tasks; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Common.Tests; +using AWS.Lambda.Powertools.Logging.Internal; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; + +namespace AWS.Lambda.Powertools.Logging.Tests.Buffering +{ + [Collection("Sequential")] + public class LogBufferingHandlerTests : IDisposable + { + private readonly ITestOutputHelper _output; + private readonly TestLoggerOutput _consoleOut; + + public LogBufferingHandlerTests(ITestOutputHelper output) + { + _output = output; + _consoleOut = new TestLoggerOutput(); + LogBufferManager.ResetForTesting(); + } + + [Fact] + public void BasicBufferingBehavior_BuffersDebugLogsOnly() + { + // Arrange + var logger = CreateLogger(LogLevel.Information, true, LogLevel.Debug); + var handler = new HandlerWithoutFlush(logger); // Use a handler that doesn't flush + LogBufferManager.SetInvocationId("test-invocation"); + + // Act - log messages without flushing + handler.TestMethod(); + + // Assert - before flush + var outputBeforeFlush = _consoleOut.ToString(); + Assert.Contains("Information message", outputBeforeFlush); + Assert.Contains("Error message", outputBeforeFlush); + Assert.Contains("custom-key", outputBeforeFlush); + Assert.Contains("custom-value", outputBeforeFlush); + Assert.DoesNotContain("Debug message", outputBeforeFlush); // Debug should be buffered + + // Now flush the buffer + Logger.FlushBuffer(); + + // Assert - after flush + var outputAfterFlush = _consoleOut.ToString(); + Assert.Contains("Debug message", outputAfterFlush); // Debug should now be present + } + + [Fact] + public void DisabledBuffering_LogsAllLevelsDirectly() + { + // Arrange + var logger = CreateLogger(LogLevel.Debug, false, LogLevel.Debug); + var handler = new Handlers(logger); + LogBufferManager.SetInvocationId("test-invocation"); + + // Act + handler.TestMethod(); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Information message", output); + Assert.Contains("Error message", output); + Assert.Contains("Debug message", output); // Should be logged directly + } + + [Fact] + public void FlushOnErrorEnabled_AutomaticallyFlushesBuffer() + { + // Arrange + var logger = CreateLoggerWithFlushOnError(true); + LogBufferManager.SetInvocationId("test-invocation"); + + // Act - with custom handler that doesn't manually flush + var handler = new CustomHandlerWithoutFlush(logger); + handler.TestMethod(); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Debug message", output); // Should be flushed by error log + Assert.Contains("Error triggering flush", output); + } + + [Fact] + public void FlushOnErrorDisabled_DoesNotAutomaticallyFlushBuffer() + { + // Arrange + var logger = CreateLoggerWithFlushOnError(false); + LogBufferManager.SetInvocationId("test-invocation"); + + // Act + var handler = new CustomHandlerWithoutFlush(logger); + handler.TestMethod(); + + // Assert + var output = _consoleOut.ToString(); + Assert.DoesNotContain("Debug message", output); // Should remain buffered + Assert.Contains("Error triggering flush", output); + } + + [Fact] + public void ClearingBuffer_RemovesBufferedLogs() + { + // Arrange + var logger = CreateLogger(LogLevel.Information, true, LogLevel.Debug); + LogBufferManager.SetInvocationId("test-invocation"); + + // Act + var handler = new ClearBufferHandler(logger); + handler.TestMethod(); + + // Assert + var output = _consoleOut.ToString(); + Assert.DoesNotContain("Debug message before clear", output); + Assert.Contains("Debug message after clear", output); + } + + [Fact] + public void MultipleInvocations_IsolateLogBuffers() + { + // Arrange + var logger = CreateLogger(LogLevel.Information, true, LogLevel.Debug); + var handler = new Handlers(logger); + + // Act + LogBufferManager.SetInvocationId("invocation-1"); + handler.TestMethod(); + + LogBufferManager.SetInvocationId("invocation-2"); + // Create a custom handler that logs different messages + var customHandler = new MultipleInvocationHandler(logger); + customHandler.TestMethod(); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Information message", output); // From first invocation + Assert.Contains("Second invocation info", output); // From second invocation + } + + [Fact] + public void MultipleProviders_AllProvidersReceiveLogs() + { + // Arrange + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions { Enabled = true, BufferAtLogLevel = LogLevel.Debug }, + LogOutput = _consoleOut + }; + + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + + // Create two separate providers + var provider1 = new BufferingLoggerProvider(config, powertoolsConfig); + var provider2 = new BufferingLoggerProvider(config, powertoolsConfig); + + var logger1 = provider1.CreateLogger("Provider1"); + var logger2 = provider2.CreateLogger("Provider2"); + + LogBufferManager.SetInvocationId("multi-provider-test"); + + // Act + logger1.LogDebug("Debug from provider 1"); + logger2.LogDebug("Debug from provider 2"); + + // Flush logs from all providers + Logger.FlushBuffer(); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Debug from provider 1", output); + Assert.Contains("Debug from provider 2", output); + } + + [Fact] + public async Task AsyncOperations_MaintainBufferContext() + { + // Arrange + var logger = CreateLogger(LogLevel.Information, true, LogLevel.Debug); + var handler = new AsyncHandler(logger); + LogBufferManager.SetInvocationId("async-test"); + + // Act + await handler.TestMethodAsync(); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Async info message", output); + Assert.Contains("Debug from task 1", output); + Assert.Contains("Debug from task 2", output); + } + + private ILogger CreateLogger(LogLevel minimumLevel, bool enableBuffering, LogLevel bufferAtLevel) + { + return LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "test-service"; + config.MinimumLogLevel = minimumLevel; + config.LogOutput = _consoleOut; + config.LogBuffering = new LogBufferingOptions + { + Enabled = enableBuffering, + BufferAtLogLevel = bufferAtLevel, + FlushOnErrorLog = false + }; + }); + }).CreatePowertoolsLogger(); + } + + private ILogger CreateLoggerWithFlushOnError(bool flushOnError) + { + return LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "test-service"; + config.MinimumLogLevel = LogLevel.Information; + config.LogOutput = _consoleOut; + config.LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug, + FlushOnErrorLog = flushOnError + }; + }); + }).CreatePowertoolsLogger(); + } + + public void Dispose() + { + // Clean up all state between tests + Logger.ClearBuffer(); + LogBufferManager.ResetForTesting(); + } + } + + // Additional test handlers with specific behavior + public class CustomHandlerWithoutFlush + { + private readonly ILogger _logger; + + public CustomHandlerWithoutFlush(ILogger logger) + { + _logger = logger; + } + + public void TestMethod() + { + _logger.LogDebug("Debug message"); + _logger.LogError("Error triggering flush"); + // No manual flush + } + } + + public class ClearBufferHandler + { + private readonly ILogger _logger; + + public ClearBufferHandler(ILogger logger) + { + _logger = logger; + } + + public void TestMethod() + { + _logger.LogDebug("Debug message before clear"); + Logger.ClearBuffer(); // Clear the buffer + _logger.LogDebug("Debug message after clear"); + Logger.FlushBuffer(); // Flush only second message + } + } + + public class MultipleInvocationHandler + { + private readonly ILogger _logger; + + public MultipleInvocationHandler(ILogger logger) + { + _logger = logger; + } + + public void TestMethod() + { + _logger.LogInformation("Second invocation info"); + _logger.LogDebug("Second invocation debug"); + _logger.FlushBuffer(); + } + } + + public class Handlers + { + private readonly ILogger _logger; + + public Handlers(ILogger logger) + { + _logger = logger; + } + + public void TestMethod() + { + _logger.AppendKey("custom-key", "custom-value"); + _logger.LogInformation("Information message"); + _logger.LogDebug("Debug message"); + + _logger.LogError("Error message"); + + _logger.FlushBuffer(); + } + } + + public class HandlerWithoutFlush + { + private readonly ILogger _logger; + + public HandlerWithoutFlush(ILogger logger) + { + _logger = logger; + } + + public void TestMethod() + { + _logger.AppendKey("custom-key", "custom-value"); + _logger.LogInformation("Information message"); + _logger.LogDebug("Debug message"); + _logger.LogError("Error message"); + // No flush here + } + } + + public class AsyncHandler + { + private readonly ILogger _logger; + + public AsyncHandler(ILogger logger) + { + _logger = logger; + } + + public async Task TestMethodAsync() + { + _logger.LogInformation("Async info message"); + _logger.LogDebug("Async debug message"); + + var task1 = Task.Run(() => { + _logger.LogDebug("Debug from task 1"); + }); + + var task2 = Task.Run(() => { + _logger.LogDebug("Debug from task 2"); + }); + + await Task.WhenAll(task1, task2); + _logger.FlushBuffer(); + } + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingTests.cs new file mode 100644 index 00000000..2d0d45cd --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingTests.cs @@ -0,0 +1,585 @@ +using System; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Common.Tests; +using AWS.Lambda.Powertools.Logging.Internal; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace AWS.Lambda.Powertools.Logging.Tests.Buffering +{ + [Collection("Sequential")] + public class LogBufferingTests : IDisposable + { + private readonly TestLoggerOutput _consoleOut; + + public LogBufferingTests() + { + _consoleOut = new TestLoggerOutput(); + } + + [Trait("Category", "BufferManager")] + [Fact] + public void SetInvocationId_IsolatesLogsBetweenInvocations() + { + // Arrange + var config = new PowertoolsLoggerConfiguration + { + LogBuffering = new LogBufferingOptions { Enabled = true }, + LogOutput = _consoleOut + }; + + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + var provider = new BufferingLoggerProvider(config, powertoolsConfig); + var logger = provider.CreateLogger("TestLogger"); + + // Act + LogBufferManager.SetInvocationId("invocation-1"); + logger.LogDebug("Debug message from invocation 1"); + + LogBufferManager.SetInvocationId("invocation-2"); + logger.LogDebug("Debug message from invocation 2"); + + LogBufferManager.SetInvocationId("invocation-1"); + logger.LogError("Error message from invocation 1"); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Error message from invocation 1", output); + Assert.Contains("Debug message from invocation 1", output); + Assert.DoesNotContain("Debug message from invocation 2", output); + } + + [Trait("Category", "BufferedLogger")] + [Fact] + public void BufferedLogger_OnlyBuffersConfiguredLogLevels() + { + // Arrange + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug + }, + LogOutput = _consoleOut + }; + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + var provider = new BufferingLoggerProvider(config, powertoolsConfig); + var logger = provider.CreateLogger("TestLogger"); + LogBufferManager.SetInvocationId("invocation-1"); + + // Act + logger.LogTrace("Trace message"); // Below buffer threshold, should be ignored + logger.LogDebug("Debug message"); // Should be buffered + logger.LogInformation("Info message"); // Above minimum, should be logged directly + + // Assert + var output = _consoleOut.ToString(); + Assert.DoesNotContain("Trace message", output); + Assert.DoesNotContain("Debug message", output); // Not flushed yet + Assert.Contains("Info message", output); + + // Flush the buffer + Logger.FlushBuffer(); + + output = _consoleOut.ToString(); + Assert.Contains("Debug message", output); // Now should be visible + } + + [Trait("Category", "BufferedLogger")] + [Fact] + public void FlushOnErrorLog_FlushesBufferWhenEnabled() + { + // Arrange + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug, + FlushOnErrorLog = true + }, + LogOutput = _consoleOut + }; + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + var provider = new BufferingLoggerProvider(config, powertoolsConfig); + var logger = provider.CreateLogger("TestLogger"); + LogBufferManager.SetInvocationId("invocation-1"); + + // Act + logger.LogDebug("Debug message 1"); // Should be buffered + logger.LogDebug("Debug message 2"); // Should be buffered + logger.LogError("Error message"); // Should trigger flush of buffer + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Debug message 1", output); + Assert.Contains("Debug message 2", output); + Assert.Contains("Error message", output); + } + + [Trait("Category", "BufferedLogger")] + [Fact] + public void ClearBuffer_RemovesAllBufferedLogs() + { + // Arrange + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug + }, + LogOutput = _consoleOut + }; + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + var provider = new BufferingLoggerProvider(config, powertoolsConfig); + var logger = provider.CreateLogger("TestLogger"); + LogBufferManager.SetInvocationId("invocation-1"); + + // Act + logger.LogDebug("Debug message 1"); // Should be buffered + logger.LogDebug("Debug message 2"); // Should be buffered + + Logger.ClearBuffer(); // Should clear all buffered logs + Logger.FlushBuffer(); // No logs should be output + + logger.LogDebug("Debug message 3"); // Should be buffered + Logger.FlushBuffer(); // Should output debug message 3 + + // Assert + var output = _consoleOut.ToString(); + Assert.DoesNotContain("Debug message 1", output); + Assert.DoesNotContain("Debug message 2", output); + Assert.Contains("Debug message 3", output); + } + + [Trait("Category", "BufferedLogger")] + [Fact] + public void BufferSizeLimit_DiscardOldestEntriesWhenExceeded() + { + // Arrange + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug, + MaxBytes = 1000 // Small buffer size to force overflow + }, + LogOutput = _consoleOut + }; + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + var provider = new BufferingLoggerProvider(config, powertoolsConfig); + var logger = provider.CreateLogger("TestLogger"); + LogBufferManager.SetInvocationId("invocation-1"); + + // Act + // Add enough logs to exceed buffer size + for (int i = 0; i < 20; i++) + { + logger.LogDebug($"Debug message {i} with enough characters to consume space in the buffer"); + } + + Logger.FlushBuffer(); + + // Assert + var output = _consoleOut.ToString(); + Assert.DoesNotContain("Debug message 0", output); // Older messages should be discarded + Assert.Contains("Debug message 19", output); // Newest messages should be kept + } + + [Trait("Category", "LoggerLifecycle")] + [Fact] + public void DisposingProvider_FlushesBufferedLogs() + { + // Arrange + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug + }, + LogOutput = _consoleOut + }; + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + var provider = new BufferingLoggerProvider(config, powertoolsConfig); + var logger = provider.CreateLogger("TestLogger"); + LogBufferManager.SetInvocationId("invocation-1"); + + // Act + logger.LogDebug("Debug message before disposal"); // Should be buffered + provider.Dispose(); // Should flush buffer + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Debug message before disposal", output); + } + + [Trait("Category", "LoggerIntegration")] + [Fact] + public void DirectLoggerAndBufferedLogger_WorkTogether() + { + // Arrange + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug + }, + LogOutput = _consoleOut + }; + + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + + // Create both standard and buffering providers + var standardProvider = new PowertoolsLoggerProvider(config, powertoolsConfig); + var bufferingProvider = new BufferingLoggerProvider(config, powertoolsConfig); + + var standardLogger = standardProvider.CreateLogger("StandardLogger"); + var bufferedLogger = bufferingProvider.CreateLogger("BufferedLogger"); + + LogBufferManager.SetInvocationId("test-invocation"); + + // Act + standardLogger.LogInformation("Direct info message"); + bufferedLogger.LogDebug("Buffered debug message"); + bufferedLogger.LogInformation("Direct info from buffered logger"); + + // Assert - before flush + var output = _consoleOut.ToString(); + Assert.Contains("Direct info message", output); + Assert.Contains("Direct info from buffered logger", output); + Assert.DoesNotContain("Buffered debug message", output); + + // Flush and check again + Logger.FlushBuffer(); + output = _consoleOut.ToString(); + Assert.Contains("Buffered debug message", output); + } + + [Trait("Category", "LoggerConfiguration")] + [Fact] + public void LoggerInitialization_RegistersWithBufferManager() + { + // Arrange + var config = new PowertoolsLoggerConfiguration + { + LogBuffering = new LogBufferingOptions { Enabled = true }, + LogOutput = _consoleOut + }; + + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + + // Act + var provider = new BufferingLoggerProvider(config, powertoolsConfig); + var logger = provider.CreateLogger("TestLogger"); + + LogBufferManager.SetInvocationId("test-id"); + logger.LogDebug("Test message"); + Logger.FlushBuffer(); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Test message", output); + } + + [Trait("Category", "LoggerOutput")] + [Fact] + public void CustomLogOutput_ReceivesLogs() + { + // Arrange + var customOutput = new TestLoggerOutput(); + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Debug, // Set to Debug to ensure we log directly + LogOutput = customOutput + }; + + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + var provider = new PowertoolsLoggerProvider(config, powertoolsConfig); + var logger = provider.CreateLogger("TestLogger"); + + // Act + logger.LogDebug("Direct debug message"); + + // Assert + var output = customOutput.ToString(); + Assert.Contains("Direct debug message", output); + } + + [Trait("Category", "LoggerIntegration")] + [Fact] + public void RegisteringMultipleProviders_AllWorkCorrectly() + { + // Arrange - create a clean configuration for this test + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug + }, + LogOutput = _consoleOut + }; + + PowertoolsLoggingBuilderExtensions.UpdateConfiguration(config); + + // Create providers using the shared configuration + var env = new PowertoolsEnvironment(); + var powertoolsConfig = new PowertoolsConfigurations(env); + + var provider1 = new BufferingLoggerProvider(config, powertoolsConfig); + var provider2 = new BufferingLoggerProvider(config, powertoolsConfig); + + var logger1 = provider1.CreateLogger("Logger1"); + var logger2 = provider2.CreateLogger("Logger2"); + + LogBufferManager.SetInvocationId("shared-invocation"); + + // Act + logger1.LogDebug("Debug from logger1"); + logger2.LogDebug("Debug from logger2"); + Logger.FlushBuffer(); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Debug from logger1", output); + Assert.Contains("Debug from logger2", output); + } + + [Trait("Category", "LoggerLifecycle")] + [Fact] + public void RegisteringLogBufferManager_HandlesMultipleProviders() + { + // Ensure we start with clean state + LogBufferManager.ResetForTesting(); + // Arrange + var config = new PowertoolsLoggerConfiguration + { + LogBuffering = new LogBufferingOptions { Enabled = true }, + LogOutput = _consoleOut + }; + + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + + // Create and register first provider + var provider1 = new BufferingLoggerProvider(config, powertoolsConfig); + var logger1 = provider1.CreateLogger("Logger1"); + // Explicitly dispose and unregister first provider + provider1.Dispose(); + + // Now create and register a second provider + var provider2 = new BufferingLoggerProvider(config, powertoolsConfig); + var logger2 = provider2.CreateLogger("Logger2"); + + LogBufferManager.SetInvocationId("test-invocation"); + + // Act + logger1.LogDebug("Debug from first provider"); + logger2.LogDebug("Debug from second provider"); + + // Only the second provider should be registered with the LogBufferManager + Logger.FlushBuffer(); + + // Assert + var output = _consoleOut.ToString(); + // Only the second provider's logs should be flushed + Assert.DoesNotContain("Debug from first provider", output); + Assert.Contains("Debug from second provider", output); + } + + [Trait("Category", "BufferEmpty")] + [Fact] + public void FlushingEmptyBuffer_DoesNotCauseErrors() + { + // Arrange + LogBufferManager.ResetForTesting(); + var config = new PowertoolsLoggerConfiguration + { + LogBuffering = new LogBufferingOptions { Enabled = true }, + LogOutput = _consoleOut + }; + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + var provider = new BufferingLoggerProvider(config, powertoolsConfig); + + // Act - flush without any logs + LogBufferManager.SetInvocationId("empty-test"); + Logger.FlushBuffer(); + + // Assert - should not throw exceptions + Assert.Empty(_consoleOut.ToString()); + } + + [Trait("Category", "LogLevelThreshold")] + [Fact] + public void LogsAtExactBufferThreshold_AreBuffered() + { + // Arrange + LogBufferManager.ResetForTesting(); + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug + }, + LogOutput = _consoleOut + }; + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + var provider = new BufferingLoggerProvider(config, powertoolsConfig); + var logger = provider.CreateLogger("TestLogger"); + + // Act + LogBufferManager.SetInvocationId("threshold-test"); + logger.LogDebug("Debug message exactly at threshold"); // Should be buffered + + // Assert before flush + Assert.DoesNotContain("Debug message exactly at threshold", _consoleOut.ToString()); + + // After flush + Logger.FlushBuffer(); + Assert.Contains("Debug message exactly at threshold", _consoleOut.ToString()); + } + + [Trait("Category", "LoggerDisabling")] + [Fact] + public void DisablingBuffering_StillLogsNormally() + { + // Arrange + LogBufferManager.ResetForTesting(); + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Debug, + LogBuffering = new LogBufferingOptions + { + Enabled = false // Buffering disabled + }, + LogOutput = _consoleOut + }; + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + var provider = new BufferingLoggerProvider(config, powertoolsConfig); + var logger = provider.CreateLogger("TestLogger"); + + // Act + LogBufferManager.SetInvocationId("disabled-test"); + logger.LogDebug("Debug message with buffering disabled"); + + // Assert - should log immediately even without flushing + Assert.Contains("Debug message with buffering disabled", _consoleOut.ToString()); + } + + [Trait("Category", "MultipleInvocations")] + [Fact] + public void SwitchingBetweenInvocations_PreservesSeparateBuffers() + { + // Arrange + LogBufferManager.ResetForTesting(); + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions { Enabled = true }, + LogOutput = _consoleOut + }; + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + var provider = new BufferingLoggerProvider(config, powertoolsConfig); + var logger = provider.CreateLogger("TestLogger"); + + // Act + // First invocation + LogBufferManager.SetInvocationId("invocation-A"); + logger.LogDebug("Debug for invocation A"); + + // Switch to second invocation + LogBufferManager.SetInvocationId("invocation-B"); + logger.LogDebug("Debug for invocation B"); + Logger.FlushBuffer(); // Only flush B + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Debug for invocation B", output); + Assert.DoesNotContain("Debug for invocation A", output); + + // Now flush A + LogBufferManager.SetInvocationId("invocation-A"); + Logger.FlushBuffer(); + + output = _consoleOut.ToString(); + Assert.Contains("Debug for invocation A", output); + } + + [Trait("Category", "ConfigurationUpdate")] + [Fact] + public void ChangingConfigurationDynamically_UpdatesBufferingBehavior() + { + // Arrange + LogBufferManager.ResetForTesting(); + var initialConfig = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Warning, // Keep this as Warning + LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Information // Buffer at info level + }, + LogOutput = _consoleOut + }; + + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + + // Create provider with initial config + var provider = new BufferingLoggerProvider(initialConfig, powertoolsConfig); + var logger = provider.CreateLogger("TestLogger"); + + LogBufferManager.SetInvocationId("config-test"); + + // Act - with initial config + logger.LogInformation("Info message with initial config"); + + // Should be buffered (Info < Warning minimum level) + Assert.DoesNotContain("Info message with initial config", _consoleOut.ToString()); + + // Update config to not buffer info anymore + var updatedConfig = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, // Changed to Information + LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug // Only buffer debug level now + }, + LogOutput = _consoleOut + }; + + // Directly update the provider's configuration + provider.UpdateConfiguration(updatedConfig); + + // Log with updated config + logger.LogInformation("Info message with updated config"); + + // Assert - should log immediately with updated config + Assert.Contains("Info message with updated config", _consoleOut.ToString()); + + // Flush and check if first message appears + Logger.FlushBuffer(); + Assert.Contains("Info message with initial config", _consoleOut.ToString()); + } + + public void Dispose() + { + // Clean up all state between tests + Logger.ClearBuffer(); + LogBufferManager.ResetForTesting(); + } + } +} \ No newline at end of file From 8abfa70acecf5b8cd4b9786119c5c83cd46651e5 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 2 Apr 2025 17:24:30 +0100 Subject: [PATCH 28/49] update and add tests. refactor logger formatter to support @ and non @ {} --- ...PowertoolsLoggerConfigurationExtensions.cs | 32 -- .../Internal/LoggerFactoryHolder.cs | 51 +- .../Internal/LoggingAspect.cs | 10 +- .../Internal/PowertoolsLogger.cs | 43 +- .../AWS.Lambda.Powertools.Logging/Logger.cs | 127 +---- .../Attributes/LoggingAttributeTest.cs | 87 ++- .../Attributes/ServiceTests.cs | 3 +- .../Buffering/LambdaContextBufferingTests.cs | 159 +++--- .../Formatter/LogFormatterTest.cs | 29 +- .../Formatter/LogFormattingTests.cs | 525 ++++++++++++++++++ .../Handlers/HandlerTests.cs | 376 +++++++++++++ .../Utilities/PowertoolsLoggerHelpersTests.cs | 10 +- 12 files changed, 1115 insertions(+), 337 deletions(-) delete mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerConfigurationExtensions.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormattingTests.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/HandlerTests.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerConfigurationExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerConfigurationExtensions.cs deleted file mode 100644 index 80b4bd93..00000000 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/PowertoolsLoggerConfigurationExtensions.cs +++ /dev/null @@ -1,32 +0,0 @@ -// using System.Text.Json; -// using Microsoft.Extensions.Logging; -// -// namespace AWS.Lambda.Powertools.Logging.Internal.Helpers; -// -// /// -// /// Extension methods for handling configuration copying between PowertoolsLogger configurations -// /// -// internal static class PowertoolsLoggerConfigurationExtensions -// { -// /// -// /// Copies configuration values from source to destination configuration -// /// -// /// The destination configuration to copy values to -// /// The source configuration to copy values from -// /// The updated destination configuration -// public static PowertoolsLoggerConfiguration CopyFrom(this PowertoolsLoggerConfiguration destination, PowertoolsLoggerConfiguration source) -// { -// destination.Service = source.Service; -// destination.SamplingRate = source.SamplingRate; -// destination.MinimumLogLevel = source.MinimumLogLevel; -// destination.LoggerOutputCase = source.LoggerOutputCase; -// destination.LoggerOutput = source.LoggerOutput; -// destination.JsonOptions = source.JsonOptions; -// destination.TimestampFormat = source.TimestampFormat; -// destination.LogFormatter = source.LogFormatter; -// destination.LogLevelKey = source.LogLevelKey; -// destination.LogBuffering = source.LogBuffering; -// -// return destination; -// } -// } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerFactoryHolder.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerFactoryHolder.cs index 74e31371..ee01dc6e 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerFactoryHolder.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerFactoryHolder.cs @@ -14,8 +14,6 @@ */ using System; -using System.Threading; -using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal.Helpers; using Microsoft.Extensions.Logging; @@ -28,31 +26,6 @@ internal static class LoggerFactoryHolder { private static ILoggerFactory? _factory; private static readonly object _lock = new object(); - private static bool _isConfigured = false; - - // private static LogLevel _currentFilterLevel = LogLevel.Information; - - // /// - // /// Updates the filter log level at runtime - // /// - // /// The new minimum log level - // public static void UpdateFilterLogLevel(LogLevel logLevel) - // { - // lock (_lock) - // { - // // Only reset if level actually changes - // if (_currentFilterLevel != logLevel) - // { - // _currentFilterLevel = logLevel; - // - // if (_factory != null) - // { - // try { _factory.Dispose(); } catch { /* Ignore */ } - // _factory = null; - // } - // } - // } - // } /// /// Gets or creates the shared logger factory @@ -77,7 +50,6 @@ public static void SetFactory(ILoggerFactory factory) lock (_lock) { _factory = factory; - _isConfigured = true; Logger.ClearInstance(); } } @@ -90,22 +62,17 @@ internal static void Reset() lock (_lock) { // Dispose the old factory if it exists - if (_factory != null) + if (_factory == null) return; + try { - try - { - _factory.Dispose(); - } - catch - { - // Ignore disposal errors - } - - _factory = null; + _factory.Dispose(); } - - // _currentFilterLevel = LogLevel.None; - _isConfigured = false; + catch + { + // Ignore disposal errors + } + + _factory = null; } } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index a141dc64..cc2272f8 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -22,7 +22,6 @@ using AspectInjector.Broker; 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; @@ -67,6 +66,9 @@ public class LoggingAspect private bool _bufferingEnabled; private PowertoolsLoggerConfiguration _currentConfig; + /// + /// Initializes a new instance of the class. + /// public LoggingAspect(ILogger logger) { _logger = logger ?? LoggerFactoryHolder.GetOrCreateFactory().CreatePowertoolsLogger(); @@ -93,13 +95,9 @@ private void InitializeLogger(LoggingAttribute trigger) if (hasSamplingRate) _currentConfig.SamplingRate = trigger.SamplingRate; // Need to refresh the logger after configuration changes - // _logger = Logger.GetPowertoolsLogger(); _logger = LoggerFactoryHelper.CreateAndConfigureFactory(_currentConfig).CreatePowertoolsLogger(); - // Logger.ClearInstance(); + Logger.ClearInstance(); } - - // Fetch the current configuration - // Set operational flags based on current configuration _isDebug = _currentConfig.MinimumLogLevel <= LogLevel.Debug; diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs index d4bae34a..3b4356fe 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -17,9 +17,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; -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; @@ -428,22 +426,38 @@ private static Dictionary GetScopeKeys(TState state) if (!string.IsNullOrWhiteSpace(key)) keys.TryAdd(key, value); } - break; + case IEnumerable> objectPairs: foreach (var (key, value) in objectPairs) { if (!string.IsNullOrWhiteSpace(key)) keys.TryAdd(key, value); } - break; + default: + // Skip property reflection for primitive types, strings and value types + if (state is string || + (state.GetType().IsPrimitive) || + state is ValueType) + { + // Don't extract properties from primitives or strings + break; + } + + // For complex objects, use reflection to get properties foreach (var property in state.GetType().GetProperties()) { - keys.TryAdd(property.Name, property.GetValue(state)); + try + { + keys.TryAdd(property.Name, property.GetValue(state)); + } + catch + { + // Safely ignore reflection exceptions + } } - break; } @@ -511,7 +525,7 @@ private Dictionary ExtractStructuredParameters(TState st { // Format the value using the specified format string formattedValue = formattable.ToString(format, System.Globalization.CultureInfo.InvariantCulture); - + // Try to preserve the numeric type if possible if (double.TryParse(formattedValue, out double numericValue)) { @@ -529,8 +543,19 @@ private Dictionary ExtractStructuredParameters(TState st } else { - // For regular objects, use the value directly - parameters[actualParamName] = prop.Value; + // For regular objects, convert to string if it's not a primitive type + if (prop.Value != null && + !(prop.Value is string) && + !(prop.Value is ValueType) && + !(prop.Value.GetType().IsPrimitive)) + { + parameters[actualParamName] = prop.Value.ToString(); + } + else + { + // For primitives, use the value directly + parameters[actualParamName] = prop.Value; + } } } } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs index b4f686dd..2cf197b8 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs @@ -17,6 +17,7 @@ using System.Text.Json; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Internal.Helpers; using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging; @@ -41,7 +42,7 @@ private static ILogger LoggerInstance { if (_loggerInstance == null) { - _loggerInstance = GetPowertoolsLogger(); + _loggerInstance = Initialize(); } } } @@ -49,6 +50,11 @@ private static ILogger LoggerInstance } } + private static ILogger Initialize() + { + return LoggerFactoryHolder.GetOrCreateFactory().CreatePowertoolsLogger(); + } + /// /// Configure with an existing logger factory /// @@ -63,114 +69,16 @@ internal static void Configure(ILoggerFactory loggerFactory) /// Configure using a configuration action /// /// - internal static void Configure(Action configure) + public static void Configure(Action configure) { lock (Lock) { - var config = GetCurrentConfiguration(); + var config = new PowertoolsLoggerConfiguration(); configure(config); - PowertoolsLoggingBuilderExtensions.UpdateConfiguration(config); + _loggerInstance = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); } } - internal static PowertoolsLoggerConfiguration GetCurrentConfiguration() - { - return PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); - } - - /// - /// Get the Powertools logger instance - /// - /// The configured Powertools logger - internal static ILogger GetPowertoolsLogger() - { - return LoggerFactoryHolder.GetOrCreateFactory().CreatePowertoolsLogger(); - } - - /// - /// Configure logger output case (snake_case, camelCase, PascalCase) - /// - /// The case to use for the output - public static void UseOutputCase(LoggerOutputCase outputCase) - { - Configure(config => { - config.LoggerOutputCase = outputCase; - }); - } - - /// - /// Configures the minimum log level - /// - /// The minimum log level to display - public static void UseMinimumLogLevel(LogLevel logLevel) - { - Configure(config => { - config.MinimumLogLevel = logLevel; - }); - - _loggerInstance = null; - } - - /// - /// Configures the service name - /// - /// The service name to use in logs - public static void UseServiceName(string serviceName) - { - if (string.IsNullOrEmpty(serviceName)) - throw new ArgumentException("Service name cannot be null or empty", nameof(serviceName)); - - Configure(config => { - config.Service = serviceName; - }); - } - - /// - /// Sets the sampling rate for logs - /// - /// The rate (0.0 to 1.0) for sampling - public static void UseSamplingRate(double samplingRate) - { - Configure(config => { - config.SamplingRate = samplingRate; - }); - } - - /// - /// Log buffering options. - /// - /// Logger.UseLogBuffering(new LogBufferingOptions - /// { - /// Enabled = true, - /// BufferAtLogLevel = LogLevel.Debug - /// }); - /// - /// - public static void UseLogBuffering(LogBufferingOptions logBuffering) - { - if (logBuffering == null) - throw new ArgumentNullException(nameof(logBuffering)); - - Configure(config => { - config.LogBuffering = logBuffering; - }); - } - -#if NET8_0_OR_GREATER - /// - /// Configure JSON serialization options - /// - /// The JSON options to use - public static void UseJsonOptions(JsonSerializerOptions jsonOptions) - { - if (jsonOptions == null) - throw new ArgumentNullException(nameof(jsonOptions)); - - Configure(config => { - config.JsonOptions = jsonOptions; - }); - } -#endif /// /// Reset the logger for testing @@ -186,19 +94,4 @@ internal static void ClearInstance() { _loggerInstance = null; } - - /// - /// Set the output for the logger - /// - /// - /// - public static void SetOutput(IConsoleWrapper consoleOut) - { - if (consoleOut == null) - throw new ArgumentNullException(nameof(consoleOut)); - - Configure(config => { - config.LogOutput = consoleOut; - }); - } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs index 0162ef97..86882370 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs @@ -69,7 +69,11 @@ public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContextAndLogDebu { // Arrange var consoleOut = GetConsoleOutput(); - + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); + // Act _testHandlers.TestMethodDebug(); @@ -110,7 +114,11 @@ public void OnEntry_WhenEventArgExist_LogEvent() // Arrange var consoleOut = GetConsoleOutput(); var correlationId = Guid.NewGuid().ToString(); - + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); + var context = new TestLambdaContext() { FunctionName = "PowertoolsLoggingSample-HelloWorldFunction-Gg8rhPwO7Wa1" @@ -156,7 +164,10 @@ public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArgAndLogDebug() { // Arrange var consoleOut = GetConsoleOutput(); - + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); // Act _testHandlers.LogEventDebug(); @@ -339,7 +350,11 @@ public void When_Setting_SamplingRate_Should_Add_Key() { // Arrange var consoleOut = GetConsoleOutput(); - + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); + // Act _testHandlers.HandlerSamplingRate(); @@ -355,7 +370,10 @@ public void When_Setting_Service_Should_Update_Key() { // Arrange var consoleOut = new TestLoggerOutput(); - Logger.SetOutput(consoleOut); + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); // Act _testHandlers.HandlerService(); @@ -371,7 +389,10 @@ public void When_Setting_LogLevel_Should_Update_LogLevel() { // Arrange var consoleOut = new TestLoggerOutput();; - Logger.SetOutput(consoleOut); + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); // Act _testHandlers.TestLogLevelCritical(); @@ -387,7 +408,11 @@ public void When_Setting_LogLevel_HigherThanInformation_Should_Not_LogEvent() { // Arrange var consoleOut = GetConsoleOutput(); - + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); + var context = new TestLambdaContext() { FunctionName = "PowertoolsLoggingSample-HelloWorldFunction-Gg8rhPwO7Wa1" @@ -405,7 +430,10 @@ public void When_LogLevel_Debug_Should_Log_Message_When_No_Context_And_LogEvent_ { // Arrange var consoleOut = GetConsoleOutput(); - + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); // Act _testHandlers.TestLogEventWithoutContext(); @@ -422,6 +450,10 @@ public void Should_Log_When_Not_Using_Decorator() { // Arrange var consoleOut = GetConsoleOutput(); + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); var test = new TestHandlers(); @@ -440,7 +472,11 @@ public void LoggingAspect_ShouldRespectDynamicLogLevelChanges() // Arrange var consoleOut = GetConsoleOutput(); - Logger.UseMinimumLogLevel(LogLevel.Warning); // Start with Warning level + Logger.Configure(options => + { + options.LogOutput = consoleOut; + options.MinimumLogLevel = LogLevel.Warning; + }); // Act _testHandlers.TestMethodDebug(); // Uses LogLevel.Debug attribute @@ -456,8 +492,11 @@ public void LoggingAspect_ShouldCorrectlyResetLogLevelAfterExecution() { // Arrange var consoleOut = GetConsoleOutput(); - - Logger.UseMinimumLogLevel(LogLevel.Warning); + Logger.Configure(options => + { + options.LogOutput = consoleOut; + options.MinimumLogLevel = LogLevel.Warning; + }); // Act - First call with Debug level attribute _testHandlers.TestMethodDebug(); @@ -478,6 +517,10 @@ public void LoggingAspect_ShouldRespectAttributePrecedenceOverEnvironment() // Arrange Environment.SetEnvironmentVariable("POWERTOOLS_LOG_LEVEL", "Error"); var consoleOut = GetConsoleOutput(); + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); // Act _testHandlers.TestMethodDebug(); // Uses LogLevel.Debug attribute @@ -493,7 +536,11 @@ public void LoggingAspect_ShouldImmediatelyApplyFilterLevelChanges() // Arrange var consoleOut = GetConsoleOutput(); - Logger.UseMinimumLogLevel(LogLevel.Error); + Logger.Configure(options => + { + options.LogOutput = consoleOut; + options.MinimumLogLevel = LogLevel.Error; + }); // Act Logger.LogInformation("This should NOT be logged"); @@ -506,21 +553,6 @@ public void LoggingAspect_ShouldImmediatelyApplyFilterLevelChanges() s.Contains("\"message\":\"This should be logged\""))); consoleOut.DidNotReceive().WriteLine(Arg.Is(s => s.Contains("\"message\":\"This should NOT be logged\""))); - - Logger.UseMinimumLogLevel(LogLevel.Warning); - - Logger.LogInformation("Information should not be logged"); - - Logger.UseMinimumLogLevel(LogLevel.Information); - Logger.LogDebug("Debug should not be logged"); - Logger.LogInformation("Information should be logged"); - - consoleOut.Received(1).WriteLine(Arg.Is(s => - s.Contains("\"message\":\"Information should be logged\""))); - consoleOut.DidNotReceive().WriteLine(Arg.Is(s => - s.Contains("\"message\":\"Information should not be logged\""))); - consoleOut.DidNotReceive().WriteLine(Arg.Is(s => - s.Contains("\"message\":\"Debug should not be logged\""))); } public void Dispose() @@ -532,7 +564,6 @@ private IConsoleWrapper GetConsoleOutput() { // Create a new mock each time var output = Substitute.For(); - Logger.SetOutput(output); return output; } diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/ServiceTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/ServiceTests.cs index 0dab6499..c1a24a97 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/ServiceTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/ServiceTests.cs @@ -23,7 +23,8 @@ public void When_Setting_Service_Should_Override_Env() Environment.SetEnvironmentVariable("POWERTOOLS_SERVICE_NAME", "Environment Service"); var consoleOut = Substitute.For(); - Logger.SetOutput(consoleOut); + Logger.Configure(options => + options.LogOutput = consoleOut); // Act _testHandler.LogWithEnv(); diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LambdaContextBufferingTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LambdaContextBufferingTests.cs index 8dd41ac1..0788b701 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LambdaContextBufferingTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LambdaContextBufferingTests.cs @@ -158,7 +158,8 @@ public StaticLoggerBufferingTests(ITestOutputHelper output) _consoleOut = new TestLoggerOutput(); // Configure static Logger with our test output - Logger.SetOutput(_consoleOut); + Logger.Configure(options => + options.LogOutput = _consoleOut); } [Fact] @@ -169,15 +170,16 @@ public void StaticLogger_BasicBufferingBehavior() Logger.Reset(); // Configure the logger with the test output - Logger.SetOutput(_consoleOut); - - // Now configure buffering options - Logger.UseMinimumLogLevel(LogLevel.Information); // Set to Debug to capture all logs - Logger.UseLogBuffering(new LogBufferingOptions + Logger.Configure(options => { - Enabled = true, - BufferAtLogLevel = LogLevel.Debug, - FlushOnErrorLog = false // Disable auto-flush to test manual flush + options.LogOutput = _consoleOut; + options.MinimumLogLevel = LogLevel.Information; + options.LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug, + FlushOnErrorLog = false // Disable auto-flush to test manual flush + }; }); // Set invocation ID manually @@ -206,7 +208,17 @@ public void StaticLogger_BasicBufferingBehavior() public void StaticLogger_WithLoggingDecoratedHandler() { // Arrange - Logger.UseMinimumLogLevel(LogLevel.Information); + Logger.Configure(options => + { + options.LogOutput = _consoleOut; + options.LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug, + FlushOnErrorLog = true + }; + }); + var handler = new StaticLambdaHandler(); var context = new TestLambdaContext { @@ -230,11 +242,15 @@ public void StaticLogger_WithLoggingDecoratedHandler() public void StaticLogger_ClearBufferRemovesLogs() { // Arrange - Logger.UseMinimumLogLevel(LogLevel.Information); - Logger.UseLogBuffering(new LogBufferingOptions + Logger.Configure(options => { - Enabled = true, - BufferAtLogLevel = LogLevel.Debug + options.LogOutput = _consoleOut; + options.MinimumLogLevel = LogLevel.Information; + options.LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug + }; }); // Set invocation ID @@ -256,12 +272,16 @@ public void StaticLogger_ClearBufferRemovesLogs() public void StaticLogger_FlushOnErrorLogEnabled() { // Arrange - Logger.UseMinimumLogLevel(LogLevel.Information); - Logger.UseLogBuffering(new LogBufferingOptions + Logger.Configure(options => { - Enabled = true, - BufferAtLogLevel = LogLevel.Debug, - FlushOnErrorLog = true + options.LogOutput = _consoleOut; + options.MinimumLogLevel = LogLevel.Information; + options.LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug, + FlushOnErrorLog = true + }; }); // Set invocation ID @@ -281,11 +301,15 @@ public void StaticLogger_FlushOnErrorLogEnabled() public void StaticLogger_MultipleInvocationsIsolated() { // Arrange - Logger.UseMinimumLogLevel(LogLevel.Information); - Logger.UseLogBuffering(new LogBufferingOptions + Logger.Configure(options => { - Enabled = true, - BufferAtLogLevel = LogLevel.Debug + options.LogOutput = _consoleOut; + options.MinimumLogLevel = LogLevel.Information; + options.LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug + }; }); // Act - first invocation @@ -316,13 +340,16 @@ public void StaticLogger_FlushOnErrorDisabled() { // Arrange Logger.Reset(); - Logger.SetOutput(_consoleOut); - Logger.UseMinimumLogLevel(LogLevel.Information); - Logger.UseLogBuffering(new LogBufferingOptions + Logger.Configure(options => { - Enabled = true, - BufferAtLogLevel = LogLevel.Debug, - FlushOnErrorLog = false // Disable auto-flush + options.LogOutput = _consoleOut; + options.MinimumLogLevel = LogLevel.Information; + options.LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug, + FlushOnErrorLog = false + }; }); LogBufferManager.SetInvocationId("test-static-request-6"); @@ -342,54 +369,21 @@ public void StaticLogger_FlushOnErrorDisabled() Assert.Contains("Debug message with auto-flush disabled", output); } - [Fact] - public void StaticLogger_WithCustomHandlerThatDoesntFlush() - { - // Arrange - Logger.Reset(); - Logger.SetOutput(_consoleOut); - Logger.UseMinimumLogLevel(LogLevel.Information); - Logger.UseLogBuffering(new LogBufferingOptions - { - Enabled = true, - BufferAtLogLevel = LogLevel.Debug, - FlushOnErrorLog = false // Disable auto-flush - }); - - var handler = new StaticHandlerWithoutFlush(); - var context = new TestLambdaContext - { - AwsRequestId = "test-static-request-7", - FunctionName = "test-function" - }; - - // Act - handler.TestMethod("test-event", context); - - // Assert - debug logs should be buffered - var output = _consoleOut.ToString(); - Assert.Contains("Information message", output); - Assert.Contains("Error message", output); - Assert.DoesNotContain("Debug message", output); // Should still be buffered - - // Manually flush and check again - Logger.FlushBuffer(); - output = _consoleOut.ToString(); - Assert.Contains("Debug message", output); // Now appears - } - [Fact] public void StaticLogger_AsyncOperationsMaintainContext() { // Arrange - Logger.Reset(); - Logger.SetOutput(_consoleOut); - Logger.UseMinimumLogLevel(LogLevel.Information); - Logger.UseLogBuffering(new LogBufferingOptions + // Logger.Reset(); + Logger.Configure(options => { - Enabled = true, - BufferAtLogLevel = LogLevel.Debug, - FlushOnErrorLog = false + options.LogOutput = _consoleOut; + options.MinimumLogLevel = LogLevel.Information; + options.LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug, + FlushOnErrorLog = false + }; }); LogBufferManager.SetInvocationId("test-static-request-8"); @@ -421,32 +415,11 @@ public void Dispose() } } - public class StaticHandlerWithoutFlush - { - [Logging(LogEvent = true)] - public void TestMethod(string message, ILambdaContext lambdaContext) - { - // Configure logging but don't manually flush - Logger.AppendKey("custom-key", "custom-value"); - Logger.LogInformation("Information message"); - Logger.LogDebug("Debug message"); - Logger.LogError("Error message"); - // No FlushBuffer call - } - } - public class StaticLambdaHandler { [Logging(LogEvent = true)] public void TestMethod(string message, ILambdaContext lambdaContext) { - // The handler will configure buffering internally - Logger.UseLogBuffering(new LogBufferingOptions - { - Enabled = true, - BufferAtLogLevel = LogLevel.Debug - }); - Logger.AppendKey("custom-key", "custom-value"); Logger.LogInformation("Information message"); Logger.LogDebug("Debug message"); diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs index 0acf12cd..37f0fd83 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs @@ -48,7 +48,10 @@ public LogFormatterTest() public void Serialize_ShouldHandleEnumValues() { var consoleOut = Substitute.For(); - Logger.SetOutput(consoleOut); + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); var lambdaContext = new TestLambdaContext { @@ -231,7 +234,11 @@ public void Should_Log_CustomFormatter_When_Decorated() { ResetAllState(); var consoleOut = Substitute.For(); - Logger.SetOutput(consoleOut); + Logger.Configure(options => + { + options.LogOutput = consoleOut; + options.LogFormatter = new CustomLogFormatter(); + }); var lambdaContext = new TestLambdaContext { @@ -242,7 +249,7 @@ public void Should_Log_CustomFormatter_When_Decorated() MemoryLimitInMB = 128 }; - Logger.UseFormatter(new CustomLogFormatter()); + // Logger.UseFormatter(new CustomLogFormatter()); _testHandler.TestCustomFormatterWithDecorator("test", lambdaContext); // serializer works differently in .net 8 and AOT. In .net 6 it writes properties that have null @@ -268,7 +275,11 @@ public void Should_Log_CustomFormatter_When_No_Decorated_Just_Log() { ResetAllState(); var consoleOut = Substitute.For(); - Logger.SetOutput(consoleOut); + Logger.Configure(options => + { + options.LogOutput = consoleOut; + options.LogFormatter = new CustomLogFormatter(); + }); var lambdaContext = new TestLambdaContext { @@ -279,7 +290,7 @@ public void Should_Log_CustomFormatter_When_No_Decorated_Just_Log() MemoryLimitInMB = 128 }; - Logger.UseFormatter(new CustomLogFormatter()); + // Logger.UseFormatter(new CustomLogFormatter()); _testHandler.TestCustomFormatterNoDecorator("test", lambdaContext); @@ -305,9 +316,13 @@ public void Should_Log_CustomFormatter_When_No_Decorated_Just_Log() public void Should_Log_CustomFormatter_When_Decorated_No_Context() { var consoleOut = Substitute.For(); - Logger.SetOutput(consoleOut); + Logger.Configure(options => + { + options.LogOutput = consoleOut; + options.LogFormatter = new CustomLogFormatter(); + }); - Logger.UseFormatter(new CustomLogFormatter()); + // Logger.UseFormatter(new CustomLogFormatter()); _testHandler.TestCustomFormatterWithDecoratorNoContext("test"); diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormattingTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormattingTests.cs new file mode 100644 index 00000000..44f2656c --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormattingTests.cs @@ -0,0 +1,525 @@ +using System; +using System.Collections.Generic; +using AWS.Lambda.Powertools.Common.Tests; +using AWS.Lambda.Powertools.Logging.Tests.Handlers; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; + +namespace AWS.Lambda.Powertools.Logging.Tests.Formatter +{ + [Collection("Sequential")] + public class LogFormattingTests + { + private readonly ITestOutputHelper _output; + + public LogFormattingTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void TestNumericFormatting() + { + // Set culture for thread and format provider + var originalCulture = System.Threading.Thread.CurrentThread.CurrentCulture; + System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US"); + + + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "format-test-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.SnakeCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + // Test numeric format specifiers + logger.LogInformation("Price: {price:0.00}", 123.4567); + logger.LogInformation("Percentage: {percent:0.0%}", 0.1234); + // Use explicit dollar sign instead of culture-dependent 'C' + // The logger explicitly uses InvariantCulture when formatting values, which uses "¤" as the currency symbol. + // This is by design to ensure consistent logging output regardless of server culture settings. + // By using $ directly in the format string as shown above, you bypass the culture-specific currency symbol and get the expected output in your tests. + logger.LogInformation("Currency: {amount:$#,##0.00}", 42.5); + + logger.LogInformation("Hex: {hex:X}", 255); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // These should all be properly formatted in the log + Assert.Contains("\"price\":123.46", logOutput); + Assert.Contains("\"percent\":\"12.3%\"", logOutput); + Assert.Contains("\"amount\":\"$42.50\"", logOutput); + Assert.Contains("\"hex\":\"FF\"", logOutput); + } + + [Fact] + public void TestCustomObjectFormatting() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "object-format-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.CamelCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + var user = new User + { + FirstName = "John", + LastName = "Doe", + Age = 42 + }; + + // Regular object formatting (uses ToString()) + logger.LogInformation("User data: {user}", user); + + // Object serialization with @ prefix + logger.LogInformation("User object: {@user}", user); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // First log should use ToString() + Assert.Contains("\"message\":\"User data: Doe, John (42)\"", logOutput); + Assert.Contains("\"user\":\"Doe, John (42)\"", logOutput); + + // Second log should serialize the object + Assert.Contains("\"user\":{", logOutput); + Assert.Contains("\"firstName\":\"John\"", logOutput); + Assert.Contains("\"lastName\":\"Doe\"", logOutput); + Assert.Contains("\"age\":42", logOutput); + } + + [Fact] + public void TestComplexObjectWithIgnoredProperties() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "complex-object-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.SnakeCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + var example = new ExampleClass + { + Name = "test", + Price = 1.999, + ThisIsBig = "big", + ThisIsHidden = "hidden" + }; + + // Test with @ prefix for serialization + logger.LogInformation("Example serialized: {@example}", example); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Should serialize the object properties + Assert.Contains("\"example\":{", logOutput); + Assert.Contains("\"name\":\"test\"", logOutput); + Assert.Contains("\"price\":1.999", logOutput); + Assert.Contains("\"this_is_big\":\"big\"", logOutput); + + // The JsonIgnore property should be excluded + Assert.DoesNotContain("this_is_hidden", logOutput); + } + + [Fact] + public void TestMixedFormatting() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "mixed-format-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.PascalCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + var user = new User + { + FirstName = "Jane", + LastName = "Smith", + Age = 35 + }; + + // Mix regular values with formatted values and objects + logger.LogInformation( + "Details: User={@user}, Price={price:$#,##0.00}, Date={date:yyyy-MM-dd}", + user, + 123.45, + new DateTime(2023, 4, 5) + ); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Verify all formatted parts + Assert.Contains("\"User\":{", logOutput); + Assert.Contains("\"FirstName\":\"Jane\"", logOutput); + Assert.Contains("\"Price\":\"$123.45\"", logOutput); + Assert.Contains("\"Date\":\"2023-04-05\"", logOutput); + } + + [Fact] + public void TestNestedObjectSerialization() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "nested-object-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.SnakeCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + var parent = new ParentClass + { + Name = "Parent", + Child = new ChildClass { Name = "Child" } + }; + + // Regular object formatting (uses ToString()) + logger.LogInformation("Parent: {parent}", parent); + + // Object serialization with @ prefix + logger.LogInformation("Parent with child: {@parent}", parent); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Regular formatting should use ToString() + Assert.Contains("\"parent\":\"Parent with Child\"", logOutput); + + // Serialized object should include nested structure + Assert.Contains("\"parent\":{", logOutput); + Assert.Contains("\"name\":\"Parent\"", logOutput); + Assert.Contains("\"child\":{", logOutput); + Assert.Contains("\"name\":\"Child\"", logOutput); + } + + [Fact] + public void TestCollectionFormatting() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "collection-format-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.CamelCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + var items = new[] { 1, 2, 3 }; + var dict = new Dictionary { ["key1"] = "value1", ["key2"] = 42 }; + + // Regular array formatting + logger.LogInformation("Array: {items}", items); + + // Serialized array with @ prefix + logger.LogInformation("Array serialized: {@items}", items); + + // Dictionary formatting + logger.LogInformation("Dictionary: {dict}", dict); + + // Serialized dictionary + logger.LogInformation("Dictionary serialized: {@dict}", dict); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Regular array formatting uses ToString() + Assert.Contains("\"items\":\"System.Int32[]\"", logOutput); + + // Serialized array should include all items + Assert.Contains("\"items\":[1,2,3]", logOutput); + + // Dictionary formatting depends on ToString() implementation + Assert.Contains("\"dict\":\"System.Collections.Generic.Dictionary", logOutput); + + // Serialized dictionary should include all key-value pairs + Assert.Contains("\"dict\":{", logOutput); + Assert.Contains("\"key1\":\"value1\"", logOutput); + Assert.Contains("\"key2\":42", logOutput); + } + + [Fact] + public void TestNullAndEdgeCases() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "null-edge-case-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.SnakeCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + User user = null; + + // Test null formatting + logger.LogInformation("Null object: {user}", user); + logger.LogInformation("Null serialized: {@user}", user); + + // Extreme values + logger.LogInformation("Max value: {max}", int.MaxValue); + logger.LogInformation("Min value: {min}", int.MinValue); + logger.LogInformation("Max double: {maxDouble}", double.MaxValue); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Null objects should be null in output + Assert.Contains("\"user\":null", logOutput); + + // Extreme values should be preserved + Assert.Contains("\"max\":2147483647", logOutput); + Assert.Contains("\"min\":-2147483648", logOutput); + Assert.Contains("\"max_double\":1.7976931348623157E+308", logOutput); + } + + [Fact] + public void TestDateTimeFormats() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "datetime-format-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.CamelCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + var date = new DateTime(2023, 12, 31, 23, 59, 59); + + // Test different date formats + logger.LogInformation("ISO: {date:o}", date); + logger.LogInformation("Short date: {date:d}", date); + logger.LogInformation("Custom: {date:yyyy-MM-dd'T'HH:mm:ss.fff}", date); + logger.LogInformation("Time only: {date:HH:mm:ss}", date); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Verify different formats + Assert.Contains("\"date\":\"2023-12-31T23:59:59", logOutput); // ISO format + Assert.Contains("\"date\":\"12/31/2023\"", logOutput); // Short date + Assert.Contains("\"date\":\"2023-12-31T23:59:59.000\"", logOutput); // Custom + Assert.Contains("\"date\":\"23:59:59\"", logOutput); // Time only + } + + [Fact] + public void TestExceptionLogging() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "exception-test-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.SnakeCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + try + { + throw new InvalidOperationException("Test exception"); + } + catch (Exception ex) + { + logger.LogError(ex, "An error occurred with {data}", "test value"); + + // Test with nested exceptions + var outerEx = new Exception("Outer exception", ex); + logger.LogError(outerEx, "Nested exception test"); + } + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Verify exception details are included + Assert.Contains("\"message\":\"An error occurred with test value\"", logOutput); + Assert.Contains("\"exception\":{", logOutput); + Assert.Contains("\"type\":\"System.InvalidOperationException\"", logOutput); + Assert.Contains("\"message\":\"Test exception\"", logOutput); + Assert.Contains("\"stack_trace\":", logOutput); + + // Verify nested exception details + Assert.Contains("\"message\":\"Nested exception test\"", logOutput); + Assert.Contains("\"inner_exception\":{", logOutput); + } + + [Fact] + public void TestScopedLogging() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "scope-test-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.SnakeCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + // Log without any scope + logger.LogInformation("Outside any scope"); + + // Create a scope and log within it + using (logger.BeginScope(new { RequestId = "req-123", UserId = "user-456" })) + { + logger.LogInformation("Inside first scope"); + + // Nested scope + using (logger.BeginScope(new { OperationId = "op-789" })) + { + logger.LogInformation("Inside nested scope"); + } + + logger.LogInformation("Back to first scope"); + } + + // Back outside all scopes + logger.LogInformation("Outside all scopes again"); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Verify scope information is included correctly + Assert.Contains("\"message\":\"Inside first scope\"", logOutput); + Assert.Contains("\"request_id\":\"req-123\"", logOutput); + Assert.Contains("\"user_id\":\"user-456\"", logOutput); + + // Nested scope should include both scopes' data + Assert.Contains("\"message\":\"Inside nested scope\"", logOutput); + Assert.Contains("\"operation_id\":\"op-789\"", logOutput); + } + + [Fact] + public void TestDifferentLogLevels() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "log-level-test-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.SnakeCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + logger.LogTrace("This is a trace message"); + logger.LogDebug("This is a debug message"); + logger.LogInformation("This is an info message"); + logger.LogWarning("This is a warning message"); + logger.LogError("This is an error message"); + logger.LogCritical("This is a critical message"); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Trace shouldn't be logged (below default) + Assert.DoesNotContain("\"level\":\"Trace\"", logOutput); + + // Debug and above should be logged + Assert.Contains("\"level\":\"Debug\"", logOutput); + Assert.Contains("\"level\":\"Information\"", logOutput); + Assert.Contains("\"level\":\"Warning\"", logOutput); + Assert.Contains("\"level\":\"Error\"", logOutput); + Assert.Contains("\"level\":\"Critical\"", logOutput); + } + + public class ParentClass + { + public string Name { get; set; } + public ChildClass Child { get; set; } + + public override string ToString() + { + return $"Parent with Child"; + } + } + + public class ChildClass + { + public string Name { get; set; } + + public override string ToString() + { + return $"Child: {Name}"; + } + } + + public class Node + { + public string Name { get; set; } + public Node Parent { get; set; } + public List Children { get; set; } = new List(); + + public override string ToString() + { + return $"Node: {Name}"; + } + } + + public class User + { + public string FirstName { get; set; } + public string LastName { get; set; } + public int Age { get; set; } + + public override string ToString() + { + return $"{LastName}, {FirstName} ({Age})"; + } + } + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/HandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/HandlerTests.cs new file mode 100644 index 00000000..249aa47e --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/HandlerTests.cs @@ -0,0 +1,376 @@ +using System.Text.Json.Serialization; +#if NET8_0_OR_GREATER + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; +using Amazon.Lambda.Core; +using Amazon.Lambda.TestUtilities; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Common.Tests; +using AWS.Lambda.Powertools.Logging.Internal.Helpers; +using AWS.Lambda.Powertools.Logging.Tests.Formatter; +using AWS.Lambda.Powertools.Logging.Tests.Utilities; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace AWS.Lambda.Powertools.Logging.Tests.Handlers; + +public class Handlers +{ + private readonly ILogger _logger; + + public Handlers(ILogger logger) + { + _logger = logger; + } + + [Logging(LogEvent = true)] + public void TestMethod(string message, ILambdaContext lambdaContext) + { + _logger.AppendKey("custom-key", "custom-value"); + _logger.LogInformation("Information message"); + _logger.LogDebug("debug message"); + + var example = new ExampleClass + { + Name = "test", + Price = 1.999, + ThisIsBig = "big", + ThisIsHidden = "hidden" + }; + + _logger.LogInformation("Example object: {example}", example); + _logger.LogInformation("Another JSON log {d:0.000}", 1.2333); + + _logger.LogDebug(example); + _logger.LogInformation(example); + } + + [Logging(LogEvent = true, CorrelationIdPath = "price")] + public void TestMethodCorrelation(ExampleClass message, ILambdaContext lambdaContext) + { + } +} + +public class StaticHandler +{ + [Logging(LogEvent = true, LoggerOutputCase = LoggerOutputCase.PascalCase, Service = "my-service122")] + public void TestMethod(string message, ILambdaContext lambdaContext) + { + Logger.LogInformation("Static method"); + } +} + +public class HandlerTests +{ + private readonly ITestOutputHelper _output; + + public HandlerTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void TestMethod() + { + var output = new TestLoggerOutput(); + + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "my-service122"; + config.SamplingRate = 0.002; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.PascalCase; + config.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + config.JsonOptions = new JsonSerializerOptions + { + WriteIndented = true + // PropertyNamingPolicy = null, + // DictionaryKeyPolicy = PascalCaseNamingPolicy.Instance, + }; + config.LogOutput = output; + }); + }).CreateLogger(); + + + var handler = new Handlers(logger); + + handler.TestMethod("Event", new TestLambdaContext + { + FunctionName = "test-function", + FunctionVersion = "1", + AwsRequestId = "123", + InvokedFunctionArn = "arn:aws:lambda:us-east-1:123456789012:function:test-function" + }); + + handler.TestMethodCorrelation(new ExampleClass + { + Name = "test-function", + Price = 1.999, + ThisIsBig = "big", + }, null); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Check if the output contains newlines and spacing (indentation) + Assert.Contains("\n", logOutput); + Assert.Contains(" ", logOutput); + + // Verify write indented JSON + Assert.Contains(" \"Level\": \"Information\",\n \"Service\": \"my-service122\",", logOutput); + } + + [Fact] + public void TestMethodCustom() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "my-service122"; + config.SamplingRate = 0.002; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.CamelCase; + config.JsonOptions = new JsonSerializerOptions + { + // PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + // DictionaryKeyPolicy = JsonNamingPolicy.KebabCaseLower + }; + + config.LogFormatter = new CustomLogFormatter(); + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + var handler = new Handlers(logger); + + handler.TestMethod("Event", new TestLambdaContext + { + FunctionName = "test-function", + FunctionVersion = "1", + AwsRequestId = "123", + InvokedFunctionArn = "arn:aws:lambda:us-east-1:123456789012:function:test-function" + }); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Verify CamelCase formatting (custom formatter) + Assert.Contains("\"service\":\"my-service122\"", logOutput); + Assert.Contains("\"level\":\"Information\"", logOutput); + Assert.Contains("\"message\":\"Information message\"", logOutput); + Assert.Contains("\"correlationIds\":{\"awsRequestId\":\"123\"}", logOutput); + + } + + [Fact] + public void TestBuffer() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + // builder.AddFilter("AWS.Lambda.Powertools.Logging.Tests.Handlers.Handlers", LogLevel.Debug); + builder.AddPowertoolsLogger(config => + { + config.Service = "my-service122"; + config.SamplingRate = 0.002; + config.MinimumLogLevel = LogLevel.Information; + config.JsonOptions = new JsonSerializerOptions + { + WriteIndented = true, + // PropertyNamingPolicy = JsonNamingPolicy.KebabCaseUpper, + DictionaryKeyPolicy = JsonNamingPolicy.KebabCaseUpper + }; + config.LogOutput = output; + config.LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug, + }; + }); + }).CreatePowertoolsLogger(); + + var handler = new Handlers(logger); + + handler.TestMethod("Event", new TestLambdaContext + { + FunctionName = "test-function", + FunctionVersion = "1", + AwsRequestId = "123", + InvokedFunctionArn = "arn:aws:lambda:us-east-1:123456789012:function:test-function" + }); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Verify buffering behavior - only Information logs or higher should be in output + Assert.Contains("Information message", logOutput); + Assert.DoesNotContain("debug message", logOutput); // Debug should be buffered + + // Verify JSON options with indentation + Assert.Contains("\n", logOutput); + Assert.Contains(" ", logOutput); // Check for indentation + + // Check that kebab-case dictionary keys are working + Assert.Contains("\"CUSTOM-KEY\"", logOutput); + } + + [Fact] + public void TestMethodStatic() + { + var output = new TestLoggerOutput(); + var handler = new StaticHandler(); + + Logger.Configure(options => + { + options.LogOutput = output; + options.LoggerOutputCase = LoggerOutputCase.CamelCase; + }); + + handler.TestMethod("Event", new TestLambdaContext + { + FunctionName = "test-function", + FunctionVersion = "1", + AwsRequestId = "123", + InvokedFunctionArn = "arn:aws:lambda:us-east-1:123456789012:function:test-function" + }); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Verify static logger configuration + // Verify override of LoggerOutputCase from attribute + Assert.Contains("\"Service\":\"my-service122\"", logOutput); + Assert.Contains("\"Level\":\"Information\"", logOutput); + Assert.Contains("\"Message\":\"Static method\"", logOutput); + } + + [Fact] + public void TestJsonOptionsPropertyNaming() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "json-options-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.JsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + WriteIndented = false + }; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + var handler = new Handlers(logger); + var example = new ExampleClass + { + Name = "TestValue", + Price = 29.99, + ThisIsBig = "LargeValue" + }; + + logger.LogInformation("Testing JSON options with example: {@example}", example); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Verify snake_case naming policy is applied + Assert.Contains("\"this_is_big\":\"LargeValue\"", logOutput); + Assert.Contains("\"name\":\"TestValue\"", logOutput); + } + + [Fact] + public void TestJsonOptionsDictionaryKeyPolicy() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "json-dictionary-service"; + config.JsonOptions = new JsonSerializerOptions + { + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = false + }; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + var dictionary = new Dictionary + { + { "UserID", 12345 }, + { "OrderDetails", new { ItemCount = 3, Total = 150.75 } }, + { "ShippingAddress", "123 Main St" } + }; + + logger.LogInformation("Dictionary with custom key policy: {@dictionary}", dictionary); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Fix assertion to match actual camelCase behavior with acronyms + Assert.Contains("\"userID\":12345", logOutput); // ID remains uppercase + Assert.Contains("\"orderDetails\":", logOutput); + Assert.Contains("\"shippingAddress\":", logOutput); + } + + [Fact] + public void TestJsonOptionsWriteIndented() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "json-indented-service"; + config.JsonOptions = new JsonSerializerOptions + { + WriteIndented = true + }; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + var example = new ExampleClass + { + Name = "IndentedTest", + Price = 59.99, + ThisIsBig = "IndentedValue" + }; + + logger.LogInformation("Testing indented JSON: {@example}", example); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Check if the output contains newlines and spacing (indentation) + Assert.Contains("\n", logOutput); + Assert.Contains(" ", logOutput); + } +} + +#endif + +public class ExampleClass +{ + public string Name { get; set; } + + public double Price { get; set; } + + public string ThisIsBig { get; set; } + + [JsonIgnore] public string ThisIsHidden { get; set; } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs index f5d707ca..f35753f8 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs @@ -75,7 +75,10 @@ public void ObjectToDictionary_NullObject_Return_New_Dictionary() public void Should_Log_With_Anonymous() { var consoleOut = Substitute.For(); - Logger.SetOutput(consoleOut); + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); // Act & Assert Logger.AppendKey("newKey", new @@ -95,7 +98,10 @@ public void Should_Log_With_Anonymous() public void Should_Log_With_Complex_Anonymous() { var consoleOut = Substitute.For(); - Logger.SetOutput(consoleOut); + Logger.Configure(options => + { + options.LogOutput = consoleOut; + }); // Act & Assert Logger.AppendKey("newKey", new From 30672a76dabfebcef0e2ec6a9a9b7d5d29e41f4b Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 2 Apr 2025 19:53:35 +0100 Subject: [PATCH 29/49] refactor: update parameter names and improve documentation in logging configuration classes --- .../Core/PowertoolsConfigurations.cs | 2 +- .../Core/PowertoolsEnvironment.cs | 1 + .../Buffer/BufferingLoggerProvider.cs | 3 - .../Internal/Converters/ByteArrayConverter.cs | 2 +- .../Internal/Helpers/LoggerFactoryHelper.cs | 10 +- .../Internal/LoggerFactoryHolder.cs | 2 +- .../Internal/LoggingAspect.cs | 2 - .../PowertoolsConfigurationsExtension.cs | 215 --------------- .../Internal/PowertoolsLogger.cs | 1 + .../Internal/PowertoolsLoggerProvider.cs | 4 - .../PowertoolsLoggerBuilder.cs | 66 ++++- .../PowertoolsLoggerConfiguration.cs | 260 +++++++++++++----- .../PowertoolsLoggerFactory.cs | 2 +- .../PowertoolsLoggingBuilderExtensions.cs | 116 ++++++-- .../PowertoolsLoggingSerializer.cs | 34 +-- 15 files changed, 366 insertions(+), 354 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs index 2fce2191..cb2c55e6 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs @@ -44,7 +44,7 @@ public class PowertoolsConfigurations : IPowertoolsConfigurations /// /// Initializes a new instance of the class. /// - /// The system wrapper. + /// internal PowertoolsConfigurations(IPowertoolsEnvironment powertoolsEnvironment) { _powertoolsEnvironment = powertoolsEnvironment; diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsEnvironment.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsEnvironment.cs index 7abb379a..649418a4 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsEnvironment.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsEnvironment.cs @@ -42,6 +42,7 @@ public string GetAssemblyVersion(T type) return version != null ? $"{version.Major}.{version.Minor}.{version.Build}" : string.Empty; } + /// public void SetExecutionEnvironment(T type) { const string envName = Constants.AwsExecutionEnvironmentVariableName; diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs index a6e1a6bb..40160099 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs @@ -13,11 +13,9 @@ * permissions and limitations under the License. */ -using System; using System.Collections.Concurrent; using AWS.Lambda.Powertools.Common; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace AWS.Lambda.Powertools.Logging.Internal; @@ -28,7 +26,6 @@ namespace AWS.Lambda.Powertools.Logging.Internal; internal class BufferingLoggerProvider : PowertoolsLoggerProvider { private readonly ConcurrentDictionary _loggers = new(); - private readonly IPowertoolsConfigurations _powertoolsConfigurations; public BufferingLoggerProvider( PowertoolsLoggerConfiguration config, diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ByteArrayConverter.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ByteArrayConverter.cs index ab709bf9..9be24b68 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ByteArrayConverter.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ByteArrayConverter.cs @@ -47,7 +47,7 @@ public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS /// Write the exception value as JSON. /// /// The unicode JsonWriter. - /// The byte array. + /// /// The JsonSerializer options. public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options) { diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs index c68ba0bf..142a34bf 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs @@ -19,17 +19,17 @@ public static ILoggerFactory CreateAndConfigureFactory(PowertoolsLoggerConfigura builder.AddPowertoolsLogger(config => { config.Service = configuration.Service; - config.SamplingRate = configuration.SamplingRate; + config.TimestampFormat = configuration.TimestampFormat; config.MinimumLogLevel = configuration.MinimumLogLevel; + config.SamplingRate = configuration.SamplingRate; config.LoggerOutputCase = configuration.LoggerOutputCase; - config.JsonOptions = configuration.JsonOptions; - config.TimestampFormat = configuration.TimestampFormat; - config.LogFormatter = configuration.LogFormatter; config.LogLevelKey = configuration.LogLevelKey; + config.LogFormatter = configuration.LogFormatter; + config.JsonOptions = configuration.JsonOptions; config.LogBuffering = configuration.LogBuffering; - config.LogEvent = configuration.LogEvent; config.LogOutput = configuration.LogOutput; config.XRayTraceId = configuration.XRayTraceId; + config.LogEvent = configuration.LogEvent; }); // Use current filter level or level from config diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerFactoryHolder.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerFactoryHolder.cs index ee01dc6e..ce96ea73 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerFactoryHolder.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggerFactoryHolder.cs @@ -24,7 +24,7 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// internal static class LoggerFactoryHolder { - private static ILoggerFactory? _factory; + private static ILoggerFactory _factory; private static readonly object _lock = new object(); /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index cc2272f8..9c79f367 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -34,8 +34,6 @@ namespace AWS.Lambda.Powertools.Logging.Internal; [Aspect(Scope.Global, Factory = typeof(LoggingAspectFactory))] public class LoggingAspect { - private readonly ILoggerFactory _loggerFactory; - /// /// The is cold start /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs index dd36ce87..4ca0b878 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs @@ -15,11 +15,7 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; using AWS.Lambda.Powertools.Common; -using AWS.Lambda.Powertools.Logging.Serializers; using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging.Internal; @@ -29,9 +25,6 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// internal static class PowertoolsConfigurationsExtension { - private static readonly object _lock = new object(); - private static PowertoolsLoggerConfiguration _config; - /// /// Maps AWS log level to .NET log level /// @@ -92,88 +85,6 @@ internal static LoggerOutputCase GetLoggerOutputCase(this IPowertoolsConfigurati return LoggingConstants.DefaultLoggerOutputCase; } - /// - /// Gets the current configuration. - /// - /// AWS.Lambda.Powertools.Logging.LoggerConfiguration. - // internal static void SetCurrentConfig(this IPowertoolsConfigurations powertoolsConfigurations, LoggerConfiguration config, ISystemWrapper systemWrapper) - // { - // lock (_lock) - // { - // _config = 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."); - // } - // - // // Set service - // _config.Service = _config.Service ?? powertoolsConfigurations.Service; - // - // // Set output case - // var loggerOutputCase = powertoolsConfigurations.GetLoggerOutputCase(_config.LoggerOutputCase); - // _config.LoggerOutputCase = loggerOutputCase; - // PowertoolsLoggingSerializer.ConfigureNamingPolicy(loggerOutputCase); - // - // // Set log level - // var minLogLevel = lambdaLogLevelEnabled ? lambdaLogLevel : logLevel; - // _config.MinimumLevel = minLogLevel; - // - // // Set sampling rate - // SetSamplingRate(powertoolsConfigurations, systemWrapper, minLogLevel); - // } - // } - - /// - /// Set sampling rate - /// - /// - /// - /// - /// - // private static void SetSamplingRate(IPowertoolsConfigurations powertoolsConfigurations, ISystemWrapper systemWrapper, LogLevel minLogLevel) - // { - // var samplingRate = _config.SamplingRate > 0 ? _config.SamplingRate : powertoolsConfigurations.LoggerSampleRate; - // samplingRate = ValidateSamplingRate(samplingRate, minLogLevel, systemWrapper); - // - // _config.SamplingRate = samplingRate; - // - // if (samplingRate > 0) - // { - // double sample = systemWrapper.GetRandom(); - // - // if (sample <= samplingRate) - // { - // systemWrapper.LogLine($"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate}, Sampler Value: {sample}."); - // _config.MinimumLevel = LogLevel.Debug; - // } - // } - // } - - /// - /// Validate Sampling rate - /// - /// - /// - /// - /// - private static double ValidateSamplingRate(double samplingRate, LogLevel minLogLevel, ISystemWrapper systemWrapper) - { - if (samplingRate < 0 || samplingRate > 1) - { - if (minLogLevel is LogLevel.Debug or LogLevel.Trace) - { - systemWrapper.LogLine($"Skipping sampling rate configuration because of invalid value. Sampling rate: {samplingRate}"); - } - return 0; - } - - return samplingRate; - } /// /// Determines whether [is lambda log level enabled]. @@ -184,130 +95,4 @@ internal static bool LambdaLogLevelEnabled(this IPowertoolsConfigurations powert { return powertoolsConfigurations.GetLambdaLogLevel() != LogLevel.None; } - - /// - /// 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(this IPowertoolsConfigurations powertoolsConfigurations, - // string correlationIdPath, LoggerOutputCase loggerOutputCase) - // { - // return powertoolsConfigurations.GetLoggerOutputCase(loggerOutputCase) switch - // { - // LoggerOutputCase.CamelCase => ToCamelCase(correlationIdPath), - // LoggerOutputCase.PascalCase => ToPascalCase(correlationIdPath), - // _ => ToSnakeCase(correlationIdPath), // default snake_case - // }; - // } - - // /// - // /// Converts a string to snake_case. - // /// - // /// - // /// The input string converted to snake_case. - // private static string ToSnakeCase(string input) - // { - // if (string.IsNullOrEmpty(input)) - // return input; - // - // var result = new StringBuilder(input.Length + 10); - // bool lastCharWasUnderscore = false; - // bool lastCharWasUpper = false; - // - // for (int i = 0; i < input.Length; i++) - // { - // char currentChar = input[i]; - // - // if (currentChar == '_') - // { - // result.Append('_'); - // lastCharWasUnderscore = true; - // lastCharWasUpper = false; - // } - // else if (char.IsUpper(currentChar)) - // { - // if (i > 0 && !lastCharWasUnderscore && - // (!lastCharWasUpper || (i + 1 < input.Length && char.IsLower(input[i + 1])))) - // { - // result.Append('_'); - // } - // - // result.Append(char.ToLowerInvariant(currentChar)); - // lastCharWasUnderscore = false; - // lastCharWasUpper = true; - // } - // else - // { - // result.Append(char.ToLowerInvariant(currentChar)); - // lastCharWasUnderscore = false; - // lastCharWasUpper = false; - // } - // } - // - // return result.ToString(); - // } - // - // - // /// - // /// Converts a string to PascalCase. - // /// - // /// - // /// The input string converted to PascalCase. - // private static string ToPascalCase(string input) - // { - // if (string.IsNullOrEmpty(input)) - // return input; - // - // var words = input.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries); - // var result = new StringBuilder(); - // - // foreach (var word in words) - // { - // if (word.Length > 0) - // { - // // Capitalize the first character of each word - // result.Append(char.ToUpperInvariant(word[0])); - // - // // Handle the rest of the characters - // if (word.Length > 1) - // { - // // If the word is all uppercase, convert the rest to lowercase - // if (word.All(char.IsUpper)) - // { - // result.Append(word.Substring(1).ToLowerInvariant()); - // } - // else - // { - // // Otherwise, keep the original casing - // result.Append(word.Substring(1)); - // } - // } - // } - // } - // - // return result.ToString(); - // } - // - // /// - // /// Converts a string to camelCase. - // /// - // /// The string to convert. - // /// The input string converted to camelCase. - // private static string ToCamelCase(string input) - // { - // if (string.IsNullOrEmpty(input)) - // return input; - // - // // First, convert to PascalCase - // string pascalCase = ToPascalCase(input); - // - // // Then convert the first character to lowercase - // return char.ToLowerInvariant(pascalCase[0]) + pascalCase.Substring(1); - // } - } \ 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 3b4356fe..9b0028cb 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -162,6 +162,7 @@ internal object LogEntry(LogLevel logLevel, TState state, Exception exce /// Entry timestamp. /// The message to be written. Can be also an object. /// The exception related to this entry. + /// The parameters for structured formatting private Dictionary GetLogEntry(LogLevel logLevel, DateTime timestamp, object message, Exception exception, Dictionary structuredParameters = null) { diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs index 4822204f..335638cb 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs @@ -16,9 +16,7 @@ using System; using System.Collections.Concurrent; using AWS.Lambda.Powertools.Common; -using AWS.Lambda.Powertools.Logging.Serializers; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace AWS.Lambda.Powertools.Logging.Internal; @@ -31,7 +29,6 @@ namespace AWS.Lambda.Powertools.Logging.Internal; internal class PowertoolsLoggerProvider : ILoggerProvider { private readonly ConcurrentDictionary _loggers = new(StringComparer.OrdinalIgnoreCase); - private readonly IDisposable? _onChangeToken; private PowertoolsLoggerConfiguration _currentConfig; private readonly IPowertoolsConfigurations _powertoolsConfigurations; private bool _environmentConfigured; @@ -164,6 +161,5 @@ public void UpdateConfiguration(PowertoolsLoggerConfiguration config) public virtual void Dispose() { _loggers.Clear(); - _onChangeToken?.Dispose(); } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs index 06f6657b..e28cc3ec 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs @@ -1,52 +1,90 @@ using System; using System.Text.Json; -using System.Text.Json.Serialization; -using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal.Helpers; using Microsoft.Extensions.Logging; namespace AWS.Lambda.Powertools.Logging; +/// +/// Builder class for creating configured PowertoolsLogger instances. +/// Provides a fluent interface for configuring logging options. +/// public class PowertoolsLoggerBuilder { private readonly PowertoolsLoggerConfiguration _configuration = new(); + /// + /// Sets the service name for the logger. + /// + /// The service name to be included in logs. + /// The builder instance for method chaining. public PowertoolsLoggerBuilder WithService(string service) { _configuration.Service = service; return this; } - + + /// + /// Sets the sampling rate for logs. + /// + /// The sampling rate between 0 and 1. + /// The builder instance for method chaining. public PowertoolsLoggerBuilder WithSamplingRate(double rate) { _configuration.SamplingRate = rate; return this; } - + + /// + /// Sets the minimum log level for the logger. + /// + /// The minimum LogLevel to capture. + /// The builder instance for method chaining. public PowertoolsLoggerBuilder WithMinimumLogLevel(LogLevel level) { _configuration.MinimumLogLevel = level; return this; } - + + /// + /// Sets custom JSON serialization options. + /// + /// JSON serializer options to use for log formatting. + /// The builder instance for method chaining. public PowertoolsLoggerBuilder WithJsonOptions(JsonSerializerOptions options) { _configuration.JsonOptions = options; return this; } - + + /// + /// Sets the timestamp format for log entries. + /// + /// The timestamp format string. + /// The builder instance for method chaining. public PowertoolsLoggerBuilder WithTimestampFormat(string format) { _configuration.TimestampFormat = format; return this; } - + + /// + /// Sets the output casing style for log properties. + /// + /// The casing style to use for log output. + /// The builder instance for method chaining. public PowertoolsLoggerBuilder WithOutputCase(LoggerOutputCase outputCase) { _configuration.LoggerOutputCase = outputCase; return this; } + /// + /// Sets a custom log formatter. + /// + /// The formatter to use for log formatting. + /// The builder instance for method chaining. + /// Thrown when formatter is null. public PowertoolsLoggerBuilder WithFormatter(ILogFormatter formatter) { _configuration.LogFormatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); @@ -54,8 +92,10 @@ public PowertoolsLoggerBuilder WithFormatter(ILogFormatter formatter) } /// - /// Enable log buffering with default options + /// Enables or disables log buffering with default options. /// + /// Whether log buffering should be enabled. + /// The builder instance for method chaining. public PowertoolsLoggerBuilder WithLogBuffering(bool enabled = true) { _configuration.LogBuffering.Enabled = enabled; @@ -63,14 +103,20 @@ public PowertoolsLoggerBuilder WithLogBuffering(bool enabled = true) } /// - /// Configure log buffering options + /// Configures log buffering with custom options. /// + /// Action to configure the log buffering options. + /// The builder instance for method chaining. public PowertoolsLoggerBuilder WithLogBuffering(Action configure) { configure?.Invoke(_configuration.LogBuffering); return this; } - + + /// + /// Builds and returns a configured logger instance. + /// + /// An ILogger configured with the specified options. public ILogger Build() { var factory = LoggerFactoryHelper.CreateAndConfigureFactory(_configuration); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs index 5e70db70..c9319c48 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.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 @@ -23,40 +23,126 @@ namespace AWS.Lambda.Powertools.Logging; /// -/// Class PowertoolsLoggerConfiguration. -/// Implements the +/// Configuration for the Powertools Logger. /// +/// +/// +/// Basic logging configuration: +/// +/// builder.Logging.AddPowertoolsLogger(options => +/// { +/// options.Service = "OrderService"; +/// options.MinimumLogLevel = LogLevel.Information; +/// options.LoggerOutputCase = LoggerOutputCase.CamelCase; +/// }); +/// +/// +/// Using with log buffering: +/// +/// builder.Logging.AddPowertoolsLogger(options => +/// { +/// options.LogBuffering = new LogBufferingOptions +/// { +/// Enabled = true, +/// BufferAtLogLevel = LogLevel.Debug, +/// FlushOnErrorLog = true +/// }; +/// }); +/// +/// +/// Custom JSON formatting: +/// +/// builder.Logging.AddPowertoolsLogger(options => +/// { +/// options.JsonOptions = new JsonSerializerOptions +/// { +/// PropertyNamingPolicy = JsonNamingPolicy.CamelCase, +/// WriteIndented = true +/// }; +/// }); +/// +/// public class PowertoolsLoggerConfiguration : IOptions { + /// + /// The configuration section name used when retrieving configuration from appsettings.json + /// or other configuration providers. + /// public const string ConfigurationSectionName = "AWS.Lambda.Powertools.Logging.Logger"; /// - /// Service name is used for logging. - /// This can be also set using the environment variable POWERTOOLS_SERVICE_NAME. + /// Specifies the service name that will be added to all logs to improve discoverability. + /// This value can also be set using the environment variable POWERTOOLS_SERVICE_NAME. /// - public string? Service { get; set; } = null; - + /// + /// + /// options.Service = "OrderProcessingService"; + /// + /// + public string Service { get; set; } = null; + /// - /// Timestamp format for logging. + /// Defines the format for timestamps in log entries. Supports standard .NET date format strings. + /// When not specified, the default ISO 8601 format is used. /// - public string? TimestampFormat { get; set; } + /// + /// + /// // Use specific format + /// options.TimestampFormat = "yyyy-MM-dd HH:mm:ss"; + /// + /// // Use ISO 8601 with milliseconds + /// options.TimestampFormat = "o"; + /// + /// + public string TimestampFormat { get; set; } /// - /// Specify the minimum log level for logging (Information, by default). - /// This can be also set using the environment variable POWERTOOLS_LOG_LEVEL. + /// Defines the minimum log level that will be processed by the logger. + /// Messages below this level will be ignored. Defaults to LogLevel.None, which means + /// the minimum level is determined by other configuration mechanisms. + /// This can also be set using the environment variable POWERTOOLS_LOG_LEVEL. /// + /// + /// + /// // Only log warnings and above + /// options.MinimumLogLevel = LogLevel.Warning; + /// + /// // Log everything including trace messages + /// options.MinimumLogLevel = LogLevel.Trace; + /// + /// public LogLevel MinimumLogLevel { get; set; } = LogLevel.None; /// - /// Dynamically set a percentage of logs to DEBUG level. - /// This can be also set using the environment variable POWERTOOLS_LOGGER_SAMPLE_RATE. + /// Sets a percentage (0.0 to 1.0) of logs that will be dynamically elevated to DEBUG level, + /// allowing for production debugging without increasing log verbosity for all requests. + /// This can also be set using the environment variable POWERTOOLS_LOGGER_SAMPLE_RATE. /// + /// + /// + /// // Sample 10% of logs to DEBUG level + /// options.SamplingRate = 0.1; + /// + /// // Sample 100% (all logs) to DEBUG level + /// options.SamplingRate = 1.0; + /// + /// public double SamplingRate { get; set; } /// - /// The logger output case. - /// This can be also set using the environment variable POWERTOOLS_LOGGER_CASE. + /// Controls the case format used for log field names in the JSON output. + /// Available options are Default, CamelCase, PascalCase, or SnakeCase. + /// This can also be set using the environment variable POWERTOOLS_LOGGER_CASE. /// + /// + /// + /// // Use camelCase for JSON field names + /// options.LoggerOutputCase = LoggerOutputCase.CamelCase; + /// + /// // Use snake_case for JSON field names + /// options.LoggerOutputCase = LoggerOutputCase.SnakeCase; + /// + /// public LoggerOutputCase LoggerOutputCase { get; set; } = LoggerOutputCase.Default; /// @@ -65,15 +151,56 @@ public class PowertoolsLoggerConfiguration : IOptions - /// Custom log formatter to use for formatting log entries + /// Provides a custom log formatter implementation to control how log entries are formatted. + /// Set this to override the default JSON formatting with your own custom format. /// - public ILogFormatter? LogFormatter { get; set; } + /// + /// + /// // Use a custom formatter implementation + /// options.LogFormatter = new MyCustomLogFormatter(); + /// + /// // Example with a simple custom formatter class: + /// public class MyCustomLogFormatter : ILogFormatter + /// { + /// public string FormatLog(LogEntry entry) + /// { + /// // Custom formatting logic here + /// return $"{entry.Timestamp}: [{entry.LogLevel}] {entry.Message}"; + /// } + /// } + /// + /// + public ILogFormatter LogFormatter { get; set; } + + private JsonSerializerOptions _jsonOptions; /// - /// JSON serializer options to use for log serialization + /// Configures the JSON serialization options used when converting log entries to JSON. + /// This allows customization of property naming, indentation, and other serialization behaviors. + /// Setting this property automatically updates the internal serializer. /// - private JsonSerializerOptions? _jsonOptions; - public JsonSerializerOptions? JsonOptions + /// + /// + /// // DictionaryNamingPolicy allows you to control the naming policy for dictionary keys + /// options.JsonOptions = new JsonSerializerOptions + /// { + /// DictionaryNamingPolicy = JsonNamingPolicy.CamelCase + /// }; + /// // Pretty-print JSON logs with indentation + /// options.JsonOptions = new JsonSerializerOptions + /// { + /// WriteIndented = true, + /// PropertyNamingPolicy = JsonNamingPolicy.CamelCase + /// }; + /// + /// // Configure to ignore null values in output + /// options.JsonOptions = new JsonSerializerOptions + /// { + /// DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + /// }; + /// + /// + public JsonSerializerOptions JsonOptions { get => _jsonOptions; set @@ -87,16 +214,30 @@ public JsonSerializerOptions? JsonOptions } /// - /// Log buffering options. - /// - /// Logger.UseLogBuffering(new LogBufferingOptions - /// { - /// Enabled = true, - /// BufferAtLogLevel = LogLevel.Debug - /// }); - /// + /// Enables or disables log buffering. Logs below the specified level will be buffered + /// until the buffer is flushed or an error occurs. + /// Buffer logs at the WARNING, INFO, and DEBUG levels and reduce CloudWatch costs by decreasing the number of emitted log messages /// - public LogBufferingOptions LogBuffering { get; set; } = new LogBufferingOptions(); + /// + /// + /// // Enable buffering for debug logs + /// options.LogBuffering = new LogBufferingOptions + /// { + /// Enabled = true, + /// BufferAtLogLevel = LogLevel.Debug, + /// FlushOnErrorLog = true + /// }; + /// + /// // Buffer all logs below Error level + /// options.LogBuffering = new LogBufferingOptions + /// { + /// Enabled = true, + /// BufferAtLogLevel = LogLevel.Warning, + /// FlushOnErrorLog = true + /// }; + /// + /// + public LogBufferingOptions LogBuffering { get; set; } = new(); /// /// Serializer instance for this configuration @@ -109,9 +250,30 @@ public JsonSerializerOptions? JsonOptions internal PowertoolsLoggingSerializer Serializer => _serializer ??= InitializeSerializer(); /// - /// The console wrapper used for output operations. Defaults to ConsoleWrapper instance. - /// Primarily useful for testing to capture and verify output. + /// Specifies the console output wrapper used for writing logs. This property allows + /// redirecting log output for testing or specialized handling scenarios. + /// Defaults to standard console output via ConsoleWrapper. /// + /// + /// + /// // Using TestLoggerOutput + /// options.LogOutput = new TestLoggerOutput(); + /// + /// // Custom console output for testing + /// options.LogOutput = new TestConsoleWrapper(); + /// + /// // Example implementation for testing: + /// public class TestConsoleWrapper : IConsoleWrapper + /// { + /// public List<string> CapturedOutput { get; } = new(); + /// + /// public void WriteLine(string message) + /// { + /// CapturedOutput.Add(message); + /// } + /// } + /// + /// public IConsoleWrapper LogOutput { get; set; } = new ConsoleWrapper(); /// @@ -124,43 +286,17 @@ private PowertoolsLoggingSerializer InitializeSerializer() { serializer.SetOptions(_jsonOptions); } + serializer.ConfigureNamingPolicy(LoggerOutputCase); return serializer; } - /// - /// Creates a deep clone of the configuration - /// - public PowertoolsLoggerConfiguration Clone() - { - return new PowertoolsLoggerConfiguration - { - Service = Service, - TimestampFormat = TimestampFormat, - MinimumLogLevel = MinimumLogLevel, - SamplingRate = SamplingRate, - LoggerOutputCase = LoggerOutputCase, - LogLevelKey = LogLevelKey, - LogFormatter = LogFormatter, - JsonOptions = JsonOptions, - LogBuffering = new LogBufferingOptions - { - Enabled = LogBuffering.Enabled, - BufferAtLogLevel = LogBuffering.BufferAtLogLevel, - FlushOnErrorLog = LogBuffering.FlushOnErrorLog, - }, - LogOutput = LogOutput, // Reference the same output for now - XRayTraceId = XRayTraceId, - LogEvent = LogEvent - }; - } - // IOptions implementation PowertoolsLoggerConfiguration IOptions.Value => this; - + internal string XRayTraceId { get; set; } internal bool LogEvent { get; set; } - + internal double Random { get; set; } = new Random().NextDouble(); /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs index 6c5a9005..f90474d7 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs @@ -9,7 +9,7 @@ internal sealed class PowertoolsLoggerFactory : IDisposable { private readonly ILoggerFactory _factory; - public PowertoolsLoggerFactory(ILoggerFactory? loggerFactory = null) + public PowertoolsLoggerFactory(ILoggerFactory loggerFactory = null) { _factory = loggerFactory ?? LoggerFactory.Create(builder => { diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs index 6b56ee02..41c4210b 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs @@ -1,29 +1,36 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; -using AWS.Lambda.Powertools.Logging.Internal.Helpers; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Configuration; -using Microsoft.Extensions.Options; namespace AWS.Lambda.Powertools.Logging; /// -/// Extension methods for configuring the Powertools logger +/// Extension methods to configure and add the Powertools logger to an . /// +/// +/// This class provides methods to integrate the AWS Lambda Powertools logging capabilities +/// with the standard .NET logging framework. +/// +/// +/// Basic usage: +/// +/// builder.Logging.AddPowertoolsLogger(); +/// +/// public static class PowertoolsLoggingBuilderExtensions { private static readonly ConcurrentBag AllProviders = new(); - private static readonly object _lock = new(); + private static readonly object Lock = new(); private static PowertoolsLoggerConfiguration _currentConfig = new(); internal static void UpdateConfiguration(PowertoolsLoggerConfiguration config) { - lock (_lock) + lock (Lock) { // Update the shared configuration _currentConfig = config; @@ -35,16 +42,41 @@ internal static void UpdateConfiguration(PowertoolsLoggerConfiguration config) } } } - + internal static PowertoolsLoggerConfiguration GetCurrentConfiguration() { - lock (_lock) + lock (Lock) { // Return a copy to prevent external modification - return _currentConfig.Clone(); + return _currentConfig; } } + /// + /// Adds the Powertools logger to the logging builder with default configuration. + /// + /// The logging builder to configure. + /// The logging builder for further configuration. + /// + /// This method registers the Powertools logger with default settings. The logger will output + /// structured JSON logs that integrate well with AWS CloudWatch and other log analysis tools. + /// + /// + /// Add the Powertools logger to your Lambda function: + /// + /// var builder = new HostBuilder() + /// .ConfigureLogging(logging => + /// { + /// logging.AddPowertoolsLogger(); + /// }); + /// + /// + /// Using with minimal API: + /// + /// var builder = WebApplication.CreateBuilder(args); + /// builder.Logging.AddPowertoolsLogger(); + /// + /// public static ILoggingBuilder AddPowertoolsLogger( this ILoggingBuilder builder) { @@ -60,26 +92,66 @@ public static ILoggingBuilder AddPowertoolsLogger( var powertoolsConfigurations = provider.GetRequiredService(); var loggerProvider = new PowertoolsLoggerProvider( - _currentConfig, + _currentConfig, powertoolsConfigurations); - - lock (_lock) + + lock (Lock) { AllProviders.Add(loggerProvider); } return loggerProvider; })); - - LoggerProviderOptions.RegisterProviderOptions - (builder.Services); return builder; } /// - /// Adds the Powertools logger to the logging builder. + /// Adds the Powertools logger to the logging builder with default configuration. /// + /// The logging builder to configure. + /// The logging builder for further configuration. + /// + /// This method registers the Powertools logger with default settings. The logger will output + /// structured JSON logs that integrate well with AWS CloudWatch and other log analysis tools. + /// + /// + /// Add the Powertools logger to your Lambda function: + /// + /// var builder = new HostBuilder() + /// .ConfigureLogging(logging => + /// { + /// logging.AddPowertoolsLogger(); + /// }); + /// + /// + /// Using with minimal API: + /// + /// var builder = WebApplication.CreateBuilder(args); + /// builder.Logging.AddPowertoolsLogger(); + /// + /// With custom configuration: + /// + /// builder.Logging.AddPowertoolsLogger(options => + /// { + /// options.MinimumLogLevel = LogLevel.Information; + /// options.LoggerOutputCase = LoggerOutputCase.PascalCase; + /// options.IncludeLogLevel = true; + /// }); + /// + /// + /// With log buffering: + /// + /// builder.Logging.AddPowertoolsLogger(options => + /// { + /// options.LogBuffering = new LogBufferingOptions + /// { + /// Enabled = true, + /// BufferAtLogLevel = LogLevel.Debug + /// }; + /// }); + /// + /// public static ILoggingBuilder AddPowertoolsLogger( this ILoggingBuilder builder, Action configure) @@ -102,7 +174,7 @@ public static ILoggingBuilder AddPowertoolsLogger( UpdateConfiguration(options); // If buffering is enabled, register buffer providers - if (options?.LogBuffering?.Enabled == true) + if (options.LogBuffering?.Enabled == true) { // Add a filter for the buffer provider builder.AddFilter( @@ -119,8 +191,8 @@ public static ILoggingBuilder AddPowertoolsLogger( var bufferingProvider = new BufferingLoggerProvider( _currentConfig, powertoolsConfigurations ); - - lock (_lock) + + lock (Lock) { AllProviders.Add(bufferingProvider); } @@ -128,18 +200,18 @@ public static ILoggingBuilder AddPowertoolsLogger( return bufferingProvider; })); } - + return builder; } - + /// /// Resets all providers and clears the configuration. /// This is useful for testing purposes to ensure a clean state. /// internal static void ResetAllProviders() { - lock (_lock) + lock (Lock) { // Clear the provider collection AllProviders.Clear(); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs index 20b3a4c0..bbb1e3dd 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -36,13 +36,14 @@ internal class PowertoolsLoggingSerializer private JsonSerializerOptions _currentOptions; private LoggerOutputCase _currentOutputCase; private JsonSerializerOptions _jsonOptions; - private readonly object _lock = new object(); - - private readonly ConcurrentBag _additionalContexts = - new ConcurrentBag(); + private readonly object _lock = new(); +#if NET8_0_OR_GREATER + private readonly ConcurrentBag _additionalContexts = new(); private static JsonSerializerContext _staticAdditionalContexts; - + private IJsonTypeInfoResolver _customTypeInfoResolver; +#endif + /// /// Gets the JsonSerializerOptions instance. /// @@ -117,9 +118,7 @@ internal string Serialize(object value, Type inputType) } #if NET8_0_OR_GREATER - - private IJsonTypeInfoResolver? _customTypeInfoResolver = null; - + /// /// Adds a JsonSerializerContext to the serializer options. /// @@ -211,25 +210,6 @@ private JsonTypeInfo GetTypeInfo(Type type) return options.TypeInfoResolver?.GetTypeInfo(type, options); } - /// - /// Checks if a type is supported by any of the configured type resolvers - /// - private bool IsTypeSupportedByAnyResolver(Type type) - { - var options = GetSerializerOptions(); - if (options.TypeInfoResolver == null) - return false; - - try - { - var typeInfo = options.TypeInfoResolver.GetTypeInfo(type, options); - return typeInfo != null; - } - catch - { - return false; - } - } #endif /// From 466262230347e87085d7621cb5e6fb397a9655f1 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 2 Apr 2025 20:28:37 +0100 Subject: [PATCH 30/49] refactor: update logger factory and builder to support log output configuration --- .../Internal/Helpers/LoggerFactoryHelper.cs | 2 +- .../PowertoolsLoggerBuilder.cs | 8 + .../PowertoolsLoggerConfiguration.cs | 36 ++- .../PowertoolsLoggerFactory.cs | 17 +- .../PowertoolsLoggerBuilderTests.cs | 209 ++++++++++++++++++ 5 files changed, 259 insertions(+), 13 deletions(-) create mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerBuilderTests.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs index 142a34bf..9ce483f8 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs @@ -12,7 +12,7 @@ internal static class LoggerFactoryHelper /// /// The Powertools logger configuration to apply /// The configured logger factory - public static ILoggerFactory CreateAndConfigureFactory(PowertoolsLoggerConfiguration configuration) + internal static ILoggerFactory CreateAndConfigureFactory(PowertoolsLoggerConfiguration configuration) { var factory = LoggerFactory.Create(builder => { diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs index e28cc3ec..4f5df599 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Text.Json; +using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal.Helpers; using Microsoft.Extensions.Logging; @@ -112,6 +113,13 @@ public PowertoolsLoggerBuilder WithLogBuffering(Action conf configure?.Invoke(_configuration.LogBuffering); return this; } + + public PowertoolsLoggerBuilder WithLogOutput(IConsoleWrapper console) + { + _configuration.LogOutput = console ?? throw new ArgumentNullException(nameof(console)); + return this; + } + /// /// Builds and returns a configured logger instance. diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs index c9319c48..35361eec 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs @@ -159,15 +159,45 @@ public class PowertoolsLoggerConfiguration : IOptions /// public ILogFormatter LogFormatter { get; set; } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs index f90474d7..b8e8cc15 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs @@ -1,5 +1,4 @@ using System; -using AWS.Lambda.Powertools.Logging.Internal; using AWS.Lambda.Powertools.Logging.Internal.Helpers; using Microsoft.Extensions.Logging; @@ -9,7 +8,7 @@ internal sealed class PowertoolsLoggerFactory : IDisposable { private readonly ILoggerFactory _factory; - public PowertoolsLoggerFactory(ILoggerFactory loggerFactory = null) + internal PowertoolsLoggerFactory(ILoggerFactory loggerFactory = null) { _factory = loggerFactory ?? LoggerFactory.Create(builder => { @@ -17,11 +16,11 @@ public PowertoolsLoggerFactory(ILoggerFactory loggerFactory = null) }); } - public PowertoolsLoggerFactory() : this(LoggerFactory.Create(builder => { builder.AddPowertoolsLogger(); })) + internal PowertoolsLoggerFactory() : this(LoggerFactory.Create(builder => { builder.AddPowertoolsLogger(); })) { } - public static PowertoolsLoggerFactory Create(Action configureOptions) + internal static PowertoolsLoggerFactory Create(Action configureOptions) { var options = new PowertoolsLoggerConfiguration(); configureOptions(options); @@ -29,25 +28,25 @@ public static PowertoolsLoggerFactory Create(Action() => CreateLogger(typeof(T).FullName ?? typeof(T).Name); + internal ILogger CreateLogger() => CreateLogger(typeof(T).FullName ?? typeof(T).Name); - public ILogger CreateLogger(string category) + internal ILogger CreateLogger(string category) { return _factory.CreateLogger(category); } - public ILogger CreatePowertoolsLogger() + internal ILogger CreatePowertoolsLogger() { return _factory.CreatePowertoolsLogger(); } diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerBuilderTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerBuilderTests.cs new file mode 100644 index 00000000..f0a8e920 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerBuilderTests.cs @@ -0,0 +1,209 @@ +using System; +using System.Text.Json; +using AWS.Lambda.Powertools.Common.Tests; +using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Tests.Formatter; +using AWS.Lambda.Powertools.Logging.Tests.Handlers; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; + +namespace AWS.Lambda.Powertools.Logging.Tests; + +public class PowertoolsLoggerBuilderTests +{ + private readonly ITestOutputHelper _output; + + public PowertoolsLoggerBuilderTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void WithService_SetsServiceName() + { + var output = new TestLoggerOutput(); + var logger = new PowertoolsLoggerBuilder() + .WithLogOutput(output) + .WithService("test-builder-service") + .Build(); + + logger.LogInformation("Testing service name"); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + Assert.Contains("\"service\":\"test-builder-service\"", logOutput, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void WithSamplingRate_SetsSamplingRate() + { + var output = new TestLoggerOutput(); + var logger = new PowertoolsLoggerBuilder() + .WithLogOutput(output) + .WithService("sampling-test") + .WithSamplingRate(0.5) + .Build(); + + // We can't directly test sampling rate in a deterministic way, + // but we can verify the logger is created successfully + logger.LogInformation("Testing sampling rate"); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + Assert.Contains("\"message\":\"Testing sampling rate\"", logOutput, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void WithMinimumLogLevel_FiltersLowerLevels() + { + var output = new TestLoggerOutput(); + var logger = new PowertoolsLoggerBuilder() + .WithLogOutput(output) + .WithService("log-level-test") + .WithMinimumLogLevel(LogLevel.Warning) + .Build(); + + logger.LogDebug("Debug message"); + logger.LogInformation("Info message"); + logger.LogWarning("Warning message"); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + Assert.DoesNotContain("Debug message", logOutput); + Assert.DoesNotContain("Info message", logOutput); + Assert.Contains("Warning message", logOutput); + } + +#if NET8_0_OR_GREATER + [Fact] + public void WithJsonOptions_AppliesFormatting() + { + var output = new TestLoggerOutput(); + var logger = new PowertoolsLoggerBuilder() + .WithService("json-options-test") + .WithLogOutput(output) + .WithJsonOptions(new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + }) + .Build(); + + var testObject = new ExampleClass + { + Name = "TestName", + ThisIsBig = "BigValue" + }; + + logger.LogInformation("Test object: {@testObject}", testObject); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + Assert.Contains("\"this_is_big\":\"BigValue\"", logOutput); + Assert.Contains("\"name\":\"TestName\"", logOutput); + Assert.Contains("\n", logOutput); // Indentation includes newlines + } +#endif + + [Fact] + public void WithTimestampFormat_FormatsTimestamp() + { + var output = new TestLoggerOutput(); + var logger = new PowertoolsLoggerBuilder() + .WithLogOutput(output) + .WithService("timestamp-test") + .WithTimestampFormat("yyyy-MM-dd") + .Build(); + + logger.LogInformation("Testing timestamp format"); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Should match yyyy-MM-dd format (e.g., "2023-04-25") + Assert.Matches("\"timestamp\":\"\\d{4}-\\d{2}-\\d{2}\"", logOutput); + } + + [Fact] + public void WithOutputCase_ChangesPropertyCasing() + { + var output = new TestLoggerOutput(); + var logger = new PowertoolsLoggerBuilder() + .WithLogOutput(output) + .WithService("case-test") + .WithOutputCase(LoggerOutputCase.PascalCase) + .Build(); + + logger.LogInformation("Testing output case"); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + Assert.Contains("\"Service\":\"case-test\"", logOutput); + Assert.Contains("\"Level\":\"Information\"", logOutput); + Assert.Contains("\"Message\":\"Testing output case\"", logOutput); + } + + [Fact] + public void WithLogBuffering_BuffersLowLevelLogs() + { + var output = new TestLoggerOutput(); + var logger = new PowertoolsLoggerBuilder() + .WithLogOutput(output) + .WithService("buffer-test") + .WithLogBuffering(options => + { + options.Enabled = true; + options.BufferAtLogLevel = LogLevel.Debug; + }) + .Build(); + + LogBufferManager.SetInvocationId("config-test"); + logger.LogDebug("Debug buffered message"); + logger.LogInformation("Info message"); + + // Without FlushBuffer(), the debug message should be buffered + var initialOutput = output.ToString(); + _output.WriteLine("Before flush: " + initialOutput); + + Assert.DoesNotContain("Debug buffered message", initialOutput); + Assert.Contains("Info message", initialOutput); + + // After flushing, the debug message should appear + logger.FlushBuffer(); + var afterFlushOutput = output.ToString(); + _output.WriteLine("After flush: " + afterFlushOutput); + + Assert.Contains("Debug buffered message", afterFlushOutput); + } + + [Fact] + public void BuilderChaining_ConfiguresAllProperties() + { + var output = new TestLoggerOutput(); + var customFormatter = new CustomLogFormatter(); + + var logger = new PowertoolsLoggerBuilder() + .WithService("chained-config-service") + .WithSamplingRate(0.1) + .WithMinimumLogLevel(LogLevel.Information) + .WithOutputCase(LoggerOutputCase.SnakeCase) + .WithFormatter(customFormatter) + .WithLogOutput(output) + .Build(); + + logger.LogInformation("Testing fully configured logger"); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Verify multiple configured properties are applied + Assert.Contains("\"service\":\"chained-config-service\"", logOutput); + Assert.Contains("\"message\":\"Testing fully configured logger\"", logOutput); + Assert.Contains("\"sample_rate\":0.1", logOutput); + } +} \ No newline at end of file From 8d5195ea92a2080b68c4d6c31e8a95ee19234249 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Wed, 2 Apr 2025 23:45:36 +0100 Subject: [PATCH 31/49] refactor: enhance log buffer management to discard oversized entries and improve entry tracking --- .../Internal/Buffer/LogBuffer.cs | 32 ++- .../LogBufferingOptions.cs | 1 + .../Buffering/LogBufferCircularCacheTests.cs | 243 ++++++++++++++++++ 3 files changed, 270 insertions(+), 6 deletions(-) create mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferCircularCacheTests.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs index 9cb74ad3..a0217462 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs @@ -114,25 +114,33 @@ public bool HasEntries /// private class InvocationBuffer { - private readonly ConcurrentQueue _buffer = new(); - private int _currentSize = 0; + private readonly ConcurrentQueue _buffer = new(); + private int _currentSize; public void Add(string logEntry, int maxBytes) { // Same implementation as before var size = 100 + (logEntry?.Length ?? 0) * 2; + // If entry size exceeds max buffer size, discard the entry completely + if (size > maxBytes) + { + // Entry is too large to ever fit in buffer, discard it + return; + } + if (_currentSize + size > maxBytes) { - while (_buffer.TryDequeue(out _) && _currentSize + size > maxBytes) + // Remove oldest entries until we have enough space + while (_currentSize + size > maxBytes && _buffer.TryDequeue(out var removed)) { - _currentSize -= 100; + _currentSize -= removed.Size; } if (_currentSize < 0) _currentSize = 0; } - _buffer.Enqueue(logEntry); + _buffer.Enqueue(new BufferedLogEntry(logEntry, size)); _currentSize += size; } @@ -144,7 +152,7 @@ public IReadOnlyCollection GetAndClear() { while (_buffer.TryDequeue(out var entry)) { - entries.Add(entry); + entries.Add(entry.Entry); } } catch (Exception) @@ -158,4 +166,16 @@ public IReadOnlyCollection GetAndClear() public bool HasEntries => !_buffer.IsEmpty; } +} + +internal class BufferedLogEntry +{ + public string Entry { get; } + public int Size { get; } + + public BufferedLogEntry(string entry, int calculatedSize) + { + Entry = entry; + Size = calculatedSize; + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LogBufferingOptions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LogBufferingOptions.cs index 93852420..72c9a590 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LogBufferingOptions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LogBufferingOptions.cs @@ -29,6 +29,7 @@ public class LogBufferingOptions /// /// Gets or sets the maximum size of the buffer in bytes + /// Default is 20KB (20480 bytes) /// public int MaxBytes { get; set; } = 20480; diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferCircularCacheTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferCircularCacheTests.cs new file mode 100644 index 00000000..7071cd7d --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferCircularCacheTests.cs @@ -0,0 +1,243 @@ +using System; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Common.Tests; +using AWS.Lambda.Powertools.Logging.Internal; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace AWS.Lambda.Powertools.Logging.Tests.Buffering; + +public class LogBufferCircularCacheTests : IDisposable +{ + private readonly TestLoggerOutput _consoleOut; + + public LogBufferCircularCacheTests() + { + _consoleOut = new TestLoggerOutput(); + LogBufferManager.ResetForTesting(); + } + + [Trait("Category", "CircularBuffer")] + [Fact] + public void Buffer_WhenMaxSizeExceeded_DiscardOldestEntries() + { + // Arrange + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug, + MaxBytes = 1024 // Small buffer size to trigger overflow + }, + LogOutput = _consoleOut + }; + + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + var provider = new BufferingLoggerProvider(config, powertoolsConfig); + var logger = provider.CreateLogger("TestLogger"); + + LogBufferManager.SetInvocationId("circular-buffer-test"); + + // Act - add many debug logs to fill buffer + for (int i = 0; i < 5; i++) + { + logger.LogDebug($"Old debug message {i} that should be removed"); + } + + // Add more logs that should push out the older ones + for (int i = 0; i < 5; i++) + { + logger.LogDebug($"New debug message {i} that should remain"); + } + + // Flush buffer + logger.FlushBuffer(); + + // Assert + var output = _consoleOut.ToString(); + + // First entries should be discarded + Assert.DoesNotContain("Old debug message 0", output); + Assert.DoesNotContain("Old debug message 1", output); + + // Later entries should be present + Assert.Contains("New debug message 3", output); + Assert.Contains("New debug message 4", output); + } + + [Trait("Category", "CircularBuffer")] + [Fact] + public void Buffer_WithLargeLogEntry_DiscardsManySmallEntries() + { + // Arrange + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug, + MaxBytes = 2048 // Small buffer size to trigger overflow + }, + LogOutput = _consoleOut + }; + + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + var provider = new BufferingLoggerProvider(config, powertoolsConfig); + var logger = provider.CreateLogger("TestLogger"); + + LogBufferManager.SetInvocationId("large-entry-test"); + + // Act - add many small entries first + for (int i = 0; i < 10; i++) + { + logger.LogDebug($"Small message {i}"); + } + + // Add one very large entry that should displace many small ones + var largeMessage = new string('X', 80); // Large enough to push out multiple small entries + logger.LogDebug($"Large message: {largeMessage}"); + + // Flush buffer + logger.FlushBuffer(); + + // Assert + var output = _consoleOut.ToString(); + + // Several early small messages should be discarded + for (int i = 0; i < 5; i++) + { + Assert.DoesNotContain($"Small message {i}", output); + } + + // Large message should be present + Assert.Contains("Large message: XXXX", output); + + // Some later small messages should remain + Assert.Contains("Small message 9", output); + } + + [Trait("Category", "CircularBuffer")] + [Fact] + public void Buffer_WithExtremelyLargeEntry_Discards() + { + // Arrange + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug, + MaxBytes = 4096 // Even with a larger buffer + }, + LogOutput = _consoleOut + }; + + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + var provider = new BufferingLoggerProvider(config, powertoolsConfig); + var logger = provider.CreateLogger("TestLogger"); + + LogBufferManager.SetInvocationId("extreme-entry-test"); + + // Act - add some small entries first + for (int i = 0; i < 5; i++) + { + logger.LogDebug($"Initial message {i}"); + } + + // Add entry larger than the entire buffer - should displace everything + var hugeMessage = new string('X', 3000); + logger.LogDebug($"Huge message: {hugeMessage}"); + + // Add more entries after + for (int i = 0; i < 5; i++) + { + logger.LogDebug($"Final message {i}"); + } + + // Flush buffer + logger.FlushBuffer(); + + // Assert + var output = _consoleOut.ToString(); + + // Initial messages should be discarded + for (int i = 0; i < 5; i++) + { + Assert.Contains($"Initial message {i}", output); + } + + // Huge message may be partially discarded depending on implementation + Assert.DoesNotContain("Huge message", output); + + // Some of the final messages should be present + Assert.Contains("Final message 4", output); + } + + [Trait("Category", "CircularBuffer")] + [Fact] + public void MultipleInvocations_EachHaveTheirOwnCircularBuffer() + { + // Arrange + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + Enabled = true, + BufferAtLogLevel = LogLevel.Debug + }, + LogOutput = _consoleOut + }; + + var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); + var provider = new BufferingLoggerProvider(config, powertoolsConfig); + var logger = provider.CreateLogger("TestLogger"); + + // Act - fill buffer for first invocation + LogBufferManager.SetInvocationId("invocation-1"); + for (int i = 0; i < 10; i++) + { + logger.LogDebug($"Invocation 1 message {i}"); + } + + // Switch to second invocation with fresh buffer + LogBufferManager.SetInvocationId("invocation-2"); + for (int i = 0; i < 5; i++) + { + logger.LogDebug($"Invocation 2 message {i}"); + } + + // Flush second invocation first + logger.FlushBuffer(); + var outputAfterSecond = _consoleOut.ToString(); + + // Flush first invocation + LogBufferManager.SetInvocationId("invocation-1"); + logger.FlushBuffer(); + var outputAfterBoth = _consoleOut.ToString(); + + // Assert + // First invocation buffer should be complete + for (int i = 0; i < 5; i++) + { + Assert.Contains($"Invocation 1 message {i}", outputAfterBoth); + } + + // Second invocation buffer should be complete (not affected by first) + for (int i = 0; i < 5; i++) + { + Assert.Contains($"Invocation 2 message {i}", outputAfterSecond); + } + } + + public void Dispose() + { + // Clean up all state between tests + Logger.ClearBuffer(); + LogBufferManager.ResetForTesting(); + } +} \ No newline at end of file From adb331a69f073925ddeca517a0ba1fe57204a0a8 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 3 Apr 2025 22:40:14 +0100 Subject: [PATCH 32/49] log buffering - remove enabled property;Tracer-Id env variable;Buffer has precedence;when overflow warn;decorator catch errors;buffer bellow bufferatloglevel --- .../Internal/Buffer/BufferedLogEntry.cs | 29 ++ .../Buffer/BufferingLoggerProvider.cs | 12 +- .../Internal/Buffer/InvocationBuffer.cs | 78 +++++ .../Internal/Buffer/LogBuffer.cs | 94 +---- .../Internal/Buffer/LogBufferManager.cs | 9 - .../Internal/Buffer/Logger.Buffer.cs | 1 - .../Buffer/PowertoolsBufferingLogger.cs | 88 +++-- .../Internal/LoggingAspect.cs | 8 +- .../Internal/PowertoolsLogger.cs | 9 + .../LogBufferingOptions.cs | 9 +- .../PowertoolsLoggerBuilder.cs | 37 +- .../PowertoolsLoggerConfiguration.cs | 2 +- .../PowertoolsLoggingBuilderExtensions.cs | 3 +- .../Buffering/LambdaContextBufferingTests.cs | 118 ++++--- .../Buffering/LogBufferCircularCacheTests.cs | 96 ++++-- .../Buffering/LogBufferingHandlerTests.cs | 49 +-- .../Buffering/LogBufferingTests.cs | 324 ++++++++---------- .../Handlers/ExceptionFunctionHandlerTests.cs | 1 + .../Handlers/HandlerTests.cs | 24 +- .../Handlers/TestHandlers.cs | 33 ++ .../PowertoolsLoggerBuilderTests.cs | 3 +- .../PowertoolsLoggerTest.cs | 18 +- 22 files changed, 572 insertions(+), 473 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferedLogEntry.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/InvocationBuffer.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferedLogEntry.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferedLogEntry.cs new file mode 100644 index 00000000..1cc65897 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferedLogEntry.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.Internal; + +internal class BufferedLogEntry +{ + public string Entry { get; } + public int Size { get; } + + public BufferedLogEntry(string entry, int calculatedSize) + { + Entry = entry; + Size = calculatedSize; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs index 40160099..7b612a83 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/BufferingLoggerProvider.cs @@ -25,13 +25,15 @@ namespace AWS.Lambda.Powertools.Logging.Internal; [ProviderAlias("PowertoolsBuffering")] internal class BufferingLoggerProvider : PowertoolsLoggerProvider { + private readonly IPowertoolsConfigurations _powertoolsConfigurations; private readonly ConcurrentDictionary _loggers = new(); - public BufferingLoggerProvider( + internal BufferingLoggerProvider( PowertoolsLoggerConfiguration config, IPowertoolsConfigurations powertoolsConfigurations) : base(config, powertoolsConfigurations) { + _powertoolsConfigurations = powertoolsConfigurations; // Register with the buffer manager LogBufferManager.RegisterProvider(this); } @@ -43,13 +45,13 @@ public override ILogger CreateLogger(string categoryName) name => new PowertoolsBufferingLogger( base.CreateLogger(name), // Use the parent's logger creation GetCurrentConfig, - name)); + _powertoolsConfigurations)); } /// /// Flush all buffered logs /// - public void FlushBuffers() + internal void FlushBuffers() { foreach (var logger in _loggers.Values) { @@ -60,7 +62,7 @@ public void FlushBuffers() /// /// Clear all buffered logs /// - public void ClearBuffers() + internal void ClearBuffers() { foreach (var logger in _loggers.Values) { @@ -71,7 +73,7 @@ public void ClearBuffers() /// /// Clear buffered logs for the current invocation only /// - public void ClearCurrentBuffer() + internal void ClearCurrentBuffer() { foreach (var logger in _loggers.Values) { diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/InvocationBuffer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/InvocationBuffer.cs new file mode 100644 index 00000000..8bf75312 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/InvocationBuffer.cs @@ -0,0 +1,78 @@ +/* + * 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.Concurrent; +using System.Collections.Generic; + +namespace AWS.Lambda.Powertools.Logging.Internal; + +/// +/// Buffer for a specific invocation +/// +internal class InvocationBuffer +{ + private readonly ConcurrentQueue _buffer = new(); + private int _currentSize; + + public void Add(string logEntry, int maxBytes, int size) + { + // If entry size exceeds max buffer size, discard the entry completely + if (size > maxBytes) + { + // Entry is too large to ever fit in buffer, discard it + return; + } + + if (_currentSize + size > maxBytes) + { + // Remove oldest entries until we have enough space + while (_currentSize + size > maxBytes && _buffer.TryDequeue(out var removed)) + { + _currentSize -= removed.Size; + HasEvictions = true; + } + + if (_currentSize < 0) _currentSize = 0; + } + + _buffer.Enqueue(new BufferedLogEntry(logEntry, size)); + _currentSize += size; + } + + public IReadOnlyCollection GetAndClear() + { + var entries = new List(); + + try + { + while (_buffer.TryDequeue(out var entry)) + { + entries.Add(entry.Entry); + } + } + catch (Exception) + { + _buffer.Clear(); + } + + _currentSize = 0; + return entries; + } + + public bool HasEntries => !_buffer.IsEmpty; + + public bool HasEvictions; +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs index a0217462..91382ee5 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs @@ -16,8 +16,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Threading; -using Microsoft.Extensions.Logging; +using AWS.Lambda.Powertools.Common; namespace AWS.Lambda.Powertools.Logging.Internal; @@ -26,27 +25,23 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// internal class LogBuffer { - // Use AsyncLocal for automatic context flow across async calls - private static readonly AsyncLocal _currentInvocationId = new AsyncLocal(); - - // Dictionary of buffers by invocation ID + private readonly IPowertoolsConfigurations _powertoolsConfigurations; + +// Dictionary of buffers by invocation ID private readonly ConcurrentDictionary _buffersByInvocation = new(); // Get the current invocation ID or create a fallback - private string CurrentInvocationId => _currentInvocationId.Value; - - /// - /// Set the current invocation ID (call this at the start of a Lambda invocation) - /// - public static void SetCurrentInvocationId(string invocationId) + private string CurrentInvocationId => _powertoolsConfigurations.XRayTraceId; + + public LogBuffer(IPowertoolsConfigurations powertoolsConfigurations) { - _currentInvocationId.Value = invocationId; + _powertoolsConfigurations = powertoolsConfigurations; } /// /// Add a log entry to the buffer for the current invocation /// - public void Add(string logEntry, int maxBytes) + public void Add(string logEntry, int maxBytes, int size) { var invocationId = CurrentInvocationId; if (string.IsNullOrEmpty(invocationId)) @@ -55,7 +50,7 @@ public void Add(string logEntry, int maxBytes) return; } var buffer = _buffersByInvocation.GetOrAdd(invocationId, _ => new InvocationBuffer()); - buffer.Add(logEntry, maxBytes); + buffer.Add(logEntry, maxBytes, size); } /// @@ -109,73 +104,12 @@ public bool HasEntries } } - /// - /// Buffer for a specific invocation - /// - private class InvocationBuffer + public bool HasEvictions { - private readonly ConcurrentQueue _buffer = new(); - private int _currentSize; - - public void Add(string logEntry, int maxBytes) - { - // Same implementation as before - var size = 100 + (logEntry?.Length ?? 0) * 2; - - // If entry size exceeds max buffer size, discard the entry completely - if (size > maxBytes) - { - // Entry is too large to ever fit in buffer, discard it - return; - } - - if (_currentSize + size > maxBytes) - { - // Remove oldest entries until we have enough space - while (_currentSize + size > maxBytes && _buffer.TryDequeue(out var removed)) - { - _currentSize -= removed.Size; - } - - if (_currentSize < 0) _currentSize = 0; - } - - _buffer.Enqueue(new BufferedLogEntry(logEntry, size)); - _currentSize += size; - } - - public IReadOnlyCollection GetAndClear() + get { - var entries = new List(); - - try - { - while (_buffer.TryDequeue(out var entry)) - { - entries.Add(entry.Entry); - } - } - catch (Exception) - { - _buffer.Clear(); - } - - _currentSize = 0; - return entries; + var invocationId = CurrentInvocationId; + return _buffersByInvocation.TryGetValue(invocationId, out var buffer) && buffer.HasEvictions; } - - public bool HasEntries => !_buffer.IsEmpty; - } -} - -internal class BufferedLogEntry -{ - public string Entry { get; } - public int Size { get; } - - public BufferedLogEntry(string entry, int calculatedSize) - { - Entry = entry; - Size = calculatedSize; } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBufferManager.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBufferManager.cs index b7f47f43..9e3a3aa8 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBufferManager.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBufferManager.cs @@ -34,14 +34,6 @@ internal static void RegisterProvider(BufferingLoggerProvider provider) Providers.Add(provider); } - /// - /// Set the current invocation ID to isolate logs between invocations - /// - internal static void SetInvocationId(string invocationId) - { - LogBuffer.SetCurrentInvocationId(invocationId); - } - /// /// Flush buffered logs for the current invocation /// @@ -93,6 +85,5 @@ internal static void UnregisterProvider(BufferingLoggerProvider provider) internal static void ResetForTesting() { Providers.Clear(); - LogBuffer.SetCurrentInvocationId(null); } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/Logger.Buffer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/Logger.Buffer.cs index f3739651..9e715c55 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/Logger.Buffer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/Logger.Buffer.cs @@ -14,7 +14,6 @@ * permissions and limitations under the License. */ -using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; namespace AWS.Lambda.Powertools.Logging; diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs index f8147418..134b365b 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs @@ -1,6 +1,21 @@ +/* + * 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 AWS.Lambda.Powertools.Common; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace AWS.Lambda.Powertools.Logging.Internal; @@ -11,17 +26,16 @@ internal class PowertoolsBufferingLogger : ILogger { private readonly ILogger _innerLogger; private readonly Func _getCurrentConfig; - private readonly string _categoryName; - private readonly LogBuffer _buffer = new(); + private readonly LogBuffer _buffer; public PowertoolsBufferingLogger( ILogger innerLogger, Func getCurrentConfig, - string categoryName) + IPowertoolsConfigurations powertoolsConfigurations) { _innerLogger = innerLogger; _getCurrentConfig = getCurrentConfig; - _categoryName = categoryName; + _buffer = new LogBuffer(powertoolsConfigurations); } public IDisposable BeginScope(TState state) @@ -31,30 +45,7 @@ public IDisposable BeginScope(TState state) public bool IsEnabled(LogLevel logLevel) { - var options = _getCurrentConfig(); - - // If buffering is disabled, defer to inner logger - if (!options.LogBuffering.Enabled) - { - return _innerLogger.IsEnabled(logLevel); - } - - // If the log level is at or above the configured minimum log level, - // let the inner logger decide - if (logLevel >= options.MinimumLogLevel) - { - return _innerLogger.IsEnabled(logLevel); - } - - // For logs below minimum level but at or above buffer threshold, - // we should handle them (buffer them) - if (logLevel >= options.LogBuffering.BufferAtLogLevel) - { - return true; - } - - // Otherwise, the log level is below our buffer threshold - return false; + return true; } public void Log( @@ -64,17 +55,11 @@ public void Log( Exception exception, Func formatter) { - // Skip if logger is not enabled for this level - if (!IsEnabled(logLevel)) - return; - var options = _getCurrentConfig(); var bufferOptions = options.LogBuffering; // Check if this log should be buffered - bool shouldBuffer = bufferOptions.Enabled && - logLevel >= bufferOptions.BufferAtLogLevel && - logLevel < options.MinimumLogLevel; + bool shouldBuffer = logLevel <= bufferOptions.BufferAtLogLevel; if (shouldBuffer) { @@ -84,7 +69,19 @@ public void Log( if (_innerLogger is PowertoolsLogger powertoolsLogger) { var logEntry = powertoolsLogger.LogEntryString(logLevel, state, exception, formatter); - _buffer.Add(logEntry, bufferOptions.MaxBytes); + + // Check the size of the log entry, log it if too large + var size = 100 + (logEntry?.Length ?? 0) * 2; + if (size > bufferOptions.MaxBytes) + { + // log the entry directly if it exceeds the buffer size + powertoolsLogger.LogLine(logEntry); + powertoolsLogger.LogWarning("Cannot add item to the buffer"); + } + else + { + _buffer.Add(logEntry, bufferOptions.MaxBytes, size); + } } } catch (Exception ex) @@ -103,15 +100,11 @@ public void Log( else { // If this is an error and we should flush on error - if (bufferOptions.Enabled && - bufferOptions.FlushOnErrorLog && + if (bufferOptions.FlushOnErrorLog && logLevel >= LogLevel.Error) { FlushBuffer(); } - - // When not buffering, forward to the inner logger - _innerLogger.Log(logLevel, eventId, state, exception, formatter); } } @@ -122,11 +115,16 @@ public void FlushBuffer() { try { - // Get all buffered entries - var entries = _buffer.GetAndClear(); - if (_innerLogger is PowertoolsLogger powertoolsLogger) { + if (_buffer.HasEvictions) + { + powertoolsLogger.LogWarning("Some logs are not displayed because they were evicted from the buffer. Increase buffer size to store more logs in the buffer"); + } + + // Get all buffered entries + var entries = _buffer.GetAndClear(); + // Log each entry directly foreach (var entry in entries) { diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index 9c79f367..acb2ec03 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -99,7 +99,7 @@ private void InitializeLogger(LoggingAttribute trigger) // Set operational flags based on current configuration _isDebug = _currentConfig.MinimumLogLevel <= LogLevel.Debug; - _bufferingEnabled = _currentConfig.LogBuffering?.Enabled ?? false; + _bufferingEnabled = _currentConfig.LogBuffering != null; } /// @@ -156,12 +156,6 @@ public void OnEntry( var eventObject = eventArgs.Args.FirstOrDefault(); CaptureXrayTraceId(); CaptureLambdaContext(eventArgs); - - if (_bufferingEnabled) - { - LogBufferManager.SetInvocationId(LoggingLambdaContext.Instance.AwsRequestId); - } - CaptureCorrelationId(eventObject, trigger.CorrelationIdPath); if(trigger.IsLogEventSet && trigger.LogEvent) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs index 9b0028cb..6421c79c 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -86,6 +86,15 @@ internal void EndScope() public bool IsEnabled(LogLevel logLevel) { var config = _currentConfig(); + + //if Buffering is enabled and the log level is below the buffer threshold, skip logging only if bellow error + if (logLevel <= config.LogBuffering?.BufferAtLogLevel + && config.LogBuffering?.BufferAtLogLevel != LogLevel.Error + && config.LogBuffering?.BufferAtLogLevel != LogLevel.Critical) + { + return false; + } + // If we have no explicit minimum level, use the default var effectiveMinLevel = config.MinimumLogLevel != LogLevel.None ? config.MinimumLogLevel diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LogBufferingOptions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LogBufferingOptions.cs index 72c9a590..9d31471d 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LogBufferingOptions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LogBufferingOptions.cs @@ -22,19 +22,18 @@ namespace AWS.Lambda.Powertools.Logging; /// public class LogBufferingOptions { - /// - /// Gets or sets whether buffering is enabled - /// - public bool Enabled { get; set; } = false; - /// /// Gets or sets the maximum size of the buffer in bytes + /// /// Default is 20KB (20480 bytes) /// public int MaxBytes { get; set; } = 20480; /// /// Gets or sets the minimum log level to buffer + /// Defaults to Debug + /// + /// Valid values are: Trace, Debug, Information, Warning /// public LogLevel BufferAtLogLevel { get; set; } = LogLevel.Debug; diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs index 4f5df599..e822b3c5 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerBuilder.cs @@ -92,17 +92,6 @@ public PowertoolsLoggerBuilder WithFormatter(ILogFormatter formatter) return this; } - /// - /// Enables or disables log buffering with default options. - /// - /// Whether log buffering should be enabled. - /// The builder instance for method chaining. - public PowertoolsLoggerBuilder WithLogBuffering(bool enabled = true) - { - _configuration.LogBuffering.Enabled = enabled; - return this; - } - /// /// Configures log buffering with custom options. /// @@ -110,10 +99,36 @@ public PowertoolsLoggerBuilder WithLogBuffering(bool enabled = true) /// The builder instance for method chaining. public PowertoolsLoggerBuilder WithLogBuffering(Action configure) { + _configuration.LogBuffering = new LogBufferingOptions(); configure?.Invoke(_configuration.LogBuffering); return this; } + /// + /// Specifies the console output wrapper used for writing logs. This property allows + /// redirecting log output for testing or specialized handling scenarios. + /// Defaults to standard console output via ConsoleWrapper. + /// + /// + /// + /// // Using TestLoggerOutput + /// .WithLogOutput(new TestLoggerOutput()); + /// + /// // Custom console output for testing + /// .WithLogOutput(new TestConsoleWrapper()); + /// + /// // Example implementation for testing: + /// public class TestConsoleWrapper : IConsoleWrapper + /// { + /// public List<string> CapturedOutput { get; } = new(); + /// + /// public void WriteLine(string message) + /// { + /// CapturedOutput.Add(message); + /// } + /// } + /// + /// public PowertoolsLoggerBuilder WithLogOutput(IConsoleWrapper console) { _configuration.LogOutput = console ?? throw new ArgumentNullException(nameof(console)); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs index 35361eec..a05b0156 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs @@ -267,7 +267,7 @@ public JsonSerializerOptions JsonOptions /// }; /// /// - public LogBufferingOptions LogBuffering { get; set; } = new(); + public LogBufferingOptions LogBuffering { get; set; } /// /// Serializer instance for this configuration diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs index 41c4210b..1c3a5420 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs @@ -110,6 +110,7 @@ public static ILoggingBuilder AddPowertoolsLogger( /// Adds the Powertools logger to the logging builder with default configuration. /// /// The logging builder to configure. + /// /// The logging builder for further configuration. /// /// This method registers the Powertools logger with default settings. The logger will output @@ -174,7 +175,7 @@ public static ILoggingBuilder AddPowertoolsLogger( UpdateConfiguration(options); // If buffering is enabled, register buffer providers - if (options.LogBuffering?.Enabled == true) + if (options.LogBuffering != null) { // Add a filter for the buffer provider builder.AddFilter( diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LambdaContextBufferingTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LambdaContextBufferingTests.cs index 0788b701..1da4b235 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LambdaContextBufferingTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LambdaContextBufferingTests.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Amazon.Lambda.Core; using Amazon.Lambda.TestUtilities; @@ -25,29 +26,29 @@ public LambdaContextBufferingTests(ITestOutputHelper output) } [Fact] - public void DisabledBuffering_LogsAllLevelsDirectly() + public void FlushOnErrorEnabled_AutomaticallyFlushesBuffer() { // Arrange - var logger = CreateLogger(LogLevel.Debug, false, LogLevel.Debug); - var handler = new LambdaHandler(logger); - var context = CreateTestContext("test-request-2"); + var logger = CreateLoggerWithFlushOnError(true); + var handler = new ErrorOnlyHandler(logger); + var context = CreateTestContext("test-request-3"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); // Act handler.TestMethod("Event", context); // Assert var output = _consoleOut.ToString(); - Assert.Contains("Information message", output); Assert.Contains("Debug message", output); - Assert.Contains("Error message", output); + Assert.Contains("Error triggering flush", output); } - + [Fact] - public void FlushOnErrorEnabled_AutomaticallyFlushesBuffer() + public void Decorator_Clears_Buffer_On_Exit() { // Arrange - var logger = CreateLoggerWithFlushOnError(true); - var handler = new ErrorOnlyHandler(logger); + var logger = CreateLoggerWithFlushOnError(false); + var handler = new NoFlushHandler(logger); var context = CreateTestContext("test-request-3"); // Act @@ -55,18 +56,38 @@ public void FlushOnErrorEnabled_AutomaticallyFlushesBuffer() // Assert var output = _consoleOut.ToString(); - Assert.Contains("Debug message", output); - Assert.Contains("Error triggering flush", output); + Assert.DoesNotContain("Debug message", output); + + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-request-3"); + Logger.FlushBuffer(); + + var debugNotFlushed = _consoleOut.ToString(); + Assert.DoesNotContain("Debug message", debugNotFlushed); + + // second event + handler.TestMethod("Event", context); + + // Assert + var output2 = _consoleOut.ToString(); + Assert.DoesNotContain("Debug message", output2); + + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-request-4"); + Logger.FlushBuffer(); + + var debugNotFlushed2 = _consoleOut.ToString(); + Assert.DoesNotContain("Debug message", debugNotFlushed2); } [Fact] public async Task AsyncOperations_MaintainBufferContext() { // Arrange - var logger = CreateLogger(LogLevel.Information, true, LogLevel.Debug); + var logger = CreateLogger(LogLevel.Information, LogLevel.Debug); var handler = new AsyncLambdaHandler(logger); var context = CreateTestContext("async-test"); - + + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); + // Act await handler.TestMethodAsync("Event", context); @@ -88,20 +109,7 @@ private TestLambdaContext CreateTestContext(string requestId) }; } - private int CountOccurrences(string text, string pattern) - { - int count = 0; - int i = 0; - while ((i = text.IndexOf(pattern, i)) != -1) - { - i += pattern.Length; - count++; - } - - return count; - } - - private ILogger CreateLogger(LogLevel minimumLevel, bool enableBuffering, LogLevel bufferAtLevel) + private ILogger CreateLogger(LogLevel minimumLevel, LogLevel bufferAtLevel) { return LoggerFactory.Create(builder => { @@ -112,7 +120,6 @@ private ILogger CreateLogger(LogLevel minimumLevel, bool enableBuffering, LogLev config.LogOutput = _consoleOut; config.LogBuffering = new LogBufferingOptions { - Enabled = enableBuffering, BufferAtLogLevel = bufferAtLevel }; }); @@ -130,7 +137,6 @@ private ILogger CreateLoggerWithFlushOnError(bool flushOnError) config.LogOutput = _consoleOut; config.LogBuffering = new LogBufferingOptions { - Enabled = true, BufferAtLogLevel = LogLevel.Debug, FlushOnErrorLog = flushOnError }; @@ -142,11 +148,14 @@ public void Dispose() { Logger.ClearBuffer(); LogBufferManager.ResetForTesting(); + Logger.Reset(); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", null); } } [Collection("Sequential")] + [SuppressMessage("Usage", "xUnit1031:Do not use blocking task operations in test method")] public class StaticLoggerBufferingTests : IDisposable { private readonly TestLoggerOutput _consoleOut; @@ -176,14 +185,14 @@ public void StaticLogger_BasicBufferingBehavior() options.MinimumLogLevel = LogLevel.Information; options.LogBuffering = new LogBufferingOptions { - Enabled = true, + BufferAtLogLevel = LogLevel.Debug, FlushOnErrorLog = false // Disable auto-flush to test manual flush }; }); // Set invocation ID manually - LogBufferManager.SetInvocationId("test-static-request-1"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-static-request-1"); // Act - log messages Logger.AppendKey("custom-key", "custom-value"); @@ -208,12 +217,13 @@ public void StaticLogger_BasicBufferingBehavior() public void StaticLogger_WithLoggingDecoratedHandler() { // Arrange + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); Logger.Configure(options => { options.LogOutput = _consoleOut; options.LogBuffering = new LogBufferingOptions { - Enabled = true, + BufferAtLogLevel = LogLevel.Debug, FlushOnErrorLog = true }; @@ -248,13 +258,13 @@ public void StaticLogger_ClearBufferRemovesLogs() options.MinimumLogLevel = LogLevel.Information; options.LogBuffering = new LogBufferingOptions { - Enabled = true, + BufferAtLogLevel = LogLevel.Debug }; }); // Set invocation ID - LogBufferManager.SetInvocationId("test-static-request-3"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-static-request-3"); // Act - log message and clear buffer Logger.LogDebug("Debug message before clear"); @@ -278,14 +288,14 @@ public void StaticLogger_FlushOnErrorLogEnabled() options.MinimumLogLevel = LogLevel.Information; options.LogBuffering = new LogBufferingOptions { - Enabled = true, + BufferAtLogLevel = LogLevel.Debug, FlushOnErrorLog = true }; }); // Set invocation ID - LogBufferManager.SetInvocationId("test-static-request-4"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-static-request-4"); // Act - log debug then error Logger.LogDebug("Debug message"); @@ -307,17 +317,17 @@ public void StaticLogger_MultipleInvocationsIsolated() options.MinimumLogLevel = LogLevel.Information; options.LogBuffering = new LogBufferingOptions { - Enabled = true, + BufferAtLogLevel = LogLevel.Debug }; }); // Act - first invocation - LogBufferManager.SetInvocationId("test-static-request-5A"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-static-request-5A"); Logger.LogDebug("Debug from invocation A"); // Switch to second invocation - LogBufferManager.SetInvocationId("test-static-request-5B"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-static-request-5B"); Logger.LogDebug("Debug from invocation B"); Logger.FlushBuffer(); // Only flush B @@ -327,7 +337,7 @@ public void StaticLogger_MultipleInvocationsIsolated() Assert.DoesNotContain("Debug from invocation A", outputAfterFirstFlush); // Switch back to first invocation and flush - LogBufferManager.SetInvocationId("test-static-request-5A"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-static-request-5A"); Logger.FlushBuffer(); // Assert - after second flush @@ -346,13 +356,12 @@ public void StaticLogger_FlushOnErrorDisabled() options.MinimumLogLevel = LogLevel.Information; options.LogBuffering = new LogBufferingOptions { - Enabled = true, BufferAtLogLevel = LogLevel.Debug, FlushOnErrorLog = false }; }); - LogBufferManager.SetInvocationId("test-static-request-6"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-static-request-6"); // Act - log debug then error Logger.LogDebug("Debug message with auto-flush disabled"); @@ -380,13 +389,12 @@ public void StaticLogger_AsyncOperationsMaintainContext() options.MinimumLogLevel = LogLevel.Information; options.LogBuffering = new LogBufferingOptions { - Enabled = true, BufferAtLogLevel = LogLevel.Debug, FlushOnErrorLog = false }; }); - LogBufferManager.SetInvocationId("test-static-request-8"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-static-request-8"); // Act - simulate async operations Task.Run(() => { Logger.LogDebug("Debug from task 1"); }).Wait(); @@ -412,6 +420,8 @@ public void Dispose() LogBufferManager.ResetForTesting(); LoggerFactoryHolder.Reset(); _consoleOut.Clear(); + Logger.Reset(); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", null); } } @@ -465,7 +475,25 @@ public void TestMethod(string message, ILambdaContext lambdaContext) _logger.LogError("Error triggering flush"); } } + + public class NoFlushHandler + { + private readonly ILogger _logger; + + public NoFlushHandler(ILogger logger) + { + _logger = logger; + } + [Logging(LogEvent = true)] + public void TestMethod(string message, ILambdaContext lambdaContext) + { + _logger.LogDebug("Debug message"); + _logger.LogError("Error triggering flush"); + // No flush here - Decorator clears buffer on exit + } + } + public class AsyncLambdaHandler { private readonly ILogger _logger; diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferCircularCacheTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferCircularCacheTests.cs index 7071cd7d..a2e9bbdd 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferCircularCacheTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferCircularCacheTests.cs @@ -2,6 +2,7 @@ using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Common.Tests; using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Internal.Helpers; using Microsoft.Extensions.Logging; using Xunit; @@ -27,18 +28,15 @@ public void Buffer_WhenMaxSizeExceeded_DiscardOldestEntries() MinimumLogLevel = LogLevel.Information, LogBuffering = new LogBufferingOptions { - Enabled = true, BufferAtLogLevel = LogLevel.Debug, MaxBytes = 1024 // Small buffer size to trigger overflow }, LogOutput = _consoleOut }; - var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); - var provider = new BufferingLoggerProvider(config, powertoolsConfig); - var logger = provider.CreateLogger("TestLogger"); + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); - LogBufferManager.SetInvocationId("circular-buffer-test"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "circular-buffer-test"); // Act - add many debug logs to fill buffer for (int i = 0; i < 5; i++) @@ -66,6 +64,46 @@ public void Buffer_WhenMaxSizeExceeded_DiscardOldestEntries() Assert.Contains("New debug message 3", output); Assert.Contains("New debug message 4", output); } + + [Trait("Category", "CircularBuffer")] + [Fact] + public void Buffer_WhenMaxSizeExceeded_DiscardOldestEntries_Warn() + { + // Arrange + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Debug, + MaxBytes = 1024 // Small buffer size to trigger overflow + }, + LogOutput = _consoleOut + }; + + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); + + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "circular-buffer-test"); + + // Act - add many debug logs to fill buffer + for (int i = 0; i < 5; i++) + { + logger.LogDebug($"Old debug message {i} that should be removed"); + } + + // Add more logs that should push out the older ones + for (int i = 0; i < 5; i++) + { + logger.LogDebug($"New debug message {i} that should remain"); + } + + // Flush buffer + logger.FlushBuffer(); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Some logs are not displayed because they were evicted from the buffer. Increase buffer size to store more logs in the buffer", output); + } [Trait("Category", "CircularBuffer")] [Fact] @@ -77,18 +115,15 @@ public void Buffer_WithLargeLogEntry_DiscardsManySmallEntries() MinimumLogLevel = LogLevel.Information, LogBuffering = new LogBufferingOptions { - Enabled = true, BufferAtLogLevel = LogLevel.Debug, MaxBytes = 2048 // Small buffer size to trigger overflow }, LogOutput = _consoleOut }; - var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); - var provider = new BufferingLoggerProvider(config, powertoolsConfig); - var logger = provider.CreateLogger("TestLogger"); + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); - LogBufferManager.SetInvocationId("large-entry-test"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "large-entry-test"); // Act - add many small entries first for (int i = 0; i < 10; i++) @@ -121,7 +156,7 @@ public void Buffer_WithLargeLogEntry_DiscardsManySmallEntries() [Trait("Category", "CircularBuffer")] [Fact] - public void Buffer_WithExtremelyLargeEntry_Discards() + public void Buffer_WithExtremelyLargeEntry_Logs_Directly_And_Warning() { // Arrange var config = new PowertoolsLoggerConfiguration @@ -129,21 +164,18 @@ public void Buffer_WithExtremelyLargeEntry_Discards() MinimumLogLevel = LogLevel.Information, LogBuffering = new LogBufferingOptions { - Enabled = true, BufferAtLogLevel = LogLevel.Debug, MaxBytes = 4096 // Even with a larger buffer }, LogOutput = _consoleOut }; - var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); - var provider = new BufferingLoggerProvider(config, powertoolsConfig); - var logger = provider.CreateLogger("TestLogger"); + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); - LogBufferManager.SetInvocationId("extreme-entry-test"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "extreme-entry-test"); // Act - add some small entries first - for (int i = 0; i < 5; i++) + for (int i = 0; i < 4; i++) { logger.LogDebug($"Initial message {i}"); } @@ -152,8 +184,15 @@ public void Buffer_WithExtremelyLargeEntry_Discards() var hugeMessage = new string('X', 3000); logger.LogDebug($"Huge message: {hugeMessage}"); + var bigMessageAndWarning = _consoleOut.ToString(); + + // Huge message may be partially discarded depending on implementation + Assert.Contains("Huge message", bigMessageAndWarning); + Assert.Contains("level\":\"Warning", bigMessageAndWarning); + Assert.Contains("Cannot add item to the buffer", bigMessageAndWarning); + // Add more entries after - for (int i = 0; i < 5; i++) + for (int i = 0; i < 4; i++) { logger.LogDebug($"Final message {i}"); } @@ -165,16 +204,13 @@ public void Buffer_WithExtremelyLargeEntry_Discards() var output = _consoleOut.ToString(); // Initial messages should be discarded - for (int i = 0; i < 5; i++) + for (int i = 0; i < 4; i++) { Assert.Contains($"Initial message {i}", output); } - // Huge message may be partially discarded depending on implementation - Assert.DoesNotContain("Huge message", output); - // Some of the final messages should be present - Assert.Contains("Final message 4", output); + Assert.Contains("Final message 3", output); } [Trait("Category", "CircularBuffer")] @@ -187,25 +223,23 @@ public void MultipleInvocations_EachHaveTheirOwnCircularBuffer() MinimumLogLevel = LogLevel.Information, LogBuffering = new LogBufferingOptions { - Enabled = true, BufferAtLogLevel = LogLevel.Debug }, LogOutput = _consoleOut }; - var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); - var provider = new BufferingLoggerProvider(config, powertoolsConfig); - var logger = provider.CreateLogger("TestLogger"); + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); // Act - fill buffer for first invocation - LogBufferManager.SetInvocationId("invocation-1"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); for (int i = 0; i < 10; i++) { logger.LogDebug($"Invocation 1 message {i}"); } // Switch to second invocation with fresh buffer - LogBufferManager.SetInvocationId("invocation-2"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-2"); + for (int i = 0; i < 5; i++) { logger.LogDebug($"Invocation 2 message {i}"); @@ -216,7 +250,7 @@ public void MultipleInvocations_EachHaveTheirOwnCircularBuffer() var outputAfterSecond = _consoleOut.ToString(); // Flush first invocation - LogBufferManager.SetInvocationId("invocation-1"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); logger.FlushBuffer(); var outputAfterBoth = _consoleOut.ToString(); @@ -239,5 +273,7 @@ public void Dispose() // Clean up all state between tests Logger.ClearBuffer(); LogBufferManager.ResetForTesting(); + Logger.Reset(); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", null); } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingHandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingHandlerTests.cs index b8406db2..cbe95411 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingHandlerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingHandlerTests.cs @@ -26,9 +26,9 @@ public LogBufferingHandlerTests(ITestOutputHelper output) public void BasicBufferingBehavior_BuffersDebugLogsOnly() { // Arrange - var logger = CreateLogger(LogLevel.Information, true, LogLevel.Debug); + var logger = CreateLogger(LogLevel.Information, LogLevel.Debug); var handler = new HandlerWithoutFlush(logger); // Use a handler that doesn't flush - LogBufferManager.SetInvocationId("test-invocation"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); // Act - log messages without flushing handler.TestMethod(); @@ -49,30 +49,12 @@ public void BasicBufferingBehavior_BuffersDebugLogsOnly() Assert.Contains("Debug message", outputAfterFlush); // Debug should now be present } - [Fact] - public void DisabledBuffering_LogsAllLevelsDirectly() - { - // Arrange - var logger = CreateLogger(LogLevel.Debug, false, LogLevel.Debug); - var handler = new Handlers(logger); - LogBufferManager.SetInvocationId("test-invocation"); - - // Act - handler.TestMethod(); - - // Assert - var output = _consoleOut.ToString(); - Assert.Contains("Information message", output); - Assert.Contains("Error message", output); - Assert.Contains("Debug message", output); // Should be logged directly - } - [Fact] public void FlushOnErrorEnabled_AutomaticallyFlushesBuffer() { // Arrange var logger = CreateLoggerWithFlushOnError(true); - LogBufferManager.SetInvocationId("test-invocation"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); // Act - with custom handler that doesn't manually flush var handler = new CustomHandlerWithoutFlush(logger); @@ -89,7 +71,7 @@ public void FlushOnErrorDisabled_DoesNotAutomaticallyFlushBuffer() { // Arrange var logger = CreateLoggerWithFlushOnError(false); - LogBufferManager.SetInvocationId("test-invocation"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); // Act var handler = new CustomHandlerWithoutFlush(logger); @@ -105,8 +87,8 @@ public void FlushOnErrorDisabled_DoesNotAutomaticallyFlushBuffer() public void ClearingBuffer_RemovesBufferedLogs() { // Arrange - var logger = CreateLogger(LogLevel.Information, true, LogLevel.Debug); - LogBufferManager.SetInvocationId("test-invocation"); + var logger = CreateLogger(LogLevel.Information, LogLevel.Debug); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); // Act var handler = new ClearBufferHandler(logger); @@ -122,14 +104,14 @@ public void ClearingBuffer_RemovesBufferedLogs() public void MultipleInvocations_IsolateLogBuffers() { // Arrange - var logger = CreateLogger(LogLevel.Information, true, LogLevel.Debug); + var logger = CreateLogger(LogLevel.Information, LogLevel.Debug); var handler = new Handlers(logger); // Act - LogBufferManager.SetInvocationId("invocation-1"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); handler.TestMethod(); - LogBufferManager.SetInvocationId("invocation-2"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-2"); // Create a custom handler that logs different messages var customHandler = new MultipleInvocationHandler(logger); customHandler.TestMethod(); @@ -147,7 +129,7 @@ public void MultipleProviders_AllProvidersReceiveLogs() var config = new PowertoolsLoggerConfiguration { MinimumLogLevel = LogLevel.Information, - LogBuffering = new LogBufferingOptions { Enabled = true, BufferAtLogLevel = LogLevel.Debug }, + LogBuffering = new LogBufferingOptions { BufferAtLogLevel = LogLevel.Debug }, LogOutput = _consoleOut }; @@ -160,7 +142,7 @@ public void MultipleProviders_AllProvidersReceiveLogs() var logger1 = provider1.CreateLogger("Provider1"); var logger2 = provider2.CreateLogger("Provider2"); - LogBufferManager.SetInvocationId("multi-provider-test"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "multi-provider-test"); // Act logger1.LogDebug("Debug from provider 1"); @@ -179,9 +161,9 @@ public void MultipleProviders_AllProvidersReceiveLogs() public async Task AsyncOperations_MaintainBufferContext() { // Arrange - var logger = CreateLogger(LogLevel.Information, true, LogLevel.Debug); + var logger = CreateLogger(LogLevel.Information, LogLevel.Debug); var handler = new AsyncHandler(logger); - LogBufferManager.SetInvocationId("async-test"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "async-test"); // Act await handler.TestMethodAsync(); @@ -193,7 +175,7 @@ public async Task AsyncOperations_MaintainBufferContext() Assert.Contains("Debug from task 2", output); } - private ILogger CreateLogger(LogLevel minimumLevel, bool enableBuffering, LogLevel bufferAtLevel) + private ILogger CreateLogger(LogLevel minimumLevel, LogLevel bufferAtLevel) { return LoggerFactory.Create(builder => { @@ -204,7 +186,6 @@ private ILogger CreateLogger(LogLevel minimumLevel, bool enableBuffering, LogLev config.LogOutput = _consoleOut; config.LogBuffering = new LogBufferingOptions { - Enabled = enableBuffering, BufferAtLogLevel = bufferAtLevel, FlushOnErrorLog = false }; @@ -223,7 +204,6 @@ private ILogger CreateLoggerWithFlushOnError(bool flushOnError) config.LogOutput = _consoleOut; config.LogBuffering = new LogBufferingOptions { - Enabled = true, BufferAtLogLevel = LogLevel.Debug, FlushOnErrorLog = flushOnError }; @@ -236,6 +216,7 @@ public void Dispose() // Clean up all state between tests Logger.ClearBuffer(); LogBufferManager.ResetForTesting(); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", null); } } diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingTests.cs index 2d0d45cd..f8fba6c2 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingTests.cs @@ -2,6 +2,7 @@ using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Common.Tests; using AWS.Lambda.Powertools.Logging.Internal; +using AWS.Lambda.Powertools.Logging.Internal.Helpers; using Microsoft.Extensions.Logging; using Xunit; @@ -24,22 +25,21 @@ public void SetInvocationId_IsolatesLogsBetweenInvocations() // Arrange var config = new PowertoolsLoggerConfiguration { - LogBuffering = new LogBufferingOptions { Enabled = true }, + LogBuffering = new LogBufferingOptions(), LogOutput = _consoleOut }; - var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); - var provider = new BufferingLoggerProvider(config, powertoolsConfig); - var logger = provider.CreateLogger("TestLogger"); + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); // Act - LogBufferManager.SetInvocationId("invocation-1"); + + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); logger.LogDebug("Debug message from invocation 1"); - LogBufferManager.SetInvocationId("invocation-2"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-2"); logger.LogDebug("Debug message from invocation 2"); - LogBufferManager.SetInvocationId("invocation-1"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); logger.LogError("Error message from invocation 1"); // Assert @@ -54,23 +54,21 @@ public void SetInvocationId_IsolatesLogsBetweenInvocations() public void BufferedLogger_OnlyBuffersConfiguredLogLevels() { // Arrange + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); + var config = new PowertoolsLoggerConfiguration { MinimumLogLevel = LogLevel.Information, LogBuffering = new LogBufferingOptions { - Enabled = true, - BufferAtLogLevel = LogLevel.Debug + BufferAtLogLevel = LogLevel.Trace }, LogOutput = _consoleOut }; - var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); - var provider = new BufferingLoggerProvider(config, powertoolsConfig); - var logger = provider.CreateLogger("TestLogger"); - LogBufferManager.SetInvocationId("invocation-1"); + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); // Act - logger.LogTrace("Trace message"); // Below buffer threshold, should be ignored + logger.LogTrace("Trace message"); // should buffer logger.LogDebug("Debug message"); // Should be buffered logger.LogInformation("Info message"); // Above minimum, should be logged directly @@ -84,7 +82,103 @@ public void BufferedLogger_OnlyBuffersConfiguredLogLevels() Logger.FlushBuffer(); output = _consoleOut.ToString(); - Assert.Contains("Debug message", output); // Now should be visible + Assert.Contains("Trace message", output); // Now should be visible + } + + [Trait("Category", "BufferedLogger")] + [Fact] + public void BufferedLogger_Buffer_Takes_Precedence_Same_Level() + { + // Arrange + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); + + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Information + }, + LogOutput = _consoleOut + }; + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); + + // Act + logger.LogTrace("Trace message"); // Below buffer threshold, should be ignored + logger.LogDebug("Debug message"); // Should be buffered + logger.LogInformation("Info message"); // Above minimum, should be logged directly + + // Assert + var output = _consoleOut.ToString(); + Assert.Empty(output); + + // Flush the buffer + Logger.FlushBuffer(); + + output = _consoleOut.ToString(); + Assert.Contains("Info message", output); // Now should be visible + } + + [Trait("Category", "BufferedLogger")] + [Fact] + public void BufferedLogger_Buffer_Takes_Precedence_Higher_Level() + { + // Arrange + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); + + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Warning + }, + LogOutput = _consoleOut + }; + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); + + // Act + logger.LogWarning("Warning message"); // Should be buffered + logger.LogInformation("Info message"); // Should be buffered + + // Assert + var output = _consoleOut.ToString(); + Assert.Empty(output); + + // Flush the buffer + Logger.FlushBuffer(); + + output = _consoleOut.ToString(); + Assert.DoesNotContain("Info message", output); // Now should be visible + Assert.Contains("Warning message", output); + } + + [Trait("Category", "BufferedLogger")] + [Fact] + public void BufferedLogger_Buffer_Log_Level_Error_Does_Not_Buffer() + { + // Arrange + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); + + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Error + }, + LogOutput = _consoleOut + }; + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); + + // Act + logger.LogError("Error message"); // Should be buffered + logger.LogInformation("Info message"); // Should be buffered + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Error message", output); + Assert.Contains("Info message", output); } [Trait("Category", "BufferedLogger")] @@ -92,21 +186,19 @@ public void BufferedLogger_OnlyBuffersConfiguredLogLevels() public void FlushOnErrorLog_FlushesBufferWhenEnabled() { // Arrange + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); var config = new PowertoolsLoggerConfiguration { MinimumLogLevel = LogLevel.Information, LogBuffering = new LogBufferingOptions { - Enabled = true, BufferAtLogLevel = LogLevel.Debug, FlushOnErrorLog = true }, LogOutput = _consoleOut }; - var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); - var provider = new BufferingLoggerProvider(config, powertoolsConfig); - var logger = provider.CreateLogger("TestLogger"); - LogBufferManager.SetInvocationId("invocation-1"); + + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); // Act logger.LogDebug("Debug message 1"); // Should be buffered @@ -125,20 +217,19 @@ public void FlushOnErrorLog_FlushesBufferWhenEnabled() public void ClearBuffer_RemovesAllBufferedLogs() { // Arrange + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); var config = new PowertoolsLoggerConfiguration { MinimumLogLevel = LogLevel.Information, LogBuffering = new LogBufferingOptions { - Enabled = true, BufferAtLogLevel = LogLevel.Debug }, LogOutput = _consoleOut }; - var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); - var provider = new BufferingLoggerProvider(config, powertoolsConfig); - var logger = provider.CreateLogger("TestLogger"); - LogBufferManager.SetInvocationId("invocation-1"); + + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); + // Act logger.LogDebug("Debug message 1"); // Should be buffered @@ -162,21 +253,19 @@ public void ClearBuffer_RemovesAllBufferedLogs() public void BufferSizeLimit_DiscardOldestEntriesWhenExceeded() { // Arrange + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); var config = new PowertoolsLoggerConfiguration { MinimumLogLevel = LogLevel.Information, LogBuffering = new LogBufferingOptions { - Enabled = true, BufferAtLogLevel = LogLevel.Debug, MaxBytes = 1000 // Small buffer size to force overflow }, LogOutput = _consoleOut }; - var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); - var provider = new BufferingLoggerProvider(config, powertoolsConfig); - var logger = provider.CreateLogger("TestLogger"); - LogBufferManager.SetInvocationId("invocation-1"); + + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); // Act // Add enough logs to exceed buffer size @@ -198,20 +287,19 @@ public void BufferSizeLimit_DiscardOldestEntriesWhenExceeded() public void DisposingProvider_FlushesBufferedLogs() { // Arrange + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); var config = new PowertoolsLoggerConfiguration { MinimumLogLevel = LogLevel.Information, LogBuffering = new LogBufferingOptions { - Enabled = true, BufferAtLogLevel = LogLevel.Debug }, LogOutput = _consoleOut }; - var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); - var provider = new BufferingLoggerProvider(config, powertoolsConfig); - var logger = provider.CreateLogger("TestLogger"); - LogBufferManager.SetInvocationId("invocation-1"); + + var provider = LoggerFactoryHelper.CreateAndConfigureFactory(config); + var logger = provider.CreatePowertoolsLogger(); // Act logger.LogDebug("Debug message before disposal"); // Should be buffered @@ -222,68 +310,20 @@ public void DisposingProvider_FlushesBufferedLogs() Assert.Contains("Debug message before disposal", output); } - [Trait("Category", "LoggerIntegration")] - [Fact] - public void DirectLoggerAndBufferedLogger_WorkTogether() - { - // Arrange - var config = new PowertoolsLoggerConfiguration - { - MinimumLogLevel = LogLevel.Information, - LogBuffering = new LogBufferingOptions - { - Enabled = true, - BufferAtLogLevel = LogLevel.Debug - }, - LogOutput = _consoleOut - }; - - var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); - - // Create both standard and buffering providers - var standardProvider = new PowertoolsLoggerProvider(config, powertoolsConfig); - var bufferingProvider = new BufferingLoggerProvider(config, powertoolsConfig); - - var standardLogger = standardProvider.CreateLogger("StandardLogger"); - var bufferedLogger = bufferingProvider.CreateLogger("BufferedLogger"); - - LogBufferManager.SetInvocationId("test-invocation"); - - // Act - standardLogger.LogInformation("Direct info message"); - bufferedLogger.LogDebug("Buffered debug message"); - bufferedLogger.LogInformation("Direct info from buffered logger"); - - // Assert - before flush - var output = _consoleOut.ToString(); - Assert.Contains("Direct info message", output); - Assert.Contains("Direct info from buffered logger", output); - Assert.DoesNotContain("Buffered debug message", output); - - // Flush and check again - Logger.FlushBuffer(); - output = _consoleOut.ToString(); - Assert.Contains("Buffered debug message", output); - } - [Trait("Category", "LoggerConfiguration")] [Fact] public void LoggerInitialization_RegistersWithBufferManager() { // Arrange + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-id"); var config = new PowertoolsLoggerConfiguration { - LogBuffering = new LogBufferingOptions { Enabled = true }, + LogBuffering = new LogBufferingOptions(), LogOutput = _consoleOut }; - var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); - - // Act - var provider = new BufferingLoggerProvider(config, powertoolsConfig); - var logger = provider.CreateLogger("TestLogger"); + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); - LogBufferManager.SetInvocationId("test-id"); logger.LogDebug("Test message"); Logger.FlushBuffer(); @@ -304,9 +344,7 @@ public void CustomLogOutput_ReceivesLogs() LogOutput = customOutput }; - var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); - var provider = new PowertoolsLoggerProvider(config, powertoolsConfig); - var logger = provider.CreateLogger("TestLogger"); + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); // Act logger.LogDebug("Direct debug message"); @@ -321,12 +359,12 @@ public void CustomLogOutput_ReceivesLogs() public void RegisteringMultipleProviders_AllWorkCorrectly() { // Arrange - create a clean configuration for this test + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "shared-invocation"); var config = new PowertoolsLoggerConfiguration { MinimumLogLevel = LogLevel.Information, LogBuffering = new LogBufferingOptions { - Enabled = true, BufferAtLogLevel = LogLevel.Debug }, LogOutput = _consoleOut @@ -344,8 +382,6 @@ public void RegisteringMultipleProviders_AllWorkCorrectly() var logger1 = provider1.CreateLogger("Logger1"); var logger2 = provider2.CreateLogger("Logger2"); - LogBufferManager.SetInvocationId("shared-invocation"); - // Act logger1.LogDebug("Debug from logger1"); logger2.LogDebug("Debug from logger2"); @@ -363,10 +399,11 @@ public void RegisteringLogBufferManager_HandlesMultipleProviders() { // Ensure we start with clean state LogBufferManager.ResetForTesting(); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); // Arrange var config = new PowertoolsLoggerConfiguration { - LogBuffering = new LogBufferingOptions { Enabled = true }, + LogBuffering = new LogBufferingOptions(), LogOutput = _consoleOut }; @@ -382,8 +419,6 @@ public void RegisteringLogBufferManager_HandlesMultipleProviders() var provider2 = new BufferingLoggerProvider(config, powertoolsConfig); var logger2 = provider2.CreateLogger("Logger2"); - LogBufferManager.SetInvocationId("test-invocation"); - // Act logger1.LogDebug("Debug from first provider"); logger2.LogDebug("Debug from second provider"); @@ -404,16 +439,16 @@ public void FlushingEmptyBuffer_DoesNotCauseErrors() { // Arrange LogBufferManager.ResetForTesting(); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "empty-test"); var config = new PowertoolsLoggerConfiguration { - LogBuffering = new LogBufferingOptions { Enabled = true }, + LogBuffering = new LogBufferingOptions(), LogOutput = _consoleOut }; var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); var provider = new BufferingLoggerProvider(config, powertoolsConfig); // Act - flush without any logs - LogBufferManager.SetInvocationId("empty-test"); Logger.FlushBuffer(); // Assert - should not throw exceptions @@ -426,12 +461,12 @@ public void LogsAtExactBufferThreshold_AreBuffered() { // Arrange LogBufferManager.ResetForTesting(); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "threshold-test"); var config = new PowertoolsLoggerConfiguration { MinimumLogLevel = LogLevel.Information, LogBuffering = new LogBufferingOptions { - Enabled = true, BufferAtLogLevel = LogLevel.Debug }, LogOutput = _consoleOut @@ -441,7 +476,6 @@ public void LogsAtExactBufferThreshold_AreBuffered() var logger = provider.CreateLogger("TestLogger"); // Act - LogBufferManager.SetInvocationId("threshold-test"); logger.LogDebug("Debug message exactly at threshold"); // Should be buffered // Assert before flush @@ -452,32 +486,6 @@ public void LogsAtExactBufferThreshold_AreBuffered() Assert.Contains("Debug message exactly at threshold", _consoleOut.ToString()); } - [Trait("Category", "LoggerDisabling")] - [Fact] - public void DisablingBuffering_StillLogsNormally() - { - // Arrange - LogBufferManager.ResetForTesting(); - var config = new PowertoolsLoggerConfiguration - { - MinimumLogLevel = LogLevel.Debug, - LogBuffering = new LogBufferingOptions - { - Enabled = false // Buffering disabled - }, - LogOutput = _consoleOut - }; - var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); - var provider = new BufferingLoggerProvider(config, powertoolsConfig); - var logger = provider.CreateLogger("TestLogger"); - - // Act - LogBufferManager.SetInvocationId("disabled-test"); - logger.LogDebug("Debug message with buffering disabled"); - - // Assert - should log immediately even without flushing - Assert.Contains("Debug message with buffering disabled", _consoleOut.ToString()); - } [Trait("Category", "MultipleInvocations")] [Fact] @@ -488,7 +496,7 @@ public void SwitchingBetweenInvocations_PreservesSeparateBuffers() var config = new PowertoolsLoggerConfiguration { MinimumLogLevel = LogLevel.Information, - LogBuffering = new LogBufferingOptions { Enabled = true }, + LogBuffering = new LogBufferingOptions(), LogOutput = _consoleOut }; var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); @@ -497,11 +505,11 @@ public void SwitchingBetweenInvocations_PreservesSeparateBuffers() // Act // First invocation - LogBufferManager.SetInvocationId("invocation-A"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-A"); logger.LogDebug("Debug for invocation A"); // Switch to second invocation - LogBufferManager.SetInvocationId("invocation-B"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-B"); logger.LogDebug("Debug for invocation B"); Logger.FlushBuffer(); // Only flush B @@ -511,75 +519,19 @@ public void SwitchingBetweenInvocations_PreservesSeparateBuffers() Assert.DoesNotContain("Debug for invocation A", output); // Now flush A - LogBufferManager.SetInvocationId("invocation-A"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-A"); Logger.FlushBuffer(); output = _consoleOut.ToString(); Assert.Contains("Debug for invocation A", output); } - [Trait("Category", "ConfigurationUpdate")] - [Fact] - public void ChangingConfigurationDynamically_UpdatesBufferingBehavior() - { - // Arrange - LogBufferManager.ResetForTesting(); - var initialConfig = new PowertoolsLoggerConfiguration - { - MinimumLogLevel = LogLevel.Warning, // Keep this as Warning - LogBuffering = new LogBufferingOptions - { - Enabled = true, - BufferAtLogLevel = LogLevel.Information // Buffer at info level - }, - LogOutput = _consoleOut - }; - - var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); - - // Create provider with initial config - var provider = new BufferingLoggerProvider(initialConfig, powertoolsConfig); - var logger = provider.CreateLogger("TestLogger"); - - LogBufferManager.SetInvocationId("config-test"); - - // Act - with initial config - logger.LogInformation("Info message with initial config"); - - // Should be buffered (Info < Warning minimum level) - Assert.DoesNotContain("Info message with initial config", _consoleOut.ToString()); - - // Update config to not buffer info anymore - var updatedConfig = new PowertoolsLoggerConfiguration - { - MinimumLogLevel = LogLevel.Information, // Changed to Information - LogBuffering = new LogBufferingOptions - { - Enabled = true, - BufferAtLogLevel = LogLevel.Debug // Only buffer debug level now - }, - LogOutput = _consoleOut - }; - - // Directly update the provider's configuration - provider.UpdateConfiguration(updatedConfig); - - // Log with updated config - logger.LogInformation("Info message with updated config"); - - // Assert - should log immediately with updated config - Assert.Contains("Info message with updated config", _consoleOut.ToString()); - - // Flush and check if first message appears - Logger.FlushBuffer(); - Assert.Contains("Info message with initial config", _consoleOut.ToString()); - } - public void Dispose() { // Clean up all state between tests Logger.ClearBuffer(); LogBufferManager.ResetForTesting(); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", null); } } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/ExceptionFunctionHandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/ExceptionFunctionHandlerTests.cs index 1bed72bf..7622ac23 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/ExceptionFunctionHandlerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/ExceptionFunctionHandlerTests.cs @@ -7,6 +7,7 @@ namespace AWS.Lambda.Powertools.Logging.Tests.Handlers; +[Collection("Sequential")] public sealed class ExceptionFunctionHandlerTests : IDisposable { [Fact] diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/HandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/HandlerTests.cs index 249aa47e..121f89d5 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/HandlerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/HandlerTests.cs @@ -6,6 +6,7 @@ using System.IO; using System.Text.Json; using System.Text.Json.Serialization; +using System.Threading.Tasks; using Amazon.Lambda.Core; using Amazon.Lambda.TestUtilities; using AWS.Lambda.Powertools.Common; @@ -193,8 +194,7 @@ public void TestBuffer() config.LogOutput = output; config.LogBuffering = new LogBufferingOptions { - Enabled = true, - BufferAtLogLevel = LogLevel.Debug, + BufferAtLogLevel = LogLevel.Debug }; }); }).CreatePowertoolsLogger(); @@ -253,6 +253,26 @@ public void TestMethodStatic() Assert.Contains("\"Level\":\"Information\"", logOutput); Assert.Contains("\"Message\":\"Static method\"", logOutput); } + + [Fact] + public async Task Should_Log_Properties_Setup_Constructor() + { + var output = new TestLoggerOutput(); + var handler = new SimpleFunctionWithStaticConfigure(output); + + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); + + await SimpleFunctionWithStaticConfigure.FunctionHandler(); + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + // Verify static logger configuration + // Verify override of LoggerOutputCase from attribute + Assert.Contains("\"service\":\"MyServiceName\"", logOutput); + Assert.Contains("\"level\":\"Information\"", logOutput); + Assert.Contains("\"message\":\"Starting up!\"", logOutput); + } [Fact] public void TestJsonOptionsPropertyNaming() diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs index 00d4fbba..9566918d 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs @@ -15,11 +15,13 @@ using System; using System.Text.Json.Serialization; +using System.Threading.Tasks; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.ApplicationLoadBalancerEvents; using Amazon.Lambda.CloudWatchEvents; using Amazon.Lambda.CloudWatchEvents.S3Events; using Amazon.Lambda.Core; +using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Tests.Serializers; using LogLevel = Microsoft.Extensions.Logging.LogLevel; @@ -197,4 +199,35 @@ public void Handler() { Logger.LogInformation("Service: Attribute Service"); } +} + +public class SimpleFunctionWithStaticConfigure +{ + public SimpleFunctionWithStaticConfigure(IConsoleWrapper output) + { + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); + // Constructor logic can go here if needed + Logger.Configure(logger => + { + logger.LogOutput = output; + logger.Service = "MyServiceName"; + logger.LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Debug, + }; + }); + } + + [Logging] + public static async Task FunctionHandler() + { + Logger.LogInformation("Starting up!"); + + // throw new Exception(); + return new APIGatewayHttpApiV2ProxyResponse + { + Body = "Hello", + StatusCode = 200 + }; + } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerBuilderTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerBuilderTests.cs index f0a8e920..bbb1c43b 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerBuilderTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerBuilderTests.cs @@ -157,12 +157,11 @@ public void WithLogBuffering_BuffersLowLevelLogs() .WithService("buffer-test") .WithLogBuffering(options => { - options.Enabled = true; options.BufferAtLogLevel = LogLevel.Debug; }) .Build(); - LogBufferManager.SetInvocationId("config-test"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "config-test"); logger.LogDebug("Debug buffered message"); logger.LogInformation("Info message"); diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs index 278c64cb..2932f3de 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs @@ -282,7 +282,7 @@ public void Log_ConfigurationIsNotProvided_ReadsFromEnvironmentVariables() configurations.LoggerSampleRate.Returns(loggerSampleRate); var systemWrapper = Substitute.For(); - + var loggerConfiguration = new PowertoolsLoggerConfiguration { Service = service, @@ -1478,11 +1478,11 @@ public void Log_Should_Serialize_TimeOnly() [Theory] - [InlineData(true, "WARN", LogLevel.Warning)] - [InlineData(false, "Fatal", LogLevel.Critical)] - [InlineData(false, "NotValid", LogLevel.Critical)] - [InlineData(true, "NotValid", LogLevel.Warning)] - public void Log_Should_Use_Powertools_Log_Level_When_Lambda_Log_Level_Enabled(bool willLog, string awsLogLevel, + [InlineData("WARN", LogLevel.Warning)] + [InlineData("Fatal", LogLevel.Critical)] + [InlineData("NotValid", LogLevel.Critical)] + [InlineData("NotValid", LogLevel.Warning)] + public void Log_Should_Use_Powertools_Log_Level_When_Lambda_Log_Level_Enabled(string awsLogLevel, LogLevel logLevel) { // Arrange @@ -1519,9 +1519,9 @@ public void Log_Should_Use_Powertools_Log_Level_When_Lambda_Log_Level_Enabled(bo } [Theory] - [InlineData(true, "WARN", LogLevel.Warning)] - [InlineData(true, "Fatal", LogLevel.Critical)] - public void Log_Should_Use_AWS_Lambda_Log_Level_When_Enabled(bool willLog, string awsLogLevel, + [InlineData("WARN", LogLevel.Warning)] + [InlineData("Fatal", LogLevel.Critical)] + public void Log_Should_Use_AWS_Lambda_Log_Level_When_Enabled(string awsLogLevel, LogLevel logLevel) { // Arrange From 08c50a40f16ca18c11c3eb4f9bd5666871f1b7bd Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 3 Apr 2025 23:22:18 +0100 Subject: [PATCH 33/49] override console logger level on lambda. --- .../Core/ConsoleWrapper.cs | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs index 75e43a67..8b1c4b3e 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs @@ -21,36 +21,39 @@ namespace AWS.Lambda.Powertools.Common; /// public class ConsoleWrapper : IConsoleWrapper { - private static bool _redirected; + /// + public void WriteLine(string message) + { + OverrideLambdaLogger(); + Console.WriteLine(message); + } - /// - /// Initializes a new instance of the class. - /// - public ConsoleWrapper() + /// + public void Debug(string message) + { + OverrideLambdaLogger(); + System.Diagnostics.Debug.WriteLine(message); + } + + /// + public void Error(string message) { - if(_redirected) - { - _redirected = false; - return; - } - - var standardOutput = new StreamWriter(Console.OpenStandardOutput()); - standardOutput.AutoFlush = true; - Console.SetOut(standardOutput); var errordOutput = new StreamWriter(Console.OpenStandardError()); errordOutput.AutoFlush = true; Console.SetError(errordOutput); + Console.Error.WriteLine(message); } - /// - public void WriteLine(string message) => Console.WriteLine(message); - /// - public void Debug(string message) => System.Diagnostics.Debug.WriteLine(message); - /// - public void Error(string message) => Console.Error.WriteLine(message); - + internal static void SetOut(StringWriter consoleOut) { - _redirected = true; Console.SetOut(consoleOut); } + + private void OverrideLambdaLogger() + { + // Force override of LambdaLogger + var standardOutput = new StreamWriter(Console.OpenStandardOutput()); + standardOutput.AutoFlush = true; + Console.SetOut(standardOutput); + } } \ No newline at end of file From c01408e40e1ef9faf00a3d10185bb2f7520f740e Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Thu, 3 Apr 2025 23:47:37 +0100 Subject: [PATCH 34/49] fix xraytraceid now from env --- .../Internal/LoggingAspect.cs | 16 +--------------- .../Internal/PowertoolsLogger.cs | 12 +++++++++++- .../Internal/PowertoolsLoggerProvider.cs | 3 ++- .../Buffering/LogBufferCircularCacheTests.cs | 2 +- .../Handlers/HandlerTests.cs | 3 +-- .../Handlers/TestHandlers.cs | 4 +++- 6 files changed, 19 insertions(+), 21 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index acb2ec03..93bc1535 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -91,7 +91,7 @@ private void InitializeLogger(LoggingAttribute trigger) if (hasService) _currentConfig.Service = trigger.Service; if (hasOutputCase) _currentConfig.LoggerOutputCase = trigger.LoggerOutputCase; if (hasSamplingRate) _currentConfig.SamplingRate = trigger.SamplingRate; - + // Need to refresh the logger after configuration changes _logger = LoggerFactoryHelper.CreateAndConfigureFactory(_currentConfig).CreatePowertoolsLogger(); Logger.ClearInstance(); @@ -138,8 +138,6 @@ public void OnEntry( Triggers = triggers }; - - var logEvent = trigger.LogEvent; _clearState = trigger.ClearState; InitializeLogger(trigger); @@ -154,7 +152,6 @@ public void OnEntry( _isContextInitialized = true; var eventObject = eventArgs.Args.FirstOrDefault(); - CaptureXrayTraceId(); CaptureLambdaContext(eventArgs); CaptureCorrelationId(eventObject, trigger.CorrelationIdPath); @@ -201,17 +198,6 @@ public void OnExit() } } - /// - /// Captures the xray trace identifier. - /// - private void CaptureXrayTraceId() - { - if (string.IsNullOrWhiteSpace(_currentConfig.XRayTraceId)) - return; - _logger.AppendKey(LoggingConstants.KeyXRayTraceId, - _currentConfig.XRayTraceId.Split(';', StringSplitOptions.RemoveEmptyEntries)[0].Replace("Root=", "")); - } - /// /// Captures the lambda context. /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs index 6421c79c..b67964ae 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -17,6 +17,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; +using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal.Helpers; using Microsoft.Extensions.Logging; @@ -39,6 +40,8 @@ internal sealed class PowertoolsLogger : ILogger /// private readonly Func _currentConfig; + private readonly IPowertoolsConfigurations _powertoolsConfigurations; + /// /// The current scope /// @@ -49,12 +52,15 @@ internal sealed class PowertoolsLogger : ILogger /// /// The name. /// + /// public PowertoolsLogger( string categoryName, - Func getCurrentConfig) + Func getCurrentConfig, + IPowertoolsConfigurations powertoolsConfigurations) { _categoryName = categoryName; _currentConfig = getCurrentConfig; + _powertoolsConfigurations = powertoolsConfigurations; } /// @@ -217,8 +223,12 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times logEntry.TryAdd(LoggingConstants.KeyTimestamp, timestamp.ToString( config.TimestampFormat ?? "o")); logEntry.TryAdd(config.LogLevelKey, logLevel.ToString()); logEntry.TryAdd(LoggingConstants.KeyService, config.Service); + if(! string.IsNullOrWhiteSpace(_powertoolsConfigurations.XRayTraceId)) + logEntry.TryAdd(LoggingConstants.KeyXRayTraceId, + _powertoolsConfigurations.XRayTraceId.Split(';', StringSplitOptions.RemoveEmptyEntries)[0].Replace("Root=", "")); logEntry.TryAdd(LoggingConstants.KeyLoggerName, _categoryName); logEntry.TryAdd(LoggingConstants.KeyMessage, message); + if (config.SamplingRate > 0) logEntry.TryAdd(LoggingConstants.KeySamplingRate, config.SamplingRate); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs index 335638cb..6511db70 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs @@ -142,7 +142,8 @@ public virtual ILogger CreateLogger(string categoryName) { return _loggers.GetOrAdd(categoryName, name => new PowertoolsLogger( name, - GetCurrentConfig)); + GetCurrentConfig, + _powertoolsConfigurations)); } internal PowertoolsLoggerConfiguration GetCurrentConfig() => _currentConfig; diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferCircularCacheTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferCircularCacheTests.cs index a2e9bbdd..adca01e6 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferCircularCacheTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferCircularCacheTests.cs @@ -29,7 +29,7 @@ public void Buffer_WhenMaxSizeExceeded_DiscardOldestEntries() LogBuffering = new LogBufferingOptions { BufferAtLogLevel = LogLevel.Debug, - MaxBytes = 1024 // Small buffer size to trigger overflow + MaxBytes = 1200 // Small buffer size to trigger overflow - Needs to be adjusted based on the log message size }, LogOutput = _consoleOut }; diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/HandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/HandlerTests.cs index 121f89d5..c00c7a3b 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/HandlerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/HandlerTests.cs @@ -260,8 +260,6 @@ public async Task Should_Log_Properties_Setup_Constructor() var output = new TestLoggerOutput(); var handler = new SimpleFunctionWithStaticConfigure(output); - Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); - await SimpleFunctionWithStaticConfigure.FunctionHandler(); var logOutput = output.ToString(); @@ -272,6 +270,7 @@ public async Task Should_Log_Properties_Setup_Constructor() Assert.Contains("\"service\":\"MyServiceName\"", logOutput); Assert.Contains("\"level\":\"Information\"", logOutput); Assert.Contains("\"message\":\"Starting up!\"", logOutput); + Assert.Contains("\"xray_trace_id\"", logOutput); } [Fact] diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs index 9566918d..436bfb08 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs @@ -205,7 +205,6 @@ public class SimpleFunctionWithStaticConfigure { public SimpleFunctionWithStaticConfigure(IConsoleWrapper output) { - Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); // Constructor logic can go here if needed Logger.Configure(logger => { @@ -221,6 +220,9 @@ public SimpleFunctionWithStaticConfigure(IConsoleWrapper output) [Logging] public static async Task FunctionHandler() { + // only set on handler + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); + Logger.LogInformation("Starting up!"); // throw new Exception(); From 5bad7b63762a6640071f5dbf5cc53b4405ef9800 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Fri, 4 Apr 2025 11:32:31 +0100 Subject: [PATCH 35/49] refactor aspect to use MethodWrapper.FlushBufferOnUncaughtError feature --- .../Internal/LoggingAspect.cs | 382 +++++++++++++----- .../LoggingAttribute.cs | 10 +- .../Attributes/LoggerAspectTests.cs | 106 ++++- .../Handlers/HandlerTests.cs | 60 ++- .../Handlers/TestHandlers.cs | 27 +- 5 files changed, 449 insertions(+), 136 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index 93bc1535..20ff1eaa 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -32,7 +32,7 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// /// [Aspect(Scope.Global, Factory = typeof(LoggingAspectFactory))] -public class LoggingAspect +public class LoggingAspect : IMethodAspectHandler { /// /// The is cold start @@ -63,6 +63,7 @@ public class LoggingAspect private bool _isDebug; private bool _bufferingEnabled; private PowertoolsLoggerConfiguration _currentConfig; + private bool _flushBufferOnUncaughtError; /// /// Initializes a new instance of the class. @@ -72,6 +73,95 @@ public LoggingAspect(ILogger logger) _logger = logger ?? LoggerFactoryHolder.GetOrCreateFactory().CreatePowertoolsLogger(); } + // [Advice(Kind.Around)] + // public object Around( + // [Argument(Source.Instance)] object instance, + // [Argument(Source.Name)] string name, + // [Argument(Source.Arguments)] object[] args, + // [Argument(Source.Type)] Type hostType, + // [Argument(Source.Metadata)] MethodBase method, + // [Argument(Source.ReturnType)] Type returnType, + // [Argument(Source.Triggers)] Attribute[] triggers, + // [Argument(Source.Target)] Func target) + // { + // var trigger = triggers.OfType().First(); + // + // try + // { + // var eventArgs = new AspectEventArgs + // { + // Instance = instance, + // Type = hostType, + // Method = method, + // Name = name, + // Args = args, + // ReturnType = returnType, + // Triggers = triggers + // }; + // + // _clearState = trigger.ClearState; + // + // InitializeLogger(trigger); + // + // if (!_initializeContext) + // { + // return target(args); + // } + // + // _logger.AppendKey(LoggingConstants.KeyColdStart, _isColdStart); + // + // _isColdStart = false; + // _initializeContext = false; + // _isContextInitialized = true; + // + // var eventObject = eventArgs.Args.FirstOrDefault(); + // CaptureLambdaContext(eventArgs); + // CaptureCorrelationId(eventObject, trigger.CorrelationIdPath); + // + // if (trigger.IsLogEventSet && trigger.LogEvent) + // { + // LogEvent(eventObject); + // } + // else if (!trigger.IsLogEventSet && _currentConfig.LogEvent) + // { + // LogEvent(eventObject); + // } + // + // var result = target(args); + // + // return result; + // } + // catch (Exception exception) + // { + // if (_bufferingEnabled && trigger.FlushBufferOnUncaughtError) + // { + // _logger.FlushBuffer(); + // } + // + // // The purpose of ExceptionDispatchInfo.Capture is to capture a potentially mutating exception's StackTrace at a point in time: + // // https://learn.microsoft.com/en-us/dotnet/standard/exceptions/best-practices-for-exceptions#capture-exceptions-to-rethrow-later + // ExceptionDispatchInfo.Capture(exception).Throw(); + // throw; + // } + // finally + // { + // if (_isContextInitialized) + // { + // if (_clearLambdaContext) + // LoggingLambdaContext.Clear(); + // if (_clearState) + // _logger.RemoveAllKeys(); + // _initializeContext = true; + // + // if (_bufferingEnabled) + // { + // // clear the buffer after the handler has finished + // _logger.ClearBuffer(); + // } + // } + // } + // } + private void InitializeLogger(LoggingAttribute trigger) { // Check which settings are explicitly provided in the attribute @@ -83,7 +173,7 @@ private void InitializeLogger(LoggingAttribute trigger) // Only update configuration if any settings were provided var needsReconfiguration = hasLogLevel || hasService || hasOutputCase || hasSamplingRate; _currentConfig = PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); - + if (needsReconfiguration) { // Apply each setting directly using the existing Logger static methods @@ -91,7 +181,7 @@ private void InitializeLogger(LoggingAttribute trigger) if (hasService) _currentConfig.Service = trigger.Service; if (hasOutputCase) _currentConfig.LoggerOutputCase = trigger.LoggerOutputCase; if (hasSamplingRate) _currentConfig.SamplingRate = trigger.SamplingRate; - + // Need to refresh the logger after configuration changes _logger = LoggerFactoryHelper.CreateAndConfigureFactory(_currentConfig).CreatePowertoolsLogger(); Logger.ClearInstance(); @@ -102,101 +192,101 @@ private void InitializeLogger(LoggingAttribute trigger) _bufferingEnabled = _currentConfig.LogBuffering != null; } - /// - /// Runs before the execution of the method marked with the Logging Attribute - /// - /// - /// - /// - /// - /// - /// - /// - [Advice(Kind.Before)] - public void OnEntry( - [Argument(Source.Instance)] object instance, - [Argument(Source.Name)] string name, - [Argument(Source.Arguments)] object[] args, - [Argument(Source.Type)] Type hostType, - [Argument(Source.Metadata)] MethodBase method, - [Argument(Source.ReturnType)] Type returnType, - [Argument(Source.Triggers)] Attribute[] triggers) - { - // Called before the method - var trigger = triggers.OfType().First(); - - try - { - var eventArgs = new AspectEventArgs - { - Instance = instance, - Type = hostType, - Method = method, - Name = name, - Args = args, - ReturnType = returnType, - Triggers = triggers - }; - - _clearState = trigger.ClearState; - - InitializeLogger(trigger); - - if (!_initializeContext) - return; - - _logger.AppendKey(LoggingConstants.KeyColdStart, _isColdStart); - - _isColdStart = false; - _initializeContext = false; - _isContextInitialized = true; - - var eventObject = eventArgs.Args.FirstOrDefault(); - CaptureLambdaContext(eventArgs); - CaptureCorrelationId(eventObject, trigger.CorrelationIdPath); - - if(trigger.IsLogEventSet && trigger.LogEvent) - { - LogEvent(eventObject); - } - else if (!trigger.IsLogEventSet && _currentConfig.LogEvent) - { - LogEvent(eventObject); - } - } - catch (Exception exception) - { - if (_bufferingEnabled && trigger.FlushBufferOnUncaughtError) - { - _logger.FlushBuffer(); - } - - // The purpose of ExceptionDispatchInfo.Capture is to capture a potentially mutating exception's StackTrace at a point in time: - // https://learn.microsoft.com/en-us/dotnet/standard/exceptions/best-practices-for-exceptions#capture-exceptions-to-rethrow-later - ExceptionDispatchInfo.Capture(exception).Throw(); - } - } - - /// - /// Handles the Kind.After event. - /// - [Advice(Kind.After)] - public void OnExit() - { - if (!_isContextInitialized) - return; - if (_clearLambdaContext) - LoggingLambdaContext.Clear(); - if (_clearState) - _logger.RemoveAllKeys(); - _initializeContext = true; - - if (_bufferingEnabled) - { - // clear the buffer after the handler has finished - _logger.ClearBuffer(); - } - } + // /// + // /// Runs before the execution of the method marked with the Logging Attribute + // /// + // /// + // /// + // /// + // /// + // /// + // /// + // /// + // [Advice(Kind.Before)] + // public void OnEntry( + // [Argument(Source.Instance)] object instance, + // [Argument(Source.Name)] string name, + // [Argument(Source.Arguments)] object[] args, + // [Argument(Source.Type)] Type hostType, + // [Argument(Source.Metadata)] MethodBase method, + // [Argument(Source.ReturnType)] Type returnType, + // [Argument(Source.Triggers)] Attribute[] triggers) + // { + // // Called before the method + // var trigger = triggers.OfType().First(); + // + // try + // { + // var eventArgs = new AspectEventArgs + // { + // Instance = instance, + // Type = hostType, + // Method = method, + // Name = name, + // Args = args, + // ReturnType = returnType, + // Triggers = triggers + // }; + // + // _clearState = trigger.ClearState; + // + // InitializeLogger(trigger); + // + // if (!_initializeContext) + // return; + // + // _logger.AppendKey(LoggingConstants.KeyColdStart, _isColdStart); + // + // _isColdStart = false; + // _initializeContext = false; + // _isContextInitialized = true; + // + // var eventObject = eventArgs.Args.FirstOrDefault(); + // CaptureLambdaContext(eventArgs); + // CaptureCorrelationId(eventObject, trigger.CorrelationIdPath); + // + // if (trigger.IsLogEventSet && trigger.LogEvent) + // { + // LogEvent(eventObject); + // } + // else if (!trigger.IsLogEventSet && _currentConfig.LogEvent) + // { + // LogEvent(eventObject); + // } + // } + // catch (Exception exception) + // { + // if (_bufferingEnabled && trigger.FlushBufferOnUncaughtError) + // { + // _logger.FlushBuffer(); + // } + // + // // The purpose of ExceptionDispatchInfo.Capture is to capture a potentially mutating exception's StackTrace at a point in time: + // // https://learn.microsoft.com/en-us/dotnet/standard/exceptions/best-practices-for-exceptions#capture-exceptions-to-rethrow-later + // ExceptionDispatchInfo.Capture(exception).Throw(); + // } + // } + + // /// + // /// Handles the Kind.After event. + // /// + // [Advice(Kind.After)] + // public void OnExit() + // { + // if (!_isContextInitialized) + // return; + // if (_clearLambdaContext) + // LoggingLambdaContext.Clear(); + // if (_clearState) + // _logger.RemoveAllKeys(); + // _initializeContext = true; + // + // if (_bufferingEnabled) + // { + // // clear the buffer after the handler has finished + // _logger.ClearBuffer(); + // } + // } /// /// Captures the lambda context. @@ -243,18 +333,18 @@ private void CaptureCorrelationId(object eventArg, string correlationIdPath) var jsonDoc = JsonDocument.Parse(_currentConfig.Serializer.Serialize(eventArg, eventArg.GetType())); - + var element = jsonDoc.RootElement; - + for (var i = 0; i < correlationIdPaths.Length; i++) { // TODO: For casing parsing to be removed from Logging v2 when we get rid of outputcase without this CorrelationIdPaths.ApiGatewayRest would not work // TODO: This will be removed and replaced by JMesPath - + var pathWithOutputCase = correlationIdPaths[i].ToCase(_currentConfig.LoggerOutputCase); if (!element.TryGetProperty(pathWithOutputCase, out var childElement)) break; - + element = childElement; if (i == correlationIdPaths.Length - 1) correlationId = element.ToString(); @@ -280,12 +370,12 @@ private void LogEvent(object eventArg) switch (eventArg) { case null: - { - if (_isDebug) - _logger.LogDebug( - "Skipping Event Log because event parameter not found."); - break; - } + { + if (_isDebug) + _logger.LogDebug( + "Skipping Event Log because event parameter not found."); + break; + } case Stream: try { @@ -318,4 +408,80 @@ internal static void ResetForTest() { LoggingLambdaContext.Clear(); } + + public void OnEntry(AspectEventArgs eventArgs) + { + var trigger = eventArgs.Triggers.OfType().First(); + try + { + _clearState = trigger.ClearState; + + InitializeLogger(trigger); + + if (!_initializeContext) + return; + + _logger.AppendKey(LoggingConstants.KeyColdStart, _isColdStart); + + _isColdStart = false; + _initializeContext = false; + _isContextInitialized = true; + _flushBufferOnUncaughtError = trigger.FlushBufferOnUncaughtError; + + var eventObject = eventArgs.Args.FirstOrDefault(); + CaptureLambdaContext(eventArgs); + CaptureCorrelationId(eventObject, trigger.CorrelationIdPath); + + if (trigger.IsLogEventSet && trigger.LogEvent) + { + LogEvent(eventObject); + } + else if (!trigger.IsLogEventSet && _currentConfig.LogEvent) + { + LogEvent(eventObject); + } + } + catch (Exception exception) + { + if (_bufferingEnabled && _flushBufferOnUncaughtError) + { + _logger.FlushBuffer(); + } + + // The purpose of ExceptionDispatchInfo.Capture is to capture a potentially mutating exception's StackTrace at a point in time: + // https://learn.microsoft.com/en-us/dotnet/standard/exceptions/best-practices-for-exceptions#capture-exceptions-to-rethrow-later + ExceptionDispatchInfo.Capture(exception).Throw(); + } + } + + public void OnSuccess(AspectEventArgs eventArgs, object result) + { + + } + + public void OnException(AspectEventArgs eventArgs, Exception exception) + { + if (_bufferingEnabled && _flushBufferOnUncaughtError) + { + _logger.FlushBuffer(); + } + ExceptionDispatchInfo.Capture(exception).Throw(); + } + + public void OnExit(AspectEventArgs eventArgs) + { + if (!_isContextInitialized) + return; + if (_clearLambdaContext) + LoggingLambdaContext.Clear(); + if (_clearState) + _logger.RemoveAllKeys(); + _initializeContext = true; + + if (_bufferingEnabled) + { + // clear the buffer after the handler has finished + _logger.ClearBuffer(); + } + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs index b52b12f1..dba6e637 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs @@ -15,6 +15,7 @@ using System; using AspectInjector.Broker; +using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; using Microsoft.Extensions.Logging; @@ -116,8 +117,8 @@ namespace AWS.Lambda.Powertools.Logging; /// /// [AttributeUsage(AttributeTargets.Method)] -[Injection(typeof(LoggingAspect))] -public class LoggingAttribute : Attribute +// [Injection(typeof(LoggingAspect))] +public class LoggingAttribute : MethodAspectAttribute { /// /// Service name is used for logging. @@ -189,4 +190,9 @@ public bool LogEvent /// When buffering is enabled, this property will flush the buffer on uncaught exceptions /// public bool FlushBufferOnUncaughtError { get; set; } + + protected override IMethodAspectHandler CreateHandler() + { + return new LoggingAspect(LoggerFactoryHolder.GetOrCreateFactory().CreatePowertoolsLogger()); + } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs index 8c498288..868dc362 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs @@ -73,9 +73,20 @@ public void OnEntry_ShouldInitializeLogger_WhenCalledWithValidArguments() } }; + var aspectArgs = new AspectEventArgs + { + Instance = instance, + Name = name, + Args = args, + Type = hostType, + Method = method, + ReturnType = returnType, + Triggers = triggers + }; + // Act var loggingAspect = new LoggingAspect(logger); - loggingAspect.OnEntry(instance, name, args, hostType, method, returnType, triggers); + loggingAspect.OnEntry(aspectArgs); // Assert consoleOut.Received().WriteLine(Arg.Is(s => @@ -119,10 +130,21 @@ public void OnEntry_ShouldLog_Event_When_EnvironmentVariable_Set() ClearState = true } }; - - // Act + + var aspectArgs = new AspectEventArgs + { + Instance = instance, + Name = name, + Args = args, + Type = hostType, + Method = method, + ReturnType = returnType, + Triggers = triggers + }; + + // Act var loggingAspect = new LoggingAspect(logger); - loggingAspect.OnEntry(instance, name, args, hostType, method, returnType, triggers); + loggingAspect.OnEntry(aspectArgs); var updatedConfig = PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); @@ -175,9 +197,21 @@ public void OnEntry_Should_NOT_Log_Event_When_EnvironmentVariable_Set_But_Attrib } }; - // Act + + var aspectArgs = new AspectEventArgs + { + Instance = instance, + Name = name, + Args = args, + Type = hostType, + Method = method, + ReturnType = returnType, + Triggers = triggers + }; + + // Act var loggingAspect = new LoggingAspect(logger); - loggingAspect.OnEntry(instance, name, args, hostType, method, returnType, triggers); + loggingAspect.OnEntry(aspectArgs); var updatedConfig = PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); @@ -225,9 +259,21 @@ public void OnEntry_ShouldLog_SamplingRate_When_EnvironmentVariable_Set() } }; - // Act + + var aspectArgs = new AspectEventArgs + { + Instance = instance, + Name = name, + Args = args, + Type = hostType, + Method = method, + ReturnType = returnType, + Triggers = triggers + }; + + // Act var loggingAspect = new LoggingAspect(logger); - loggingAspect.OnEntry(instance, name, args, hostType, method, returnType, triggers); + loggingAspect.OnEntry(aspectArgs); // Assert var updatedConfig = PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); @@ -268,9 +314,16 @@ public void OnEntry_ShouldLogEvent_WhenLogEventIsTrue() }; // Act - + + var aspectArgs = new AspectEventArgs + { + Args = new object[] { eventObject }, + Triggers = triggers + }; + + // Act var loggingAspect = new LoggingAspect(logger); - loggingAspect.OnEntry(null, null, new object[] { eventObject }, null, null, null, triggers); + loggingAspect.OnEntry(aspectArgs); // Assert consoleOut.Received().WriteLine(Arg.Is(s => @@ -312,9 +365,21 @@ public void OnEntry_ShouldNot_Log_Info_When_LogLevel_Higher_EnvironmentVariable( } }; - // Act + + var aspectArgs = new AspectEventArgs + { + Instance = instance, + Name = name, + Args = args, + Type = hostType, + Method = method, + ReturnType = returnType, + Triggers = triggers + }; + + // Act var loggingAspect = new LoggingAspect(logger); - loggingAspect.OnEntry(instance, name, args, hostType, method, returnType, triggers); + loggingAspect.OnEntry(aspectArgs); var updatedConfig = PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); @@ -359,10 +424,21 @@ public void OnEntry_Should_LogDebug_WhenSet_EnvironmentVariable() var logger = PowertoolsLoggerFactory.Create(config).CreatePowertoolsLogger(); - - // Act + + var aspectArgs = new AspectEventArgs + { + Instance = instance, + Name = name, + Args = args, + Type = hostType, + Method = method, + ReturnType = returnType, + Triggers = triggers + }; + + // Act var loggingAspect = new LoggingAspect(logger); - loggingAspect.OnEntry(instance, name, args, hostType, method, returnType, triggers); + loggingAspect.OnEntry(aspectArgs); // Assert var updatedConfig = PowertoolsLoggingBuilderExtensions.GetCurrentConfiguration(); diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/HandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/HandlerTests.cs index c00c7a3b..329c1a01 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/HandlerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/HandlerTests.cs @@ -1,6 +1,5 @@ using System.Text.Json.Serialization; #if NET8_0_OR_GREATER - using System; using System.Collections.Generic; using System.IO; @@ -28,6 +27,7 @@ public class Handlers public Handlers(ILogger logger) { _logger = logger; + PowertoolsLoggingBuilderExtensions.ResetAllProviders(); } [Logging(LogEvent = true)] @@ -124,7 +124,7 @@ public void TestMethod() // Check if the output contains newlines and spacing (indentation) Assert.Contains("\n", logOutput); Assert.Contains(" ", logOutput); - + // Verify write indented JSON Assert.Contains(" \"Level\": \"Information\",\n \"Service\": \"my-service122\",", logOutput); } @@ -170,7 +170,6 @@ public void TestMethodCustom() Assert.Contains("\"level\":\"Information\"", logOutput); Assert.Contains("\"message\":\"Information message\"", logOutput); Assert.Contains("\"correlationIds\":{\"awsRequestId\":\"123\"}", logOutput); - } [Fact] @@ -253,26 +252,69 @@ public void TestMethodStatic() Assert.Contains("\"Level\":\"Information\"", logOutput); Assert.Contains("\"Message\":\"Static method\"", logOutput); } - + [Fact] public async Task Should_Log_Properties_Setup_Constructor() { var output = new TestLoggerOutput(); - var handler = new SimpleFunctionWithStaticConfigure(output); - + _ = new SimpleFunctionWithStaticConfigure(output); + await SimpleFunctionWithStaticConfigure.FunctionHandler(); var logOutput = output.ToString(); _output.WriteLine(logOutput); - // Verify static logger configuration - // Verify override of LoggerOutputCase from attribute + Assert.Contains("\"service\":\"MyServiceName\"", logOutput); Assert.Contains("\"level\":\"Information\"", logOutput); Assert.Contains("\"message\":\"Starting up!\"", logOutput); Assert.Contains("\"xray_trace_id\"", logOutput); } + [Fact] + public async Task Should_Flush_On_Exception_Async() + { + var output = new TestLoggerOutput(); + var handler = new SimpleFunctionWithStaticConfigure(output); + + try + { + await handler.AsyncException(); + } + catch + { + } + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + Assert.Contains("\"level\":\"Debug\"", logOutput); + Assert.Contains("\"message\":\"Debug!!\"", logOutput); + Assert.Contains("\"xray_trace_id\"", logOutput); + } + + [Fact] + public void Should_Flush_On_Exception() + { + var output = new TestLoggerOutput(); + var handler = new SimpleFunctionWithStaticConfigure(output); + + try + { + handler.SyncException(); + } + catch + { + } + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + Assert.Contains("\"level\":\"Debug\"", logOutput); + Assert.Contains("\"message\":\"Debug!!\"", logOutput); + Assert.Contains("\"xray_trace_id\"", logOutput); + } + [Fact] public void TestJsonOptionsPropertyNaming() { @@ -341,7 +383,7 @@ public void TestJsonOptionsDictionaryKeyPolicy() _output.WriteLine(logOutput); // Fix assertion to match actual camelCase behavior with acronyms - Assert.Contains("\"userID\":12345", logOutput); // ID remains uppercase + Assert.Contains("\"userID\":12345", logOutput); // ID remains uppercase Assert.Contains("\"orderDetails\":", logOutput); Assert.Contains("\"shippingAddress\":", logOutput); } diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs index 436bfb08..0c9de3e1 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/TestHandlers.cs @@ -224,12 +224,35 @@ public static async Task FunctionHandler() Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); Logger.LogInformation("Starting up!"); - - // throw new Exception(); + return new APIGatewayHttpApiV2ProxyResponse { Body = "Hello", StatusCode = 200 }; } + + [Logging(FlushBufferOnUncaughtError = true)] + public APIGatewayHttpApiV2ProxyResponse SyncException() + { + // only set on handler + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); + + Logger.LogDebug("Debug!!"); + Logger.LogInformation("Starting up!"); + + throw new Exception(); + } + + [Logging(FlushBufferOnUncaughtError = true)] + public async Task AsyncException() + { + // only set on handler + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); + + Logger.LogDebug("Debug!!"); + Logger.LogInformation("Starting up!"); + + throw new Exception(); + } } \ No newline at end of file From c77c8bedcd078dd4180ed2254950c56c6db90043 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Fri, 4 Apr 2025 22:44:00 +0100 Subject: [PATCH 36/49] Logging Aspect with MethodWrapper. Log buffering with clear old invocations. ColdStart logic for provisioned-concurrency --- .../Core/ConsoleWrapper.cs | 8 + .../Core/Constants.cs | 2 + .../Core/IPowertoolsConfigurations.cs | 11 + .../Core/LambdaLifecycleTracker.cs | 62 +++++ .../Core/PowertoolsConfigurations.cs | 33 ++- .../Core/PowertoolsEnvironment.cs | 2 +- .../Internal/Buffer/LogBuffer.cs | 10 + .../Buffer/PowertoolsBufferingLogger.cs | 4 +- .../Internal/LoggingAspect.cs | 222 ++---------------- .../PowertoolsConfigurationsExtension.cs | 26 ++ .../Internal/PowertoolsLogger.cs | 46 ++-- .../LoggingAttribute.cs | 4 + .../PowertoolsLoggingBuilderExtensions.cs | 2 +- .../Attributes/LoggerAspectTests.cs | 52 ++-- .../Attributes/LoggingAttributeTest.cs | 107 +++++---- .../Attributes/ServiceTests.cs | 18 +- .../Buffering/LambdaContextBufferingTests.cs | 41 +++- .../Buffering/LogBufferCircularCacheTests.cs | 119 +++++----- .../Buffering/LogBufferingTests.cs | 60 +---- .../Handlers/HandlerTests.cs | 7 +- .../PowertoolsLoggerTest.cs | 58 ++++- 21 files changed, 455 insertions(+), 439 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.Common/Core/LambdaLifecycleTracker.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs index 8b1c4b3e..f22ec16e 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs @@ -56,4 +56,12 @@ private void OverrideLambdaLogger() standardOutput.AutoFlush = true; Console.SetOut(standardOutput); } + + internal static void WriteLine(string logLevel, string message) + { + // var standardOutput = new StreamWriter(Console.OpenStandardOutput()); + // standardOutput.AutoFlush = true; + // Console.SetOut(standardOutput); + Console.WriteLine($"{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ss.fffZ}\t{logLevel}\t{message}"); + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/Constants.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/Constants.cs index 343faa68..0eb23325 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/Constants.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/Constants.cs @@ -20,6 +20,8 @@ namespace AWS.Lambda.Powertools.Common; /// internal static class Constants { + internal const string AWSInitializationTypeEnv = "AWS_LAMBDA_INITIALIZATION_TYPE"; + /// /// Constant for POWERTOOLS_SERVICE_NAME environment variable /// diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsConfigurations.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsConfigurations.cs index 58955a50..755d33ef 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsConfigurations.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsConfigurations.cs @@ -167,4 +167,15 @@ public interface IPowertoolsConfigurations /// Gets a value indicating whether Metrics are disabled. /// bool MetricsDisabled { get; } + + /// + /// Indicates if the current execution is a cold start. + /// + bool IsColdStart { get; } + + /// + /// AWS Lambda initialization type. + /// This is set to "on-demand" for on-demand Lambda functions and "provisioned-concurrency" for provisioned concurrency. + /// + string AwsInitializationType { get; } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/LambdaLifecycleTracker.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/LambdaLifecycleTracker.cs new file mode 100644 index 00000000..4c786f38 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/LambdaLifecycleTracker.cs @@ -0,0 +1,62 @@ +using System; +using System.Threading; + +namespace AWS.Lambda.Powertools.Common.Core; + +/// +/// Tracks Lambda lifecycle state including cold starts +/// +public static class LambdaLifecycleTracker +{ + // Static flag that's true only for the first Lambda container initialization + private static bool _isFirstContainer = true; + + // Store the cold start state for the current invocation + private static readonly AsyncLocal CurrentInvocationColdStart = new AsyncLocal(); + + private static string _lambdaInitType; + private static string LambdaInitType => _lambdaInitType ?? Environment.GetEnvironmentVariable("AWS_LAMBDA_INITIALIZATION_TYPE"); + + /// + /// Returns true if the current Lambda invocation is a cold start + /// + public static bool IsColdStart + { + get + { + if(LambdaInitType == "provisioned-concurrency") + { + // If the Lambda is provisioned concurrency, it is not a cold start + return false; + } + + // Initialize the cold start state for this invocation if not already set + if (!CurrentInvocationColdStart.Value.HasValue) + { + // Capture the container's cold start state for this entire invocation + CurrentInvocationColdStart.Value = _isFirstContainer; + + // After detecting the first invocation, mark future ones as warm + if (_isFirstContainer) + { + _isFirstContainer = false; + } + } + + // Return the cold start state for this invocation (cannot change during the invocation) + return CurrentInvocationColdStart.Value ?? false; + } + } + + + + /// + /// Resets the cold start state for testing + /// + internal static void Reset() + { + _isFirstContainer = true; + CurrentInvocationColdStart.Value = null; + _lambdaInitType = null; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs index cb2c55e6..0f059a25 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/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 @@ -14,6 +14,7 @@ */ using System.Globalization; +using AWS.Lambda.Powertools.Common.Core; namespace AWS.Lambda.Powertools.Common; @@ -22,7 +23,7 @@ namespace AWS.Lambda.Powertools.Common; /// Implements the /// /// -public class PowertoolsConfigurations : IPowertoolsConfigurations +internal class PowertoolsConfigurations : IPowertoolsConfigurations { private readonly IPowertoolsEnvironment _powertoolsEnvironment; @@ -157,7 +158,8 @@ public bool GetEnvironmentVariableOrDefault(string variable, bool defaultValue) /// /// The logger sample rate. public double LoggerSampleRate => - double.TryParse(_powertoolsEnvironment.GetEnvironmentVariable(Constants.LoggerSampleRateNameEnv), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var result) + double.TryParse(_powertoolsEnvironment.GetEnvironmentVariable(Constants.LoggerSampleRateNameEnv), + NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var result) ? result : 0; @@ -187,7 +189,7 @@ public bool GetEnvironmentVariableOrDefault(string variable, bool defaultValue) /// /// true if this instance is Lambda; otherwise, false. public bool IsLambdaEnvironment => GetEnvironmentVariable(Constants.LambdaTaskRoot) is not null; - + /// /// Gets a value indicating whether [tracing is disabled]. /// @@ -206,17 +208,28 @@ public void SetExecutionEnvironment(T type) GetEnvironmentVariableOrDefault(Constants.IdempotencyDisabledEnv, false); /// - public string BatchProcessingErrorHandlingPolicy => GetEnvironmentVariableOrDefault(Constants.BatchErrorHandlingPolicyEnv, "DeriveFromEvent"); + public string BatchProcessingErrorHandlingPolicy => + GetEnvironmentVariableOrDefault(Constants.BatchErrorHandlingPolicyEnv, "DeriveFromEvent"); /// - public bool BatchParallelProcessingEnabled => GetEnvironmentVariableOrDefault(Constants.BatchParallelProcessingEnabled, false); + public bool BatchParallelProcessingEnabled => + GetEnvironmentVariableOrDefault(Constants.BatchParallelProcessingEnabled, false); /// - public int BatchProcessingMaxDegreeOfParallelism => GetEnvironmentVariableOrDefault(Constants.BatchMaxDegreeOfParallelismEnv, 1); + public int BatchProcessingMaxDegreeOfParallelism => + GetEnvironmentVariableOrDefault(Constants.BatchMaxDegreeOfParallelismEnv, 1); /// - public bool BatchThrowOnFullBatchFailureEnabled => GetEnvironmentVariableOrDefault(Constants.BatchThrowOnFullBatchFailureEnv, true); + public bool BatchThrowOnFullBatchFailureEnabled => + GetEnvironmentVariableOrDefault(Constants.BatchThrowOnFullBatchFailureEnv, true); /// public bool MetricsDisabled => GetEnvironmentVariableOrDefault(Constants.PowertoolsMetricsDisabledEnv, false); + + /// + public bool IsColdStart => LambdaLifecycleTracker.IsColdStart; + + /// + public string AwsInitializationType => + GetEnvironmentVariable(Constants.AWSInitializationTypeEnv); } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsEnvironment.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsEnvironment.cs index 649418a4..7ae3b305 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsEnvironment.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsEnvironment.cs @@ -4,7 +4,7 @@ namespace AWS.Lambda.Powertools.Common; /// -public class PowertoolsEnvironment : IPowertoolsEnvironment +internal class PowertoolsEnvironment : IPowertoolsEnvironment { /// /// The instance diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs index 91382ee5..ac83ca10 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs @@ -29,6 +29,7 @@ internal class LogBuffer // Dictionary of buffers by invocation ID private readonly ConcurrentDictionary _buffersByInvocation = new(); + private string _lastInvocationId; // Get the current invocation ID or create a fallback private string CurrentInvocationId => _powertoolsConfigurations.XRayTraceId; @@ -49,6 +50,15 @@ public void Add(string logEntry, int maxBytes, int size) // No invocation ID set, do not buffer return; } + + // If this is a new invocation ID, clear previous buffers + if (_lastInvocationId != invocationId) + { + if (_lastInvocationId != null) + _buffersByInvocation.Clear(); + _lastInvocationId = invocationId; + } + var buffer = _buffersByInvocation.GetOrAdd(invocationId, _ => new InvocationBuffer()); buffer.Add(logEntry, maxBytes, size); } diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs index 134b365b..36f4f3ed 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs @@ -76,7 +76,7 @@ public void Log( { // log the entry directly if it exceeds the buffer size powertoolsLogger.LogLine(logEntry); - powertoolsLogger.LogWarning("Cannot add item to the buffer"); + ConsoleWrapper.WriteLine(LogLevel.Warning.ToLambdaLogLevel(), "Cannot add item to the buffer"); } else { @@ -119,7 +119,7 @@ public void FlushBuffer() { if (_buffer.HasEvictions) { - powertoolsLogger.LogWarning("Some logs are not displayed because they were evicted from the buffer. Increase buffer size to store more logs in the buffer"); + ConsoleWrapper.WriteLine(LogLevel.Warning.ToLambdaLogLevel(), "Some logs are not displayed because they were evicted from the buffer. Increase buffer size to store more logs in the buffer"); } // Get all buffered entries diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index 20ff1eaa..5fa77925 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -16,10 +16,8 @@ using System; using System.IO; using System.Linq; -using System.Reflection; using System.Runtime.ExceptionServices; using System.Text.Json; -using AspectInjector.Broker; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal.Helpers; using Microsoft.Extensions.Logging; @@ -31,14 +29,8 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// Scope.Global is singleton /// /// -[Aspect(Scope.Global, Factory = typeof(LoggingAspectFactory))] public class LoggingAspect : IMethodAspectHandler { - /// - /// The is cold start - /// - private bool _isColdStart = true; - /// /// The initialize context /// @@ -73,95 +65,6 @@ public LoggingAspect(ILogger logger) _logger = logger ?? LoggerFactoryHolder.GetOrCreateFactory().CreatePowertoolsLogger(); } - // [Advice(Kind.Around)] - // public object Around( - // [Argument(Source.Instance)] object instance, - // [Argument(Source.Name)] string name, - // [Argument(Source.Arguments)] object[] args, - // [Argument(Source.Type)] Type hostType, - // [Argument(Source.Metadata)] MethodBase method, - // [Argument(Source.ReturnType)] Type returnType, - // [Argument(Source.Triggers)] Attribute[] triggers, - // [Argument(Source.Target)] Func target) - // { - // var trigger = triggers.OfType().First(); - // - // try - // { - // var eventArgs = new AspectEventArgs - // { - // Instance = instance, - // Type = hostType, - // Method = method, - // Name = name, - // Args = args, - // ReturnType = returnType, - // Triggers = triggers - // }; - // - // _clearState = trigger.ClearState; - // - // InitializeLogger(trigger); - // - // if (!_initializeContext) - // { - // return target(args); - // } - // - // _logger.AppendKey(LoggingConstants.KeyColdStart, _isColdStart); - // - // _isColdStart = false; - // _initializeContext = false; - // _isContextInitialized = true; - // - // var eventObject = eventArgs.Args.FirstOrDefault(); - // CaptureLambdaContext(eventArgs); - // CaptureCorrelationId(eventObject, trigger.CorrelationIdPath); - // - // if (trigger.IsLogEventSet && trigger.LogEvent) - // { - // LogEvent(eventObject); - // } - // else if (!trigger.IsLogEventSet && _currentConfig.LogEvent) - // { - // LogEvent(eventObject); - // } - // - // var result = target(args); - // - // return result; - // } - // catch (Exception exception) - // { - // if (_bufferingEnabled && trigger.FlushBufferOnUncaughtError) - // { - // _logger.FlushBuffer(); - // } - // - // // The purpose of ExceptionDispatchInfo.Capture is to capture a potentially mutating exception's StackTrace at a point in time: - // // https://learn.microsoft.com/en-us/dotnet/standard/exceptions/best-practices-for-exceptions#capture-exceptions-to-rethrow-later - // ExceptionDispatchInfo.Capture(exception).Throw(); - // throw; - // } - // finally - // { - // if (_isContextInitialized) - // { - // if (_clearLambdaContext) - // LoggingLambdaContext.Clear(); - // if (_clearState) - // _logger.RemoveAllKeys(); - // _initializeContext = true; - // - // if (_bufferingEnabled) - // { - // // clear the buffer after the handler has finished - // _logger.ClearBuffer(); - // } - // } - // } - // } - private void InitializeLogger(LoggingAttribute trigger) { // Check which settings are explicitly provided in the attribute @@ -192,102 +95,6 @@ private void InitializeLogger(LoggingAttribute trigger) _bufferingEnabled = _currentConfig.LogBuffering != null; } - // /// - // /// Runs before the execution of the method marked with the Logging Attribute - // /// - // /// - // /// - // /// - // /// - // /// - // /// - // /// - // [Advice(Kind.Before)] - // public void OnEntry( - // [Argument(Source.Instance)] object instance, - // [Argument(Source.Name)] string name, - // [Argument(Source.Arguments)] object[] args, - // [Argument(Source.Type)] Type hostType, - // [Argument(Source.Metadata)] MethodBase method, - // [Argument(Source.ReturnType)] Type returnType, - // [Argument(Source.Triggers)] Attribute[] triggers) - // { - // // Called before the method - // var trigger = triggers.OfType().First(); - // - // try - // { - // var eventArgs = new AspectEventArgs - // { - // Instance = instance, - // Type = hostType, - // Method = method, - // Name = name, - // Args = args, - // ReturnType = returnType, - // Triggers = triggers - // }; - // - // _clearState = trigger.ClearState; - // - // InitializeLogger(trigger); - // - // if (!_initializeContext) - // return; - // - // _logger.AppendKey(LoggingConstants.KeyColdStart, _isColdStart); - // - // _isColdStart = false; - // _initializeContext = false; - // _isContextInitialized = true; - // - // var eventObject = eventArgs.Args.FirstOrDefault(); - // CaptureLambdaContext(eventArgs); - // CaptureCorrelationId(eventObject, trigger.CorrelationIdPath); - // - // if (trigger.IsLogEventSet && trigger.LogEvent) - // { - // LogEvent(eventObject); - // } - // else if (!trigger.IsLogEventSet && _currentConfig.LogEvent) - // { - // LogEvent(eventObject); - // } - // } - // catch (Exception exception) - // { - // if (_bufferingEnabled && trigger.FlushBufferOnUncaughtError) - // { - // _logger.FlushBuffer(); - // } - // - // // The purpose of ExceptionDispatchInfo.Capture is to capture a potentially mutating exception's StackTrace at a point in time: - // // https://learn.microsoft.com/en-us/dotnet/standard/exceptions/best-practices-for-exceptions#capture-exceptions-to-rethrow-later - // ExceptionDispatchInfo.Capture(exception).Throw(); - // } - // } - - // /// - // /// Handles the Kind.After event. - // /// - // [Advice(Kind.After)] - // public void OnExit() - // { - // if (!_isContextInitialized) - // return; - // if (_clearLambdaContext) - // LoggingLambdaContext.Clear(); - // if (_clearState) - // _logger.RemoveAllKeys(); - // _initializeContext = true; - // - // if (_bufferingEnabled) - // { - // // clear the buffer after the handler has finished - // _logger.ClearBuffer(); - // } - // } - /// /// Captures the lambda context. /// @@ -299,7 +106,7 @@ private void CaptureLambdaContext(AspectEventArgs eventArgs) { _clearLambdaContext = LoggingLambdaContext.Extract(eventArgs); if (LoggingLambdaContext.Instance is null && _isDebug) - _logger.LogDebug( + ConsoleWrapper.WriteLine(LogLevel.Warning.ToLambdaLogLevel(), "Skipping Lambda Context injection because ILambdaContext context parameter not found."); } @@ -322,7 +129,7 @@ private void CaptureCorrelationId(object eventArg, string correlationIdPath) if (eventArg is null) { if (_isDebug) - _logger.LogDebug( + ConsoleWrapper.WriteLine(LogLevel.Warning.ToLambdaLogLevel(), "Skipping CorrelationId capture because event parameter not found."); return; } @@ -356,7 +163,7 @@ private void CaptureCorrelationId(object eventArg, string correlationIdPath) catch (Exception e) { if (_isDebug) - _logger.LogDebug( + ConsoleWrapper.WriteLine(LogLevel.Warning.ToLambdaLogLevel(), $"Skipping CorrelationId capture because of error caused while parsing the event object {e.Message}."); } } @@ -372,7 +179,7 @@ private void LogEvent(object eventArg) case null: { if (_isDebug) - _logger.LogDebug( + ConsoleWrapper.WriteLine(LogLevel.Warning.ToLambdaLogLevel(), "Skipping Event Log because event parameter not found."); break; } @@ -409,6 +216,10 @@ internal static void ResetForTest() LoggingLambdaContext.Clear(); } + /// + /// Entry point for the aspect. + /// + /// public void OnEntry(AspectEventArgs eventArgs) { var trigger = eventArgs.Triggers.OfType().First(); @@ -421,9 +232,6 @@ public void OnEntry(AspectEventArgs eventArgs) if (!_initializeContext) return; - _logger.AppendKey(LoggingConstants.KeyColdStart, _isColdStart); - - _isColdStart = false; _initializeContext = false; _isContextInitialized = true; _flushBufferOnUncaughtError = trigger.FlushBufferOnUncaughtError; @@ -454,11 +262,21 @@ public void OnEntry(AspectEventArgs eventArgs) } } + /// + /// When the method returns successfully, this method is called. + /// + /// + /// public void OnSuccess(AspectEventArgs eventArgs, object result) { } + /// + /// When the method throws an exception, this method is called. + /// + /// + /// public void OnException(AspectEventArgs eventArgs, Exception exception) { if (_bufferingEnabled && _flushBufferOnUncaughtError) @@ -468,6 +286,10 @@ public void OnException(AspectEventArgs eventArgs, Exception exception) ExceptionDispatchInfo.Capture(exception).Throw(); } + /// + /// WHen the method exits, this method is called even if it throws an exception. + /// + /// public void OnExit(AspectEventArgs eventArgs) { if (!_isContextInitialized) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs index 4ca0b878..3fedb7e6 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsConfigurationsExtension.cs @@ -20,6 +20,32 @@ namespace AWS.Lambda.Powertools.Logging.Internal; +internal static class LambdaLogLevelMapper +{ + public static string ToLambdaLogLevel(this LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Trace: + return "trace"; + case LogLevel.Debug: + return "debug"; + case LogLevel.Information: + return "info"; + case LogLevel.Warning: + return "warn"; + case LogLevel.Error: + return "error"; + case LogLevel.Critical: + return "fatal"; + default: + return "info"; + } + } +} + + + /// /// Class PowertoolsConfigurationsExtension. /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs index b67964ae..76a88196 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -183,18 +183,35 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times { var logEntry = new Dictionary(); - // Add Custom Keys - foreach (var (key, value) in this.GetAllKeys()) - { - logEntry.TryAdd(key, value); - } - + var config = _currentConfig(); + logEntry.TryAdd(config.LogLevelKey, logLevel.ToString()); + logEntry.TryAdd(LoggingConstants.KeyMessage, message); + logEntry.TryAdd(LoggingConstants.KeyTimestamp, timestamp.ToString( config.TimestampFormat ?? "o")); + logEntry.TryAdd(LoggingConstants.KeyService, config.Service); + logEntry.TryAdd(LoggingConstants.KeyColdStart, _powertoolsConfigurations.IsColdStart); + // Add Lambda Context Keys if (LoggingLambdaContext.Instance is not null) { AddLambdaContextKeys(logEntry); } - + + + if(! string.IsNullOrWhiteSpace(_powertoolsConfigurations.XRayTraceId)) + logEntry.TryAdd(LoggingConstants.KeyXRayTraceId, + _powertoolsConfigurations.XRayTraceId.Split(';', StringSplitOptions.RemoveEmptyEntries)[0].Replace("Root=", "")); + logEntry.TryAdd(LoggingConstants.KeyLoggerName, _categoryName); + + if (config.SamplingRate > 0) + logEntry.TryAdd(LoggingConstants.KeySamplingRate, config.SamplingRate); + + // Add Custom Keys + foreach (var (key, value) in this.GetAllKeys()) + { + logEntry.TryAdd(key, value); + } + + // Add Extra Fields if (CurrentScope?.ExtraKeys is not null) { @@ -218,19 +235,6 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times } } } - - var config = _currentConfig(); - logEntry.TryAdd(LoggingConstants.KeyTimestamp, timestamp.ToString( config.TimestampFormat ?? "o")); - logEntry.TryAdd(config.LogLevelKey, logLevel.ToString()); - logEntry.TryAdd(LoggingConstants.KeyService, config.Service); - if(! string.IsNullOrWhiteSpace(_powertoolsConfigurations.XRayTraceId)) - logEntry.TryAdd(LoggingConstants.KeyXRayTraceId, - _powertoolsConfigurations.XRayTraceId.Split(';', StringSplitOptions.RemoveEmptyEntries)[0].Replace("Root=", "")); - logEntry.TryAdd(LoggingConstants.KeyLoggerName, _categoryName); - logEntry.TryAdd(LoggingConstants.KeyMessage, message); - - if (config.SamplingRate > 0) - logEntry.TryAdd(LoggingConstants.KeySamplingRate, config.SamplingRate); // Use the AddExceptionDetails method instead of adding exception directly if (exception != null) @@ -401,10 +405,10 @@ private void AddLambdaContextKeys(Dictionary logEntry) { var context = LoggingLambdaContext.Instance; logEntry.TryAdd(LoggingConstants.KeyFunctionName, context.FunctionName); - logEntry.TryAdd(LoggingConstants.KeyFunctionVersion, context.FunctionVersion); logEntry.TryAdd(LoggingConstants.KeyFunctionMemorySize, context.MemoryLimitInMB); logEntry.TryAdd(LoggingConstants.KeyFunctionArn, context.InvokedFunctionArn); logEntry.TryAdd(LoggingConstants.KeyFunctionRequestId, context.AwsRequestId); + logEntry.TryAdd(LoggingConstants.KeyFunctionVersion, context.FunctionVersion); } /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs index dba6e637..747cf7db 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs @@ -191,6 +191,10 @@ public bool LogEvent /// public bool FlushBufferOnUncaughtError { get; set; } + /// + /// Creates the aspect with the Logger + /// + /// protected override IMethodAspectHandler CreateHandler() { return new LoggingAspect(LoggerFactoryHolder.GetOrCreateFactory().CreatePowertoolsLogger()); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs index 1c3a5420..651e4f25 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggingBuilderExtensions.cs @@ -180,7 +180,7 @@ public static ILoggingBuilder AddPowertoolsLogger( // Add a filter for the buffer provider builder.AddFilter( null, - options.LogBuffering.BufferAtLogLevel); + LogLevel.Trace); // Register the buffer provider as an enumerable service // Using singleton to ensure it's properly tracked diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs index 868dc362..82962fcf 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs @@ -14,6 +14,7 @@ */ using System; +using System.IO; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal; using AWS.Lambda.Powertools.Logging.Tests.Handlers; @@ -89,10 +90,13 @@ public void OnEntry_ShouldInitializeLogger_WhenCalledWithValidArguments() loggingAspect.OnEntry(aspectArgs); // Assert - consoleOut.Received().WriteLine(Arg.Is(s => - s.Contains( - "\"Level\":\"Information\",\"Service\":\"TestService\",\"Name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":null},\"SamplingRate\":0.5}") - && s.Contains("\"CorrelationId\":\"20\"") + consoleOut.Received(1).WriteLine(Arg.Is(s => + s.Contains("\"Level\":\"Information\"") && + s.Contains("\"Service\":\"TestService\"") && + s.Contains("\"Name\":\"AWS.Lambda.Powertools.Logging.Logger\"") && + s.Contains("\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":null}") && + s.Contains("\"CorrelationId\":\"20\"") && + s.Contains("\"SamplingRate\":0.5") )); } @@ -154,10 +158,12 @@ public void OnEntry_ShouldLog_Event_When_EnvironmentVariable_Set() Assert.Equal(0, updatedConfig.SamplingRate); Assert.True(updatedConfig.LogEvent); - consoleOut.Received().WriteLine(Arg.Is(s => - s.Contains( - "\"Level\":\"Information\",\"Service\":\"TestService\",\"Name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":null}}") - && s.Contains("\"CorrelationId\":\"20\"") + consoleOut.Received(1).WriteLine(Arg.Is(s => + s.Contains("\"Level\":\"Information\"") && + s.Contains("\"Service\":\"TestService\"") && + s.Contains("\"Name\":\"AWS.Lambda.Powertools.Logging.Logger\"") && + s.Contains("\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":null}") && + s.Contains("\"CorrelationId\":\"20\"") )); } @@ -281,11 +287,14 @@ public void OnEntry_ShouldLog_SamplingRate_When_EnvironmentVariable_Set() Assert.Equal("TestService", updatedConfig.Service); Assert.Equal(LoggerOutputCase.PascalCase, updatedConfig.LoggerOutputCase); Assert.Equal(0.5, updatedConfig.SamplingRate); - - consoleOut.Received().WriteLine(Arg.Is(s => - s.Contains( - "\"Level\":\"Information\",\"Service\":\"TestService\",\"Name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":null},\"SamplingRate\":0.5}") - && s.Contains("\"CorrelationId\":\"20\"") + + consoleOut.Received(1).WriteLine(Arg.Is(s => + s.Contains("\"Level\":\"Information\"") && + s.Contains("\"Service\":\"TestService\"") && + s.Contains("\"Name\":\"AWS.Lambda.Powertools.Logging.Logger\"") && + s.Contains("\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":null}") && + s.Contains("\"CorrelationId\":\"20\"") && + s.Contains("\"SamplingRate\":0.5") )); } @@ -326,9 +335,11 @@ public void OnEntry_ShouldLogEvent_WhenLogEventIsTrue() loggingAspect.OnEntry(aspectArgs); // Assert - consoleOut.Received().WriteLine(Arg.Is(s => - s.Contains( - "\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":{\"test_data\":\"test-data\"}}") + consoleOut.Received(1).WriteLine(Arg.Is(s => + s.Contains("\"level\":\"Information\"") && + s.Contains("\"service\":\"TestService\"") && + s.Contains("\"name\":\"AWS.Lambda.Powertools.Logging.Logger\"") && + s.Contains("\"message\":{\"test_data\":\"test-data\"}") )); } @@ -437,6 +448,8 @@ public void OnEntry_Should_LogDebug_WhenSet_EnvironmentVariable() }; // Act + var stringWriter = new StringWriter(); + Console.SetOut(stringWriter); var loggingAspect = new LoggingAspect(logger); loggingAspect.OnEntry(aspectArgs); @@ -447,10 +460,9 @@ public void OnEntry_Should_LogDebug_WhenSet_EnvironmentVariable() Assert.Equal(LoggerOutputCase.PascalCase, updatedConfig.LoggerOutputCase); Assert.Equal(LogLevel.Debug, updatedConfig.MinimumLogLevel); - consoleOut.Received(1).WriteLine(Arg.Is(s => - s.Contains( - "\"Level\":\"Debug\",\"Service\":\"TestService\",\"Name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"Message\":\"Skipping Lambda Context injection because ILambdaContext context parameter not found.\"}"))); - + string consoleOutput = stringWriter.ToString(); + Assert.Contains("Skipping Lambda Context injection because ILambdaContext context parameter not found.", consoleOutput); + consoleOut.Received(1).WriteLine(Arg.Is(s => s.Contains("\"CorrelationId\":\"test\"") && s.Contains( diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs index 86882370..677c3c2c 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs @@ -15,12 +15,14 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.ApplicationLoadBalancerEvents; using Amazon.Lambda.CloudWatchEvents.S3Events; using Amazon.Lambda.TestUtilities; using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Common.Core; using AWS.Lambda.Powertools.Common.Tests; using AWS.Lambda.Powertools.Logging.Internal; using AWS.Lambda.Powertools.Logging.Tests.Handlers; @@ -42,11 +44,11 @@ public LoggingAttributeTests() } [Fact] - public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContext() + public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContext_No_Debug() { // Arrange - var consoleOut = GetConsoleOutput(); - + var stringWriter = new StringWriter(); + Console.SetOut(stringWriter); // Act _testHandlers.TestMethod(); @@ -54,14 +56,10 @@ public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContext() var allKeys = Logger.GetAllKeys() .ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value); - Assert.True(allKeys.ContainsKey(LoggingConstants.KeyColdStart)); - Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionName)); - Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionVersion)); - Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionMemorySize)); - Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionArn)); - Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionRequestId)); - - consoleOut.DidNotReceive().WriteLine(Arg.Any()); + Assert.Empty(allKeys); + + var st = stringWriter.ToString(); + Assert.Empty(st); } [Fact] @@ -69,6 +67,8 @@ public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContextAndLogDebu { // Arrange var consoleOut = GetConsoleOutput(); + var stringWriter = new StringWriter(); + Console.SetOut(stringWriter); Logger.Configure(options => { options.LogOutput = consoleOut; @@ -81,17 +81,10 @@ public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContextAndLogDebu var allKeys = Logger.GetAllKeys() .ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value); - Assert.True(allKeys.ContainsKey(LoggingConstants.KeyColdStart)); - Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionName)); - Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionVersion)); - Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionMemorySize)); - Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionArn)); - Assert.False(allKeys.ContainsKey(LoggingConstants.KeyFunctionRequestId)); + Assert.Empty(allKeys); - consoleOut.Received(1).WriteLine( - Arg.Is(i => - i.Contains("\"level\":\"Debug\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Skipping Lambda Context injection because ILambdaContext context parameter not found.\"}")) - ); + var st = stringWriter.ToString(); + Assert.Contains("Skipping Lambda Context injection because ILambdaContext context parameter not found", st); } [Fact] @@ -164,20 +157,20 @@ public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArgAndLogDebug() { // Arrange var consoleOut = GetConsoleOutput(); + var stringWriter = new StringWriter(); + Console.SetOut(stringWriter); Logger.Configure(options => { options.LogOutput = consoleOut; }); + // Act _testHandlers.LogEventDebug(); - consoleOut.Received(1).WriteLine( - Arg.Is(i => i.Contains("\"level\":\"Debug\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Skipping Event Log because event parameter not found.\"}")) - ); - - consoleOut.Received(1).WriteLine( - Arg.Is(i => i.Contains("\"level\":\"Debug\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Skipping Lambda Context injection because ILambdaContext context parameter not found.\"}")) - ); + // Assert + var st = stringWriter.ToString(); + Assert.Contains("Skipping Event Log because event parameter not found.", st); + Assert.Contains("Skipping Lambda Context injection because ILambdaContext context parameter not found", st); } [Fact] @@ -359,10 +352,13 @@ public void When_Setting_SamplingRate_Should_Add_Key() _testHandlers.HandlerSamplingRate(); // Assert - - consoleOut.Received().WriteLine( - Arg.Is(i => i.Contains("\"message\":\"test\",\"samplingRate\":0.5")) - ); + consoleOut.Received(1).WriteLine(Arg.Is(s => + s.Contains("\"level\":\"Information\"") && + s.Contains("\"service\":\"service_undefined\"") && + s.Contains("\"name\":\"AWS.Lambda.Powertools.Logging.Logger\"") && + s.Contains("\"message\":\"test\"") && + s.Contains("\"samplingRate\":0.5") + )); } [Fact] @@ -381,7 +377,11 @@ public void When_Setting_Service_Should_Update_Key() // Assert var st = consoleOut.ToString(); - Assert.Contains("\"level\":\"Information\",\"service\":\"test\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"test\"", st); + + Assert.Contains("\"level\":\"Information\"", st); + Assert.Contains("\"service\":\"test\"", st); + Assert.Contains("\"name\":\"AWS.Lambda.Powertools.Logging.Logger\"", st); + Assert.Contains("\"message\":\"test\"", st); } [Fact] @@ -430,19 +430,22 @@ public void When_LogLevel_Debug_Should_Log_Message_When_No_Context_And_LogEvent_ { // Arrange var consoleOut = GetConsoleOutput(); + var stringWriter = new StringWriter(); + Console.SetOut(stringWriter); + Logger.Configure(options => { options.LogOutput = consoleOut; }); + // Act _testHandlers.TestLogEventWithoutContext(); - - // Assert - consoleOut.Received(1).WriteLine(Arg.Is(s => - s.Contains("\"level\":\"Debug\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Skipping Event Log because event parameter not found.\"}"))); - consoleOut.Received(1).WriteLine(Arg.Is(s => - s.Contains("\"level\":\"Debug\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Skipping Lambda Context injection because ILambdaContext context parameter not found.\"}"))); + // Assert + var st = stringWriter.ToString(); + Assert.Contains("Skipping Event Log because event parameter not found.", st); + Assert.Contains("Skipping Lambda Context injection because ILambdaContext context parameter not found", st); + } [Fact] @@ -461,9 +464,12 @@ public void Should_Log_When_Not_Using_Decorator() test.TestLogNoDecorator(); // Assert - consoleOut.Received().WriteLine( - Arg.Is(i => i.Contains("\"level\":\"Information\",\"service\":\"service_undefined\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"test\"}")) - ); + consoleOut.Received(1).WriteLine(Arg.Is(s => + s.Contains("\"level\":\"Information\"") && + s.Contains("\"service\":\"service_undefined\"") && + s.Contains("\"name\":\"AWS.Lambda.Powertools.Logging.Logger\"") && + s.Contains("\"message\":\"test\"") + )); } [Fact] @@ -471,7 +477,9 @@ public void LoggingAspect_ShouldRespectDynamicLogLevelChanges() { // Arrange var consoleOut = GetConsoleOutput(); - + var stringWriter = new StringWriter(); + Console.SetOut(stringWriter); + Logger.Configure(options => { options.LogOutput = consoleOut; @@ -482,9 +490,8 @@ public void LoggingAspect_ShouldRespectDynamicLogLevelChanges() _testHandlers.TestMethodDebug(); // Uses LogLevel.Debug attribute // Assert - consoleOut.Received(1).WriteLine(Arg.Is(s => - s.Contains("\"level\":\"Debug\"") && - s.Contains("Skipping Lambda Context injection"))); + var st = stringWriter.ToString(); + Assert.Contains("Skipping Lambda Context injection because ILambdaContext context parameter not found", st); } [Fact] @@ -517,6 +524,9 @@ public void LoggingAspect_ShouldRespectAttributePrecedenceOverEnvironment() // Arrange Environment.SetEnvironmentVariable("POWERTOOLS_LOG_LEVEL", "Error"); var consoleOut = GetConsoleOutput(); + var stringWriter = new StringWriter(); + Console.SetOut(stringWriter); + Logger.Configure(options => { options.LogOutput = consoleOut; @@ -526,8 +536,8 @@ public void LoggingAspect_ShouldRespectAttributePrecedenceOverEnvironment() _testHandlers.TestMethodDebug(); // Uses LogLevel.Debug attribute // Assert - consoleOut.Received().WriteLine(Arg.Is(s => - s.Contains("\"level\":\"Debug\""))); + var st = stringWriter.ToString(); + Assert.Contains("Skipping Lambda Context injection because ILambdaContext context parameter not found", st); } [Fact] @@ -587,6 +597,7 @@ private void ResetAllState() LoggerOutputCase = LoggerOutputCase.SnakeCase }; PowertoolsLoggingBuilderExtensions.UpdateConfiguration(config); + LambdaLifecycleTracker.Reset(); } } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/ServiceTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/ServiceTests.cs index c1a24a97..657730e0 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/ServiceTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/ServiceTests.cs @@ -32,12 +32,18 @@ public void When_Setting_Service_Should_Override_Env() // Assert - consoleOut.Received(1).WriteLine( - Arg.Is(i => i.Contains("\"level\":\"Information\",\"service\":\"Environment Service\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Service: Environment Service\"")) - ); - consoleOut.Received(1).WriteLine( - Arg.Is(i => i.Contains("\"level\":\"Information\",\"service\":\"Attribute Service\",\"name\":\"AWS.Lambda.Powertools.Logging.Logger\",\"message\":\"Service: Attribute Service\"")) - ); + consoleOut.Received(1).WriteLine(Arg.Is(i => + i.Contains("\"level\":\"Information\"") && + i.Contains("\"service\":\"Environment Service\"") && + i.Contains("\"name\":\"AWS.Lambda.Powertools.Logging.Logger\"") && + i.Contains("\"message\":\"Service: Environment Service\"") + )); + consoleOut.Received(1).WriteLine(Arg.Is(i => + i.Contains("\"level\":\"Information\"") && + i.Contains("\"service\":\"Attribute Service\"") && + i.Contains("\"name\":\"AWS.Lambda.Powertools.Logging.Logger\"") && + i.Contains("\"message\":\"Service: Attribute Service\"") + )); } public void Dispose() diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LambdaContextBufferingTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LambdaContextBufferingTests.cs index 1da4b235..b6172371 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LambdaContextBufferingTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LambdaContextBufferingTests.cs @@ -97,6 +97,27 @@ public async Task AsyncOperations_MaintainBufferContext() Assert.Contains("Debug from task 1", output); Assert.Contains("Debug from task 2", output); } + + [Fact] + public async Task Should_Log_All_Levels_Bellow() + { + // Arrange + var logger = CreateLogger(LogLevel.Information, LogLevel.Information); + var handler = new AsyncLambdaHandler(logger); + var context = CreateTestContext("async-test"); + + // Act + await handler.TestMethodAsync("Event", context); + + // Assert + var output = _consoleOut.ToString(); + Assert.Contains("Async info message", output); + Assert.Contains("Async debug message", output); + Assert.Contains("Async trace message", output); + Assert.Contains("Async warning message", output); + Assert.Contains("Debug from task 1", output); + Assert.Contains("Debug from task 2", output); + } private TestLambdaContext CreateTestContext(string requestId) { @@ -308,7 +329,7 @@ public void StaticLogger_FlushOnErrorLogEnabled() } [Fact] - public void StaticLogger_MultipleInvocationsIsolated() + public void StaticLogger_MultipleInvocationsIsolated_And_Clear() { // Arrange Logger.Configure(options => @@ -329,20 +350,18 @@ public void StaticLogger_MultipleInvocationsIsolated() // Switch to second invocation Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-static-request-5B"); Logger.LogDebug("Debug from invocation B"); - Logger.FlushBuffer(); // Only flush B - - // Assert - after first flush - var outputAfterFirstFlush = _consoleOut.ToString(); - Assert.Contains("Debug from invocation B", outputAfterFirstFlush); - Assert.DoesNotContain("Debug from invocation A", outputAfterFirstFlush); // Switch back to first invocation and flush - Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-static-request-5A"); + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-static-request-5C"); + Logger.LogDebug("Debug from invocation C"); + Logger.FlushBuffer(); // Assert - after second flush var outputAfterSecondFlush = _consoleOut.ToString(); - Assert.Contains("Debug from invocation A", outputAfterSecondFlush); + Assert.DoesNotContain("Debug from invocation A", outputAfterSecondFlush); + Assert.DoesNotContain("Debug from invocation B", outputAfterSecondFlush); + Assert.Contains("Debug from invocation C", outputAfterSecondFlush); } [Fact] @@ -506,8 +525,12 @@ public AsyncLambdaHandler(ILogger logger) [Logging(LogEvent = true)] public async Task TestMethodAsync(string message, ILambdaContext lambdaContext) { + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "test-invocation"); + _logger.LogInformation("Async info message"); _logger.LogDebug("Async debug message"); + _logger.LogTrace("Async trace message"); + _logger.LogWarning("Async warning message"); var task1 = Task.Run(() => { _logger.LogDebug("Debug from task 1"); }); diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferCircularCacheTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferCircularCacheTests.cs index adca01e6..c0640c92 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferCircularCacheTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferCircularCacheTests.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Common.Tests; using AWS.Lambda.Powertools.Logging.Internal; @@ -85,6 +86,9 @@ public void Buffer_WhenMaxSizeExceeded_DiscardOldestEntries_Warn() Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "circular-buffer-test"); + var stringWriter = new StringWriter(); + Console.SetOut(stringWriter); + // Act - add many debug logs to fill buffer for (int i = 0; i < 5; i++) { @@ -101,8 +105,51 @@ public void Buffer_WhenMaxSizeExceeded_DiscardOldestEntries_Warn() logger.FlushBuffer(); // Assert - var output = _consoleOut.ToString(); - Assert.Contains("Some logs are not displayed because they were evicted from the buffer. Increase buffer size to store more logs in the buffer", output); + var st = stringWriter.ToString(); + Assert.Contains("Some logs are not displayed because they were evicted from the buffer. Increase buffer size to store more logs in the buffer", st); + } + + [Trait("Category", "CircularBuffer")] + [Fact] + public void Buffer_WhenMaxSizeExceeded_DiscardOldestEntries_Warn_With_Warning_Level() + { + // Arrange + var config = new PowertoolsLoggerConfiguration + { + MinimumLogLevel = LogLevel.Information, + LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Warning, + MaxBytes = 1024 // Small buffer size to trigger overflow + }, + LogOutput = _consoleOut + }; + + var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); + + Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "circular-buffer-test"); + + var stringWriter = new StringWriter(); + Console.SetOut(stringWriter); + + // Act - add many debug logs to fill buffer + for (int i = 0; i < 5; i++) + { + logger.LogDebug($"Old debug message {i} that should be removed"); + } + + // Add more logs that should push out the older ones + for (int i = 0; i < 5; i++) + { + logger.LogDebug($"New debug message {i} that should remain"); + } + + // Flush buffer + logger.FlushBuffer(); + + // Assert + var st = stringWriter.ToString(); + Assert.Contains("Some logs are not displayed because they were evicted from the buffer. Increase buffer size to store more logs in the buffer", st); } [Trait("Category", "CircularBuffer")] @@ -165,7 +212,7 @@ public void Buffer_WithExtremelyLargeEntry_Logs_Directly_And_Warning() LogBuffering = new LogBufferingOptions { BufferAtLogLevel = LogLevel.Debug, - MaxBytes = 4096 // Even with a larger buffer + MaxBytes = 5096 // Even with a larger buffer }, LogOutput = _consoleOut }; @@ -174,6 +221,9 @@ public void Buffer_WithExtremelyLargeEntry_Logs_Directly_And_Warning() Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "extreme-entry-test"); + var stringWriter = new StringWriter(); + Console.SetOut(stringWriter); + // Act - add some small entries first for (int i = 0; i < 4; i++) { @@ -184,12 +234,8 @@ public void Buffer_WithExtremelyLargeEntry_Logs_Directly_And_Warning() var hugeMessage = new string('X', 3000); logger.LogDebug($"Huge message: {hugeMessage}"); - var bigMessageAndWarning = _consoleOut.ToString(); - - // Huge message may be partially discarded depending on implementation - Assert.Contains("Huge message", bigMessageAndWarning); - Assert.Contains("level\":\"Warning", bigMessageAndWarning); - Assert.Contains("Cannot add item to the buffer", bigMessageAndWarning); + var st = stringWriter.ToString(); + Assert.Contains("Cannot add item to the buffer", st); // Add more entries after for (int i = 0; i < 4; i++) @@ -213,61 +259,6 @@ public void Buffer_WithExtremelyLargeEntry_Logs_Directly_And_Warning() Assert.Contains("Final message 3", output); } - [Trait("Category", "CircularBuffer")] - [Fact] - public void MultipleInvocations_EachHaveTheirOwnCircularBuffer() - { - // Arrange - var config = new PowertoolsLoggerConfiguration - { - MinimumLogLevel = LogLevel.Information, - LogBuffering = new LogBufferingOptions - { - BufferAtLogLevel = LogLevel.Debug - }, - LogOutput = _consoleOut - }; - - var logger = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger(); - - // Act - fill buffer for first invocation - Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); - for (int i = 0; i < 10; i++) - { - logger.LogDebug($"Invocation 1 message {i}"); - } - - // Switch to second invocation with fresh buffer - Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-2"); - - for (int i = 0; i < 5; i++) - { - logger.LogDebug($"Invocation 2 message {i}"); - } - - // Flush second invocation first - logger.FlushBuffer(); - var outputAfterSecond = _consoleOut.ToString(); - - // Flush first invocation - Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); - logger.FlushBuffer(); - var outputAfterBoth = _consoleOut.ToString(); - - // Assert - // First invocation buffer should be complete - for (int i = 0; i < 5; i++) - { - Assert.Contains($"Invocation 1 message {i}", outputAfterBoth); - } - - // Second invocation buffer should be complete (not affected by first) - for (int i = 0; i < 5; i++) - { - Assert.Contains($"Invocation 2 message {i}", outputAfterSecond); - } - } - public void Dispose() { // Clean up all state between tests diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingTests.cs index f8fba6c2..00d1e759 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Buffering/LogBufferingTests.cs @@ -20,7 +20,7 @@ public LogBufferingTests() [Trait("Category", "BufferManager")] [Fact] - public void SetInvocationId_IsolatesLogsBetweenInvocations() + public void SetInvocationId_IsolatesLogsBetweenInvocations_And_Clear() { // Arrange var config = new PowertoolsLoggerConfiguration @@ -39,14 +39,12 @@ public void SetInvocationId_IsolatesLogsBetweenInvocations() Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-2"); logger.LogDebug("Debug message from invocation 2"); - Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-1"); - logger.LogError("Error message from invocation 1"); - + logger.FlushBuffer(); + // Assert var output = _consoleOut.ToString(); - Assert.Contains("Error message from invocation 1", output); - Assert.Contains("Debug message from invocation 1", output); - Assert.DoesNotContain("Debug message from invocation 2", output); + Assert.DoesNotContain("Debug message from invocation 1", output); + Assert.Contains("Debug message from invocation 2", output); } [Trait("Category", "BufferedLogger")] @@ -117,6 +115,8 @@ public void BufferedLogger_Buffer_Takes_Precedence_Same_Level() output = _consoleOut.ToString(); Assert.Contains("Info message", output); // Now should be visible + Assert.Contains("Debug message", output); // Now should be visible + Assert.Contains("Trace message", output); // Now should be visible } [Trait("Category", "BufferedLogger")] @@ -140,7 +140,8 @@ public void BufferedLogger_Buffer_Takes_Precedence_Higher_Level() // Act logger.LogWarning("Warning message"); // Should be buffered logger.LogInformation("Info message"); // Should be buffered - + logger.LogDebug("Debug message"); + // Assert var output = _consoleOut.ToString(); Assert.Empty(output); @@ -149,8 +150,9 @@ public void BufferedLogger_Buffer_Takes_Precedence_Higher_Level() Logger.FlushBuffer(); output = _consoleOut.ToString(); - Assert.DoesNotContain("Info message", output); // Now should be visible + Assert.Contains("Info message", output); // Now should be visible Assert.Contains("Warning message", output); + Assert.Contains("Debug message", output); } [Trait("Category", "BufferedLogger")] @@ -486,46 +488,6 @@ public void LogsAtExactBufferThreshold_AreBuffered() Assert.Contains("Debug message exactly at threshold", _consoleOut.ToString()); } - - [Trait("Category", "MultipleInvocations")] - [Fact] - public void SwitchingBetweenInvocations_PreservesSeparateBuffers() - { - // Arrange - LogBufferManager.ResetForTesting(); - var config = new PowertoolsLoggerConfiguration - { - MinimumLogLevel = LogLevel.Information, - LogBuffering = new LogBufferingOptions(), - LogOutput = _consoleOut - }; - var powertoolsConfig = new PowertoolsConfigurations(new PowertoolsEnvironment()); - var provider = new BufferingLoggerProvider(config, powertoolsConfig); - var logger = provider.CreateLogger("TestLogger"); - - // Act - // First invocation - Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-A"); - logger.LogDebug("Debug for invocation A"); - - // Switch to second invocation - Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-B"); - logger.LogDebug("Debug for invocation B"); - Logger.FlushBuffer(); // Only flush B - - // Assert - var output = _consoleOut.ToString(); - Assert.Contains("Debug for invocation B", output); - Assert.DoesNotContain("Debug for invocation A", output); - - // Now flush A - Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "invocation-A"); - Logger.FlushBuffer(); - - output = _consoleOut.ToString(); - Assert.Contains("Debug for invocation A", output); - } - public void Dispose() { // Clean up all state between tests diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/HandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/HandlerTests.cs index 329c1a01..c58c75ca 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/HandlerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Handlers/HandlerTests.cs @@ -126,7 +126,12 @@ public void TestMethod() Assert.Contains(" ", logOutput); // Verify write indented JSON - Assert.Contains(" \"Level\": \"Information\",\n \"Service\": \"my-service122\",", logOutput); + Assert.Contains("\"Level\": \"Information\"", logOutput); + Assert.Contains("\"Service\": \"my-service122\"", logOutput); + Assert.Contains("\"Message\": \"Information message\"", logOutput); + Assert.Contains("\"Custom-key\": \"custom-value\"", logOutput); + Assert.Contains("\"FunctionName\": \"test-function\"", logOutput); + Assert.Contains("\"SamplingRate\": 0.002", logOutput); } [Fact] diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs index 2932f3de..50e28a2d 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs @@ -20,6 +20,7 @@ using System.Linq; using System.Text; using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Common.Core; using AWS.Lambda.Powertools.Common.Tests; using AWS.Lambda.Powertools.Logging.Internal; using AWS.Lambda.Powertools.Logging.Serializers; @@ -1129,8 +1130,16 @@ public void Log_Inner_Exception() )); systemWrapper.Received(1).WriteLine(Arg.Is(s => - s.Contains("\"level\":\"Error\",\"service\":\"" + service + "\",\"name\":\"" + loggerName + - "\",\"message\":\"Something went wrong and we logged an exception itself with an inner exception. This is a param 12345\",\"exception\":{\"type\":\"System.InvalidOperationException\",\"message\":\"Parent exception message\",\"inner_exception\":{\"type\":\"System.ArgumentNullException\",\"message\":\"Very important inner exception message (Parameter 'service')\"}}}") + s.Contains("\"level\":\"Error\"") && + s.Contains("\"service\":\"" + service + "\"") && + s.Contains("\"name\":\"" + loggerName + "\"") && + s.Contains("\"message\":\"Something went wrong and we logged an exception itself with an inner exception. This is a param 12345\"") && + s.Contains("\"exception\":{") && + s.Contains("\"type\":\"System.InvalidOperationException\"") && + s.Contains("\"message\":\"Parent exception message\"") && + s.Contains("\"inner_exception\":{") && + s.Contains("\"type\":\"System.ArgumentNullException\"") && + s.Contains("\"message\":\"Very important inner exception message (Parameter 'service')\"") )); } @@ -1170,8 +1179,16 @@ public void Log_Nested_Inner_Exception() // Assert systemWrapper.Received(1).WriteLine(Arg.Is(s => - s.Contains( - "\"message\":\"Something went wrong and we logged an exception itself with an inner exception. This is a param 12345\",\"exception\":{\"type\":\"System.InvalidOperationException\",\"message\":\"Parent exception message\",\"inner_exception\":{\"type\":\"System.ArgumentNullException\",\"message\":\"service\",\"inner_exception\":{\"type\":\"System.Exception\",\"message\":\"Very important nested inner exception message\"}}}}") + s.Contains("\"message\":\"Something went wrong and we logged an exception itself with an inner exception. This is a param 12345\"") && + s.Contains("\"exception\":{") && + s.Contains("\"type\":\"System.InvalidOperationException\"") && + s.Contains("\"message\":\"Parent exception message\"") && + s.Contains("\"inner_exception\":{") && + s.Contains("\"type\":\"System.ArgumentNullException\"") && + s.Contains("\"message\":\"service\"") && + s.Contains("\"inner_exception\":{") && + s.Contains("\"type\":\"System.Exception\"") && + s.Contains("\"message\":\"Very important nested inner exception message\"") )); } @@ -1427,7 +1444,7 @@ public void Log_Should_Serialize_DateOnly() systemWrapper.Received(1).WriteLine( Arg.Is(s => s.Contains( - "\"message\":{\"propOne\":\"Value 1\",\"propTwo\":\"Value 2\",\"propThree\":{\"propFour\":1},\"date\":\"2022-01-01\"}}") + "\"message\":{\"propOne\":\"Value 1\",\"propTwo\":\"Value 2\",\"propThree\":{\"propFour\":1},\"date\":\"2022-01-01\"}") ) ); } @@ -1721,11 +1738,38 @@ public void Log_Should_Use_Powertools_Log_Level_When_Set(bool willLog, LogLevel Assert.True(logger.IsEnabled(logLevel)); Assert.Equal(logLevel.ToString(), configurations.LogLevel); } + + [Theory] + [InlineData(true, "on-demand")] + [InlineData(false, "provisioned-concurrency")] + public void Log_Cold_Start(bool willLog, string awsInitType) + { + // Arrange + var logOutput = new TestLoggerOutput(); + Environment.SetEnvironmentVariable("AWS_LAMBDA_INITIALIZATION_TYPE", awsInitType); + var configurations = new PowertoolsConfigurations(new PowertoolsEnvironment()); + + var loggerConfiguration = new PowertoolsLoggerConfiguration + { + LoggerOutputCase = LoggerOutputCase.CamelCase, + LogOutput = logOutput + }; + + var provider = new PowertoolsLoggerProvider(loggerConfiguration, configurations); + var logger = provider.CreateLogger("temp"); + + // Act + logger.LogInformation("Hello"); + + var outPut = logOutput.ToString(); + // Assert + Assert.Contains($"\"coldStart\":{willLog.ToString().ToLower()}", outPut); + } public void Dispose() { - // PowertoolsLoggingSerializer.ClearOptions(); - // LoggingAspect.ResetForTest(); + // Environment.SetEnvironmentVariable("AWS_LAMBDA_INITIALIZATION_TYPE", null); + LambdaLifecycleTracker.Reset(); } } } \ No newline at end of file From 8e867354e8e0af05723e8aa85ab97e20a089fe65 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Sat, 5 Apr 2025 18:59:12 +0100 Subject: [PATCH 37/49] Remove duplicate keys.Add universalwrapper and attribute. Fix aspect and common reference. Fix TypeInfoResolver --- .../Core/LambdaLifecycleTracker.cs | 2 +- .../AWS.Lambda.Powertools.Logging.csproj | 2 +- .../Converters/ConstantClassConverter.cs | 2 +- .../Internal/Converters/DateOnlyConverter.cs | 2 +- .../Internal/LoggingLambdaContext.cs | 2 +- .../Internal/PowertoolsLogger.cs | 42 ++++++- .../PowertoolsLoggingSerializer.cs | 56 ++++++--- libraries/src/Directory.Build.props | 23 ++-- .../Formatter/LogFormattingTests.cs | 110 ++++++++++++++++++ 9 files changed, 209 insertions(+), 32 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/LambdaLifecycleTracker.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/LambdaLifecycleTracker.cs index 4c786f38..66d0c90e 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/LambdaLifecycleTracker.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/LambdaLifecycleTracker.cs @@ -6,7 +6,7 @@ namespace AWS.Lambda.Powertools.Common.Core; /// /// Tracks Lambda lifecycle state including cold starts /// -public static class LambdaLifecycleTracker +internal static class LambdaLifecycleTracker { // Static flag that's true only for the first Lambda container initialization private static bool _isFirstContainer = true; 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 ccf8c3ea..f68c3297 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 @@ -16,7 +16,7 @@ - + diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ConstantClassConverter.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ConstantClassConverter.cs index 1bc0f6e9..e6c3aebb 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ConstantClassConverter.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ConstantClassConverter.cs @@ -23,7 +23,7 @@ namespace AWS.Lambda.Powertools.Logging.Internal.Converters; /// /// JsonConvert to handle the AWS SDK for .NET custom enum classes that derive from the class called ConstantClass. /// -public class ConstantClassConverter : JsonConverter +internal class ConstantClassConverter : JsonConverter { private static readonly HashSet ConstantClassNames = new() { diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/DateOnlyConverter.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/DateOnlyConverter.cs index a6f969e5..ecfd62a4 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/DateOnlyConverter.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/DateOnlyConverter.cs @@ -23,7 +23,7 @@ namespace AWS.Lambda.Powertools.Logging.Internal.Converters; /// /// DateOnly JSON converter /// -public class DateOnlyConverter : JsonConverter +internal class DateOnlyConverter : JsonConverter { private const string DateFormat = "yyyy-MM-dd"; diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingLambdaContext.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingLambdaContext.cs index 17c5a3a8..9732bad0 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingLambdaContext.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingLambdaContext.cs @@ -7,7 +7,7 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// /// Lambda Context /// -public class LoggingLambdaContext +internal class LoggingLambdaContext { /// /// The AWS request ID associated with the request. diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs index 76a88196..8b19e20b 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -196,7 +196,6 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times AddLambdaContextKeys(logEntry); } - if(! string.IsNullOrWhiteSpace(_powertoolsConfigurations.XRayTraceId)) logEntry.TryAdd(LoggingConstants.KeyXRayTraceId, _powertoolsConfigurations.XRayTraceId.Split(';', StringSplitOptions.RemoveEmptyEntries)[0].Replace("Root=", "")); @@ -208,10 +207,13 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times // Add Custom Keys foreach (var (key, value) in this.GetAllKeys()) { - logEntry.TryAdd(key, value); + // Skip keys that are already defined in LoggingConstants + if (!IsLogConstantKey(key)) + { + logEntry.TryAdd(key, value); + } } - // Add Extra Fields if (CurrentScope?.ExtraKeys is not null) { @@ -219,7 +221,10 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times { if (!string.IsNullOrWhiteSpace(key)) { - logEntry.TryAdd(key, value); + if (!IsLogConstantKey(key)) + { + logEntry.TryAdd(key, value); + } } } } @@ -231,7 +236,10 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times { if (!string.IsNullOrWhiteSpace(key) && key != "json") { - logEntry.TryAdd(key, value); + if (!IsLogConstantKey(key)) + { + logEntry.TryAdd(key, value); + } } } } @@ -244,6 +252,30 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times return logEntry; } + + /// + /// Checks if a key is defined in LoggingConstants + /// + /// The key to check + /// true if the key is a LoggingConstants key + private bool IsLogConstantKey(string key) + { + return string.Equals(key.ToPascal(), LoggingConstants.KeyColdStart, StringComparison.OrdinalIgnoreCase) + // || string.Equals(key.ToPascal(), LoggingConstants.KeyCorrelationId, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyException, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyFunctionArn, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyFunctionMemorySize, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyFunctionName, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyFunctionRequestId, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyFunctionVersion, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyLoggerName, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyLogLevel, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyMessage, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeySamplingRate, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyService, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyTimestamp, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyXRayTraceId, StringComparison.OrdinalIgnoreCase); + } /// /// Gets a formatted log entry. For custom log formatter diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs index bbb1e3dd..d695b776 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -43,7 +43,7 @@ internal class PowertoolsLoggingSerializer private static JsonSerializerContext _staticAdditionalContexts; private IJsonTypeInfoResolver _customTypeInfoResolver; #endif - + /// /// Gets the JsonSerializerOptions instance. /// @@ -112,13 +112,14 @@ internal string Serialize(object value, Type inputType) throw new JsonSerializerException( $"Type {inputType} is not known to the serializer. Ensure it's included in the JsonSerializerContext."); } + return JsonSerializer.Serialize(value, typeInfo); #endif } #if NET8_0_OR_GREATER - + /// /// Adds a JsonSerializerContext to the serializer options. /// @@ -141,7 +142,7 @@ internal void AddSerializerContext(JsonSerializerContext context) } } } - + internal static void AddStaticSerializerContext(JsonSerializerContext context) { ArgumentNullException.ThrowIfNull(context); @@ -161,7 +162,7 @@ private IJsonTypeInfoResolver GetCompositeResolver() { resolvers.Add(_customTypeInfoResolver); } - + // add any static resolvers if (_staticAdditionalContexts != null) { @@ -220,24 +221,53 @@ private void BuildJsonSerializerOptions(JsonSerializerOptions options = null) { lock (_lock) { - // This should already be in a lock when called - _jsonOptions = options ?? new JsonSerializerOptions(); + // Create a completely new options instance regardless + _jsonOptions = new JsonSerializerOptions(); - SetOutputCase(); + // Copy any properties from the original options if provided + if (options != null) + { + // Copy standard properties + _jsonOptions.DefaultIgnoreCondition = options.DefaultIgnoreCondition; + _jsonOptions.PropertyNameCaseInsensitive = options.PropertyNameCaseInsensitive; + _jsonOptions.PropertyNamingPolicy = options.PropertyNamingPolicy; + _jsonOptions.DictionaryKeyPolicy = options.DictionaryKeyPolicy; + _jsonOptions.WriteIndented = options.WriteIndented; + _jsonOptions.ReferenceHandler = options.ReferenceHandler; + _jsonOptions.MaxDepth = options.MaxDepth; + _jsonOptions.IgnoreReadOnlyFields = options.IgnoreReadOnlyFields; + _jsonOptions.IgnoreReadOnlyProperties = options.IgnoreReadOnlyProperties; + _jsonOptions.IncludeFields = options.IncludeFields; + _jsonOptions.NumberHandling = options.NumberHandling; + _jsonOptions.ReadCommentHandling = options.ReadCommentHandling; + _jsonOptions.UnknownTypeHandling = options.UnknownTypeHandling; + _jsonOptions.AllowTrailingCommas = options.AllowTrailingCommas; - AddConverters(); +#if NET8_0_OR_GREATER + // Handle type resolver extraction without setting it yet + if (options.TypeInfoResolver != null) + { + _customTypeInfoResolver = options.TypeInfoResolver; + + // If it's a JsonSerializerContext, also add it to our contexts + if (_customTypeInfoResolver is JsonSerializerContext jsonContext) + { + AddSerializerContext(jsonContext); + } + } +#endif + } + // Set output case and other properties + SetOutputCase(); + AddConverters(); _jsonOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; _jsonOptions.PropertyNameCaseInsensitive = true; #if NET8_0_OR_GREATER - - // Only add TypeInfoResolver if AOT mode + // Set TypeInfoResolver last, as this makes options read-only if (!RuntimeFeatureWrapper.IsDynamicCodeSupported) { - HandleJsonOptionsTypeResolver(_jsonOptions); - - // Ensure the TypeInfoResolver is set _jsonOptions.TypeInfoResolver = GetCompositeResolver(); } #endif diff --git a/libraries/src/Directory.Build.props b/libraries/src/Directory.Build.props index ab02fae1..406e28a1 100644 --- a/libraries/src/Directory.Build.props +++ b/libraries/src/Directory.Build.props @@ -34,15 +34,20 @@ - - - - - - Common\%(RecursiveDir)%(Filename)%(Extension) - - - + + + + + + Common\Common\ + + + + \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormattingTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormattingTests.cs index 44f2656c..9f7a0ff9 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormattingTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormattingTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using AWS.Lambda.Powertools.Common.Core; using AWS.Lambda.Powertools.Common.Tests; using AWS.Lambda.Powertools.Logging.Tests.Handlers; using Microsoft.Extensions.Logging; @@ -476,6 +477,113 @@ public void TestDifferentLogLevels() Assert.Contains("\"level\":\"Error\"", logOutput); Assert.Contains("\"level\":\"Critical\"", logOutput); } + + [Fact] + public void Should_Log_Multiple_Formats_No_Duplicates() + { + var output = new TestLoggerOutput(); + LambdaLifecycleTracker.Reset(); + LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "log-level-test-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.SnakeCase; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + var user = new User + { + FirstName = "John", + LastName = "Doe", + Age = 42, + TimeStamp = "FakeTime" + }; + + Logger.LogInformation(user, "{Name} and is {Age} years old", new object[]{user.Name, user.Age}); + Assert.Contains("\"first_name\":\"John\"", output.ToString()); + Assert.Contains("\"last_name\":\"Doe\"", output.ToString()); + Assert.Contains("\"age\":42", output.ToString()); + Assert.Contains("\"name\":\"AWS.Lambda.Powertools.Logging.Logger\"", output.ToString()); // does not override name + + output.Clear(); + + Logger.LogInformation("{level}", user); + Assert.Contains("\"level\":\"Information\"", output.ToString()); // does not override level + Assert.Contains("\"message\":\"Doe, John (42)\"", output.ToString()); // does not override message + Assert.DoesNotContain("\"timestamp\":\"FakeTime\"", output.ToString()); + + output.Clear(); + + Logger.LogInformation("{coldstart}", user); // still not sure if convert to PascalCase to compare or not + Assert.Contains("\"cold_start\":true", output.ToString()); + + output.Clear(); + + Logger.AppendKey("level", "Override"); + Logger.AppendKey("message", "Override"); + Logger.AppendKey("timestamp", "Override"); + Logger.AppendKey("name", "Override"); + Logger.AppendKey("service", "Override"); + Logger.AppendKey("cold_start", "Override"); + Logger.AppendKey("message2", "Its ok!"); + + Logger.LogInformation("no override"); + Assert.DoesNotContain("\"level\":\"Override\"", output.ToString()); + Assert.DoesNotContain("\"message\":\"Override\"", output.ToString()); + Assert.DoesNotContain("\"timestamp\":\"Override\"", output.ToString()); + Assert.DoesNotContain("\"name\":\"Override\"", output.ToString()); + Assert.DoesNotContain("\"service\":\"Override\"", output.ToString()); + Assert.DoesNotContain("\"cold_start\":\"Override\"", output.ToString()); + Assert.Contains("\"message2\":\"Its ok!\"", output.ToString()); + Assert.Contains("\"level\":\"Information\"", output.ToString()); + } + + [Fact] + public void Should_Log_Multiple_Formats() + { + var output = new TestLoggerOutput(); + var logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "log-level-test-service"; + config.MinimumLogLevel = LogLevel.Debug; + config.LoggerOutputCase = LoggerOutputCase.SnakeCase; + config.LogOutput = output; + }); + }).CreatePowertoolsLogger(); + + + + var user = new User + { + FirstName = "John", + LastName = "Doe", + Age = 42 + }; + Logger.LogInformation(user, "{Name} and is {Age} years old", new object[]{user.FirstName, user.Age}); + //{"level":"Information","message":"John and is 42 years old","timestamp":"2025-04-04T21:53:35.2085220Z","service":"log-level-test-service","cold_start":true,"name":"AWS.Lambda.Powertools.Logging.Logger","first_name":"John","last_name":"Doe","age":42} + + Logger.LogInformation("{user}", user); + //{"level":"Information","message":"Doe, John (42)","timestamp":"2025-04-04T21:53:35.2419180Z","service":"log-level-test-service","cold_start":true,"name":"AWS.Lambda.Powertools.Logging.Logger","user":"Doe, John (42)"} + + Logger.LogInformation("{@user}", user); + //{"level":"Information","message":"Doe, John (42)","timestamp":"2025-04-04T21:53:35.2422190Z","service":"log-level-test-service","cold_start":true,"name":"AWS.Lambda.Powertools.Logging.Logger","user":{"first_name":"John","last_name":"Doe","age":42}} + + Logger.LogInformation("{cold_start}", user); + //{"level":"Information","message":"Doe, John (42)","timestamp":"2025-04-04T21:53:35.2440630Z","service":"log-level-test-service","cold_start":true,"name":"AWS.Lambda.Powertools.Logging.Logger","level":"Doe, John (42)"} + + Logger.AppendKey("level", "Doe, John (42)"); + Logger.LogInformation("no override"); + //{"level":"Information","message":"Doe, John (42)","timestamp":"2025-04-04T21:55:58.1410950Z","service":"log-level-test-service","cold_start":true,"name":"AWS.Lambda.Powertools.Logging.Logger","level":"Doe, John (42)"} + + var logOutput = output.ToString(); + _output.WriteLine(logOutput); + + } public class ParentClass { @@ -515,6 +623,8 @@ public class User public string FirstName { get; set; } public string LastName { get; set; } public int Age { get; set; } + public string Name => $"{FirstName} {LastName}"; + public string TimeStamp { get; set; } public override string ToString() { From 91195a4070b3481127d3b47c91843640716580bb Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 7 Apr 2025 12:23:11 +0100 Subject: [PATCH 38/49] Update all projects for new release target. Bump logging package to 2.0.0-preview.1. Update documentation for v2. Add integration tests. --- docs/core/logging-v2.md | 1529 +++++++++++++++++ docs/core/logging.md | 2 +- libraries/AWS.Lambda.Powertools.sln | 15 + ...S.Lambda.Powertools.BatchProcessing.csproj | 2 +- .../Core/PowertoolsConfigurations.cs | 2 +- .../Core/PowertoolsEnvironment.cs | 2 +- .../AWS.Lambda.Powertools.Idempotency.csproj | 2 +- .../AWS.Lambda.Powertools.Metrics.csproj | 2 +- .../AWS.Lambda.Powertools.Parameters.csproj | 2 +- .../AWS.Lambda.Powertools.Tracing.csproj | 2 +- .../e2e/InfraShared/FunctionConstruct.cs | 1 + .../AOT-Function-ILogger.csproj | 33 + .../src/AOT-Function-ILogger/Function.cs | 74 + .../aws-lambda-tools-defaults.json | 16 + .../logging/Function/src/Function/Function.cs | 187 +- .../test/Function.Tests/FunctionTests.cs | 105 +- libraries/tests/e2e/infra-aot/CoreAotStack.cs | 15 +- mkdocs.yml | 4 +- version.json | 2 +- 19 files changed, 1968 insertions(+), 29 deletions(-) create mode 100644 docs/core/logging-v2.md create mode 100644 libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/AOT-Function-ILogger.csproj create mode 100644 libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/Function.cs create mode 100644 libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/aws-lambda-tools-defaults.json diff --git a/docs/core/logging-v2.md b/docs/core/logging-v2.md new file mode 100644 index 00000000..aa7dc516 --- /dev/null +++ b/docs/core/logging-v2.md @@ -0,0 +1,1529 @@ +--- +title: Logging V2 +description: Core utility +--- + +The logging utility provides a Lambda optimized logger with output structured as JSON. + +## Key features + +* Capture key fields from Lambda context, cold start and structures logging output as JSON +* Log Lambda event when instructed (disabled by default) +* Log sampling enables DEBUG log level for a percentage of requests (disabled by default) +* Append additional keys to structured log at any point in time +* Ahead-of-Time compilation to native code + support [AOT](https://docs.aws.amazon.com/lambda/latest/dg/dotnet-native-aot.html) +* Custom log formatter to override default log structure +* Support + for [AWS Lambda Advanced Logging Controls (ALC)](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs-advanced.html) + {target="_blank"} +* Support for Microsoft.Extensions.Logging + and [ILogger](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.ilogger?view=dotnet-plat-ext-7.0) + interface +* Support + for [ILoggerFactory](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.iloggerfactory?view=dotnet-plat-ext-7.0) + interface +* Support for message templates `{}` and `{@}` for structured logging + +## Installation + +Powertools for AWS Lambda (.NET) are available as NuGet packages. You can install the packages +from [NuGet Gallery](https://www.nuget.org/packages?q=AWS+Lambda+Powertools*){target="_blank"} or from Visual Studio +editor by searching `AWS.Lambda.Powertools*` to see various utilities available. + +* [AWS.Lambda.Powertools.Logging](https://www.nuget.org/packages?q=AWS.Lambda.Powertools.Logging): + + `dotnet add package AWS.Lambda.Powertools.Logging` + +## Getting started + +!!! info + + AOT Support + If loooking for AOT specific configurations navigate to the [AOT section](#aot-support) + +Logging requires two settings: + + Setting | Description | Environment variable | Attribute parameter +-------------------|---------------------------------------------------------------------|---------------------------|--------------------- + **Service** | Sets **Service** key that will be present across all log statements | `POWERTOOLS_SERVICE_NAME` | `Service` + **Logging level** | Sets how verbose Logger should be (Information, by default) | `POWERTOOLS_LOG_LEVEL` | `LogLevel` + +### Full list of environment variables + +| Environment variable | Description | Default | +|-----------------------------------|----------------------------------------------------------------------------------------|-----------------------| +| **POWERTOOLS_SERVICE_NAME** | Sets service name used for tracing namespace, metrics dimension and structured logging | `"service_undefined"` | +| **POWERTOOLS_LOG_LEVEL** | Sets logging level | `Information` | +| **POWERTOOLS_LOGGER_CASE** | Override the default casing for log keys | `SnakeCase` | +| **POWERTOOLS_LOGGER_LOG_EVENT** | Logs incoming event | `false` | +| **POWERTOOLS_LOGGER_SAMPLE_RATE** | Debug log sampling | `0` | + +### Setting up the logger + +You can set up the logger in different ways. The most common way is to use the `Logging` attribute on your Lambda. +You can also use the `ILogger` interface to log messages. This interface is part of the Microsoft.Extensions.Logging. + +=== "Using decorator" + + ```c# hl_lines="6 10" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + [Logging(Service = "payment", LogLevel = LogLevel.Debug)] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + Logger.LogInformation("Collecting payment"); + ... + } + } + ``` + +=== "Logger Factory" + + ```c# hl_lines="6 10-17 23" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + private readonly ILogger _logger; + + public Function(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "TestService"; + config.LoggerOutputCase = LoggerOutputCase.PascalCase; + }); + }).CreatePowertoolsLogger(); + } + + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + _logger.LogInformation("Collecting payment"); + ... + } + } + ``` + +=== "Powertools Logger Builder" + + ```c# hl_lines="6 10-13 19" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + private readonly ILogger _logger; + + public Function(ILogger logger) + { + _logger = logger ?? new PowertoolsLoggerBuilder() + .WithService("TestService") + .WithOutputCase(LoggerOutputCase.PascalCase) + .Build(); + } + + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + _logger.LogInformation("Collecting payment"); + ... + } + } + ``` + +### Customizing the logger + +You can customize the logger by setting the following properties in the `Logger.Configure` method: + +| Property | Description | +|:----------------------|--------------------------------------------------------------------------------------------------| +| `Service` | The name of the service. This is used to identify the service in the logs. | +| `MinimumLogLevel` | The minimum log level to log. This is used to filter out logs below the specified level. | +| `LogFormatter` | The log formatter to use. This is used to customize the structure of the log entries. | +| `JsonOptions` | The JSON options to use. This is used to customize the serialization of logs.| +| `LogBuffering` | The log buffering options. This is used to configure log buffering. | +| `TimestampFormat` | The format of the timestamp. This is used to customize the format of the timestamp in the logs.| +| `SamplingRate` | Sets a percentage (0.0 to 1.0) of logs that will be dynamically elevated to DEBUG level | +| `LoggerOutputCase` | The output casing of the logger. This is used to customize the casing of the log entries. | +| `LogOutput` | Specifies the console output wrapper used for writing logs. This property allows redirecting log output for testing or specialized handling scenarios. | + + +### Configuration + +You can configure Powertools Logger using the static `Logger` class. This class is a singleton and is created when the +Lambda function is initialized. You can configure the logger using the `Logger.Configure` method. + +=== "Configure static Logger" + +```c# hl_lines="5-9" + public class Function + { + public Function() + { + Logger.Configure(options => + { + options.MinimumLogLevel = LogLevel.Information; + options.LoggerOutputCase = LoggerOutputCase.CamelCase; + }); + } + + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + Logger.LogInformation("Collecting payment"); + ... + } + } +``` + +### ILogger +You can also use the `ILogger` interface to log messages. This interface is part of the Microsoft.Extensions.Logging. +With this approach you get more flexibility and testability using dependency injection (DI). + +=== "Configure with LoggerFactory or Builder" + + ```c# hl_lines="5-12" + public class Function + { + public Function(ILogger logger) + { + _logger = logger ?? LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "TestService"; + config.LoggerOutputCase = LoggerOutputCase.PascalCase; + }); + }).CreatePowertoolsLogger(); + } + + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + Logger.LogInformation("Collecting payment"); + ... + } + } + ``` + +## Standard structured keys + +Your logs will always include the following keys to your structured logging: + + Key | Type | Example | Description +------------------------|--------|------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------ + **Level** | string | "Information" | Logging level + **Message** | string | "Collecting payment" | Log statement value. Unserializable JSON values will be cast to string + **Timestamp** | string | "2020-05-24 18:17:33,774" | Timestamp of actual log statement + **Service** | string | "payment" | Service name defined. "service_undefined" will be used if unknown + **ColdStart** | bool | true | ColdStart value. + **FunctionName** | string | "example-powertools-HelloWorldFunction-1P1Z6B39FLU73" + **FunctionMemorySize** | string | "128" + **FunctionArn** | string | "arn:aws:lambda:eu-west-1:012345678910:function:example-powertools-HelloWorldFunction-1P1Z6B39FLU73" + **FunctionRequestId** | string | "899856cb-83d1-40d7-8611-9e78f15f32f4" | AWS Request ID from lambda context + **FunctionVersion** | string | "12" + **XRayTraceId** | string | "1-5759e988-bd862e3fe1be46a994272793" | X-Ray Trace ID when Lambda function has enabled Tracing + **Name** | string | "Powertools for AWS Lambda (.NET) Logger" | Logger name + **SamplingRate** | int | 0.1 | Debug logging sampling rate in percentage e.g. 10% in this case + **Customer Keys** | | | + +## Message templates + +You can use message templates to extract properties from your objects and log them as structured data. + +!!! info + + Override the `ToString()` method of your object to return a meaningful string representation of the object. + + This is especially important when using `{}` to log the object as a string. + + ```csharp + public class User + { + public string FirstName { get; set; } + public string LastName { get; set; } + public int Age { get; set; } + + public override string ToString() + { + return $"{LastName}, {FirstName} ({Age})"; + } + } + ``` + +If you want to log the object as a JSON object, use `{@}`. This will serialize the object and log it as a JSON object. + +=== "Message template {@}" + + ```c# hl_lines="7-14" + public class Function + { + [Logging(Service = "user-service", LogLevel = LogLevel.Information)] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + var user = new User + { + FirstName = "John", + LastName = "Doe", + Age = 42 + }; + + logger.LogInformation("User object: {@user}", user); + ... + } + } + ``` + +=== "{@} Output" + + ```json hl_lines="3 8-12" + { + "level": "Information", + "message": "User object: Doe, John (42)", + "timestamp": "2025-04-07 09:06:30.708", + "service": "user-service", + "coldStart": true, + "name": "AWS.Lambda.Powertools.Logging.Logger", + "user": { + "firstName": "John", + "lastName": "Doe", + "age": 42 + }, + ... + } + ``` + +If you want to log the object as a string, use `{}`. This will call the `ToString()` method of the object and log it as +a string. + +=== "Message template {} ToString" + + ```c# hl_lines="7-12 14 18 19" + public class Function + { + [Logging(Service = "user", LogLevel = LogLevel.Information)] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + var user = new User + { + FirstName = "John", + LastName = "Doe", + Age = 42 + }; + + logger.LogInformation("User data: {user}", user); + + // Also works with numbers, dates, etc. + + logger.LogInformation("Price: {price:0.00}", 123.4567); // will respect decimal places + logger.LogInformation("Percentage: {percent:0.0%}", 0.1234); + ... + } + } + ``` + +=== "Output {} ToString" + + ```json hl_lines="3 8 12 17 21 26" + { + "level": "Information", + "message": "User data: Doe, John (42)", + "timestamp": "2025-04-07 09:06:30.689", + "service": "user-servoice", + "coldStart": true, + "name": "AWS.Lambda.Powertools.Logging.Logger", + "user": "Doe, John (42)" + } + { + "level": "Information", + "message": "Price: 123.46", + "timestamp": "2025-04-07 09:23:01.235", + "service": "user-servoice", + "cold_start": true, + "name": "AWS.Lambda.Powertools.Logging.Logger", + "price": 123.46 + } + { + "level": "Information", + "message": "Percentage: 12.3%", + "timestamp": "2025-04-07 09:23:01.260", + "service": "user-servoice", + "cold_start": true, + "name": "AWS.Lambda.Powertools.Logging.Logger", + "percent": "12.3%" + } + ``` + + +## Logging incoming event + +When debugging in non-production environments, you can instruct Logger to log the incoming event with `LogEvent` +parameter or via `POWERTOOLS_LOGGER_LOG_EVENT` environment variable. + +!!! warning +Log event is disabled by default to prevent sensitive info being logged. + +=== "Function.cs" + + ```c# hl_lines="6" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + [Logging(LogEvent = true)] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + ... + } + } + ``` + +## Setting a Correlation ID + +You can set a Correlation ID using `CorrelationIdPath` parameter by passing +a [JSON Pointer expression](https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-json-pointer-03){target="_blank"}. + +!!! Attention +The JSON Pointer expression is `case sensitive`. In the bellow example `/headers/my_request_id_header` would work but +`/Headers/my_request_id_header` would not find the element. + +=== "Function.cs" + + ```c# hl_lines="6" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + [Logging(CorrelationIdPath = "/headers/my_request_id_header")] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + ... + } + } + ``` + +=== "Example Event" + + ```json hl_lines="3" + { + "headers": { + "my_request_id_header": "correlation_id_value" + } + } + ``` + +=== "Example CloudWatch Logs excerpt" + + ```json hl_lines="15" + { + "level": "Information", + "message": "Collecting payment", + "timestamp": "2021-12-13T20:32:22.5774262Z", + "service": "lambda-example", + "cold_start": true, + "function_name": "test", + "function_memory_size": 128, + "function_arn": "arn:aws:lambda:eu-west-1:12345678910:function:test", + "function_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72", + "function_version": "$LATEST", + "xray_trace_id": "1-61b7add4-66532bb81441e1b060389429", + "name": "AWS.Lambda.Powertools.Logging.Logger", + "sampling_rate": 0.7, + "correlation_id": "correlation_id_value", + } + ``` + +We provide [built-in JSON Pointer expression](https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-json-pointer-03) +{target="_blank"} +for known event sources, where either a request ID or X-Ray Trace ID are present. + +=== "Function.cs" + + ```c# hl_lines="6" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + [Logging(CorrelationIdPath = CorrelationIdPaths.ApiGatewayRest)] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + ... + } + } + ``` + +=== "Example Event" + + ```json hl_lines="3" + { + "RequestContext": { + "RequestId": "correlation_id_value" + } + } + ``` + +=== "Example CloudWatch Logs excerpt" + + ```json hl_lines="15" + { + "level": "Information", + "message": "Collecting payment", + "timestamp": "2021-12-13T20:32:22.5774262Z", + "service": "lambda-example", + "cold_start": true, + "function_name": "test", + "function_memory_size": 128, + "function_arn": "arn:aws:lambda:eu-west-1:12345678910:function:test", + "function_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72", + "function_version": "$LATEST", + "xray_trace_id": "1-61b7add4-66532bb81441e1b060389429", + "name": "AWS.Lambda.Powertools.Logging.Logger", + "sampling_rate": 0.7, + "correlation_id": "correlation_id_value", + } + ``` + +## Appending additional keys + +!!! info "Custom keys are persisted across warm invocations" +Always set additional keys as part of your handler to ensure they have the latest value, or explicitly clear them with [ +`ClearState=true`](#clearing-all-state). + +You can append your own keys to your existing logs via `AppendKey`. Typically this value would be passed into the +function via the event. Appended keys are added to all subsequent log entries in the current execution from the point +the logger method is called. To ensure the key is added to all log entries, call this method as early as possible in the +Lambda handler. + +=== "Function.cs" + + ```c# hl_lines="21" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + [Logging(LogEvent = true)] + public async Task FunctionHandler(APIGatewayProxyRequest apigwProxyEvent, + ILambdaContext context) + { + var requestContextRequestId = apigwProxyEvent.RequestContext.RequestId; + + var lookupInfo = new Dictionary() + { + {"LookupInfo", new Dictionary{{ "LookupId", requestContextRequestId }}} + }; + + // Appended keys are added to all subsequent log entries in the current execution. + // Call this method as early as possible in the Lambda handler. + // Typically this is value would be passed into the function via the event. + // Set the ClearState = true to force the removal of keys across invocations, + Logger.AppendKeys(lookupInfo); + + Logger.LogInformation("Getting ip address from external service"); + + } + ``` + +=== "Example CloudWatch Logs excerpt" + + ```json hl_lines="4 5 6" + { + "level": "Information", + "message": "Getting ip address from external service" + "timestamp": "2022-03-14T07:25:20.9418065Z", + "service": "powertools-dotnet-logging-sample", + "cold_start": false, + "function_name": "PowertoolsLoggingSample-HelloWorldFunction-hm1r10VT3lCy", + "function_memory_size": 256, + "function_arn": "arn:aws:lambda:function:PowertoolsLoggingSample-HelloWorldFunction-hm1r10VT3lCy", + "function_request_id": "96570b2c-f00e-471c-94ad-b25e95ba7347", + "function_version": "$LATEST", + "xray_trace_id": "1-622eede0-647960c56a91f3b071a9fff1", + "name": "AWS.Lambda.Powertools.Logging.Logger", + "lookup_info": { + "lookup_id": "4c50eace-8b1e-43d3-92ba-0efacf5d1625" + }, + } + ``` + +### Removing additional keys + +You can remove any additional key from entry using `Logger.RemoveKeys()`. + +=== "Function.cs" + + ```c# hl_lines="21 22" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + [Logging(LogEvent = true)] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + ... + Logger.AppendKey("test", "willBeLogged"); + ... + var customKeys = new Dictionary + { + {"test1", "value1"}, + {"test2", "value2"} + }; + + Logger.AppendKeys(customKeys); + ... + Logger.RemoveKeys("test"); + Logger.RemoveKeys("test1", "test2"); + ... + } + } + ``` + +## Extra Keys + +Extra keys allow you to append additional keys to a log entry. Unlike `AppendKey`, extra keys will only apply to the +current log entry. + +Extra keys argument is available for all log levels' methods, as implemented in the standard logging library - e.g. +Logger.Information, Logger.Warning. + +It accepts any dictionary, and all keyword arguments will be added as part of the root structure of the logs for that +log statement. + +!!! info +Any keyword argument added using extra keys will not be persisted for subsequent messages. + +=== "Function.cs" + + ```c# hl_lines="16" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + [Logging(LogEvent = true)] + public async Task FunctionHandler(APIGatewayProxyRequest apigwProxyEvent, + ILambdaContext context) + { + var requestContextRequestId = apigwProxyEvent.RequestContext.RequestId; + + var lookupId = new Dictionary() + { + { "LookupId", requestContextRequestId } + }; + + // Appended keys are added to all subsequent log entries in the current execution. + // Call this method as early as possible in the Lambda handler. + // Typically this is value would be passed into the function via the event. + // Set the ClearState = true to force the removal of keys across invocations, + Logger.AppendKeys(lookupId); + } + ``` + +### Clearing all state + +Logger is commonly initialized in the global scope. Due +to [Lambda Execution Context reuse](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-context.html), this means that +custom keys can be persisted across invocations. If you want all custom keys to be deleted, you can use +`ClearState=true` attribute on `[Logging]` attribute. + +=== "Function.cs" + + ```cs hl_lines="6 13" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + [Logging(ClearState = true)] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + ... + if (apigProxyEvent.Headers.ContainsKey("SomeSpecialHeader")) + { + Logger.AppendKey("SpecialKey", "value"); + } + + Logger.LogInformation("Collecting payment"); + ... + } + } + ``` + +=== "#1 Request" + + ```json hl_lines="11" + { + "level": "Information", + "message": "Collecting payment", + "timestamp": "2021-12-13T20:32:22.5774262Z", + "service": "payment", + "cold_start": true, + "function_name": "test", + "function_memory_size": 128, + "function_arn": "arn:aws:lambda:eu-west-1:12345678910:function:test", + "function_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72", + "special_key": "value" + } + ``` + +=== "#2 Request" + + ```json + { + "level": "Information", + "message": "Collecting payment", + "timestamp": "2021-12-13T20:32:22.5774262Z", + "service": "payment", + "cold_start": true, + "function_name": "test", + "function_memory_size": 128, + "function_arn": "arn:aws:lambda:eu-west-1:12345678910:function:test", + "function_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72" + } + ``` + +## Sampling debug logs + +You can dynamically set a percentage of your logs to **DEBUG** level via env var `POWERTOOLS_LOGGER_SAMPLE_RATE` or +via `SamplingRate` parameter on attribute. + +!!! info +Configuration on environment variable is given precedence over sampling rate configuration on attribute, provided it's +in valid value range. + +=== "Sampling via attribute parameter" + + ```c# hl_lines="6" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + [Logging(SamplingRate = 0.5)] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + ... + } + } + ``` + +=== "Sampling via environment variable" + + ```yaml hl_lines="8" + + Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function + Properties: + ... + Environment: + Variables: + POWERTOOLS_LOGGER_SAMPLE_RATE: 0.5 + ``` + +## Configure Log Output Casing + +By definition Powertools for AWS Lambda (.NET) outputs logging keys using **snake case** (e.g. *"function_memory_size": +128*). This allows developers using different Powertools for AWS Lambda (.NET) runtimes, to search logs across services +written in languages such as Python or TypeScript. + +If you want to override the default behavior you can either set the desired casing through attributes, as described in +the example below, or by setting the `POWERTOOLS_LOGGER_CASE` environment variable on your AWS Lambda function. Allowed +values are: `CamelCase`, `PascalCase` and `SnakeCase`. + +=== "Output casing via attribute parameter" + + ```c# hl_lines="6" + /** + * Handler for requests to Lambda function. + */ + public class Function + { + [Logging(LoggerOutputCase = LoggerOutputCase.CamelCase)] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + ... + } + } + ``` + +Below are some output examples for different casing. + +=== "Camel Case" + + ```json + { + "level": "Information", + "message": "Collecting payment", + "timestamp": "2021-12-13T20:32:22.5774262Z", + "service": "payment", + "coldStart": true, + "functionName": "test", + "functionMemorySize": 128, + "functionArn": "arn:aws:lambda:eu-west-1:12345678910:function:test", + "functionRequestId": "52fdfc07-2182-154f-163f-5f0f9a621d72" + } + ``` + +=== "Pascal Case" + + ```json + { + "Level": "Information", + "Message": "Collecting payment", + "Timestamp": "2021-12-13T20:32:22.5774262Z", + "Service": "payment", + "ColdStart": true, + "FunctionName": "test", + "FunctionMemorySize": 128, + "FunctionArn": "arn:aws:lambda:eu-west-1:12345678910:function:test", + "FunctionRequestId": "52fdfc07-2182-154f-163f-5f0f9a621d72" + } + ``` + +=== "Snake Case" + + ```json + { + "level": "Information", + "message": "Collecting payment", + "timestamp": "2021-12-13T20:32:22.5774262Z", + "service": "payment", + "cold_start": true, + "function_name": "test", + "function_memory_size": 128, + "function_arn": "arn:aws:lambda:eu-west-1:12345678910:function:test", + "function_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72" + } + ``` + + +## Advanced + +### Log Levels + +The default log level is `Information` and can be set using the `MinimumLogLevel` property option or by using the `POWERTOOLS_LOG_LEVEL` environment variable. + +We support the following log levels: + +| Level | Numeric value | Lambda Level | +|---------------|---------------|--------------| +| `Trace` | 0 | `trace` | +| `Debug` | 1 | `debug` | +| `Information` | 2 | `info` | +| `Warning` | 3 | `warn` | +| `Error` | 4 | `error` | +| `Critical` | 5 | `fatal` | +| `None` | 6 | | + +### Using AWS Lambda Advanced Logging Controls (ALC) + +!!! question "When is it useful?" +When you want to set a logging policy to drop informational or verbose logs for one or all AWS Lambda functions, +regardless of runtime and logger used. + +With [AWS Lambda Advanced Logging Controls (ALC)](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-advanced) +{target="_blank"}, you can enforce a minimum log level that Lambda will accept from your application code. + +When enabled, you should keep `Logger` and ALC log level in sync to avoid data loss. + +!!! warning "When using AWS Lambda Advanced Logging Controls (ALC)" +- When Powertools Logger output is set to `PascalCase` **`Level`** property name will be replaced by **`LogLevel`** as + a property name. +- ALC takes precedence over **`POWERTOOLS_LOG_LEVEL`** and when setting it in code using **`[Logging(LogLevel = )]`** + +Here's a sequence diagram to demonstrate how ALC will drop both `Information` and `Debug` logs emitted from `Logger`, +when ALC log level is stricter than `Logger`. + +```mermaid +sequenceDiagram + title Lambda ALC allows WARN logs only + participant Lambda service + participant Lambda function + participant Application Logger + + Note over Lambda service: AWS_LAMBDA_LOG_LEVEL="WARN" + Note over Application Logger: POWERTOOLS_LOG_LEVEL="DEBUG" + Lambda service->>Lambda function: Invoke (event) + Lambda function->>Lambda function: Calls handler + Lambda function->>Application Logger: Logger.Warning("Something happened") + Lambda function-->>Application Logger: Logger.Debug("Something happened") + Lambda function-->>Application Logger: Logger.Information("Something happened") + + Lambda service->>Lambda service: DROP INFO and DEBUG logs + + Lambda service->>CloudWatch Logs: Ingest error logs +``` + +**Priority of log level settings in Powertools for AWS Lambda** + +We prioritise log level settings in this order: + +1. AWS_LAMBDA_LOG_LEVEL environment variable +2. Setting the log level in code using `[Logging(LogLevel = )]` +3. POWERTOOLS_LOG_LEVEL environment variable + +If you set `Logger` level lower than ALC, we will emit a warning informing you that your messages will be discarded by +Lambda. + +> **NOTE** +> With ALC enabled, we are unable to increase the minimum log level below the `AWS_LAMBDA_LOG_LEVEL` environment +> variable value, +> see [AWS Lambda service documentation](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-log-level) +> {target="_blank"} for more details. + +### Using JsonSerializerOptions + +Powertools supports customizing the serialization and deserialization of Lambda JSON events and your own types using +`JsonSerializerOptions`. +You can do this by creating a custom `JsonSerializerOptions` and passing it to the `JsonOptions` of the Powertools +Logger. + +Supports `TypeInfoResolver` and `DictionaryKeyPolicy` options. These two options are the most common ones used to +customize the serialization of Powertools Logger. + +- `TypeInfoResolver`: This option allows you to specify a custom `JsonSerializerContext` that contains the types you + want to serialize and deserialize. This is especially useful when using AOT compilation, as it allows you to specify + the types that should be included in the generated assembly. +- `DictionaryKeyPolicy`: This option allows you to specify a custom naming policy for the properties in the JSON output. + This is useful when you want to change the casing of the property names or use a different naming convention. + +!!! info +If you want to preserve the original casing of the property names (keys), you can set the `DictionaryKeyPolicy` to +`null`. + +```csharp +builder.Logging.AddPowertoolsLogger(options => +{ + options.JsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // Override output casing + TypeInfoResolver = MyCustomJsonSerializerContext.Default // Your custom JsonSerializerContext + }; +}); +``` + +### 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 ``LogFormatter`` property in the `configure` options. + +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.Configure(options => + { + options.LogFormatter = 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 + } + } + ``` + +### Buffering logs + +Log buffering enables you to buffer logs for a specific request or invocation. Enable log buffering by passing `LogBufferingOptions` when configuring a Logger instance. You can buffer logs at the `Warning`, `Information`, `Debug` or `Trace` level, and flush them automatically on error or manually as needed. + +!!! tip "This is useful when you want to reduce the number of log messages emitted while still having detailed logs when needed, such as when troubleshooting issues." + +=== "LogBufferingOptions" + + ```csharp hl_lines="5-14" + public class Function + { + public Function() + { + Logger.Configure(logger => + { + logger.Service = "MyServiceName"; + logger.LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Debug, + MaxBytes = 123455, // Default is 20KB (20480 bytes) + FlushOnErrorLog = true // default true + }; + }); + + Logger.LogDebug('This is a debug message'); // This is NOT buffered + } + + [Logging] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + Logger.LogDebug('This is a debug message'); // This is buffered + Logger.LogInformation('This is an info message'); + + // your business logic here + + Logger.LogError('This is an error message'); // This also flushes the buffer + } + } + + ``` + +#### Configuring the buffer + +When configuring the buffer, you can set the following options to fine-tune how logs are captured, stored, and emitted. You can configure the following options in the `logBufferOptions` constructor parameter: + +| Parameter | Description | Configuration | Default | +|---------------------|------------------------------------------------- |--------------------------------------------|---------| +| `MaxBytes` | Maximum size of the log buffer in bytes | `number` | `20480` | +| `BufferAtLogLevel` | Minimum log level to buffer | `Trace`, `Debug`, `Information`, `Warning` | `Debug` | +| `FlushOnErrorLog` | Automatically flush buffer when logging an error | `True`, `False` | `True` | + +=== "BufferAtLogLevel" + + ```csharp hl_lines="10" + public class Function + { + public Function() + { + Logger.Configure(logger => + { + logger.Service = "MyServiceName"; + logger.LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Warning + }; + }); + } + + [Logging] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + // All logs below are buffered + Logger.LogDebug('This is a debug message'); + Logger.LogInformation('This is an info message'); + Logger.LogWarning('This is a warn message'); + + Logger.ClearBuffer(); // This will clear the buffer without emitting the logs + } + } + ``` + + 1. Setting `BufferAtLogLevel: 'Warning'` configures log buffering for `Warning` and all lower severity levels like `Information`, `Debug`, and `Trace`. + 2. Calling `Logger.ClearBuffer()` will clear the buffer without emitting the logs. + +=== "FlushOnErrorLog" + + ```csharp hl_lines="10" + public class Function + { + public Function() + { + Logger.Configure(logger => + { + logger.Service = "MyServiceName"; + logger.LogBuffering = new LogBufferingOptions + { + FlushOnErrorLog = false + }; + }); + } + + [Logging] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + Logger.LogDebug('This is a debug message'); // this is buffered + + try + { + throw new Exception(); + } + catch (Exception e) + { + Logger.LogError(e.Message); // this does NOT flush the buffer + } + + Logger.LogDebug("Debug!!"); // this is buffered + + try + { + throw new Exception(); + } + catch (Exception e) + { + Logger.LogError(e.Message); // this does NOT flush the buffer + Logger.FlushBuffer(); // Manually flush + } + } + } + ``` + + 1. Disabling `FlushOnErrorLog` will not flush the buffer when logging an error. This is useful when you want to control when the buffer is flushed by calling the `Logger.FlushBuffer()` method. + +#### Flushing on errors + +When using the `Logger` decorator, you can configure the logger to automatically flush the buffer when an error occurs. This is done by setting the `FlushBufferOnUncaughtError` option to `true` in the decorator. + +=== "FlushBufferOnUncaughtError" + + ```csharp hl_lines="15" + public class Function + { + public Function() + { + Logger.Configure(logger => + { + logger.Service = "MyServiceName"; + logger.LogBuffering = new LogBufferingOptions + { + BufferAtLogLevel = LogLevel.Debug + }; + }); + } + + [Logging(FlushBufferOnUncaughtError = true)] + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + Logger.LogDebug('This is a debug message'); + + throw new Exception(); // This causes the buffer to be flushed + } + } + ``` + +#### Buffering workflows + +##### Manual flush + +
+```mermaid +sequenceDiagram + participant Client + participant Lambda + participant Logger + participant CloudWatch + Client->>Lambda: Invoke Lambda + Lambda->>Logger: Initialize with DEBUG level buffering + Logger-->>Lambda: Logger buffer ready + Lambda->>Logger: logger.debug("First debug log") + Logger-->>Logger: Buffer first debug log + Lambda->>Logger: logger.info("Info log") + Logger->>CloudWatch: Directly log info message + Lambda->>Logger: logger.debug("Second debug log") + Logger-->>Logger: Buffer second debug log + Lambda->>Logger: logger.flush_buffer() + Logger->>CloudWatch: Emit buffered logs to stdout + Lambda->>Client: Return execution result +``` +Flushing buffer manually +
+ +##### Flushing when logging an error + +
+```mermaid +sequenceDiagram + participant Client + participant Lambda + participant Logger + participant CloudWatch + Client->>Lambda: Invoke Lambda + Lambda->>Logger: Initialize with DEBUG level buffering + Logger-->>Lambda: Logger buffer ready + Lambda->>Logger: logger.debug("First log") + Logger-->>Logger: Buffer first debug log + Lambda->>Logger: logger.debug("Second log") + Logger-->>Logger: Buffer second debug log + Lambda->>Logger: logger.debug("Third log") + Logger-->>Logger: Buffer third debug log + Lambda->>Lambda: Exception occurs + Lambda->>Logger: logger.error("Error details") + Logger->>CloudWatch: Emit buffered debug logs + Logger->>CloudWatch: Emit error log + Lambda->>Client: Raise exception +``` +Flushing buffer when an error happens +
+ +##### Flushing on error + +This works only when using the `Logger` decorator. You can configure the logger to automatically flush the buffer when an error occurs by setting the `FlushBufferOnUncaughtError` option to `true` in the decorator. + +
+```mermaid +sequenceDiagram + participant Client + participant Lambda + participant Logger + participant CloudWatch + Client->>Lambda: Invoke Lambda + Lambda->>Logger: Using decorator + Logger-->>Lambda: Logger context injected + Lambda->>Logger: logger.debug("First log") + Logger-->>Logger: Buffer first debug log + Lambda->>Logger: logger.debug("Second log") + Logger-->>Logger: Buffer second debug log + Lambda->>Lambda: Uncaught Exception + Lambda->>CloudWatch: Automatically emit buffered debug logs + Lambda->>Client: Raise uncaught exception +``` +Flushing buffer when an uncaught exception happens +
+ +#### Buffering FAQs + +1. **Does the buffer persist across Lambda invocations?** + No, each Lambda invocation has its own buffer. The buffer is initialized when the Lambda function is invoked and is cleared after the function execution completes or when flushed manually. + +2. **Are my logs buffered during cold starts?** + No, we never buffer logs during cold starts. This is because we want to ensure that logs emitted during this phase are always available for debugging and monitoring purposes. The buffer is only used during the execution of the Lambda function. + +3. **How can I prevent log buffering from consuming excessive memory?** + You can limit the size of the buffer by setting the `MaxBytes` option in the `LogBufferingOptions` constructor parameter. This will ensure that the buffer does not grow indefinitely and consume excessive memory. + +4. **What happens if the log buffer reaches its maximum size?** + Older logs are removed from the buffer to make room for new logs. This means that if the buffer is full, you may lose some logs if they are not flushed before the buffer reaches its maximum size. When this happens, we emit a warning when flushing the buffer to indicate that some logs have been dropped. + +5. **How is the log size of a log line calculated?** + The log size is calculated based on the size of the serialized log line in bytes. This includes the size of the log message, the size of any additional keys, and the size of the timestamp. + +6. **What timestamp is used when I flush the logs?** + The timestamp preserves the original time when the log record was created. If you create a log record at 11:00:10 and flush it at 11:00:25, the log line will retain its original timestamp of 11:00:10. + +7. **What happens if I try to add a log line that is bigger than max buffer size?** + The log will be emitted directly to standard output and not buffered. When this happens, we emit a warning to indicate that the log line was too big to be buffered. + +8. **What happens if Lambda times out without flushing the buffer?** + Logs that are still in the buffer will be lost. If you are using the log buffer to log asynchronously, you should ensure that the buffer is flushed before the Lambda function times out. You can do this by calling the `Logger.FlushBuffer()` method at the end of your Lambda function. + +### Timestamp formatting + +You can customize the timestamp format by setting the `TimestampFormat` property in the `Logger.Configure` method. The default format is `o`, which is the ISO 8601 format. +You can use any valid [DateTime format string](https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings) to customize the timestamp format. +For example, to use the `yyyy-MM-dd HH:mm:ss` format, you can do the following: + +```csharp +Logger.Configure(logger => +{ + logger.TimestampFormat = "yyyy-MM-dd HH:mm:ss"; +}); +``` +This will output the timestamp in the following format: + +```json +{ + "level": "Information", + "message": "Test Message", + "timestamp": "2021-12-13 20:32:22", + "service": "lambda-example", + ... +} +``` + +## AOT Support + +!!! info + + If you want to use the `LogEvent`, `Custom Log Formatter` features, or serialize your own types when Logging events, you need to either pass `JsonSerializerContext` or make changes in your Lambda `Main` method. + +!!! info + + Starting from version 1.6.0, it is required to update the Amazon.Lambda.Serialization.SystemTextJson NuGet package to version 2.4.3 in your csproj. + +### Using JsonSerializerOptions + +To be able to serializer your own types, you need to pass your `JsonSerializerContext` to the `TypeInfoResolver` of the `Logger.Configure` method. + +```csharp +Logger.Configure(logger => +{ + logger.JsonOptions = new JsonSerializerOptions + { + TypeInfoResolver = YourJsonSerializerContext.Default + }; +}); +``` + +### Using PowertoolsSourceGeneratorSerializer + +Replace `SourceGeneratorLambdaJsonSerializer` with `PowertoolsSourceGeneratorSerializer`. + +This change enables Powertools to construct an instance of `JsonSerializerOptions` used to customize the serialization +and deserialization of Lambda JSON events and your own types. + +=== "Before" + + ```csharp + Func> handler = FunctionHandler; + await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer()) + .Build() + .RunAsync(); + ``` + +=== "After" + + ```csharp hl_lines="2" + Func> handler = FunctionHandler; + await LambdaBootstrapBuilder.Create(handler, new PowertoolsSourceGeneratorSerializer()) + .Build() + .RunAsync(); + ``` + +For example when you have your own Demo type + +```csharp +public class Demo +{ + public string Name { get; set; } + public Headers Headers { get; set; } +} +``` + +To be able to serialize it in AOT you have to have your own `JsonSerializerContext` + +```csharp +[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest))] +[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyResponse))] +[JsonSerializable(typeof(Demo))] +public partial class MyCustomJsonSerializerContext : JsonSerializerContext +{ +} +``` + +When you update your code to use `PowertoolsSourceGeneratorSerializer`, we combine your +`JsonSerializerContext` with Powertools' `JsonSerializerContext`. This allows Powertools to serialize your types and +Lambda events. + +### Custom Log Formatter + +To use a custom log formatter with AOT, pass an instance of `ILogFormatter` to `PowertoolsSourceGeneratorSerializer` +instead of using the static `Logger.UseFormatter` in the Function constructor as you do in non-AOT Lambdas. + +=== "Function Main method" + + ```csharp hl_lines="5" + + Func> handler = FunctionHandler; + await LambdaBootstrapBuilder.Create(handler, + new PowertoolsSourceGeneratorSerializer + ( + new CustomLogFormatter() + ) + ) + .Build() + .RunAsync(); + + ``` + +=== "CustomLogFormatter.cs" + + ```csharp + 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 + }, + }; + } + } + ``` + +### Anonymous types + +!!! note + + While we support anonymous type serialization by converting to a `Dictionary`, this is **not** a best practice and is **not recommended** when using native AOT. + + We recommend using concrete classes and adding them to your `JsonSerializerContext`. + +## Testing + +You can change where the `Logger` will output its logs by setting the `LogOutput` property. +We also provide a helper class for tests `TestLoggerOutput` or you can provider your own implementation of `IConsoleWrapper`. + +```csharp +// Using TestLoggerOutput +options.LogOutput = new TestLoggerOutput(); +// Custom console output for testing +options.LogOutput = new TestConsoleWrapper(); + +// Example implementation for testing: +public class TestConsoleWrapper : IConsoleWrapper +{ + public List CapturedOutput { get; } = new(); + + public void WriteLine(string message) + { + CapturedOutput.Add(message); + } +} +``` +### ILogger + +If you are using ILogger interface you can inject the logger in a dedicated constructor for your Lambda function and thus you can mock your ILogger instance. + +```csharp +public class Function +{ + private readonly ILogger _logger; + + public Function() + { + _logger = oggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "TestService"; + config.LoggerOutputCase = LoggerOutputCase.PascalCase; + }); + }).CreatePowertoolsLogger(); + } + + // constructor used for tests - pass the mock ILogger + public Function(ILogger logger) + { + _logger = logger ?? loggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "TestService"; + config.LoggerOutputCase = LoggerOutputCase.PascalCase; + }); + }).CreatePowertoolsLogger(); + } + + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + _logger.LogInformation("Collecting payment"); + ... + } +} +``` + + diff --git a/docs/core/logging.md b/docs/core/logging.md index 7c99d17a..a8a2cfce 100644 --- a/docs/core/logging.md +++ b/docs/core/logging.md @@ -19,7 +19,7 @@ Powertools for AWS Lambda (.NET) are available as NuGet packages. You can instal * [AWS.Lambda.Powertools.Logging](https://www.nuget.org/packages?q=AWS.Lambda.Powertools.Logging): - `dotnet add package AWS.Lambda.Powertools.Logging` + `dotnet add package AWS.Lambda.Powertools.Logging --version 1.6.5` ## Getting started diff --git a/libraries/AWS.Lambda.Powertools.sln b/libraries/AWS.Lambda.Powertools.sln index c0dc580f..07122c3a 100644 --- a/libraries/AWS.Lambda.Powertools.sln +++ b/libraries/AWS.Lambda.Powertools.sln @@ -103,6 +103,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Metri EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Metrics", "Metrics", "{A566F2D7-F8FE-466A-8306-85F266B7E656}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AOT-Function-ILogger", "tests\e2e\functions\core\logging\AOT-Function-ILogger\src\AOT-Function-ILogger\AOT-Function-ILogger.csproj", "{7FC6DD65-0352-4139-8D08-B25C0A0403E3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -548,6 +550,18 @@ Global {F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB}.Release|x64.Build.0 = Release|Any CPU {F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB}.Release|x86.ActiveCfg = Release|Any CPU {F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB}.Release|x86.Build.0 = Release|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Debug|x64.ActiveCfg = Debug|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Debug|x64.Build.0 = Debug|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Debug|x86.ActiveCfg = Debug|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Debug|x86.Build.0 = Debug|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Release|Any CPU.Build.0 = Release|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Release|x64.ActiveCfg = Release|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Release|x64.Build.0 = Release|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Release|x86.ActiveCfg = Release|Any CPU + {7FC6DD65-0352-4139-8D08-B25C0A0403E3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution @@ -596,5 +610,6 @@ Global {A566F2D7-F8FE-466A-8306-85F266B7E656} = {1CFF5568-8486-475F-81F6-06105C437528} {F8F80477-1EAD-4C5C-A329-CBC0A60C7CAB} = {A566F2D7-F8FE-466A-8306-85F266B7E656} {A422C742-2CF9-409D-BDAE-15825AB62113} = {A566F2D7-F8FE-466A-8306-85F266B7E656} + {7FC6DD65-0352-4139-8D08-B25C0A0403E3} = {4EAB66F9-C9CB-4E8A-BEE6-A14CD7FDE02F} EndGlobalSection EndGlobal diff --git a/libraries/src/AWS.Lambda.Powertools.BatchProcessing/AWS.Lambda.Powertools.BatchProcessing.csproj b/libraries/src/AWS.Lambda.Powertools.BatchProcessing/AWS.Lambda.Powertools.BatchProcessing.csproj index 54af1670..366ebe42 100644 --- a/libraries/src/AWS.Lambda.Powertools.BatchProcessing/AWS.Lambda.Powertools.BatchProcessing.csproj +++ b/libraries/src/AWS.Lambda.Powertools.BatchProcessing/AWS.Lambda.Powertools.BatchProcessing.csproj @@ -13,6 +13,6 @@ - + diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs index 0f059a25..5ec587b7 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs @@ -23,7 +23,7 @@ namespace AWS.Lambda.Powertools.Common; /// Implements the ///
/// -internal class PowertoolsConfigurations : IPowertoolsConfigurations +public class PowertoolsConfigurations : IPowertoolsConfigurations { private readonly IPowertoolsEnvironment _powertoolsEnvironment; diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsEnvironment.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsEnvironment.cs index 7ae3b305..649418a4 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsEnvironment.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsEnvironment.cs @@ -4,7 +4,7 @@ namespace AWS.Lambda.Powertools.Common; /// -internal class PowertoolsEnvironment : IPowertoolsEnvironment +public class PowertoolsEnvironment : IPowertoolsEnvironment { /// /// The instance diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj b/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj index 00150b72..dc3d6e0a 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj @@ -15,7 +15,7 @@ - + diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/AWS.Lambda.Powertools.Metrics.csproj b/libraries/src/AWS.Lambda.Powertools.Metrics/AWS.Lambda.Powertools.Metrics.csproj index 77ec07a3..8f15d771 100644 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/AWS.Lambda.Powertools.Metrics.csproj +++ b/libraries/src/AWS.Lambda.Powertools.Metrics/AWS.Lambda.Powertools.Metrics.csproj @@ -9,7 +9,7 @@ - + diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/AWS.Lambda.Powertools.Parameters.csproj b/libraries/src/AWS.Lambda.Powertools.Parameters/AWS.Lambda.Powertools.Parameters.csproj index 526f6694..36e6c177 100644 --- a/libraries/src/AWS.Lambda.Powertools.Parameters/AWS.Lambda.Powertools.Parameters.csproj +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/AWS.Lambda.Powertools.Parameters.csproj @@ -20,7 +20,7 @@ - + diff --git a/libraries/src/AWS.Lambda.Powertools.Tracing/AWS.Lambda.Powertools.Tracing.csproj b/libraries/src/AWS.Lambda.Powertools.Tracing/AWS.Lambda.Powertools.Tracing.csproj index 550a8e83..499d9d39 100644 --- a/libraries/src/AWS.Lambda.Powertools.Tracing/AWS.Lambda.Powertools.Tracing.csproj +++ b/libraries/src/AWS.Lambda.Powertools.Tracing/AWS.Lambda.Powertools.Tracing.csproj @@ -17,7 +17,7 @@ - + diff --git a/libraries/tests/e2e/InfraShared/FunctionConstruct.cs b/libraries/tests/e2e/InfraShared/FunctionConstruct.cs index 6dfeb84b..c3bb7d9e 100644 --- a/libraries/tests/e2e/InfraShared/FunctionConstruct.cs +++ b/libraries/tests/e2e/InfraShared/FunctionConstruct.cs @@ -27,6 +27,7 @@ public FunctionConstruct(Construct scope, string id, FunctionConstructProps prop Tracing = Tracing.ACTIVE, Timeout = Duration.Seconds(10), Environment = props.Environment, + LoggingFormat = LoggingFormat.TEXT, Code = Code.FromCustomCommand(distPath, [ command diff --git a/libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/AOT-Function-ILogger.csproj b/libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/AOT-Function-ILogger.csproj new file mode 100644 index 00000000..8655735e --- /dev/null +++ b/libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/AOT-Function-ILogger.csproj @@ -0,0 +1,33 @@ + + + Exe + net8.0 + enable + enable + Lambda + + true + + true + + true + + partial + + + + + + + + + + TestHelper.cs + + + + + + \ No newline at end of file diff --git a/libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/Function.cs b/libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/Function.cs new file mode 100644 index 00000000..16234c5b --- /dev/null +++ b/libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/Function.cs @@ -0,0 +1,74 @@ +using System.Text.Json; +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport; +using System.Text.Json.Serialization; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.Serialization.SystemTextJson; +using AWS.Lambda.Powertools.Logging; +using AWS.Lambda.Powertools.Logging.Serializers; +using Helpers; + +namespace AOT_Function; + +public static class Function +{ + private static async Task Main() + { + Logger.Configure(logger => + { + logger.Service = "TestService"; + logger.LoggerOutputCase = LoggerOutputCase.PascalCase; + logger.JsonOptions = new JsonSerializerOptions + { + TypeInfoResolver = LambdaFunctionJsonSerializerContext.Default + }; + }); + + Func handler = FunctionHandler; + await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer()) + .Build() + .RunAsync(); + } + + [Logging(LogEvent = true, CorrelationIdPath = CorrelationIdPaths.ApiGatewayRest)] + public static APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest apigwProxyEvent, ILambdaContext context) + { + Logger.LogInformation("Processing request started"); + + var requestContextRequestId = apigwProxyEvent.RequestContext.RequestId; + var lookupInfo = new Dictionary() + { + {"LookupInfo", new Dictionary{{ "LookupId", requestContextRequestId }}} + }; + + var customKeys = new Dictionary + { + {"test1", "value1"}, + {"test2", "value2"} + }; + + Logger.AppendKeys(lookupInfo); + Logger.AppendKeys(customKeys); + + Logger.LogWarning("Warn with additional keys"); + + Logger.RemoveKeys("test1", "test2"); + + var error = new InvalidOperationException("Parent exception message", + new ArgumentNullException(nameof(apigwProxyEvent), + new Exception("Very important nested inner exception message"))); + Logger.LogError(error, "Oops something went wrong"); + return new APIGatewayProxyResponse() + { + StatusCode = 200, + Body = apigwProxyEvent.Body.ToUpper() + }; + } +} + +[JsonSerializable(typeof(APIGatewayProxyRequest))] +[JsonSerializable(typeof(APIGatewayProxyResponse))] +public partial class LambdaFunctionJsonSerializerContext : JsonSerializerContext +{ + +} \ No newline at end of file diff --git a/libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/aws-lambda-tools-defaults.json b/libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/aws-lambda-tools-defaults.json new file mode 100644 index 00000000..be3c7ec1 --- /dev/null +++ b/libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/aws-lambda-tools-defaults.json @@ -0,0 +1,16 @@ +{ + "Information": [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help", + "All the command line options for the Lambda command can be specified in this file." + ], + "profile": "", + "region": "", + "configuration": "Release", + "function-runtime": "dotnet8", + "function-memory-size": 512, + "function-timeout": 30, + "function-handler": "AOT-Function", + "msbuild-parameters": "--self-contained true" +} \ No newline at end of file diff --git a/libraries/tests/e2e/functions/core/logging/Function/src/Function/Function.cs b/libraries/tests/e2e/functions/core/logging/Function/src/Function/Function.cs index 8a4d3a8b..958f36ff 100644 --- a/libraries/tests/e2e/functions/core/logging/Function/src/Function/Function.cs +++ b/libraries/tests/e2e/functions/core/logging/Function/src/Function/Function.cs @@ -2,24 +2,191 @@ using Amazon.Lambda.Core; using AWS.Lambda.Powertools.Logging; using Helpers; +using Microsoft.Extensions.Logging; // Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. [assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -namespace Function; - -public class Function +namespace Function { - [Logging(LogEvent = true, LoggerOutputCase = LoggerOutputCase.PascalCase, Service = "TestService", + public class Function + { + [Logging(LogEvent = true, LoggerOutputCase = LoggerOutputCase.PascalCase, Service = "TestService", CorrelationIdPath = CorrelationIdPaths.ApiGatewayRest)] - public APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest apigwProxyEvent, ILambdaContext context) + public APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest apigwProxyEvent, ILambdaContext context) + { + TestHelper.TestMethod(apigwProxyEvent); + + return new APIGatewayProxyResponse() + { + StatusCode = 200, + Body = apigwProxyEvent.Body.ToUpper() + }; + } + } +} + +namespace StaticConfiguration +{ + public class Function + { + public Function() + { + Logger.Configure(config => + { + config.Service = "TestService"; + config.LoggerOutputCase = LoggerOutputCase.PascalCase; + }); + } + + [Logging(LogEvent = true, CorrelationIdPath = CorrelationIdPaths.ApiGatewayRest)] + public APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest apigwProxyEvent, ILambdaContext context) + { + TestHelper.TestMethod(apigwProxyEvent); + + return new APIGatewayProxyResponse() + { + StatusCode = 200, + Body = apigwProxyEvent.Body.ToUpper() + }; + } + } +} + +namespace StaticILoggerConfiguration +{ + public class Function { - TestHelper.TestMethod(apigwProxyEvent); + public Function() + { + LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "TestService"; + config.LoggerOutputCase = LoggerOutputCase.PascalCase; + }); + }); + } - return new APIGatewayProxyResponse() + [Logging(LogEvent = true, CorrelationIdPath = CorrelationIdPaths.ApiGatewayRest)] + public APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest apigwProxyEvent, ILambdaContext context) { - StatusCode = 200, - Body = apigwProxyEvent.Body.ToUpper() - }; + TestHelper.TestMethod(apigwProxyEvent); + + return new APIGatewayProxyResponse() + { + StatusCode = 200, + Body = apigwProxyEvent.Body.ToUpper() + }; + } + } +} + +namespace ILoggerConfiguration +{ + public class Function + { + private readonly ILogger _logger; + + public Function() + { + _logger = LoggerFactory.Create(builder => + { + builder.AddPowertoolsLogger(config => + { + config.Service = "TestService"; + config.LoggerOutputCase = LoggerOutputCase.PascalCase; + }); + }).CreatePowertoolsLogger(); + } + + [Logging(LogEvent = true, CorrelationIdPath = CorrelationIdPaths.ApiGatewayRest)] + public APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest apigwProxyEvent, ILambdaContext context) + { + _logger.LogInformation("Processing request started"); + + var requestContextRequestId = apigwProxyEvent.RequestContext.RequestId; + var lookupInfo = new Dictionary() + { + {"LookupInfo", new Dictionary{{ "LookupId", requestContextRequestId }}} + }; + + var customKeys = new Dictionary + { + {"test1", "value1"}, + {"test2", "value2"} + }; + + _logger.AppendKeys(lookupInfo); + _logger.AppendKeys(customKeys); + + _logger.LogWarning("Warn with additional keys"); + + _logger.RemoveKeys("test1", "test2"); + + var error = new InvalidOperationException("Parent exception message", + new ArgumentNullException(nameof(apigwProxyEvent), + new Exception("Very important nested inner exception message"))); + _logger.LogError(error, "Oops something went wrong"); + + return new APIGatewayProxyResponse() + { + StatusCode = 200, + Body = apigwProxyEvent.Body.ToUpper() + }; + } + } +} + +namespace ILoggerBuilder +{ + public class Function + { + private readonly ILogger _logger; + + public Function() + { + _logger = new PowertoolsLoggerBuilder() + .WithService("TestService") + .WithOutputCase(LoggerOutputCase.PascalCase) + .Build(); + } + + [Logging(LogEvent = true, CorrelationIdPath = CorrelationIdPaths.ApiGatewayRest)] + public APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest apigwProxyEvent, ILambdaContext context) + { + _logger.LogInformation("Processing request started"); + + var requestContextRequestId = apigwProxyEvent.RequestContext.RequestId; + var lookupInfo = new Dictionary() + { + {"LookupInfo", new Dictionary{{ "LookupId", requestContextRequestId }}} + }; + + var customKeys = new Dictionary + { + {"test1", "value1"}, + {"test2", "value2"} + }; + + _logger.AppendKeys(lookupInfo); + _logger.AppendKeys(customKeys); + + _logger.LogWarning("Warn with additional keys"); + + _logger.RemoveKeys("test1", "test2"); + + var error = new InvalidOperationException("Parent exception message", + new ArgumentNullException(nameof(apigwProxyEvent), + new Exception("Very important nested inner exception message"))); + _logger.LogError(error, "Oops something went wrong"); + + return new APIGatewayProxyResponse() + { + StatusCode = 200, + Body = apigwProxyEvent.Body.ToUpper() + }; + } } } \ No newline at end of file diff --git a/libraries/tests/e2e/functions/core/logging/Function/test/Function.Tests/FunctionTests.cs b/libraries/tests/e2e/functions/core/logging/Function/test/Function.Tests/FunctionTests.cs index ca3a857a..b80be012 100644 --- a/libraries/tests/e2e/functions/core/logging/Function/test/Function.Tests/FunctionTests.cs +++ b/libraries/tests/e2e/functions/core/logging/Function/test/Function.Tests/FunctionTests.cs @@ -5,6 +5,7 @@ using Amazon.Lambda.Model; using TestUtils; using Xunit.Abstractions; +using Environment = Amazon.Lambda.Model.Environment; namespace Function.Tests; @@ -22,10 +23,21 @@ public FunctionTests(ITestOutputHelper testOutputHelper) [Trait("Category", "AOT")] [Theory] - [InlineData("E2ETestLambda_X64_AOT_NET8_logging")] - [InlineData("E2ETestLambda_ARM_AOT_NET8_logging")] + [InlineData("E2ETestLambda_X64_AOT_NET8_logging_AOT-Function")] + [InlineData("E2ETestLambda_ARM_AOT_NET8_logging_AOT-Function")] public async Task AotFunctionTest(string functionName) { + // await ResetFunction(functionName); + await TestFunction(functionName); + } + + [Trait("Category", "AOT")] + [Theory] + [InlineData("E2ETestLambda_X64_AOT_NET8_logging_AOT-Function-ILogger")] + [InlineData("E2ETestLambda_ARM_AOT_NET8_logging_AOT-Function-ILogger")] + public async Task AotILoggerFunctionTest(string functionName) + { + // await ResetFunction(functionName); await TestFunction(functionName); } @@ -36,6 +48,51 @@ public async Task AotFunctionTest(string functionName) [InlineData("E2ETestLambda_ARM_NET8_logging")] public async Task FunctionTest(string functionName) { + await UpdateFunctionHandler(functionName, "Function::Function.Function::FunctionHandler"); + await TestFunction(functionName); + } + + [Theory] + [InlineData("E2ETestLambda_X64_NET6_logging")] + [InlineData("E2ETestLambda_ARM_NET6_logging")] + [InlineData("E2ETestLambda_X64_NET8_logging")] + [InlineData("E2ETestLambda_ARM_NET8_logging")] + public async Task StaticConfigurationFunctionTest(string functionName) + { + await UpdateFunctionHandler(functionName, "Function::StaticConfiguration.Function::FunctionHandler"); + await TestFunction(functionName); + } + + [Theory] + [InlineData("E2ETestLambda_X64_NET6_logging")] + [InlineData("E2ETestLambda_ARM_NET6_logging")] + [InlineData("E2ETestLambda_X64_NET8_logging")] + [InlineData("E2ETestLambda_ARM_NET8_logging")] + public async Task StaticILoggerConfigurationFunctionTest(string functionName) + { + await UpdateFunctionHandler(functionName, "Function::StaticILoggerConfiguration.Function::FunctionHandler"); + await TestFunction(functionName); + } + + [Theory] + [InlineData("E2ETestLambda_X64_NET6_logging")] + [InlineData("E2ETestLambda_ARM_NET6_logging")] + [InlineData("E2ETestLambda_X64_NET8_logging")] + [InlineData("E2ETestLambda_ARM_NET8_logging")] + public async Task ILoggerConfigurationFunctionTest(string functionName) + { + await UpdateFunctionHandler(functionName, "Function::ILoggerConfiguration.Function::FunctionHandler"); + await TestFunction(functionName); + } + + [Theory] + [InlineData("E2ETestLambda_X64_NET6_logging")] + [InlineData("E2ETestLambda_ARM_NET6_logging")] + [InlineData("E2ETestLambda_X64_NET8_logging")] + [InlineData("E2ETestLambda_ARM_NET8_logging")] + public async Task ILoggerBuilderFunctionTest(string functionName) + { + await UpdateFunctionHandler(functionName, "Function::ILoggerBuilder.Function::FunctionHandler"); await TestFunction(functionName); } @@ -243,4 +300,48 @@ private void AssertExceptionLog(string functionName, bool isColdStart, string ou Assert.False(root.TryGetProperty("Test1", out JsonElement _)); Assert.False(root.TryGetProperty("Test2", out JsonElement _)); } + + private async Task UpdateFunctionHandler(string functionName, string handler) + { + var updateRequest = new UpdateFunctionConfigurationRequest + { + FunctionName = functionName, + Handler = handler + }; + + var updateResponse = await _lambdaClient.UpdateFunctionConfigurationAsync(updateRequest); + + if (updateResponse.HttpStatusCode == System.Net.HttpStatusCode.OK) + { + Console.WriteLine($"Successfully updated the handler for function {functionName} to {handler}"); + } + else + { + Assert.Fail( + $"Failed to update the handler for function {functionName}. Status code: {updateResponse.HttpStatusCode}"); + } + + //wait a few seconds for the changes to take effect + await Task.Delay(1000); + } + + private async Task ResetFunction(string functionName) + { + var updateRequest = new UpdateFunctionConfigurationRequest + { + FunctionName = functionName, + Environment = new Environment + { + Variables = + { + {"Updated", DateTime.UtcNow.ToString("G")} + } + } + }; + + await _lambdaClient.UpdateFunctionConfigurationAsync(updateRequest); + + //wait a few seconds for the changes to take effect + await Task.Delay(1000); + } } \ No newline at end of file diff --git a/libraries/tests/e2e/infra-aot/CoreAotStack.cs b/libraries/tests/e2e/infra-aot/CoreAotStack.cs index 4387892c..2d85a941 100644 --- a/libraries/tests/e2e/infra-aot/CoreAotStack.cs +++ b/libraries/tests/e2e/infra-aot/CoreAotStack.cs @@ -15,29 +15,30 @@ internal CoreAotStack(Construct scope, string id, PowertoolsDefaultStackProps pr if (props != null) _architecture = props.ArchitectureString == "arm64" ? Architecture.ARM_64 : Architecture.X86_64; CreateFunctionConstructs("logging"); + CreateFunctionConstructs("logging", "AOT-Function-ILogger"); CreateFunctionConstructs("metrics"); CreateFunctionConstructs("tracing"); } - private void CreateFunctionConstructs(string utility) + private void CreateFunctionConstructs(string utility, string function = "AOT-Function" ) { - var baseAotPath = $"../functions/core/{utility}/AOT-Function/src/AOT-Function"; - var distAotPath = $"../functions/core/{utility}/AOT-Function/dist"; + var baseAotPath = $"../functions/core/{utility}/{function}/src/{function}"; + var distAotPath = $"../functions/core/{utility}/{function}/dist/{function}"; var arch = _architecture == Architecture.X86_64 ? "X64" : "ARM"; - CreateFunctionConstruct(this, $"{utility}_{arch}_aot_net8", Runtime.DOTNET_8, _architecture, - $"E2ETestLambda_{arch}_AOT_NET8_{utility}", baseAotPath, distAotPath); + CreateFunctionConstruct(this, $"{utility}_{arch}_aot_net8_{function}", Runtime.DOTNET_8, _architecture, + $"E2ETestLambda_{arch}_AOT_NET8_{utility}_{function}", baseAotPath, distAotPath, function); } private void CreateFunctionConstruct(Construct scope, string id, Runtime runtime, Architecture architecture, - string name, string sourcePath, string distPath) + string name, string sourcePath, string distPath, string handler) { _ = new FunctionConstruct(scope, id, new FunctionConstructProps { Runtime = runtime, Architecture = architecture, Name = name, - Handler = "AOT-Function", + Handler = handler, SourcePath = sourcePath, DistPath = distPath, IsAot = true diff --git a/mkdocs.yml b/mkdocs.yml index 24f86cf6..bd8426e8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,7 +14,9 @@ nav: - We Made This (Community): we_made_this.md - Workshop 🆕: https://s12d.com/powertools-for-aws-lambda-workshop" target="_blank - Core utilities: - - core/logging.md + - Logging: + - core/logging.md + - core/logging-v2.md - Metrics: - core/metrics.md - core/metrics-v2.md diff --git a/version.json b/version.json index c5124a2f..d45c76a0 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "Core": { - "Logging": "1.6.4", + "Logging": "2.0.0-preview.1", "Metrics": "2.0.0", "Tracing": "1.6.1", "Metrics.AspNetCore": "0.1.0" From 6331ba76a6e3220086ce75caa31b55bd6d33c13e Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 7 Apr 2025 13:12:40 +0100 Subject: [PATCH 39/49] fix(build): update ProjectReference condition to always include AWS.Lambda.Powertools.Common project --- libraries/tests/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/tests/Directory.Build.props b/libraries/tests/Directory.Build.props index d662fc45..887e46d9 100644 --- a/libraries/tests/Directory.Build.props +++ b/libraries/tests/Directory.Build.props @@ -4,6 +4,6 @@ false - + From eb194732ed8a1eb6549865b786b726a7129b8844 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 7 Apr 2025 13:49:47 +0100 Subject: [PATCH 40/49] fix(tests): update AWS_EXECUTION_ENV version in assertions to 1.0.0 --- .../Core/ConsoleWrapper.cs | 17 +++++-- .../Internal/BatchProcessingInternalTests.cs | 6 +-- .../ConsoleWrapperTests.cs | 44 ++++++++++++++++++- .../Internal/IdempotentAspectTests.cs | 2 +- .../PowertoolsLoggerTest.cs | 2 +- .../MetricsTests.cs | 2 +- .../XRayRecorderTests.cs | 2 +- 7 files changed, 64 insertions(+), 11 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs index f22ec16e..52652f54 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs @@ -21,6 +21,8 @@ namespace AWS.Lambda.Powertools.Common; /// public class ConsoleWrapper : IConsoleWrapper { + private static bool _override; + /// public void WriteLine(string message) { @@ -38,19 +40,28 @@ public void Debug(string message) /// public void Error(string message) { - var errordOutput = new StreamWriter(Console.OpenStandardError()); - errordOutput.AutoFlush = true; - Console.SetError(errordOutput); + if (!_override) + { + var errordOutput = new StreamWriter(Console.OpenStandardError()); + errordOutput.AutoFlush = true; + Console.SetError(errordOutput); + } + Console.Error.WriteLine(message); } internal static void SetOut(StringWriter consoleOut) { + _override = true; Console.SetOut(consoleOut); } private void OverrideLambdaLogger() { + if (_override) + { + return; + } // Force override of LambdaLogger var standardOutput = new StreamWriter(Console.OpenStandardOutput()); standardOutput.AutoFlush = true; diff --git a/libraries/tests/AWS.Lambda.Powertools.BatchProcessing.Tests/Internal/BatchProcessingInternalTests.cs b/libraries/tests/AWS.Lambda.Powertools.BatchProcessing.Tests/Internal/BatchProcessingInternalTests.cs index 1356df69..c218e419 100644 --- a/libraries/tests/AWS.Lambda.Powertools.BatchProcessing.Tests/Internal/BatchProcessingInternalTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.BatchProcessing.Tests/Internal/BatchProcessingInternalTests.cs @@ -35,7 +35,7 @@ public void BatchProcessing_Set_Execution_Environment_Context_SQS() var sqsBatchProcessor = new SqsBatchProcessor(conf); // Assert - Assert.Equal($"{Constants.FeatureContextIdentifier}/BatchProcessing/0.0.1", + Assert.Equal($"{Constants.FeatureContextIdentifier}/BatchProcessing/1.0.0", env.GetEnvironmentVariable("AWS_EXECUTION_ENV")); Assert.NotNull(sqsBatchProcessor); @@ -52,7 +52,7 @@ public void BatchProcessing_Set_Execution_Environment_Context_Kinesis() var KinesisEventBatchProcessor = new KinesisEventBatchProcessor(conf); // Assert - Assert.Equal($"{Constants.FeatureContextIdentifier}/BatchProcessing/0.0.1", + Assert.Equal($"{Constants.FeatureContextIdentifier}/BatchProcessing/1.0.0", env.GetEnvironmentVariable("AWS_EXECUTION_ENV")); Assert.NotNull(KinesisEventBatchProcessor); @@ -69,7 +69,7 @@ public void BatchProcessing_Set_Execution_Environment_Context_DynamoDB() var dynamoDbStreamBatchProcessor = new DynamoDbStreamBatchProcessor(conf); // Assert - Assert.Equal($"{Constants.FeatureContextIdentifier}/BatchProcessing/0.0.1", + Assert.Equal($"{Constants.FeatureContextIdentifier}/BatchProcessing/1.0.0", env.GetEnvironmentVariable("AWS_EXECUTION_ENV")); Assert.NotNull(dynamoDbStreamBatchProcessor); diff --git a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs index c70c0aa0..cbd513d8 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs @@ -12,7 +12,7 @@ public void WriteLine_Should_Write_To_Console() // Arrange var consoleWrapper = new ConsoleWrapper(); var writer = new StringWriter(); - Console.SetOut(writer); + ConsoleWrapper.SetOut(writer); // Act consoleWrapper.WriteLine("test message"); @@ -27,6 +27,7 @@ public void Error_Should_Write_To_Error_Console() // Arrange var consoleWrapper = new ConsoleWrapper(); var writer = new StringWriter(); + ConsoleWrapper.SetOut(writer); Console.SetError(writer); // Act @@ -36,4 +37,45 @@ public void Error_Should_Write_To_Error_Console() // Assert Assert.Equal($"error message{Environment.NewLine}", writer.ToString()); } + + [Fact] + public void SetOut_Should_Override_Console_Output() + { + // Arrange + var consoleWrapper = new ConsoleWrapper(); + var writer = new StringWriter(); + ConsoleWrapper.SetOut(writer); + + // Act + consoleWrapper.WriteLine("test message"); + + // Assert + Assert.Equal($"test message{Environment.NewLine}", writer.ToString()); + } + + [Fact] + public void OverrideLambdaLogger_Should_Override_Console_Out() + { +// Arrange + var originalOut = Console.Out; + try + { + var consoleWrapper = new ConsoleWrapper(); + + // Act - create a custom StringWriter and set it after constructor + // but before WriteLine (which triggers OverrideLambdaLogger) + var writer = new StringWriter(); + Console.SetOut(writer); + + consoleWrapper.WriteLine("test message"); + + // Assert + Assert.Equal($"test message{Environment.NewLine}", writer.ToString()); + } + finally + { + // Restore original console out + Console.SetOut(originalOut); + } + } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs index 218b6c2d..be80a4c4 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs @@ -272,7 +272,7 @@ public void Idempotency_Set_Execution_Environment_Context() var xRayRecorder = new Idempotency(conf); // Assert - Assert.Equal($"{Constants.FeatureContextIdentifier}/Idempotency/0.0.1", + Assert.Equal($"{Constants.FeatureContextIdentifier}/Idempotency/1.0.0", env.GetEnvironmentVariable("AWS_EXECUTION_ENV")); Assert.NotNull(xRayRecorder); diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs index 50e28a2d..f4ba2a43 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/PowertoolsLoggerTest.cs @@ -1369,7 +1369,7 @@ public void Log_Set_Execution_Environment_Context() logger.LogInformation("Test"); // Assert - Assert.Equal($"{Constants.FeatureContextIdentifier}/Logging/0.0.1", + Assert.Equal($"{Constants.FeatureContextIdentifier}/Logging/1.0.0", env.GetEnvironmentVariable("AWS_EXECUTION_ENV")); } diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs index af82750c..adcee372 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/MetricsTests.cs @@ -24,7 +24,7 @@ public void Metrics_Set_Execution_Environment_Context() _ = new Metrics(conf); // Assert - Assert.Equal($"{Constants.FeatureContextIdentifier}/Metrics/0.0.1", + Assert.Equal($"{Constants.FeatureContextIdentifier}/Metrics/1.0.0", env.GetEnvironmentVariable("AWS_EXECUTION_ENV")); } diff --git a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/XRayRecorderTests.cs b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/XRayRecorderTests.cs index 9c02bdab..5318b7b3 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/XRayRecorderTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/XRayRecorderTests.cs @@ -40,7 +40,7 @@ public void Tracing_Set_Execution_Environment_Context() var xRayRecorder = new XRayRecorder(awsXray, conf); // Assert - Assert.Equal($"{Constants.FeatureContextIdentifier}/Tracing/0.0.1", + Assert.Equal($"{Constants.FeatureContextIdentifier}/Tracing/1.0.0", env.GetEnvironmentVariable("AWS_EXECUTION_ENV")); Assert.NotNull(xRayRecorder); From 45a22115e9ae2613960f1893dc80a53831d106f3 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:03:26 +0100 Subject: [PATCH 41/49] feat(logger): enhance random number generation and improve regex match timeout --- .../Internal/PowertoolsLogger.cs | 3 ++- .../PowertoolsLoggerConfiguration.cs | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs index 8b19e20b..0a6e12ed 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -17,6 +17,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Logging.Internal.Helpers; using Microsoft.Extensions.Logging; @@ -544,7 +545,7 @@ private Dictionary ExtractStructuredParameters(TState st // Extract format specifiers from the template var matches = System.Text.RegularExpressions.Regex.Matches( template, - @"{([@\w]+)(?::([^{}]+))?}"); + @"{([@\w]+)(?::([^{}]+))?}", RegexOptions.None, TimeSpan.FromSeconds(2)); foreach (System.Text.RegularExpressions.Match match in matches) { diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs index a05b0156..9e09fb13 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs @@ -14,6 +14,7 @@ */ using System; +using System.Security.Cryptography; using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -327,7 +328,7 @@ private PowertoolsLoggingSerializer InitializeSerializer() internal string XRayTraceId { get; set; } internal bool LogEvent { get; set; } - internal double Random { get; set; } = new Random().NextDouble(); + internal double Random { get; set; } = GetSafeRandom(); /// /// Gets random number @@ -337,4 +338,12 @@ internal virtual double GetRandom() { return Random; } + + internal static double GetSafeRandom() + { + var randomGenerator = RandomNumberGenerator.Create(); + byte[] data = new byte[16]; + randomGenerator.GetBytes(data); + return BitConverter.ToDouble(data); + } } \ No newline at end of file From f0936337d1a79512bf8d4f204d69b5f04155154a Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 7 Apr 2025 15:49:00 +0100 Subject: [PATCH 42/49] sonar fixes and doc updates --- docs/core/logging-v2.md | 26 +- .../Core/ConsoleWrapper.cs | 3 - .../Core/SystemWrapper.cs | 14 +- .../Internal/Buffer/LogBuffer.cs | 2 +- .../Internal/Converters/ByteArrayConverter.cs | 2 +- .../Internal/LoggingAspect.cs | 11 +- .../Internal/PowertoolsLogger.cs | 265 ++++++++++-------- .../Internal/PowertoolsLoggerProvider.cs | 3 - .../PowertoolsLoggerFactory.cs | 7 +- .../PowertoolsLoggingSerializer.cs | 12 +- .../Formatter/LogFormattingTests.cs | 73 ++++- libraries/tests/e2e/infra/CoreStack.cs | 49 ++-- 12 files changed, 272 insertions(+), 195 deletions(-) diff --git a/docs/core/logging-v2.md b/docs/core/logging-v2.md index aa7dc516..93b6d51b 100644 --- a/docs/core/logging-v2.md +++ b/docs/core/logging-v2.md @@ -113,7 +113,7 @@ You can also use the `ILogger` interface to log messages. This interface is part } ``` -=== "Powertools Logger Builder" +=== "With Builder" ```c# hl_lines="6 10-13 19" /** @@ -919,7 +919,7 @@ builder.Logging.AddPowertoolsLogger(options => { options.JsonOptions = new JsonSerializerOptions { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // Override output casing + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, // Override output casing TypeInfoResolver = MyCustomJsonSerializerContext.Default // Your custom JsonSerializerContext }; }); @@ -1044,7 +1044,7 @@ Log buffering enables you to buffer logs for a specific request or invocation. E logger.LogBuffering = new LogBufferingOptions { BufferAtLogLevel = LogLevel.Debug, - MaxBytes = 123455, // Default is 20KB (20480 bytes) + MaxBytes = 20480, // Default is 20KB (20480 bytes) FlushOnErrorLog = true // default true }; }); @@ -1206,13 +1206,13 @@ sequenceDiagram Client->>Lambda: Invoke Lambda Lambda->>Logger: Initialize with DEBUG level buffering Logger-->>Lambda: Logger buffer ready - Lambda->>Logger: logger.debug("First debug log") + Lambda->>Logger: Logger.LogDebug("First debug log") Logger-->>Logger: Buffer first debug log - Lambda->>Logger: logger.info("Info log") + Lambda->>Logger: Logger.LogInformation("Info log") Logger->>CloudWatch: Directly log info message - Lambda->>Logger: logger.debug("Second debug log") + Lambda->>Logger: Logger.LogDebug("Second debug log") Logger-->>Logger: Buffer second debug log - Lambda->>Logger: logger.flush_buffer() + Lambda->>Logger: Logger.FlushBuffer() Logger->>CloudWatch: Emit buffered logs to stdout Lambda->>Client: Return execution result ``` @@ -1231,14 +1231,14 @@ sequenceDiagram Client->>Lambda: Invoke Lambda Lambda->>Logger: Initialize with DEBUG level buffering Logger-->>Lambda: Logger buffer ready - Lambda->>Logger: logger.debug("First log") + Lambda->>Logger: Logger.LogDebug("First log") Logger-->>Logger: Buffer first debug log - Lambda->>Logger: logger.debug("Second log") + Lambda->>Logger: Logger.LogDebug("Second log") Logger-->>Logger: Buffer second debug log - Lambda->>Logger: logger.debug("Third log") + Lambda->>Logger: Logger.LogDebug("Third log") Logger-->>Logger: Buffer third debug log Lambda->>Lambda: Exception occurs - Lambda->>Logger: logger.error("Error details") + Lambda->>Logger: Logger.LogError("Error details") Logger->>CloudWatch: Emit buffered debug logs Logger->>CloudWatch: Emit error log Lambda->>Client: Raise exception @@ -1260,9 +1260,9 @@ sequenceDiagram Client->>Lambda: Invoke Lambda Lambda->>Logger: Using decorator Logger-->>Lambda: Logger context injected - Lambda->>Logger: logger.debug("First log") + Lambda->>Logger: Logger.LogDebug("First log") Logger-->>Logger: Buffer first debug log - Lambda->>Logger: logger.debug("Second log") + Lambda->>Logger: Logger.LogDebug("Second log") Logger-->>Logger: Buffer second debug log Lambda->>Lambda: Uncaught Exception Lambda->>CloudWatch: Automatically emit buffered debug logs diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs index 52652f54..36d8aeea 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs @@ -70,9 +70,6 @@ private void OverrideLambdaLogger() internal static void WriteLine(string logLevel, string message) { - // var standardOutput = new StreamWriter(Console.OpenStandardOutput()); - // standardOutput.AutoFlush = true; - // Console.SetOut(standardOutput); Console.WriteLine($"{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ss.fffZ}\t{logLevel}\t{message}"); } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs index 2c5fc9c1..8f42bda4 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs @@ -41,13 +41,13 @@ public SystemWrapper(IPowertoolsEnvironment powertoolsEnvironment) _powertoolsEnvironment = powertoolsEnvironment; _instance ??= this; - // // Clear AWS SDK Console injected parameters StdOut and StdErr - // var standardOutput = new StreamWriter(Console.OpenStandardOutput()); - // standardOutput.AutoFlush = true; - // Console.SetOut(standardOutput); - // var errordOutput = new StreamWriter(Console.OpenStandardError()); - // errordOutput.AutoFlush = true; - // Console.SetError(errordOutput); + // Clear AWS SDK Console injected parameters StdOut and StdErr + var standardOutput = new StreamWriter(Console.OpenStandardOutput()); + standardOutput.AutoFlush = true; + Console.SetOut(standardOutput); + var errordOutput = new StreamWriter(Console.OpenStandardError()); + errordOutput.AutoFlush = true; + Console.SetError(errordOutput); } /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs index ac83ca10..db19e096 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/LogBuffer.cs @@ -99,7 +99,7 @@ public void Clear() public void ClearCurrentInvocation() { var invocationId = CurrentInvocationId; - if (_buffersByInvocation.TryRemove(invocationId, out _)) {} + _buffersByInvocation.TryRemove(invocationId, out _); } /// diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ByteArrayConverter.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ByteArrayConverter.cs index 9be24b68..b868aa64 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ByteArrayConverter.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Converters/ByteArrayConverter.cs @@ -35,7 +35,7 @@ internal class ByteArrayConverter : JsonConverter public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.Null) - return null; + return []; if (reader.TokenType == JsonTokenType.String) return Convert.FromBase64String(reader.GetString()!); diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs index 5fa77925..75f93dc8 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs @@ -240,13 +240,12 @@ public void OnEntry(AspectEventArgs eventArgs) CaptureLambdaContext(eventArgs); CaptureCorrelationId(eventObject, trigger.CorrelationIdPath); - if (trigger.IsLogEventSet && trigger.LogEvent) + switch (trigger.IsLogEventSet) { - LogEvent(eventObject); - } - else if (!trigger.IsLogEventSet && _currentConfig.LogEvent) - { - LogEvent(eventObject); + case true when trigger.LogEvent: + case false when _currentConfig.LogEvent: + LogEvent(eventObject); + break; } } catch (Exception exception) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs index 0a6e12ed..8f38e1d4 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -31,6 +31,8 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// internal sealed class PowertoolsLogger : ILogger { + private static string _originalformat = "{OriginalFormat}"; + /// /// The name /// @@ -93,7 +95,7 @@ internal void EndScope() public bool IsEnabled(LogLevel logLevel) { var config = _currentConfig(); - + //if Buffering is enabled and the log level is below the buffer threshold, skip logging only if bellow error if (logLevel <= config.LogBuffering?.BufferAtLogLevel && config.LogBuffering?.BufferAtLogLevel != LogLevel.Error @@ -101,18 +103,18 @@ public bool IsEnabled(LogLevel logLevel) { return false; } - + // If we have no explicit minimum level, use the default var effectiveMinLevel = config.MinimumLogLevel != LogLevel.None ? config.MinimumLogLevel : LoggingConstants.DefaultLogLevel; - + // Log diagnostic info for Debug/Trace levels if (logLevel <= LogLevel.Debug) { return logLevel >= effectiveMinLevel; } - + // Standard check return logLevel >= effectiveMinLevel; } @@ -133,22 +135,24 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except { return; } - + _currentConfig().LogOutput.WriteLine(LogEntryString(logLevel, state, exception, formatter)); } - + internal void LogLine(string message) { _currentConfig().LogOutput.WriteLine(message); } - internal string LogEntryString(LogLevel logLevel, TState state, Exception exception, Func formatter) + internal string LogEntryString(LogLevel logLevel, TState state, Exception exception, + Func formatter) { var logEntry = LogEntry(logLevel, state, exception, formatter); return _currentConfig().Serializer.Serialize(logEntry, typeof(object)); } - - internal object LogEntry(LogLevel logLevel, TState state, Exception exception, Func formatter) + + internal object LogEntry(LogLevel logLevel, TState state, Exception exception, + Func formatter) { var timestamp = DateTime.UtcNow; @@ -156,8 +160,8 @@ internal object LogEntry(LogLevel logLevel, TState state, Exception exce throw new ArgumentNullException(nameof(formatter)); // Extract structured parameters for template-style logging - var structuredParameters = ExtractStructuredParameters(state, out string messageTemplate); - + var structuredParameters = ExtractStructuredParameters(state, out _); + // Format the message var message = CustomFormatter(state, exception, out var customMessage) && customMessage is not null ? customMessage @@ -187,24 +191,25 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times var config = _currentConfig(); logEntry.TryAdd(config.LogLevelKey, logLevel.ToString()); logEntry.TryAdd(LoggingConstants.KeyMessage, message); - logEntry.TryAdd(LoggingConstants.KeyTimestamp, timestamp.ToString( config.TimestampFormat ?? "o")); + logEntry.TryAdd(LoggingConstants.KeyTimestamp, timestamp.ToString(config.TimestampFormat ?? "o")); logEntry.TryAdd(LoggingConstants.KeyService, config.Service); logEntry.TryAdd(LoggingConstants.KeyColdStart, _powertoolsConfigurations.IsColdStart); - + // Add Lambda Context Keys if (LoggingLambdaContext.Instance is not null) { AddLambdaContextKeys(logEntry); } - - if(! string.IsNullOrWhiteSpace(_powertoolsConfigurations.XRayTraceId)) + + if (!string.IsNullOrWhiteSpace(_powertoolsConfigurations.XRayTraceId)) logEntry.TryAdd(LoggingConstants.KeyXRayTraceId, - _powertoolsConfigurations.XRayTraceId.Split(';', StringSplitOptions.RemoveEmptyEntries)[0].Replace("Root=", "")); + _powertoolsConfigurations.XRayTraceId.Split(';', StringSplitOptions.RemoveEmptyEntries)[0] + .Replace("Root=", "")); logEntry.TryAdd(LoggingConstants.KeyLoggerName, _categoryName); - + if (config.SamplingRate > 0) logEntry.TryAdd(LoggingConstants.KeySamplingRate, config.SamplingRate); - + // Add Custom Keys foreach (var (key, value) in this.GetAllKeys()) { @@ -214,18 +219,16 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times logEntry.TryAdd(key, value); } } - + // Add Extra Fields if (CurrentScope?.ExtraKeys is not null) { foreach (var (key, value) in CurrentScope.ExtraKeys) { - if (!string.IsNullOrWhiteSpace(key)) + if (string.IsNullOrWhiteSpace(key)) continue; + if (!IsLogConstantKey(key)) { - if (!IsLogConstantKey(key)) - { - logEntry.TryAdd(key, value); - } + logEntry.TryAdd(key, value); } } } @@ -244,7 +247,7 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times } } } - + // Use the AddExceptionDetails method instead of adding exception directly if (exception != null) { @@ -253,7 +256,7 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times return logEntry; } - + /// /// Checks if a key is defined in LoggingConstants /// @@ -265,9 +268,11 @@ private bool IsLogConstantKey(string key) // || string.Equals(key.ToPascal(), LoggingConstants.KeyCorrelationId, StringComparison.OrdinalIgnoreCase) || string.Equals(key.ToPascal(), LoggingConstants.KeyException, StringComparison.OrdinalIgnoreCase) || string.Equals(key.ToPascal(), LoggingConstants.KeyFunctionArn, StringComparison.OrdinalIgnoreCase) - || string.Equals(key.ToPascal(), LoggingConstants.KeyFunctionMemorySize, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyFunctionMemorySize, + StringComparison.OrdinalIgnoreCase) || string.Equals(key.ToPascal(), LoggingConstants.KeyFunctionName, StringComparison.OrdinalIgnoreCase) - || string.Equals(key.ToPascal(), LoggingConstants.KeyFunctionRequestId, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.ToPascal(), LoggingConstants.KeyFunctionRequestId, + StringComparison.OrdinalIgnoreCase) || string.Equals(key.ToPascal(), LoggingConstants.KeyFunctionVersion, StringComparison.OrdinalIgnoreCase) || string.Equals(key.ToPascal(), LoggingConstants.KeyLoggerName, StringComparison.OrdinalIgnoreCase) || string.Equals(key.ToPascal(), LoggingConstants.KeyLogLevel, StringComparison.OrdinalIgnoreCase) @@ -301,7 +306,7 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec Service = config.Service, Name = _categoryName, Message = message, - Exception = exception, // Keep this to maintain compatibility + Exception = exception, // Keep this to maintain compatibility SamplingRate = config.SamplingRate, }; @@ -338,7 +343,7 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec } } } - + // Add structured parameters if (structuredParameters != null && structuredParameters.Count > 0) { @@ -350,13 +355,13 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec } } } - + // Add detailed exception information if (exception != null) { var exceptionDetails = new Dictionary(); exceptionDetails.TryAdd(LoggingConstants.KeyException, exception); - + // Add exception details to extra keys foreach (var (key, value) in exceptionDetails) { @@ -378,7 +383,7 @@ 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 @@ -418,13 +423,13 @@ private static bool CustomFormatter(TState state, Exception exception, o if (stateKeys is null || stateKeys.Count != 2) return false; - if (!stateKeys.TryGetValue("{OriginalFormat}", out var originalFormat)) + if (!stateKeys.TryGetValue(_originalformat, out var originalFormat)) return false; if (originalFormat?.ToString() != LoggingConstants.KeyJsonFormatter) return false; - message = stateKeys.First(k => k.Key != "{OriginalFormat}").Value; + message = stateKeys.First(k => k.Key != _originalformat).Value; return true; } @@ -483,26 +488,28 @@ private static Dictionary GetScopeKeys(TState state) if (!string.IsNullOrWhiteSpace(key)) keys.TryAdd(key, value); } + break; - + case IEnumerable> objectPairs: foreach (var (key, value) in objectPairs) { if (!string.IsNullOrWhiteSpace(key)) keys.TryAdd(key, value); } + break; - + default: // Skip property reflection for primitive types, strings and value types - if (state is string || - (state.GetType().IsPrimitive) || + if (state is string || + (state.GetType().IsPrimitive) || state is ValueType) { // Don't extract properties from primitives or strings break; } - + // For complex objects, use reflection to get properties foreach (var property in state.GetType().GetProperties()) { @@ -515,6 +522,7 @@ private static Dictionary GetScopeKeys(TState state) // Safely ignore reflection exceptions } } + break; } @@ -524,100 +532,121 @@ private static Dictionary GetScopeKeys(TState state) /// /// Extracts structured parameter key-value pairs from the log state /// + /// Type of the state being logged + /// The log state containing parameters + /// Output parameter for the message template + /// Dictionary of extracted parameter names and values private Dictionary ExtractStructuredParameters(TState state, out string messageTemplate) { messageTemplate = string.Empty; var parameters = new Dictionary(); - - if (state is IEnumerable> stateProps) + + if (!(state is IEnumerable> stateProps)) { - // Dictionary to store format specifiers for each parameter - var formatSpecifiers = new Dictionary(); - - // First pass - extract template and identify format specifiers - foreach (var prop in stateProps) + return parameters; + } + + // Dictionary to store format specifiers for each parameter + var formatSpecifiers = new Dictionary(); + var statePropsArray = stateProps.ToArray(); + + // First pass - extract message template and identify format specifiers + ExtractFormatSpecifiers(ref messageTemplate, statePropsArray, formatSpecifiers); + + // Second pass - process values with extracted format specifiers + ProcessValuesWithSpecifiers(statePropsArray, formatSpecifiers, parameters); + + return parameters; + } + + private void ProcessValuesWithSpecifiers(KeyValuePair[] statePropsArray, Dictionary formatSpecifiers, + Dictionary parameters) + { + foreach (var prop in statePropsArray) + { + if (prop.Key == _originalformat) + continue; + + // Extract parameter name without braces + var paramName = ExtractParameterName(prop.Key); + if (string.IsNullOrEmpty(paramName)) + continue; + + // Handle special serialization designators (like @) + var useStructuredSerialization = paramName.StartsWith('@'); + var actualParamName = useStructuredSerialization ? paramName.Substring(1) : paramName; + + if (!useStructuredSerialization && + formatSpecifiers.TryGetValue(paramName, out var format) && + prop.Value is IFormattable formattable) { - // The original message template is stored with key "{OriginalFormat}" - if (prop.Key == "{OriginalFormat}" && prop.Value is string template) + // Format the value using the specified format + var formattedValue = formattable.ToString(format, System.Globalization.CultureInfo.InvariantCulture); + + // Try to preserve the numeric type if possible + if (double.TryParse(formattedValue, out var numericValue)) { - messageTemplate = template; - - // Extract format specifiers from the template - var matches = System.Text.RegularExpressions.Regex.Matches( - template, - @"{([@\w]+)(?::([^{}]+))?}", RegexOptions.None, TimeSpan.FromSeconds(2)); - - foreach (System.Text.RegularExpressions.Match match in matches) - { - string paramName = match.Groups[1].Value; - if (match.Groups.Count > 2 && match.Groups[2].Success) - { - formatSpecifiers[paramName] = match.Groups[2].Value; - } - } - - continue; + parameters[actualParamName] = numericValue; + } + else + { + parameters[actualParamName] = formattedValue; } } - - // Second pass - process values with extracted format specifiers - foreach (var prop in stateProps) + else if (useStructuredSerialization) { - if (prop.Key == "{OriginalFormat}") - continue; - - // Extract parameter name without braces - string paramName = ExtractParameterName(prop.Key); - if (string.IsNullOrEmpty(paramName)) - continue; - - // Handle special serialization designators (like @) - bool useStructuredSerialization = paramName.StartsWith("@"); - string actualParamName = useStructuredSerialization ? paramName.Substring(1) : paramName; - - // Apply formatting if a format specifier exists and the value can be formatted - if (!useStructuredSerialization && - formatSpecifiers.TryGetValue(paramName, out string format) && - prop.Value is IFormattable formattable) + // Serialize the entire object + parameters[actualParamName] = prop.Value; + } + else + { + // Handle regular values appropriately + if (prop.Value != null && + !(prop.Value is string) && + !(prop.Value is ValueType) && + !(prop.Value.GetType().IsPrimitive)) { - // Format the value using the specified format - string formattedValue = formattable.ToString(format, System.Globalization.CultureInfo.InvariantCulture); - - // Try to preserve the numeric type if possible - if (double.TryParse(formattedValue, out double numericValue)) - { - parameters[actualParamName] = numericValue; - } - else - { - parameters[actualParamName] = formattedValue; - } + // For complex objects, use ToString() representation + parameters[actualParamName] = prop.Value.ToString(); } - else if (useStructuredSerialization) + else { - // Serialize the entire object + // For primitives and other simple types, use the value directly parameters[actualParamName] = prop.Value; } - else + } + } + } + + private static void ExtractFormatSpecifiers(ref string messageTemplate, KeyValuePair[] statePropsArray, + Dictionary formatSpecifiers) + { + foreach (var prop in statePropsArray) + { + // The original message template is stored with key "{OriginalFormat}" + if (prop.Key == _originalformat && prop.Value is string template) + { + messageTemplate = template; + + // Extract format specifiers using regex pattern for parameters + var matches = Regex.Matches( + template, + @"{([@\w]+)(?::([^{}]+))?}", + RegexOptions.None, + TimeSpan.FromSeconds(1)); + + foreach (Match match in matches) { - // For regular objects, convert to string if it's not a primitive type - if (prop.Value != null && - !(prop.Value is string) && - !(prop.Value is ValueType) && - !(prop.Value.GetType().IsPrimitive)) + var paramName = match.Groups[1].Value; + if (match.Groups.Count > 2 && match.Groups[2].Success) { - parameters[actualParamName] = prop.Value.ToString(); - } - else - { - // For primitives, use the value directly - parameters[actualParamName] = prop.Value; + formatSpecifiers[paramName] = match.Groups[2].Value; } } + + break; } } - - return parameters; } /// @@ -626,16 +655,16 @@ private Dictionary ExtractStructuredParameters(TState st private string ExtractParameterName(string key) { // If it's already a proper parameter name without braces, return it - if (!key.StartsWith("{") || !key.EndsWith("}")) + if (!key.StartsWith('{') || !key.EndsWith('}')) return key; - + // Remove the braces var nameWithPossibleFormat = key.Substring(1, key.Length - 2); - + // If there's a format specifier, remove it var colonIndex = nameWithPossibleFormat.IndexOf(':'); - return colonIndex > 0 - ? nameWithPossibleFormat.Substring(0, colonIndex) + return colonIndex > 0 + ? nameWithPossibleFormat.Substring(0, colonIndex) : nameWithPossibleFormat; } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs index 6511db70..d29138e8 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs @@ -76,9 +76,6 @@ public void ConfigureFromEnvironment() // Set log level from environment ONLY if not explicitly set var minLogLevel = lambdaLogLevelEnabled ? lambdaLogLevel : logLevel; _currentConfig.MinimumLogLevel = minLogLevel != LogLevel.None ? minLogLevel : LoggingConstants.DefaultLogLevel; - - // LoggerFactoryHolder.UpdateFilterLogLevel(minLogLevel); - _currentConfig.XRayTraceId = _powertoolsConfigurations.XRayTraceId; _currentConfig.LogEvent = _powertoolsConfigurations.LoggerLogEvent; diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs index b8e8cc15..062a7c15 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerFactory.cs @@ -8,12 +8,9 @@ internal sealed class PowertoolsLoggerFactory : IDisposable { private readonly ILoggerFactory _factory; - internal PowertoolsLoggerFactory(ILoggerFactory loggerFactory = null) + internal PowertoolsLoggerFactory(ILoggerFactory loggerFactory) { - _factory = loggerFactory ?? LoggerFactory.Create(builder => - { - builder.AddPowertoolsLogger(); - }); + _factory = loggerFactory; } internal PowertoolsLoggerFactory() : this(LoggerFactory.Create(builder => { builder.AddPowertoolsLogger(); })) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs index d695b776..7d70984f 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -289,16 +289,8 @@ internal void SetOutputCase() default: // Snake case #if NET8_0_OR_GREATER // If is default (Not Set) and JsonOptions provided with DictionaryKeyPolicy or PropertyNamingPolicy, use it - if (_jsonOptions.DictionaryKeyPolicy != null || _jsonOptions.PropertyNamingPolicy != null) - { - _jsonOptions.DictionaryKeyPolicy = _jsonOptions.DictionaryKeyPolicy; - _jsonOptions.PropertyNamingPolicy = _jsonOptions.PropertyNamingPolicy; - } - else - { - _jsonOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; - _jsonOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; - } + _jsonOptions.DictionaryKeyPolicy ??= JsonNamingPolicy.SnakeCaseLower; + _jsonOptions.PropertyNamingPolicy ??= JsonNamingPolicy.SnakeCaseLower; #else _jsonOptions.PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance; _jsonOptions.DictionaryKeyPolicy = SnakeCaseNamingPolicy.Instance; diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormattingTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormattingTests.cs index 9f7a0ff9..d5effd4a 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormattingTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormattingTests.cs @@ -544,6 +544,7 @@ public void Should_Log_Multiple_Formats_No_Duplicates() [Fact] public void Should_Log_Multiple_Formats() { + LambdaLifecycleTracker.Reset(); var output = new TestLoggerOutput(); var logger = LoggerFactory.Create(builder => { @@ -556,32 +557,80 @@ public void Should_Log_Multiple_Formats() }); }).CreatePowertoolsLogger(); - - var user = new User { FirstName = "John", LastName = "Doe", Age = 42 }; - Logger.LogInformation(user, "{Name} and is {Age} years old", new object[]{user.FirstName, user.Age}); - //{"level":"Information","message":"John and is 42 years old","timestamp":"2025-04-04T21:53:35.2085220Z","service":"log-level-test-service","cold_start":true,"name":"AWS.Lambda.Powertools.Logging.Logger","first_name":"John","last_name":"Doe","age":42} + Logger.LogInformation(user, "{Name} is {Age} years old", new object[]{user.FirstName, user.Age}); + + var logOutput = output.ToString(); + Assert.Contains("\"level\":\"Information\"", logOutput); + Assert.Contains("\"message\":\"John is 42 years old\"", logOutput); + Assert.Contains("\"service\":\"log-level-test-service\"", logOutput); + Assert.Contains("\"name\":\"AWS.Lambda.Powertools.Logging.Logger\"", logOutput); + Assert.Contains("\"first_name\":\"John\"", logOutput); + Assert.Contains("\"last_name\":\"Doe\"", logOutput); + Assert.Contains("\"age\":42", logOutput); + + output.Clear(); + + // Message template string Logger.LogInformation("{user}", user); - //{"level":"Information","message":"Doe, John (42)","timestamp":"2025-04-04T21:53:35.2419180Z","service":"log-level-test-service","cold_start":true,"name":"AWS.Lambda.Powertools.Logging.Logger","user":"Doe, John (42)"} + logOutput = output.ToString(); + Assert.Contains("\"level\":\"Information\"", logOutput); + Assert.Contains("\"message\":\"Doe, John (42)\"", logOutput); + Assert.Contains("\"service\":\"log-level-test-service\"", logOutput); + Assert.Contains("\"name\":\"AWS.Lambda.Powertools.Logging.Logger\"", logOutput); + Assert.Contains("\"user\":\"Doe, John (42)\"", logOutput); + // Verify user properties are NOT included in output (since @ prefix wasn't used) + Assert.DoesNotContain("\"first_name\":", logOutput); + Assert.DoesNotContain("\"last_name\":", logOutput); + Assert.DoesNotContain("\"age\":", logOutput); + + output.Clear(); + + // Object serialization with @ prefix Logger.LogInformation("{@user}", user); - //{"level":"Information","message":"Doe, John (42)","timestamp":"2025-04-04T21:53:35.2422190Z","service":"log-level-test-service","cold_start":true,"name":"AWS.Lambda.Powertools.Logging.Logger","user":{"first_name":"John","last_name":"Doe","age":42}} - Logger.LogInformation("{cold_start}", user); - //{"level":"Information","message":"Doe, John (42)","timestamp":"2025-04-04T21:53:35.2440630Z","service":"log-level-test-service","cold_start":true,"name":"AWS.Lambda.Powertools.Logging.Logger","level":"Doe, John (42)"} + logOutput = output.ToString(); + Assert.Contains("\"level\":\"Information\"", logOutput); + Assert.Contains("\"message\":\"Doe, John (42)\"", logOutput); + Assert.Contains("\"service\":\"log-level-test-service\"", logOutput); + Assert.Contains("\"cold_start\":true", logOutput); + Assert.Contains("\"name\":\"AWS.Lambda.Powertools.Logging.Logger\"", logOutput); + // Verify serialized user object with all properties + Assert.Contains("\"user\":{", logOutput); + Assert.Contains("\"first_name\":\"John\"", logOutput); + Assert.Contains("\"last_name\":\"Doe\"", logOutput); + Assert.Contains("\"age\":42", logOutput); + Assert.Contains("\"name\":\"John Doe\"", logOutput); + Assert.Contains("\"time_stamp\":null", logOutput); + Assert.Contains("}", logOutput); + + output.Clear(); + + Logger.LogInformation("{cold_start}", false); - Logger.AppendKey("level", "Doe, John (42)"); + logOutput = output.ToString(); + // Assert that the reserved field wasn't replaced + Assert.Contains("\"cold_start\":true", logOutput); + Assert.DoesNotContain("\"cold_start\":false", logOutput); + + output.Clear(); + + Logger.AppendKey("level", "fakeLevel"); Logger.LogInformation("no override"); - //{"level":"Information","message":"Doe, John (42)","timestamp":"2025-04-04T21:55:58.1410950Z","service":"log-level-test-service","cold_start":true,"name":"AWS.Lambda.Powertools.Logging.Logger","level":"Doe, John (42)"} - var logOutput = output.ToString(); - _output.WriteLine(logOutput); + logOutput = output.ToString(); + + Assert.Contains("\"level\":\"Information\"", logOutput); + Assert.DoesNotContain("\"level\":\"fakeLevel\"", logOutput); + + _output.WriteLine(logOutput); } diff --git a/libraries/tests/e2e/infra/CoreStack.cs b/libraries/tests/e2e/infra/CoreStack.cs index d77c725a..15f3fd6d 100644 --- a/libraries/tests/e2e/infra/CoreStack.cs +++ b/libraries/tests/e2e/infra/CoreStack.cs @@ -6,6 +6,28 @@ namespace Infra { + public class ConstructArgs + { + public ConstructArgs(Construct scope, string id, Runtime runtime, Architecture architecture, string name, string sourcePath, string distPath) + { + Scope = scope; + Id = id; + Runtime = runtime; + Architecture = architecture; + Name = name; + SourcePath = sourcePath; + DistPath = distPath; + } + + public Construct Scope { get; private set; } + public string Id { get; private set; } + public Runtime Runtime { get; private set; } + public Architecture Architecture { get; private set; } + public string Name { get; private set; } + public string SourcePath { get; private set; } + public string DistPath { get; private set; } + } + public class CoreStack : Stack { internal CoreStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props) @@ -20,27 +42,22 @@ private void CreateFunctionConstructs(string utility) var basePath = $"../functions/core/{utility}/Function/src/Function"; var distPath = $"../functions/core/{utility}/Function/dist"; - CreateFunctionConstruct(this, $"{utility}_X64_net8", Runtime.DOTNET_8, Architecture.X86_64, - $"E2ETestLambda_X64_NET8_{utility}", basePath, distPath); - CreateFunctionConstruct(this, $"{utility}_arm_net8", Runtime.DOTNET_8, Architecture.ARM_64, - $"E2ETestLambda_ARM_NET8_{utility}", basePath, distPath); - CreateFunctionConstruct(this, $"{utility}_X64_net6", Runtime.DOTNET_6, Architecture.X86_64, - $"E2ETestLambda_X64_NET6_{utility}", basePath, distPath); - CreateFunctionConstruct(this, $"{utility}_arm_net6", Runtime.DOTNET_6, Architecture.ARM_64, - $"E2ETestLambda_ARM_NET6_{utility}", basePath, distPath); + CreateFunctionConstruct(new ConstructArgs(this, $"{utility}_X64_net8", Runtime.DOTNET_8, Architecture.X86_64, $"E2ETestLambda_X64_NET8_{utility}", basePath, distPath)); + CreateFunctionConstruct(new ConstructArgs(this, $"{utility}_arm_net8", Runtime.DOTNET_8, Architecture.ARM_64, $"E2ETestLambda_ARM_NET8_{utility}", basePath, distPath)); + CreateFunctionConstruct(new ConstructArgs(this, $"{utility}_X64_net6", Runtime.DOTNET_6, Architecture.X86_64, $"E2ETestLambda_X64_NET6_{utility}", basePath, distPath)); + CreateFunctionConstruct(new ConstructArgs(this, $"{utility}_arm_net6", Runtime.DOTNET_6, Architecture.ARM_64, $"E2ETestLambda_ARM_NET6_{utility}", basePath, distPath)); } - private void CreateFunctionConstruct(Construct scope, string id, Runtime runtime, Architecture architecture, - string name, string sourcePath, string distPath) + private void CreateFunctionConstruct(ConstructArgs constructArgs) { - _ = new FunctionConstruct(scope, id, new FunctionConstructProps + _ = new FunctionConstruct(constructArgs.Scope, constructArgs.Id, new FunctionConstructProps { - Runtime = runtime, - Architecture = architecture, - Name = name, + Runtime = constructArgs.Runtime, + Architecture = constructArgs.Architecture, + Name = constructArgs.Name, Handler = "Function::Function.Function::FunctionHandler", - SourcePath = sourcePath, - DistPath = distPath, + SourcePath = constructArgs.SourcePath, + DistPath = constructArgs.DistPath, }); } } From 738dbf5332a52b9d393f2a2a59232b9d6ef9ae60 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 7 Apr 2025 16:38:13 +0100 Subject: [PATCH 43/49] sonar refactor --- .../Buffer/PowertoolsBufferingLogger.cs | 16 +++---- .../Internal/PowertoolsLogger.cs | 16 +++---- libraries/tests/e2e/infra-aot/CoreAotStack.cs | 44 ++++++++++++++----- 3 files changed, 48 insertions(+), 28 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs index 36f4f3ed..fb2f32c0 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/Buffer/PowertoolsBufferingLogger.cs @@ -24,23 +24,23 @@ namespace AWS.Lambda.Powertools.Logging.Internal; /// internal class PowertoolsBufferingLogger : ILogger { - private readonly ILogger _innerLogger; + private readonly ILogger _logger; private readonly Func _getCurrentConfig; private readonly LogBuffer _buffer; public PowertoolsBufferingLogger( - ILogger innerLogger, + ILogger logger, Func getCurrentConfig, IPowertoolsConfigurations powertoolsConfigurations) { - _innerLogger = innerLogger; + _logger = logger; _getCurrentConfig = getCurrentConfig; _buffer = new LogBuffer(powertoolsConfigurations); } public IDisposable BeginScope(TState state) { - return _innerLogger.BeginScope(state); + return _logger.BeginScope(state); } public bool IsEnabled(LogLevel logLevel) @@ -66,7 +66,7 @@ public void Log( // Add to buffer instead of logging try { - if (_innerLogger is PowertoolsLogger powertoolsLogger) + if (_logger is PowertoolsLogger powertoolsLogger) { var logEntry = powertoolsLogger.LogEntryString(logLevel, state, exception, formatter); @@ -89,7 +89,7 @@ public void Log( // If buffering fails, try to log an error about it try { - _innerLogger.LogError(ex, "Failed to buffer log entry"); + _logger.LogError(ex, "Failed to buffer log entry"); } catch { @@ -115,7 +115,7 @@ public void FlushBuffer() { try { - if (_innerLogger is PowertoolsLogger powertoolsLogger) + if (_logger is PowertoolsLogger powertoolsLogger) { if (_buffer.HasEvictions) { @@ -137,7 +137,7 @@ public void FlushBuffer() // If the entire flush operation fails, try to log an error try { - _innerLogger.LogError(ex, "Failed to flush log buffer"); + _logger.LogError(ex, "Failed to flush log buffer"); } catch { diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs index 8f38e1d4..ccbac6c3 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs @@ -238,12 +238,10 @@ private Dictionary GetLogEntry(LogLevel logLevel, DateTime times { foreach (var (key, value) in structuredParameters) { - if (!string.IsNullOrWhiteSpace(key) && key != "json") + if (string.IsNullOrWhiteSpace(key) || key == "json") continue; + if (!IsLogConstantKey(key)) { - if (!IsLogConstantKey(key)) - { - logEntry.TryAdd(key, value); - } + logEntry.TryAdd(key, value); } } } @@ -551,15 +549,15 @@ private Dictionary ExtractStructuredParameters(TState st var statePropsArray = stateProps.ToArray(); // First pass - extract message template and identify format specifiers - ExtractFormatSpecifiers(ref messageTemplate, statePropsArray, formatSpecifiers); + ExtractFormatSpecifiers(ref messageTemplate, statePropsArray, formatSpecifiers); // Second pass - process values with extracted format specifiers - ProcessValuesWithSpecifiers(statePropsArray, formatSpecifiers, parameters); + ProcessValuesWithSpecifiers(statePropsArray, formatSpecifiers, parameters); return parameters; } - private void ProcessValuesWithSpecifiers(KeyValuePair[] statePropsArray, Dictionary formatSpecifiers, + private void ProcessValuesWithSpecifiers(KeyValuePair[] statePropsArray, Dictionary formatSpecifiers, Dictionary parameters) { foreach (var prop in statePropsArray) @@ -618,7 +616,7 @@ private void ProcessValuesWithSpecifiers(KeyValuePair[] } } - private static void ExtractFormatSpecifiers(ref string messageTemplate, KeyValuePair[] statePropsArray, + private static void ExtractFormatSpecifiers(ref string messageTemplate, KeyValuePair[] statePropsArray, Dictionary formatSpecifiers) { foreach (var prop in statePropsArray) diff --git a/libraries/tests/e2e/infra-aot/CoreAotStack.cs b/libraries/tests/e2e/infra-aot/CoreAotStack.cs index 2d85a941..29c51f81 100644 --- a/libraries/tests/e2e/infra-aot/CoreAotStack.cs +++ b/libraries/tests/e2e/infra-aot/CoreAotStack.cs @@ -6,6 +6,30 @@ namespace InfraAot; +public class ConstructArgs +{ + public ConstructArgs(Construct scope, string id, Runtime runtime, Architecture architecture, string name, string sourcePath, string distPath, string handler) + { + Scope = scope; + Id = id; + Runtime = runtime; + Architecture = architecture; + Name = name; + SourcePath = sourcePath; + DistPath = distPath; + Handler = handler; + } + + public Construct Scope { get; private set; } + public string Id { get; private set; } + public Runtime Runtime { get; private set; } + public Architecture Architecture { get; private set; } + public string Name { get; private set; } + public string SourcePath { get; private set; } + public string DistPath { get; private set; } + public string Handler { get; private set; } +} + public class CoreAotStack : Stack { private readonly Architecture _architecture; @@ -26,21 +50,19 @@ private void CreateFunctionConstructs(string utility, string function = "AOT-Fun var distAotPath = $"../functions/core/{utility}/{function}/dist/{function}"; var arch = _architecture == Architecture.X86_64 ? "X64" : "ARM"; - CreateFunctionConstruct(this, $"{utility}_{arch}_aot_net8_{function}", Runtime.DOTNET_8, _architecture, - $"E2ETestLambda_{arch}_AOT_NET8_{utility}_{function}", baseAotPath, distAotPath, function); + CreateFunctionConstruct(new ConstructArgs(this, $"{utility}_{arch}_aot_net8_{function}", Runtime.DOTNET_8, _architecture, $"E2ETestLambda_{arch}_AOT_NET8_{utility}_{function}", baseAotPath, distAotPath, function)); } - private void CreateFunctionConstruct(Construct scope, string id, Runtime runtime, Architecture architecture, - string name, string sourcePath, string distPath, string handler) + private void CreateFunctionConstruct(ConstructArgs constructArgs) { - _ = new FunctionConstruct(scope, id, new FunctionConstructProps + _ = new FunctionConstruct(constructArgs.Scope, constructArgs.Id, new FunctionConstructProps { - Runtime = runtime, - Architecture = architecture, - Name = name, - Handler = handler, - SourcePath = sourcePath, - DistPath = distPath, + Runtime = constructArgs.Runtime, + Architecture = constructArgs.Architecture, + Name = constructArgs.Name, + Handler = constructArgs.Handler, + SourcePath = constructArgs.SourcePath, + DistPath = constructArgs.DistPath, IsAot = true }); } From fe360e174b85fb17d3e813969999ce146dfdc0b1 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 7 Apr 2025 16:54:19 +0100 Subject: [PATCH 44/49] test coverage --- .../ConsoleWrapperTests.cs | 98 ++++++++++++ .../FactoryTests.cs | 148 ++++++++++++++++++ .../Utilities/Converters.cs | 108 +++++++++++++ 3 files changed, 354 insertions(+) create mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/FactoryTests.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/Converters.cs diff --git a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs index cbd513d8..979176a3 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs @@ -78,4 +78,102 @@ public void OverrideLambdaLogger_Should_Override_Console_Out() Console.SetOut(originalOut); } } + + [Fact] + public void WriteLine_WritesMessageToConsole() + { + // Arrange + var consoleWrapper = new ConsoleWrapper(); + var originalOutput = Console.Out; + using var stringWriter = new StringWriter(); + Console.SetOut(stringWriter); + + try + { + // Act + consoleWrapper.WriteLine("Test message"); + + // Assert + var output = stringWriter.ToString(); + Assert.Contains("Test message", output); + } + finally + { + // Restore original output + Console.SetOut(originalOutput); + } + } + + [Fact] + public void Error_WritesMessageToErrorOutput() + { + // Arrange + var consoleWrapper = new ConsoleWrapper(); + var writer = new StringWriter(); + + // This sets _override = true, preventing Error from creating a new stream + ConsoleWrapper.SetOut(writer); + Console.SetError(writer); + + // Act + consoleWrapper.Error("Error message"); + writer.Flush(); + + // Assert + var output = writer.ToString(); + Assert.Contains("Error message", output); + } + + [Fact] + public void SetOut_OverridesConsoleOutput() + { + // Arrange + var originalOutput = Console.Out; + using var stringWriter = new StringWriter(); + + try + { + // Act + typeof(ConsoleWrapper) + .GetMethod("SetOut", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static) + ?.Invoke(null, new object[] { stringWriter }); + + Console.WriteLine("Test override"); + + // Assert + var output = stringWriter.ToString(); + Assert.Contains("Test override", output); + } + finally + { + // Restore original output + Console.SetOut(originalOutput); + } + } + + [Fact] + public void StaticWriteLine_FormatsLogMessageCorrectly() + { + // Arrange + var originalOutput = Console.Out; + using var stringWriter = new StringWriter(); + Console.SetOut(stringWriter); + + try + { + // Act + typeof(ConsoleWrapper) + .GetMethod("WriteLine", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static, null, new[] { typeof(string), typeof(string) }, null) + ?.Invoke(null, new object[] { "INFO", "Test log message" }); + + // Assert + var output = stringWriter.ToString(); + Assert.Matches(@"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\tINFO\tTest log message", output); + } + finally + { + // Restore original output + Console.SetOut(originalOutput); + } + } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/FactoryTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/FactoryTests.cs new file mode 100644 index 00000000..c113ff07 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/FactoryTests.cs @@ -0,0 +1,148 @@ +using AWS.Lambda.Powertools.Logging.Internal; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace AWS.Lambda.Powertools.Logging.Tests; + +public class LoggingAspectFactoryTests +{ + [Fact] + public void GetInstance_ShouldReturnLoggingAspectInstance() + { + // Act + var result = LoggingAspectFactory.GetInstance(typeof(LoggingAspectFactoryTests)); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } +} + +public class PowertoolsLoggerFactoryTests + { + [Fact] + public void Constructor_WithLoggerFactory_CreatesPowertoolsLoggerFactory() + { + // Arrange + var mockFactory = Substitute.For(); + + // Act + var factory = new PowertoolsLoggerFactory(mockFactory); + + // Assert + Assert.NotNull(factory); + } + + [Fact] + public void DefaultConstructor_CreatesPowertoolsLoggerFactory() + { + // Act + var factory = new PowertoolsLoggerFactory(); + + // Assert + Assert.NotNull(factory); + } + + [Fact] + public void Create_WithConfigAction_ReturnsPowertoolsLoggerFactory() + { + // Act + var factory = PowertoolsLoggerFactory.Create(options => + { + options.Service = "TestService"; + }); + + // Assert + Assert.NotNull(factory); + } + + [Fact] + public void Create_WithConfiguration_ReturnsLoggerFactory() + { + // Arrange + var configuration = new PowertoolsLoggerConfiguration + { + Service = "TestService" + }; + + // Act + var factory = PowertoolsLoggerFactory.Create(configuration); + + // Assert + Assert.NotNull(factory); + } + + [Fact] + public void CreateBuilder_ReturnsLoggerBuilder() + { + // Act + var builder = PowertoolsLoggerFactory.CreateBuilder(); + + // Assert + Assert.NotNull(builder); + Assert.IsType(builder); + } + + [Fact] + public void CreateLogger_Generic_ReturnsLogger() + { + // Arrange + var mockFactory = Substitute.For(); + mockFactory.CreateLogger(Arg.Any()).Returns(Substitute.For()); + var factory = new PowertoolsLoggerFactory(mockFactory); + + // Act + var logger = factory.CreateLogger(); + + // Assert + Assert.NotNull(logger); + mockFactory.Received(1).CreateLogger(typeof(PowertoolsLoggerFactoryTests).FullName); + } + + [Fact] + public void CreateLogger_WithCategory_ReturnsLogger() + { + // Arrange + var mockFactory = Substitute.For(); + mockFactory.CreateLogger("TestCategory").Returns(Substitute.For()); + var factory = new PowertoolsLoggerFactory(mockFactory); + + // Act + var logger = factory.CreateLogger("TestCategory"); + + // Assert + Assert.NotNull(logger); + mockFactory.Received(1).CreateLogger("TestCategory"); + } + + [Fact] + public void CreatePowertoolsLogger_ReturnsPowertoolsLogger() + { + // Arrange + var mockFactory = Substitute.For(); + mockFactory.CreatePowertoolsLogger().Returns(Substitute.For()); + var factory = new PowertoolsLoggerFactory(mockFactory); + + // Act + var logger = factory.CreatePowertoolsLogger(); + + // Assert + Assert.NotNull(logger); + mockFactory.Received(1).CreatePowertoolsLogger(); + } + + [Fact] + public void Dispose_DisposesInnerFactory() + { + // Arrange + var mockFactory = Substitute.For(); + var factory = new PowertoolsLoggerFactory(mockFactory); + + // Act + factory.Dispose(); + + // Assert + mockFactory.Received(1).Dispose(); + } + } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/Converters.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/Converters.cs new file mode 100644 index 00000000..b0f58e26 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/Converters.cs @@ -0,0 +1,108 @@ +using System; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using AWS.Lambda.Powertools.Logging.Internal.Converters; +using Xunit; + +namespace AWS.Lambda.Powertools.Logging.Tests.Utilities; + +public class ByteArrayConverterTests + { + private readonly JsonSerializerOptions _options; + + public ByteArrayConverterTests() + { + _options = new JsonSerializerOptions(); + _options.Converters.Add(new ByteArrayConverter()); + } + + [Fact] + public void Write_WhenByteArrayIsNull_WritesNullValue() + { + // Arrange + var testObject = new TestClass { Data = null }; + + // Act + var json = JsonSerializer.Serialize(testObject, _options); + + // Assert + Assert.Contains("\"data\":null", json); + } + + [Fact] + public void Write_WithByteArray_WritesBase64String() + { + // Arrange + byte[] testData = { 1, 2, 3, 4, 5 }; + var testObject = new TestClass { Data = testData }; + var expectedBase64 = Convert.ToBase64String(testData); + + // Act + var json = JsonSerializer.Serialize(testObject, _options); + + // Assert + Assert.Contains($"\"data\":\"{expectedBase64}\"", json); + } + + [Fact] + public void Read_WithBase64String_ReturnsByteArray() + { + // Arrange + byte[] expectedData = { 1, 2, 3, 4, 5 }; + var base64 = Convert.ToBase64String(expectedData); + var json = $"{{\"data\":\"{base64}\"}}"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.Equal(expectedData, result.Data); + } + + [Fact] + public void Read_WithInvalidType_ThrowsJsonException() + { + // Arrange + var json = "{\"data\":123}"; + + // Act & Assert + Assert.Throws(() => + JsonSerializer.Deserialize(json, _options)); + } + + [Fact] + public void Read_WithEmptyString_ReturnsEmptyByteArray() + { + // Arrange + var json = "{\"data\":\"\"}"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.NotNull(result.Data); + Assert.Empty(result.Data); + } + + [Fact] + public void WriteAndRead_RoundTrip_PreservesData() + { + // Arrange + byte[] originalData = Encoding.UTF8.GetBytes("Test data with special chars: !@#$%^&*()"); + var testObject = new TestClass { Data = originalData }; + + // Act + var json = JsonSerializer.Serialize(testObject, _options); + var deserializedObject = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.Equal(originalData, deserializedObject.Data); + } + + private class TestClass + { + [JsonPropertyName("data")] + public byte[] Data { get; set; } + } + } \ No newline at end of file From 99073bafc03cd768c0f14cbf52896a9b0ed1e240 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 7 Apr 2025 17:15:02 +0100 Subject: [PATCH 45/49] fix console tests --- .../Core/ConsoleWrapper.cs | 5 +++ .../ConsoleWrapperTests.cs | 37 ++++++------------- .../Handlers/FunctionHandlerTests.cs | 1 + 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs index 36d8aeea..c71c05ae 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs @@ -72,4 +72,9 @@ internal static void WriteLine(string logLevel, string message) { Console.WriteLine($"{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ss.fffZ}\t{logLevel}\t{message}"); } + + public static void ResetForTest() + { + _override = false; + } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs index 979176a3..6aea7bdb 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs @@ -4,7 +4,7 @@ namespace AWS.Lambda.Powertools.Common.Tests; -public class ConsoleWrapperTests +public class ConsoleWrapperTests : IDisposable { [Fact] public void WriteLine_Should_Write_To_Console() @@ -61,11 +61,11 @@ public void OverrideLambdaLogger_Should_Override_Console_Out() try { var consoleWrapper = new ConsoleWrapper(); - + // Act - create a custom StringWriter and set it after constructor // but before WriteLine (which triggers OverrideLambdaLogger) var writer = new StringWriter(); - Console.SetOut(writer); + ConsoleWrapper.SetOut(writer); consoleWrapper.WriteLine("test message"); @@ -75,7 +75,7 @@ public void OverrideLambdaLogger_Should_Override_Console_Out() finally { // Restore original console out - Console.SetOut(originalOut); + ConsoleWrapper.ResetForTest(); } } @@ -86,7 +86,7 @@ public void WriteLine_WritesMessageToConsole() var consoleWrapper = new ConsoleWrapper(); var originalOutput = Console.Out; using var stringWriter = new StringWriter(); - Console.SetOut(stringWriter); + ConsoleWrapper.SetOut(stringWriter); try { @@ -100,30 +100,10 @@ public void WriteLine_WritesMessageToConsole() finally { // Restore original output - Console.SetOut(originalOutput); + ConsoleWrapper.ResetForTest(); } } - [Fact] - public void Error_WritesMessageToErrorOutput() - { - // Arrange - var consoleWrapper = new ConsoleWrapper(); - var writer = new StringWriter(); - - // This sets _override = true, preventing Error from creating a new stream - ConsoleWrapper.SetOut(writer); - Console.SetError(writer); - - // Act - consoleWrapper.Error("Error message"); - writer.Flush(); - - // Assert - var output = writer.ToString(); - Assert.Contains("Error message", output); - } - [Fact] public void SetOut_OverridesConsoleOutput() { @@ -176,4 +156,9 @@ public void StaticWriteLine_FormatsLogMessageCorrectly() Console.SetOut(originalOutput); } } + + public void Dispose() + { + ConsoleWrapper.ResetForTest(); + } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs index 462b183c..7b053739 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs @@ -417,5 +417,6 @@ public void Dispose() { Metrics.ResetForTest(); MetricsAspect.ResetForTest(); + ConsoleWrapper.ResetForTest(); } } \ No newline at end of file From 98659cecb51e131950cdb28a01c48c21d8aa7e27 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 7 Apr 2025 17:28:10 +0100 Subject: [PATCH 46/49] more test coverage --- .../PowertoolsLoggingSerializer.cs | 19 -- .../PowertoolsLoggingSerializerTests.cs | 193 ++++++++++++++++++ 2 files changed, 193 insertions(+), 19 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs index 7d70984f..9e38dcbf 100644 --- a/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs +++ b/libraries/src/AWS.Lambda.Powertools.Logging/Serializers/PowertoolsLoggingSerializer.cs @@ -181,25 +181,6 @@ private IJsonTypeInfoResolver GetCompositeResolver() return new CompositeJsonTypeInfoResolver(resolvers.ToArray()); } - /// - /// Handles the TypeInfoResolver from the JsonSerializerOptions. - /// - private void HandleJsonOptionsTypeResolver(JsonSerializerOptions options) - { - // Check for TypeInfoResolver and ensure it's not lost - if (options?.TypeInfoResolver != null && - options.TypeInfoResolver != GetCompositeResolver()) - { - _customTypeInfoResolver = options.TypeInfoResolver; - - // If it's a JsonSerializerContext, also add it to our contexts - if (_customTypeInfoResolver is JsonSerializerContext jsonContext) - { - AddSerializerContext(jsonContext); - } - } - } - /// /// Gets the JsonTypeInfo for a given type. /// diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs index fb2397d0..c83ef324 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs @@ -6,6 +6,7 @@ using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using Amazon.Lambda.Serialization.SystemTextJson; +using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Common.Utils; using AWS.Lambda.Powertools.Logging.Internal; using AWS.Lambda.Powertools.Logging.Internal.Converters; @@ -400,6 +401,198 @@ private class ComplexTestObject public TimeOnly Time { get; set; } #endif } + + [Fact] + public void ConfigureNamingPolicy_WhenChanged_RebuildsOptions() + { + // Arrange + var serializer = new PowertoolsLoggingSerializer(); + + // Force initialization of _jsonOptions + _ = serializer.GetSerializerOptions(); + + // Act + serializer.ConfigureNamingPolicy(LoggerOutputCase.CamelCase); + var options = serializer.GetSerializerOptions(); + + // Assert + Assert.Equal(JsonNamingPolicy.CamelCase, options.PropertyNamingPolicy); + Assert.Equal(JsonNamingPolicy.CamelCase, options.DictionaryKeyPolicy); + } + + [Fact] + public void ConfigureNamingPolicy_WhenAlreadySet_DoesNothing() + { + // Arrange + var serializer = new PowertoolsLoggingSerializer(); + serializer.ConfigureNamingPolicy(LoggerOutputCase.CamelCase); + + // Get the initial options + var initialOptions = serializer.GetSerializerOptions(); + + // Act - set the same case again + serializer.ConfigureNamingPolicy(LoggerOutputCase.CamelCase); + var newOptions = serializer.GetSerializerOptions(); + + // Assert - should be the same instance + Assert.Same(initialOptions, newOptions); + } + + [Fact] + public void Serialize_WithValidObject_ReturnsJsonString() + { + // Arrange + var serializer = new PowertoolsLoggingSerializer(); + var testObj = new TestClass { Name = "Test", Value = 123 }; + + // Act + var json = serializer.Serialize(testObj, typeof(TestClass)); + + // Assert + Assert.Contains("\"name\"", json); + Assert.Contains("\"value\"", json); + Assert.Contains("123", json); + Assert.Contains("Test", json); + } + +#if NET8_0_OR_GREATER + [Fact] + public void AddSerializerContext_AddsContext() + { + // Arrange + var serializer = new PowertoolsLoggingSerializer(); + var context = new TestJsonContext(new JsonSerializerOptions()); + + // Act + serializer.AddSerializerContext(context); + + // No immediate assertion - the context is added internally + // We'll verify it works through serialization tests + } + + [Fact] + public void SetOptions_WithTypeInfoResolver_SetsCustomResolver() + { + // Arrange + var serializer = new PowertoolsLoggingSerializer(); + + // Explicitly disable dynamic code - important to set before creating options + RuntimeFeatureWrapper.SetIsDynamicCodeSupported(false); + + var context = new TestJsonContext(new JsonSerializerOptions()); + var options = new JsonSerializerOptions + { + TypeInfoResolver = context + }; + + // Act + serializer.SetOptions(options); + var serializerOptions = serializer.GetSerializerOptions(); + + // Assert - options are properly configured + Assert.NotNull(serializerOptions.TypeInfoResolver); + } + + [Fact] + public void SetOptions_WithContextAsResolver_AddsToContexts() + { + // Arrange + var serializer = new PowertoolsLoggingSerializer(); + var context = new TestJsonContext(new JsonSerializerOptions()); + var options = new JsonSerializerOptions + { + TypeInfoResolver = context + }; + + // Act - This adds the context automatically + serializer.SetOptions(options); + + // No direct assertion possible for internal state, but we can test it works + // through proper serialization + } +#endif + + [Fact] + public void SetOutputCase_CamelCase_SetsPoliciesCorrectly() + { + // Arrange + var serializer = new PowertoolsLoggingSerializer(); + serializer.ConfigureNamingPolicy(LoggerOutputCase.CamelCase); + + // Act + var options = serializer.GetSerializerOptions(); + + // Assert + Assert.Equal(JsonNamingPolicy.CamelCase, options.PropertyNamingPolicy); + Assert.Equal(JsonNamingPolicy.CamelCase, options.DictionaryKeyPolicy); + } + + [Fact] + public void SetOutputCase_PascalCase_SetsPoliciesCorrectly() + { + // Arrange + var serializer = new PowertoolsLoggingSerializer(); + serializer.ConfigureNamingPolicy(LoggerOutputCase.PascalCase); + + // Act + var options = serializer.GetSerializerOptions(); + + // Assert + Assert.IsType(options.PropertyNamingPolicy); + Assert.IsType(options.DictionaryKeyPolicy); + } + + [Fact] + public void SetOutputCase_SnakeCase_SetsPoliciesCorrectly() + { + // Arrange + var serializer = new PowertoolsLoggingSerializer(); + serializer.ConfigureNamingPolicy(LoggerOutputCase.SnakeCase); + + // Act + var options = serializer.GetSerializerOptions(); + +#if NET8_0_OR_GREATER + // Assert - in .NET 8 we use built-in SnakeCaseLower + Assert.Equal(JsonNamingPolicy.SnakeCaseLower, options.PropertyNamingPolicy); + Assert.Equal(JsonNamingPolicy.SnakeCaseLower, options.DictionaryKeyPolicy); +#else + // Assert - in earlier versions, we use custom SnakeCaseNamingPolicy + Assert.IsType(options.PropertyNamingPolicy); + Assert.IsType(options.DictionaryKeyPolicy); +#endif + } + + [Fact] + public void GetSerializerOptions_AddsAllConverters() + { + // Arrange + var serializer = new PowertoolsLoggingSerializer(); + + // Act + var options = serializer.GetSerializerOptions(); + + // Assert + Assert.Contains(options.Converters, c => c is ByteArrayConverter); + Assert.Contains(options.Converters, c => c is ExceptionConverter); + Assert.Contains(options.Converters, c => c is MemoryStreamConverter); + Assert.Contains(options.Converters, c => c is ConstantClassConverter); + Assert.Contains(options.Converters, c => c is DateOnlyConverter); + Assert.Contains(options.Converters, c => c is TimeOnlyConverter); +#if NET8_0_OR_GREATER || NET6_0 + Assert.Contains(options.Converters, c => c is LogLevelJsonConverter); +#endif + } + + // Test class for serialization + private class TestClass + { + public string Name { get; set; } + public int Value { get; set; } + } + + + public void Dispose() { From 592b9315883d4bbc27865db4bffca2bf0063a650 Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 7 Apr 2025 18:14:05 +0100 Subject: [PATCH 47/49] fix sonar add coverage --- .../PowertoolsLoggingSerializerTests.cs | 31 -- .../Utilities/Converters.cs | 267 +++++++++++------- libraries/tests/e2e/infra-aot/CoreAotStack.cs | 42 +-- 3 files changed, 191 insertions(+), 149 deletions(-) diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs index c83ef324..58b42e3f 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/PowertoolsLoggingSerializerTests.cs @@ -456,19 +456,6 @@ public void Serialize_WithValidObject_ReturnsJsonString() } #if NET8_0_OR_GREATER - [Fact] - public void AddSerializerContext_AddsContext() - { - // Arrange - var serializer = new PowertoolsLoggingSerializer(); - var context = new TestJsonContext(new JsonSerializerOptions()); - - // Act - serializer.AddSerializerContext(context); - - // No immediate assertion - the context is added internally - // We'll verify it works through serialization tests - } [Fact] public void SetOptions_WithTypeInfoResolver_SetsCustomResolver() @@ -492,24 +479,6 @@ public void SetOptions_WithTypeInfoResolver_SetsCustomResolver() // Assert - options are properly configured Assert.NotNull(serializerOptions.TypeInfoResolver); } - - [Fact] - public void SetOptions_WithContextAsResolver_AddsToContexts() - { - // Arrange - var serializer = new PowertoolsLoggingSerializer(); - var context = new TestJsonContext(new JsonSerializerOptions()); - var options = new JsonSerializerOptions - { - TypeInfoResolver = context - }; - - // Act - This adds the context automatically - serializer.SetOptions(options); - - // No direct assertion possible for internal state, but we can test it works - // through proper serialization - } #endif [Fact] diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/Converters.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/Converters.cs index b0f58e26..b7e975e4 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/Converters.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/Converters.cs @@ -8,101 +8,174 @@ namespace AWS.Lambda.Powertools.Logging.Tests.Utilities; public class ByteArrayConverterTests +{ + private readonly JsonSerializerOptions _options; + + public ByteArrayConverterTests() + { + _options = new JsonSerializerOptions(); + _options.Converters.Add(new ByteArrayConverter()); + } + + [Fact] + public void Write_WhenByteArrayIsNull_WritesNullValue() + { + // Arrange + var testObject = new TestClass { Data = null }; + + // Act + var json = JsonSerializer.Serialize(testObject, _options); + + // Assert + Assert.Contains("\"data\":null", json); + } + + [Fact] + public void Write_WithByteArray_WritesBase64String() + { + // Arrange + byte[] testData = { 1, 2, 3, 4, 5 }; + var testObject = new TestClass { Data = testData }; + var expectedBase64 = Convert.ToBase64String(testData); + + // Act + var json = JsonSerializer.Serialize(testObject, _options); + + // Assert + Assert.Contains($"\"data\":\"{expectedBase64}\"", json); + } + + [Fact] + public void Read_WithBase64String_ReturnsByteArray() + { + // Arrange + byte[] expectedData = { 1, 2, 3, 4, 5 }; + var base64 = Convert.ToBase64String(expectedData); + var json = $"{{\"data\":\"{base64}\"}}"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.Equal(expectedData, result.Data); + } + + [Fact] + public void Read_WithInvalidType_ThrowsJsonException() + { + // Arrange + var json = "{\"data\":123}"; + + // Act & Assert + Assert.Throws(() => + JsonSerializer.Deserialize(json, _options)); + } + + [Fact] + public void Read_WithEmptyString_ReturnsEmptyByteArray() + { + // Arrange + var json = "{\"data\":\"\"}"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.NotNull(result.Data); + Assert.Empty(result.Data); + } + + [Fact] + public void WriteAndRead_RoundTrip_PreservesData() + { + // Arrange + byte[] originalData = Encoding.UTF8.GetBytes("Test data with special chars: !@#$%^&*()"); + var testObject = new TestClass { Data = originalData }; + + // Act + var json = JsonSerializer.Serialize(testObject, _options); + var deserializedObject = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.Equal(originalData, deserializedObject.Data); + } + + private class TestClass + { + [JsonPropertyName("data")] public byte[] Data { get; set; } + } + + [Fact] + public void ByteArrayConverter_Write_ShouldHandleNullValue() + { + // Arrange + var converter = new ByteArrayConverter(); + var options = new JsonSerializerOptions(); + var testObject = new { Data = (byte[])null }; + + // Act + var json = JsonSerializer.Serialize(testObject, options); + + // Assert + Assert.Contains("\"Data\":null", json); + } + + [Fact] + public void ByteArrayConverter_Read_ShouldHandleNullToken() + { + // Arrange + var converter = new ByteArrayConverter(); + var json = "{\"Data\":null}"; + var options = new JsonSerializerOptions(); + options.Converters.Add(converter); + + // Act + var result = JsonSerializer.Deserialize(json, options); + + // Assert + Assert.Null(result.Data); + } + + [Fact] + public void ByteArrayConverter_Read_ShouldHandleStringToken() + { + // Arrange + var converter = new ByteArrayConverter(); + var expectedBytes = new byte[] { 1, 2, 3, 4 }; + var base64String = Convert.ToBase64String(expectedBytes); + var json = $"{{\"Data\":\"{base64String}\"}}"; + + var options = new JsonSerializerOptions(); + options.Converters.Add(converter); + + // Act + var result = JsonSerializer.Deserialize(json, options); + + // Assert + Assert.NotNull(result.Data); + Assert.Equal(expectedBytes, result.Data); + } + + [Fact] + public void ByteArrayConverter_Read_ShouldThrowOnInvalidToken() + { + // Arrange + var converter = new ByteArrayConverter(); + var json = "{\"Data\":123}"; // Number instead of string + + var options = new JsonSerializerOptions(); + options.Converters.Add(converter); + + // Act & Assert + var ex = Assert.Throws(() => + JsonSerializer.Deserialize(json, options)); + + Assert.Contains("Expected string value for byte array", ex.Message); + } + +// Helper class for testing byte array deserialization + private class TestByteArrayClass { - private readonly JsonSerializerOptions _options; - - public ByteArrayConverterTests() - { - _options = new JsonSerializerOptions(); - _options.Converters.Add(new ByteArrayConverter()); - } - - [Fact] - public void Write_WhenByteArrayIsNull_WritesNullValue() - { - // Arrange - var testObject = new TestClass { Data = null }; - - // Act - var json = JsonSerializer.Serialize(testObject, _options); - - // Assert - Assert.Contains("\"data\":null", json); - } - - [Fact] - public void Write_WithByteArray_WritesBase64String() - { - // Arrange - byte[] testData = { 1, 2, 3, 4, 5 }; - var testObject = new TestClass { Data = testData }; - var expectedBase64 = Convert.ToBase64String(testData); - - // Act - var json = JsonSerializer.Serialize(testObject, _options); - - // Assert - Assert.Contains($"\"data\":\"{expectedBase64}\"", json); - } - - [Fact] - public void Read_WithBase64String_ReturnsByteArray() - { - // Arrange - byte[] expectedData = { 1, 2, 3, 4, 5 }; - var base64 = Convert.ToBase64String(expectedData); - var json = $"{{\"data\":\"{base64}\"}}"; - - // Act - var result = JsonSerializer.Deserialize(json, _options); - - // Assert - Assert.Equal(expectedData, result.Data); - } - - [Fact] - public void Read_WithInvalidType_ThrowsJsonException() - { - // Arrange - var json = "{\"data\":123}"; - - // Act & Assert - Assert.Throws(() => - JsonSerializer.Deserialize(json, _options)); - } - - [Fact] - public void Read_WithEmptyString_ReturnsEmptyByteArray() - { - // Arrange - var json = "{\"data\":\"\"}"; - - // Act - var result = JsonSerializer.Deserialize(json, _options); - - // Assert - Assert.NotNull(result.Data); - Assert.Empty(result.Data); - } - - [Fact] - public void WriteAndRead_RoundTrip_PreservesData() - { - // Arrange - byte[] originalData = Encoding.UTF8.GetBytes("Test data with special chars: !@#$%^&*()"); - var testObject = new TestClass { Data = originalData }; - - // Act - var json = JsonSerializer.Serialize(testObject, _options); - var deserializedObject = JsonSerializer.Deserialize(json, _options); - - // Assert - Assert.Equal(originalData, deserializedObject.Data); - } - - private class TestClass - { - [JsonPropertyName("data")] - public byte[] Data { get; set; } - } - } \ No newline at end of file + public byte[] Data { get; set; } + } +} \ No newline at end of file diff --git a/libraries/tests/e2e/infra-aot/CoreAotStack.cs b/libraries/tests/e2e/infra-aot/CoreAotStack.cs index 29c51f81..282fce25 100644 --- a/libraries/tests/e2e/infra-aot/CoreAotStack.cs +++ b/libraries/tests/e2e/infra-aot/CoreAotStack.cs @@ -8,26 +8,14 @@ namespace InfraAot; public class ConstructArgs { - public ConstructArgs(Construct scope, string id, Runtime runtime, Architecture architecture, string name, string sourcePath, string distPath, string handler) - { - Scope = scope; - Id = id; - Runtime = runtime; - Architecture = architecture; - Name = name; - SourcePath = sourcePath; - DistPath = distPath; - Handler = handler; - } - - public Construct Scope { get; private set; } - public string Id { get; private set; } - public Runtime Runtime { get; private set; } - public Architecture Architecture { get; private set; } - public string Name { get; private set; } - public string SourcePath { get; private set; } - public string DistPath { get; private set; } - public string Handler { get; private set; } + public Construct Scope { get; set; } + public string Id { get; set; } + public Runtime Runtime { get; set; } + public Architecture Architecture { get; set; } + public string Name { get; set; } + public string SourcePath { get; set; } + public string DistPath { get; set; } + public string Handler { get; set; } } public class CoreAotStack : Stack @@ -50,7 +38,19 @@ private void CreateFunctionConstructs(string utility, string function = "AOT-Fun var distAotPath = $"../functions/core/{utility}/{function}/dist/{function}"; var arch = _architecture == Architecture.X86_64 ? "X64" : "ARM"; - CreateFunctionConstruct(new ConstructArgs(this, $"{utility}_{arch}_aot_net8_{function}", Runtime.DOTNET_8, _architecture, $"E2ETestLambda_{arch}_AOT_NET8_{utility}_{function}", baseAotPath, distAotPath, function)); + var construct = new ConstructArgs + { + Scope = this, + Id = $"{utility}_{arch}_aot_net8_{function}", + Runtime = Runtime.DOTNET_8, + Architecture = _architecture, + Name = $"E2ETestLambda_{arch}_AOT_NET8_{utility}_{function}", + SourcePath = baseAotPath, + DistPath = distAotPath, + Handler = $"{function}.Function::AWS.Lambda.Powertools.{utility}.{function}.Function.FunctionHandler" + }; + + CreateFunctionConstruct(construct); } private void CreateFunctionConstruct(ConstructArgs constructArgs) From 8e2e167e7f903cf89dcca4a66f85e2a6423ebe1e Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Mon, 7 Apr 2025 18:21:12 +0100 Subject: [PATCH 48/49] fix test --- .../ConsoleWrapperTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs index 6aea7bdb..fdc79d95 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/ConsoleWrapperTests.cs @@ -127,7 +127,7 @@ public void SetOut_OverridesConsoleOutput() finally { // Restore original output - Console.SetOut(originalOutput); + ConsoleWrapper.ResetForTest(); } } @@ -137,7 +137,7 @@ public void StaticWriteLine_FormatsLogMessageCorrectly() // Arrange var originalOutput = Console.Out; using var stringWriter = new StringWriter(); - Console.SetOut(stringWriter); + ConsoleWrapper.SetOut(stringWriter); try { @@ -153,7 +153,7 @@ public void StaticWriteLine_FormatsLogMessageCorrectly() finally { // Restore original output - Console.SetOut(originalOutput); + ConsoleWrapper.ResetForTest(); } } From 399ba3672d64ffa9ce8f88b10d4a0fc9767d9f6e Mon Sep 17 00:00:00 2001 From: Henrique <999396+hjgraca@users.noreply.github.com> Date: Tue, 8 Apr 2025 10:43:27 +0100 Subject: [PATCH 49/49] remove duplicate --- libraries/src/AWS.Lambda.Powertools.Common/Core/Constants.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/Constants.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/Constants.cs index 83fd0e46..456a5092 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/Constants.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/Constants.cs @@ -20,8 +20,6 @@ namespace AWS.Lambda.Powertools.Common; /// internal static class Constants { - internal const string AWSInitializationTypeEnv = "AWS_LAMBDA_INITIALIZATION_TYPE"; - /// /// Constant for AWS_LAMBDA_INITIALIZATION_TYPE environment variable /// This is used to determine if the Lambda function is running in provisioned concurrency mode