diff --git a/Libraries/src/Amazon.Lambda.Core/ILambdaLogger.cs b/Libraries/src/Amazon.Lambda.Core/ILambdaLogger.cs index 113ced4bd..d12f2152d 100644 --- a/Libraries/src/Amazon.Lambda.Core/ILambdaLogger.cs +++ b/Libraries/src/Amazon.Lambda.Core/ILambdaLogger.cs @@ -150,8 +150,8 @@ public interface ILambdaLogger private const string ParameterizedPreviewMessage = "Parameterized logging is in preview till a new version of .NET Lambda runtime client that supports parameterized logging " + "has been deployed to the .NET Lambda managed runtime. Till deployment has been made the feature can be used by deploying as an " + - "executable including the latest version of Amazon.Lambda.RuntimeSupport and setting the \"LangVersion\" in the Lambda " + - "project file to \"preview\""; + "executable including the latest version of Amazon.Lambda.RuntimeSupport and setting the \"EnablePreviewFeatures\" in the Lambda " + + "project file to \"true\""; /// /// Log message catagorized by the given log level @@ -164,7 +164,7 @@ public interface ILambdaLogger /// Message to log. /// Values to be replaced in log messages that are parameterized. [RequiresPreviewFeatures(ParameterizedPreviewMessage)] - void Log(string level, string message, params object[] args) => Log(level, message); + void Log(string level, string message, params object[] args) => Log(level, message, args); /// /// Log message catagorized by the given log level @@ -177,11 +177,11 @@ public interface ILambdaLogger /// Exception to include with the logging. /// Message to log. /// Values to be replaced in log messages that are parameterized. - [RequiresPreviewFeatures("Parameterized logging is in preview till new version of .NET Lambda runtime client is deployed to the .NET Lambda managed runtime.")] + [RequiresPreviewFeatures(ParameterizedPreviewMessage)] void Log(string level, Exception exception, string message, params object[] args) { - Log(level, message); - Log(level, exception.ToString()); + Log(level, message, args); + Log(level, exception.ToString(), args); } /// diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs index ee9b7f9c8..9686f767e 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs @@ -36,30 +36,30 @@ public interface IConsoleLoggerWriter /// /// The current aws request id /// - /// + /// The AWS request id for the function invocation added to each log message. void SetCurrentAwsRequestId(string awsRequestId); /// /// Format message with default log level /// - /// + /// Message to log. void FormattedWriteLine(string message); /// /// Format message with given log level /// - /// - /// - /// + /// The level of the log message. + /// Message to log. + /// Arguments to be applied to the log message. void FormattedWriteLine(string level, string message, params object[] args); /// /// Format message with given log level /// - /// - /// - /// - /// + /// The level of the log message. + /// Exception to log. + /// Message to log. + /// Arguments to be applied to the log message. void FormattedWriteLine(string level, Exception exception, string message, params object[] args); } @@ -112,7 +112,10 @@ public void FormattedWriteLine(string level, string message, params object[] arg public void FormattedWriteLine(string level, Exception exception, string message, params object[] args) { _writer.WriteLine(message); - _writer.WriteLine(exception.ToString()); + if (exception != null) + { + _writer.WriteLine(exception.ToString()); + } } } @@ -310,6 +313,10 @@ public WrapperTextWriter(TextWriter innerWriter, string defaultLogLevel) { _minmumLogLevel = result; } + else + { + InternalLogger.GetDefaultLogger().LogInformation($"Failed to parse log level enum value: {envLogLevel}"); + } } var envLogFormat = GetEnviromentVariable(NET_RIC_LOG_FORMAT_ENVIRONMENT_VARIABLE, LAMBDA_LOG_FORMAT_ENVIRONMENT_VARIABLE); diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/AbstractLogMessageFormatter.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/AbstractLogMessageFormatter.cs index 115c545a6..11da7db6d 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/AbstractLogMessageFormatter.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/AbstractLogMessageFormatter.cs @@ -31,8 +31,8 @@ private enum LogFormatParserState : byte /// /// Parse the message template for all message properties. /// - /// - /// + /// The message template users passed in as the log message. + /// List of MessageProperty objects detected by parsing the message template. public virtual IReadOnlyList ParseProperties(string messageTemplate) { // Check to see if this message template has already been parsed before. @@ -101,17 +101,21 @@ public virtual IReadOnlyList ParseProperties(string messageTemp /// /// Subclasses to implement to format the message given the requirements of the subclass. /// - /// - /// + /// The state of the message to log. + /// The full log message to send to CloudWatch Logs. public abstract string FormatMessage(MessageState state); internal const string DateFormat = "yyyy-MM-ddTHH:mm:ss.fffZ"; + internal const string DateOnlyFormat = "yyyy-MM-dd"; + + internal const string TimeOnlyFormat = "HH:mm:ss.fff"; + /// /// Format the timestamp of the log message in format Lambda service prefers. /// - /// - /// + /// The state of the message to log. + /// Timestamp formatted for logging. protected string FormatTimestamp(MessageState state) { return state.TimeStamp.ToString(DateFormat, CultureInfo.InvariantCulture); @@ -123,7 +127,7 @@ protected string FormatTimestamp(MessageState state) /// /// /// - /// + /// The log message with logging arguments replaced with the values. public string ApplyMessageProperties(string messageTemplate, IReadOnlyList messageProperties, object[] messageArguments) { if(messageProperties.Count == 0) @@ -243,7 +247,7 @@ public string ApplyMessageProperties(string messageTemplate, IReadOnlyList /// - /// + /// True of the logging arguments are positional public bool UsingPositionalArguments(IReadOnlyList messageProperties) { var min = int.MaxValue; @@ -251,6 +255,7 @@ public bool UsingPositionalArguments(IReadOnlyList messagePrope HashSet positions = new HashSet(); foreach(var property in messageProperties) { + // If any logging arguments use non-numeric identifier then they are not using positional arguments. if (!int.TryParse(property.Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var position)) { return false; @@ -267,6 +272,10 @@ public bool UsingPositionalArguments(IReadOnlyList messagePrope } } + // At this point the HashSet is the collection of all of the int logging arguments. + // If there are no gaps or duplicates in the logging statement then the smallest value + // in the hashset should be 0 and the max value equals the count of the hashset. If + // either of those conditions are not true then it can't be positional arguments. if(positions.Count != (max + 1) || min != 0) { return false; diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/JsonLogMessageFormatter.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/JsonLogMessageFormatter.cs index 6037ec06b..2cf471a82 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/JsonLogMessageFormatter.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/JsonLogMessageFormatter.cs @@ -109,69 +109,71 @@ private void WriteMessageAttributes(Utf8JsonWriter writer, IReadOnlyList) + if (messageArgument is IList && messageArgument is not IList) + { + writer.WriteStartArray(); + foreach (var item in ((IList)messageArgument)) { - writer.WriteStartArray(); - foreach (var item in ((IList)messageArgument)) - { - FormatJsonValue(writer, item, messageProperties[i].FormatArgument, messageProperties[i].FormatDirective); - } - writer.WriteEndArray(); + FormatJsonValue(writer, item, messageProperties[i].FormatArgument, messageProperties[i].FormatDirective); } - else if (messageArgument is IDictionary) + writer.WriteEndArray(); + } + else if (messageArgument is IDictionary) + { + writer.WriteStartObject(); + foreach (DictionaryEntry entry in ((IDictionary)messageArgument)) { - writer.WriteStartObject(); - foreach (DictionaryEntry entry in ((IDictionary)messageArgument)) - { - writer.WritePropertyName(entry.Key.ToString()); + writer.WritePropertyName(entry.Key.ToString()); - FormatJsonValue(writer, entry.Value, messageProperties[i].FormatArgument, messageProperties[i].FormatDirective); - } - writer.WriteEndObject(); - } - else - { - FormatJsonValue(writer, messageArgument, messageProperties[i].FormatArgument, messageProperties[i].FormatDirective); + FormatJsonValue(writer, entry.Value, messageProperties[i].FormatArgument, messageProperties[i].FormatDirective); } + writer.WriteEndObject(); + } + else + { + FormatJsonValue(writer, messageArgument, messageProperties[i].FormatArgument, messageProperties[i].FormatDirective); } } } @@ -271,6 +273,12 @@ private void FormatJsonValue(Utf8JsonWriter writer, object value, string formatA case DateTimeOffset dateTimeOffsetValue: writer.WriteStringValue(dateTimeOffsetValue.ToString(DateFormat, CultureInfo.InvariantCulture)); break; + case DateOnly dateOnly: + writer.WriteStringValue(dateOnly.ToString(DateOnlyFormat, CultureInfo.InvariantCulture)); + break; + case TimeOnly timeOnly: + writer.WriteStringValue(timeOnly.ToString(TimeOnlyFormat, CultureInfo.InvariantCulture)); + break; case byte[] byteArrayValue: writer.WriteStringValue(MessageProperty.FormatByteArray(byteArrayValue)); break; diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/MessageProperty.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/MessageProperty.cs index c8c1b97bc..20e74059b 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/MessageProperty.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/MessageProperty.cs @@ -16,7 +16,7 @@ public class MessageProperty private static readonly char[] PARAM_FORMAT_DELIMITERS = { ':' }; /// - /// Parse the string string representation of the message property without the brackets + /// Parse the string representation of the message property without the brackets /// to construct the MessageProperty. /// /// @@ -57,7 +57,19 @@ public MessageProperty(ReadOnlySpan messageToken) /// public string MessageToken { get; private set; } - public enum Directive { Default, JsonSerialization }; + /// + /// Enum for controlling the formatting of complex logging arguments. + /// + public enum Directive { + /// + /// Perform a string formatting for the logging argument. + /// + Default, + /// + /// Perform a JSON serialization on the logging argument. + /// + JsonSerialization + }; /// /// The Name of the message property. @@ -110,6 +122,14 @@ public string FormatForMessage(object value) { return dto.ToString(AbstractLogMessageFormatter.DateFormat, CultureInfo.InvariantCulture); } + if (value is DateOnly dateOnly) + { + return dateOnly.ToString(AbstractLogMessageFormatter.DateOnlyFormat, CultureInfo.InvariantCulture); + } + if (value is TimeOnly timeOnly) + { + return timeOnly.ToString(AbstractLogMessageFormatter.TimeOnlyFormat, CultureInfo.InvariantCulture); + } return value.ToString(); } 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 9075a4794..378e7a4e0 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 @@ -105,6 +105,8 @@ public void FormatJsonWithStringMessageProperties() [Fact] public void FormatJsonWithAllPossibleTypes() { + var dateOnly = new DateOnly(2024, 2, 18); + var timeOnly = new TimeOnly(15, 19, 45, 545); var timestamp = DateTime.UtcNow; var formattedTimestamp = timestamp.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); @@ -117,8 +119,8 @@ public void FormatJsonWithAllPossibleTypes() Level = Helpers.LogLevelLoggerWriter.LogLevel.Warning, MessageTemplate = "bool: {bool}, byte: {byte}, char: {char}, decimal: {decimal}, double: {double}, float: {float}, " + "int: {int}, uint: {uint}, long: {long}, ulong: {ulong}, short: {short}, ushort: {ushort}, null: {null}, " + - "DateTime: {DateTime}, DateTimeOffset: {DateTimeOffset}, default: {default}", - MessageArguments = new object[] {true, (byte)1, 'a', (decimal)4.5, (double)5.6, (float)7.7, (int)-10, (uint)10, (long)-100, (ulong)100, (short)-50, (ushort)50, null, timestamp, timestamp, product}, + "DateTime: {DateTime}, DateTimeOffset: {DateTimeOffset}, default: {default}, DateOnly {DateOnly}, TimeOnly: {TimeOnly}", + MessageArguments = new object[] {true, (byte)1, 'a', (decimal)4.5, (double)5.6, (float)7.7, (int)-10, (uint)10, (long)-100, (ulong)100, (short)-50, (ushort)50, null, timestamp, timestamp, product, dateOnly, timeOnly}, TimeStamp = timestamp }; @@ -127,7 +129,7 @@ public void FormatJsonWithAllPossibleTypes() Assert.Equal("bool: True, byte: 1, char: a, decimal: 4.5, double: 5.6, float: 7.7, " + "int: -10, uint: 10, long: -100, ulong: 100, short: -50, ushort: 50, null: {null}, " + - $"DateTime: {formattedTimestamp}, DateTimeOffset: {formattedTimestamp}, default: Widget 100", + $"DateTime: {formattedTimestamp}, DateTimeOffset: {formattedTimestamp}, default: Widget 100, DateOnly 2024-02-18, TimeOnly: 15:19:45.545", doc.RootElement.GetProperty("message").GetString()); Assert.True(doc.RootElement.GetProperty("bool").GetBoolean()); @@ -146,6 +148,8 @@ public void FormatJsonWithAllPossibleTypes() Assert.Equal(formattedTimestamp, doc.RootElement.GetProperty("DateTime").GetString()); Assert.Equal(formattedTimestamp, doc.RootElement.GetProperty("DateTimeOffset").GetString()); Assert.Equal("Widget 100", doc.RootElement.GetProperty("default").GetString()); + Assert.Equal("2024-02-18", doc.RootElement.GetProperty("DateOnly").GetString()); + Assert.Equal("15:19:45.545", doc.RootElement.GetProperty("TimeOnly").GetString()); } [Fact] diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/MessagePropertyTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/MessagePropertyTests.cs index b7427331e..f49457c0c 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/MessagePropertyTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/MessagePropertyTests.cs @@ -13,7 +13,7 @@ public void SimpleName() var property = new MessageProperty("name"); Assert.Equal(MessageProperty.Directive.Default, property.FormatDirective); Assert.Equal("name", property.Name); - Assert.Equal("name", property.MessageToken); + Assert.Equal("{name}", property.MessageToken); Assert.Null(property.FormatArgument); } @@ -23,7 +23,7 @@ public void WithFormatArgument() var property = new MessageProperty("count:000"); Assert.Equal(MessageProperty.Directive.Default, property.FormatDirective); Assert.Equal("count", property.Name); - Assert.Equal("count:000", property.MessageToken); + Assert.Equal("{count:000}", property.MessageToken); Assert.Equal("000", property.FormatArgument); } @@ -33,7 +33,7 @@ public void WithJsonDirective() var property = new MessageProperty("@user"); Assert.Equal(MessageProperty.Directive.JsonSerialization, property.FormatDirective); Assert.Equal("user", property.Name); - Assert.Equal("@user", property.MessageToken); + Assert.Equal("{@user}", property.MessageToken); Assert.Null(property.FormatArgument); } @@ -43,7 +43,7 @@ public void WithJsonDirectiveAndIgnorableFormatArgument() var property = new MessageProperty("@user:000"); Assert.Equal(MessageProperty.Directive.JsonSerialization, property.FormatDirective); Assert.Equal("user", property.Name); - Assert.Equal("@user:000", property.MessageToken); + Assert.Equal("{@user:000}", property.MessageToken); Assert.Equal("000", property.FormatArgument); } @@ -53,7 +53,7 @@ public void WithFormatArgumentMissingValues() var property = new MessageProperty("count:"); Assert.Equal(MessageProperty.Directive.Default, property.FormatDirective); Assert.Equal("count", property.Name); - Assert.Equal("count:", property.MessageToken); + Assert.Equal("{count:}", property.MessageToken); Assert.Null(property.FormatArgument); } @@ -63,7 +63,7 @@ public void NameWithSpace() var property = new MessageProperty(" first last "); Assert.Equal(MessageProperty.Directive.Default, property.FormatDirective); Assert.Equal("first last", property.Name); - Assert.Equal(" first last ", property.MessageToken); + Assert.Equal("{ first last }", property.MessageToken); Assert.Null(property.FormatArgument); } @@ -73,7 +73,7 @@ public void NameAndFormatArgumentWithSpace() var property = new MessageProperty(" first last : 000 "); Assert.Equal(MessageProperty.Directive.Default, property.FormatDirective); Assert.Equal("first last", property.Name); - Assert.Equal(" first last : 000 ", property.MessageToken); + Assert.Equal("{ first last : 000 }", property.MessageToken); Assert.Equal("000", property.FormatArgument); }