From 5819fa67110ab0859f288a27737b13bbd64c8048 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Wed, 10 Sep 2025 17:33:40 -0700 Subject: [PATCH 1/2] Fix NullReferenceException when using structured logging and having a null message template --- .../84836f7b-8e69-44a1-87dc-22971f426990.json | 18 ++++++++ .../LambdaILogger.cs | 5 +++ .../Helpers/ConsoleLoggerWriter.cs | 2 +- .../Logging/AbstractLogMessageFormatter.cs | 4 +- .../Logging/JsonLogMessageFormatter.cs | 10 ++--- .../LoggingTests.cs | 43 ++++++++++++++++++- .../LogMessageFormatterTests.cs | 36 +++++++++++++++- 7 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 .autover/changes/84836f7b-8e69-44a1-87dc-22971f426990.json diff --git a/.autover/changes/84836f7b-8e69-44a1-87dc-22971f426990.json b/.autover/changes/84836f7b-8e69-44a1-87dc-22971f426990.json new file mode 100644 index 000000000..fdb34a9c5 --- /dev/null +++ b/.autover/changes/84836f7b-8e69-44a1-87dc-22971f426990.json @@ -0,0 +1,18 @@ +{ + "Projects": [ + { + "Name": "Amazon.Lambda.RuntimeSupport", + "Type": "Patch", + "ChangelogMessages": [ + "Fix NullReferenceException when logging a null logging message with structured logging enabled." + ] + }, + { + "Name": "Amazon.Lambda.Logging.AspNetCore", + "Type": "Patch", + "ChangelogMessages": [ + "Fix NullReferenceException when logging a null logging message with structured logging enabled." + ] + } + ] +} \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs b/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs index 0482c91d7..2898a8472 100644 --- a/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs +++ b/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs @@ -70,6 +70,11 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except } } + if (messageTemplate == null) + { + messageTemplate = formatter.Invoke(state, exception); + } + Amazon.Lambda.Core.LambdaLogger.Log(lambdaLogLevel, exception, messageTemplate, parameters.ToArray()); } else diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs index d1cce94ee..0449673f2 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs @@ -401,7 +401,7 @@ internal void FormattedWriteLine(string level, Exception exeception, string mess var messageState = new MessageState(); - messageState.MessageTemplate = messageTemplate; + messageState.MessageTemplate = messageTemplate ?? string.Empty; messageState.MessageArguments = args; messageState.TimeStamp = DateTime.UtcNow; messageState.AwsRequestId = CurrentAwsRequestId; diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/AbstractLogMessageFormatter.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/AbstractLogMessageFormatter.cs index b400b494f..75535970f 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/AbstractLogMessageFormatter.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/AbstractLogMessageFormatter.cs @@ -1,4 +1,4 @@ -#if NET6_0_OR_GREATER +#if NET6_0_OR_GREATER using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -285,4 +285,4 @@ public bool UsingPositionalArguments(IReadOnlyList messagePrope } } } -#endif \ No newline at end of file +#endif diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/JsonLogMessageFormatter.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/JsonLogMessageFormatter.cs index e0e7f2b09..be94b1acc 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/JsonLogMessageFormatter.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/JsonLogMessageFormatter.cs @@ -1,4 +1,4 @@ -#if NET6_0_OR_GREATER +#if NET6_0_OR_GREATER using System; using System.Buffers; using System.Collections; @@ -48,15 +48,15 @@ public override string FormatMessage(MessageState state) if (state.MessageArguments?.Length == 0) { messageProperties = _emptyMessageProperties; - message = state.MessageTemplate; + message = state.MessageTemplate ?? string.Empty; } else { // Parse the message template for any message properties like "{count}". - messageProperties = ParseProperties(state.MessageTemplate); + messageProperties = ParseProperties(state.MessageTemplate ?? string.Empty); // Replace any message properties in the message template with the provided argument values. - message = ApplyMessageProperties(state.MessageTemplate, messageProperties, state.MessageArguments); + message = ApplyMessageProperties(state.MessageTemplate ?? string.Empty, messageProperties, state.MessageArguments); } @@ -310,4 +310,4 @@ private void FormatJsonValue(Utf8JsonWriter writer, object value, string formatA private static string ToInvariantString(object obj) => Convert.ToString(obj, CultureInfo.InvariantCulture); } } -#endif \ No newline at end of file +#endif diff --git a/Libraries/test/Amazon.Lambda.Logging.AspNetCore.Tests/LoggingTests.cs b/Libraries/test/Amazon.Lambda.Logging.AspNetCore.Tests/LoggingTests.cs index b854982ef..241237e00 100644 --- a/Libraries/test/Amazon.Lambda.Logging.AspNetCore.Tests/LoggingTests.cs +++ b/Libraries/test/Amazon.Lambda.Logging.AspNetCore.Tests/LoggingTests.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Text; using Xunit; namespace Amazon.Lambda.Tests @@ -581,6 +582,46 @@ public void TestJSONParameterLogging() } + [Fact] + public void JsonLoggingWithNoOriginalFormat() + { + Environment.SetEnvironmentVariable("AWS_LAMBDA_LOG_FORMAT", "JSON"); + try + { + using (var writer = new StringWriter()) + { + ConnectLoggingActionToLogger(message => writer.Write(message)); + + var configuration = new ConfigurationBuilder() + .AddJsonFile(GetAppSettingsPath("appsettings.json")) + .Build(); + + var loggerOptions = new LambdaLoggerOptions(configuration); + var loggerFactory = new TestLoggerFactory() + .AddLambdaLogger(loggerOptions); + + var logger = loggerFactory.CreateLogger("JSONLogging"); + + logger.Log(LogLevel.Error, new EventId(1), new Dictionary() { { "Param1", "Value1" } }, null, (state, e) => + { + var sb = new StringBuilder(); + foreach(var kvp in state) + { + sb.AppendFormat("{0}:{1}\n", kvp.Key, kvp.Value); + } + return sb.ToString(); + }); + + var text = writer.ToString(); + Assert.Contains("Param1:Value1", text); + } + } + finally + { + Environment.SetEnvironmentVariable("AWS_LAMBDA_LOG_FORMAT", null); + } + } + private static string GetAppSettingsPath(string fileName) { return Path.Combine(APPSETTINGS_DIR, fileName); @@ -609,7 +650,7 @@ private static void ConnectLoggingActionToLogger(Action loggingAction) loggingWithLevelActionField.SetValue(null, loggingWithLevelAction); Action loggingWithExceptionLevelAction = (level, exception, message, parameters) => { - var formattedMessage = $"{level}: {message}: parameter count: {parameters?.Length}\n{exception.Message}"; + var formattedMessage = $"{level}: {message}: parameter count: {parameters?.Length}\n{exception?.Message}"; loggingAction(formattedMessage); }; diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LogMessageFormatterTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LogMessageFormatterTests.cs index 74a0fd13c..b97638d0f 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LogMessageFormatterTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LogMessageFormatterTests.cs @@ -1,4 +1,4 @@ -using Amazon.Lambda.RuntimeSupport.Helpers; +using Amazon.Lambda.RuntimeSupport.Helpers; using Amazon.Lambda.RuntimeSupport.Helpers.Logging; using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities; using System; @@ -103,6 +103,40 @@ public void FormatJsonWithStringMessageProperties() Assert.Equal("Hello AWS", doc.RootElement.GetProperty("message").GetString()); } + [Fact] + public void FormatJsonNullMessageTemplate() + { + var timestamp = DateTime.UtcNow; + var formatter = new JsonLogMessageFormatter(); + var state = new MessageState() + { + MessageTemplate = null, + AwsRequestId = "1234", + Level = Helpers.LogLevelLoggerWriter.LogLevel.Warning, + TimeStamp = timestamp + }; + + var json = formatter.FormatMessage(state); + var doc = JsonDocument.Parse(json); + Assert.Equal(string.Empty, doc.RootElement.GetProperty("message").GetString()); + Assert.Equal("1234", doc.RootElement.GetProperty("requestId").GetString()); + + // Currently the arguments are ignored because they don't exist in the message template but + // having arguments uses a different code path so we need to make sure a NPE doesn't happen. + state = new MessageState() + { + MessageTemplate = null, + MessageArguments = new object[] { true }, + AwsRequestId = "1234", + Level = Helpers.LogLevelLoggerWriter.LogLevel.Warning, + TimeStamp = timestamp + }; + + json = formatter.FormatMessage(state); + Assert.Equal(string.Empty, doc.RootElement.GetProperty("message").GetString()); + Assert.Equal("1234", doc.RootElement.GetProperty("requestId").GetString()); + } + [Fact] public void FormatJsonWithAllPossibleTypes() { From 43f1966c1ee25c64dabe3e5afada985fafc85a2f Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Tue, 16 Sep 2025 11:22:11 -0700 Subject: [PATCH 2/2] Address PR comments --- .../LogMessageFormatterTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LogMessageFormatterTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LogMessageFormatterTests.cs index b97638d0f..4326a72b8 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LogMessageFormatterTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LogMessageFormatterTests.cs @@ -133,6 +133,7 @@ public void FormatJsonNullMessageTemplate() }; json = formatter.FormatMessage(state); + doc = JsonDocument.Parse(json); Assert.Equal(string.Empty, doc.RootElement.GetProperty("message").GetString()); Assert.Equal("1234", doc.RootElement.GetProperty("requestId").GetString()); }