diff --git a/Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs b/Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs index 554d3b4e1..5b4078675 100644 --- a/Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs +++ b/Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs @@ -12,12 +12,41 @@ public static class LambdaLogger // Logging action, logs to Console by default private static Action _loggingAction = LogToConsole; +#if NET6_0_OR_GREATER + private static System.Buffers.ReadOnlySpanAction _dataLoggingAction = LogUtf8BytesToConsole; +#endif + // Logs message to console private static void LogToConsole(string message) { Console.WriteLine(message); } +#if NET6_0_OR_GREATER + private static void LogUtf8BytesToConsole(ReadOnlySpan utf8Message, object state) + { + try + { + Console.WriteLine(Console.OutputEncoding.GetString(utf8Message)); + } + catch + { + // ignore any encoding error + } + } + + /// + /// Logs a message to AWS CloudWatch Logs.
+ /// Logging will not be done: + /// If the role provided to the function does not have sufficient permissions. + ///
+ /// The message as UTF-8 encoded data. + public static void Log(ReadOnlySpan utf8Message) + { + _dataLoggingAction(utf8Message, null); + } +#endif + /// /// Logs a message to AWS CloudWatch Logs. /// diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs index aab83d918..82feba553 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs @@ -106,6 +106,25 @@ private LambdaBootstrap(HttpClient httpClient, LambdaBootstrapHandler handler, L Client = new RuntimeApiClient(new SystemEnvironmentVariables(), _httpClient); } + /// + /// Create a LambdaBootstrap that will call the given initializer and handler. + /// + /// The HTTP client to use with the Lambda runtime. + /// Delegate called for each invocation of the Lambda function. + /// Delegate called to initialize the Lambda function. If not provided the initialization step is skipped. + /// Whether the instance owns the HTTP client and should dispose of it. + /// Instance of to call the runtime API. + /// + internal LambdaBootstrap(HttpClient httpClient, LambdaBootstrapHandler handler, LambdaBootstrapInitializer initializer, bool ownsHttpClient, IRuntimeApiClient runtimeApiClient) + { + _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + _ownsHttpClient = ownsHttpClient; + _initializer = initializer; + _httpClient.Timeout = RuntimeApiHttpTimeout; + Client = runtimeApiClient; + } + /// /// Run the initialization Func if provided. /// Then run the invoke loop, calling the handler for each invocation. diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs index 020e132a4..79b410c08 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs @@ -1,5 +1,6 @@ using Amazon.Lambda.RuntimeSupport.Bootstrap; using System; +using System.Buffers; using System.IO; using System.Reflection; using System.Text; @@ -82,6 +83,8 @@ public void FormattedWriteLine(string level, string message) #if NET6_0_OR_GREATER + internal enum LogFormatType { Default, Unformatted } + /// /// Formats log messages with time, request id, log level and message /// @@ -145,18 +148,18 @@ public LogLevelLoggerWriter() { var stdOutWriter = FileDescriptorLogFactory.GetWriter(fileDescriptorLogId); var stdErrorWriter = FileDescriptorLogFactory.GetWriter(fileDescriptorLogId); - Initialize(stdOutWriter, stdErrorWriter); + Initialize(stdOutWriter, stdErrorWriter, stdOutWriter.BaseStream); InternalLogger.GetDefaultLogger().LogInformation("Using file descriptor stream writer for logging."); } - catch(Exception ex) + catch (Exception ex) { InternalLogger.GetDefaultLogger().LogError(ex, "Error creating file descriptor log stream writer. Fallback to stdout and stderr."); - Initialize(Console.Out, Console.Error); + Initialize(Console.Out, Console.Error, null); } } else { - Initialize(Console.Out, Console.Error); + Initialize(Console.Out, Console.Error, null); InternalLogger.GetDefaultLogger().LogInformation("Using stdout and stderr for logging."); } @@ -171,15 +174,16 @@ public LogLevelLoggerWriter() ConfigureLoggingActionField(); } - public LogLevelLoggerWriter(TextWriter stdOutWriter, TextWriter stdErrorWriter) + public LogLevelLoggerWriter(TextWriter stdOutWriter, TextWriter stdErrorWriter, Stream outputStream) { - Initialize(stdOutWriter, stdErrorWriter); + Initialize(stdOutWriter, stdErrorWriter, outputStream); + ConfigureLoggingActionField(); } - private void Initialize(TextWriter stdOutWriter, TextWriter stdErrorWriter) + private void Initialize(TextWriter stdOutWriter, TextWriter stdErrorWriter, Stream outputStream) { - _wrappedStdOutWriter = new WrapperTextWriter(stdOutWriter, LogLevel.Information.ToString()); - _wrappedStdErrorWriter = new WrapperTextWriter(stdErrorWriter, LogLevel.Error.ToString()); + _wrappedStdOutWriter = new WrapperTextWriter(stdOutWriter, outputStream, LogLevel.Information.ToString()); + _wrappedStdErrorWriter = new WrapperTextWriter(stdErrorWriter, outputStream, LogLevel.Error.ToString()); } /// @@ -199,6 +203,10 @@ private void ConfigureLoggingActionField() Action callback = (message => FormattedWriteLine(null, message)); loggingActionField.SetValue(null, callback); + + var dataLoggingActionField = lambdaILoggerType.GetTypeInfo().GetField("_dataLoggingAction", BindingFlags.NonPublic | BindingFlags.Static); + ReadOnlySpanAction dataLoggingAction = (data, state) => FormattedWriteBytes(null, data); + dataLoggingActionField.SetValue(null, dataLoggingAction); } public void SetCurrentAwsRequestId(string awsRequestId) @@ -217,6 +225,16 @@ public void FormattedWriteLine(string level, string message) _wrappedStdOutWriter.FormattedWriteLine(level, message); } + public void FormattedWriteBytes(string level, ReadOnlySpan message) + { + _wrappedStdOutWriter.FormattedWriteBytes(level, message); + } + + internal void SetLogFormatType(LogFormatType logFormatType) + { + _wrappedStdOutWriter.SetLogFormatType(logFormatType); + _wrappedStdErrorWriter.SetLogFormatType(logFormatType); + } /// /// Wraps around a provided TextWriter. In normal usage the wrapped TextWriter will either be stdout or stderr. @@ -226,6 +244,7 @@ public void FormattedWriteLine(string level, string message) class WrapperTextWriter : TextWriter { private readonly TextWriter _innerWriter; + private readonly Stream _innerOutputStream; private string _defaultLogLevel; const string LOG_LEVEL_ENVIRONMENT_VARAIBLE = "AWS_LAMBDA_HANDLER_LOG_LEVEL"; @@ -233,8 +252,6 @@ class WrapperTextWriter : TextWriter private LogLevel _minmumLogLevel = LogLevel.Information; - enum LogFormatType { Default, Unformatted } - private LogFormatType _logFormatType = LogFormatType.Default; public string CurrentAwsRequestId { get; set; } = string.Empty; @@ -247,9 +264,19 @@ enum LogFormatType { Default, Unformatted } /// internal object LockObject { get; set; } = new object(); - public WrapperTextWriter(TextWriter innerWriter, string defaultLogLevel) + /// + /// Initializes an instance of . + /// + /// The inner instance used to write the output message. + /// + /// The inner output that provides direct write access to Telemetry file descriptor. + /// In case Telemetry file descriptor is not available this parameter is set to NULL. + /// + /// The default logging level. + public WrapperTextWriter(TextWriter innerWriter, Stream innerOutputStream, string defaultLogLevel) { _innerWriter = innerWriter; + _innerOutputStream = innerOutputStream; _defaultLogLevel = defaultLogLevel; var envLogLevel = Environment.GetEnvironmentVariable(LOG_LEVEL_ENVIRONMENT_VARAIBLE); @@ -271,6 +298,11 @@ public WrapperTextWriter(TextWriter innerWriter, string defaultLogLevel) } } + internal void SetLogFormatType(LogFormatType logFormatType) + { + _logFormatType = logFormatType; + } + internal void FormattedWriteLine(string message) { FormattedWriteLine(_defaultLogLevel, message); @@ -278,7 +310,7 @@ internal void FormattedWriteLine(string message) internal void FormattedWriteLine(string level, string message) { - lock(LockObject) + lock (LockObject) { var displayLevel = level; if (Enum.TryParse(level, true, out var levelEnum)) @@ -310,6 +342,58 @@ internal void FormattedWriteLine(string level, string message) } } + internal void FormattedWriteBytes(string level, ReadOnlySpan message) + { + if (_innerOutputStream is null) + { + // the telemetry FD output stream is not present, we delegate to FormattedWriteLine() + FormattedWriteLine(level, _innerWriter.Encoding.GetString(message)); + return; + } + + lock (LockObject) + { + var displayLevel = level; + if (Enum.TryParse(level, true, out var levelEnum)) + { + if (levelEnum < _minmumLogLevel) + return; + + displayLevel = ConvertLogLevelToLabel(levelEnum); + } + + if (_logFormatType == LogFormatType.Unformatted) + { + _innerOutputStream.Write(message); + } + else + { + string linePrefix; + if (!string.IsNullOrEmpty(displayLevel)) + { + linePrefix = $"{DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ")}\t{CurrentAwsRequestId}\t{displayLevel}\t"; + } + else + { + linePrefix = $"{DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ")}\t{CurrentAwsRequestId}\t"; + } + + // acquire memory to put the prefix and the message together + var logEntryBuffer = ArrayPool.Shared.Rent(_innerWriter.Encoding.GetMaxByteCount(linePrefix.Length) + message.Length); + try + { + var prefixLength = _innerWriter.Encoding.GetBytes(linePrefix, logEntryBuffer.AsSpan()); + message.CopyTo(logEntryBuffer.AsSpan().Slice(prefixLength)); + _innerOutputStream.Write(logEntryBuffer.AsSpan().Slice(0, prefixLength + message.Length)); + } + finally + { + ArrayPool.Shared.Return(logEntryBuffer); + } + } + } + } + private Task FormattedWriteLineAsync(string message) { FormattedWriteLine(message); diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/FileDescriptorLogStream.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/FileDescriptorLogStream.cs index aa6fb519f..bc6d3bbc3 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/FileDescriptorLogStream.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/FileDescriptorLogStream.cs @@ -4,6 +4,9 @@ using System.Text; using Microsoft.Win32.SafeHandles; using System.Collections.Concurrent; +#if NET6_0_OR_GREATER +using System.Buffers.Binary; +#endif namespace Amazon.Lambda.RuntimeSupport.Helpers { @@ -27,7 +30,7 @@ public static class FileDescriptorLogFactory /// public static StreamWriter GetWriter(string fileDescriptorId) { - var writer = _writers.GetOrAdd(fileDescriptorId, + var writer = _writers.GetOrAdd(fileDescriptorId, (x) => { SafeFileHandle handle = new SafeFileHandle(new IntPtr(int.Parse(fileDescriptorId)), false); return InitializeWriter(new FileStream(handle, FileAccess.Write)); @@ -107,6 +110,29 @@ public override void Write(byte[] buffer, int offset, int count) } } +#if NET6_0_OR_GREATER + public override void Write(ReadOnlySpan buffer) + { + while (buffer.Length > 0) + { + var bufferToWrite = buffer.Length > MaxCloudWatchLogEventSize ? buffer.Slice(0, MaxCloudWatchLogEventSize) : buffer; + DoWriteBuffer(bufferToWrite); + buffer = buffer.Slice(bufferToWrite.Length); + } + } + + private void DoWriteBuffer(ReadOnlySpan buffer) + { + Span typeAndLength = stackalloc byte[LambdaTelemetryLogHeaderLength]; + BinaryPrimitives.WriteUInt32BigEndian(typeAndLength[..4], LambdaTelemetryLogHeaderFrameType); + BinaryPrimitives.WriteInt32BigEndian(typeAndLength.Slice(4, 4), buffer.Length); + + _fileDescriptorStream.Write(typeAndLength); + _fileDescriptorStream.Write(buffer); + _fileDescriptorStream.Flush(); + } +#endif + #region Not implemented read and seek operations public override bool CanRead => false; diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/Amazon.Lambda.RuntimeSupport.UnitTests.csproj b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/Amazon.Lambda.RuntimeSupport.UnitTests.csproj index fb5219ab3..522a048ce 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/Amazon.Lambda.RuntimeSupport.UnitTests.csproj +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/Amazon.Lambda.RuntimeSupport.UnitTests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + netcoreapp3.1;net6.0 diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/FileDescriptorLogStreamTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/FileDescriptorLogStreamTests.cs index e8e07aefb..b4fa6ef2c 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/FileDescriptorLogStreamTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/FileDescriptorLogStreamTests.cs @@ -19,6 +19,166 @@ public class FileDescriptorLogStreamTests 0xA5, 0x5A, 0x00, 0x01 }; +#if NET6_0_OR_GREATER + [Fact] + public void MultiLineLogDataInSingleLogEntryWithTlvFormat() + { + var logs = new List(); + var offsets = new List(); + var counts = new List(); + var stream = new TestFileStream((log, offset, count) => + { + logs.Add(log); + offsets.Add(offset); + counts.Add(count); + }); + var writer = FileDescriptorLogFactory.InitializeWriter(stream); + var writerStream = writer.BaseStream; + // assert that initializing the stream does not result in UTF-8 preamble log entry + Assert.Empty(counts); + Assert.Empty(offsets); + Assert.Empty(logs); + + const string logMessage = "hello world\nsomething else on a new line."; + int logMessageLength = logMessage.Length; + writerStream.Write(writer.Encoding.GetBytes(logMessage).AsSpan()); + + Assert.Equal(2, offsets.Count); + int headerLogEntryOffset = offsets[0]; + int consoleLogEntryOffset = offsets[1]; + Assert.Equal(0, headerLogEntryOffset); + Assert.Equal(0, consoleLogEntryOffset); + + Assert.Equal(2, counts.Count); + int headerLogEntrySize = counts[0]; + int consoleLogEntrySize = counts[1]; + Assert.Equal(HeaderLength, headerLogEntrySize); + Assert.Equal(logMessageLength, consoleLogEntrySize); + + Assert.Equal(2, logs.Count); + byte[] headerLogEntry = logs[0]; + byte[] consoleLogEntry = logs[1]; + Assert.Equal(HeaderLength, headerLogEntry.Length); + Assert.Equal(logMessageLength, consoleLogEntry.Length); + + byte[] expectedLengthBytes = + { + 0x00, 0x00, 0x00, 0x29 + }; + AssertHeaderBytes(headerLogEntry, expectedLengthBytes); + Assert.Equal(logMessage, Encoding.UTF8.GetString(consoleLogEntry)); + } + + [Fact] + public void LogDataMaxSizeProducesOneLogFrame() + { + var logs = new List(); + var offsets = new List(); + var counts = new List(); + var stream = new TestFileStream((log, offset, count) => + { + logs.Add(log); + offsets.Add(offset); + counts.Add(count); + }); + var writer = FileDescriptorLogFactory.InitializeWriter(stream); + var writerStream = writer.BaseStream; + + string logMessage = new string('a', LogEntryMaxLength - 1) + "b"; + writerStream.Write(writer.Encoding.GetBytes(logMessage).AsSpan()); + + Assert.Equal(2, offsets.Count); + int headerLogEntryOffset = offsets[0]; + int consoleLogEntryOffset = offsets[1]; + Assert.Equal(0, headerLogEntryOffset); + Assert.Equal(0, consoleLogEntryOffset); + + Assert.Equal(2, counts.Count); + int headerLogEntrySize = counts[0]; + int consoleLogEntrySize = counts[1]; + Assert.Equal(HeaderLength, headerLogEntrySize); + Assert.Equal(LogEntryMaxLength, consoleLogEntrySize); + + Assert.Equal(2, logs.Count); + byte[] headerLogEntry = logs[0]; + byte[] consoleLogEntry = logs[1]; + Assert.Equal(HeaderLength, headerLogEntry.Length); + Assert.Equal(LogEntryMaxLength, consoleLogEntry.Length); + + byte[] expectedLengthBytes = + { + 0x00, 0x03, 0xFF, 0xE6 + }; + AssertHeaderBytes(headerLogEntry, expectedLengthBytes); + Assert.Equal(logMessage, Encoding.UTF8.GetString(consoleLogEntry)); + } + + [Fact] + public void LogDataAboveMaxSizeProducesMultipleLogFrames() + { + var logs = new List(); + var offsets = new List(); + var counts = new List(); + var stream = new TestFileStream((log, offset, count) => + { + logs.Add(log); + offsets.Add(offset); + counts.Add(count); + }); + var writer = FileDescriptorLogFactory.InitializeWriter(stream); + var writerStream = writer.BaseStream; + + string logMessage = new string('a', LogEntryMaxLength) + "b"; + writerStream.Write(writer.Encoding.GetBytes(logMessage).AsSpan()); + + Assert.Equal(4, offsets.Count); + int headerLogEntryOffset = offsets[0]; + int consoleLogEntryOffset = offsets[1]; + int headerLogSecondEntryOffset = offsets[2]; + int consoleLogSecondEntryOffset = offsets[3]; + Assert.Equal(0, headerLogEntryOffset); + Assert.Equal(0, consoleLogEntryOffset); + Assert.Equal(0, headerLogSecondEntryOffset); + Assert.Equal(0, consoleLogSecondEntryOffset); + + Assert.Equal(4, counts.Count); + int headerLogEntrySize = counts[0]; + int consoleLogEntrySize = counts[1]; + int headerLogSecondEntrySize = counts[2]; + int consoleLogSecondEntrySize = counts[3]; + Assert.Equal(HeaderLength, headerLogEntrySize); + Assert.Equal(LogEntryMaxLength, consoleLogEntrySize); + Assert.Equal(HeaderLength, headerLogSecondEntrySize); + Assert.Equal(1, consoleLogSecondEntrySize); + + Assert.Equal(4, logs.Count); + byte[] headerLogEntry = logs[0]; + byte[] consoleLogEntry = logs[1]; + byte[] headerLogSecondEntry = logs[2]; + byte[] consoleLogSecondEntry = logs[3]; + Assert.Equal(HeaderLength, headerLogEntry.Length); + Assert.Equal(LogEntryMaxLength, consoleLogEntry.Length); + Assert.Equal(HeaderLength, headerLogSecondEntry.Length); + Assert.Single(consoleLogSecondEntry); + + byte[] expectedLengthBytes = + { + 0x00, 0x03, 0xFF, 0xE6 + }; + AssertHeaderBytes(headerLogEntry, expectedLengthBytes); + + byte[] expectedLengthBytesSecondEntry = + { + 0x00, 0x00, 0x00, 0x01 + }; + AssertHeaderBytes(headerLogSecondEntry, expectedLengthBytesSecondEntry); + string expectedLogEntry = logMessage.Substring(0, LogEntryMaxLength); + string expectedSecondLogEntry = logMessage.Substring(LogEntryMaxLength); + Assert.Equal(expectedLogEntry, Encoding.UTF8.GetString(consoleLogEntry)); + Assert.Equal(expectedSecondLogEntry, Encoding.UTF8.GetString(consoleLogSecondEntry)); + } +#endif + [Fact] public void MultilineLoggingInSingleLogEntryWithTlvFormat() { diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/HandlerTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/HandlerTests.cs index c782b85a7..823eea03e 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/HandlerTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/HandlerTests.cs @@ -389,10 +389,7 @@ private async Task ExecHandlerAsync(string handler, string dataIn var userCodeLoader = new UserCodeLoader(handler, _internalLogger); var handlerWrapper = HandlerWrapper.GetHandlerWrapper(userCodeLoader.Invoke); var initializer = new UserCodeInitializer(userCodeLoader, _internalLogger); - var bootstrap = new LambdaBootstrap(handlerWrapper, initializer.InitializeAsync) - { - Client = testRuntimeApiClient - }; + using var bootstrap = new LambdaBootstrap(new System.Net.Http.HttpClient(), handlerWrapper.Handler, initializer.InitializeAsync, true, testRuntimeApiClient); if (assertLoggedByInitialize != null) { diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LogLevelLoggerWriterTest.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LogLevelLoggerWriterTest.cs new file mode 100644 index 000000000..25f223465 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LogLevelLoggerWriterTest.cs @@ -0,0 +1,87 @@ +#if NET6_0_OR_GREATER +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport.Helpers; +using System; +using System.Buffers.Binary; +using System.Globalization; +using System.IO; +using System.Text; +using Xunit; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests +{ + public class LogLevelLoggerWriterTest + { + [Fact] + public void WriteBytesUnformattedShouldWriteLogFrame() + { + using var outputStream = new MemoryStream(); + using var streamWriter = FileDescriptorLogFactory.InitializeWriter(outputStream); + + const string logMessage = "hello world\nsomething else on a new line."; + var loggerWriter = new LogLevelLoggerWriter(streamWriter, streamWriter, streamWriter.BaseStream); + loggerWriter.SetLogFormatType(LogFormatType.Unformatted); + loggerWriter.FormattedWriteBytes(null, streamWriter.Encoding.GetBytes(logMessage)); + + AssertLogFrame(outputStream.ToArray(), m => Assert.Equal(m, logMessage)); + } + + [Fact] + public void WriteBytesFormattedShouldWriteFormattedLogFrame() + { + const string logMessage = "hello world\nsomething else on a new line."; + + using var outputStream = new MemoryStream(); + using var streamWriter = FileDescriptorLogFactory.InitializeWriter(outputStream); + + var requestId = Guid.NewGuid().ToString(); + var loggerWriter = new LogLevelLoggerWriter(streamWriter, streamWriter, streamWriter.BaseStream); + loggerWriter.SetCurrentAwsRequestId(requestId); + loggerWriter.SetLogFormatType(LogFormatType.Default); + loggerWriter.FormattedWriteBytes(null, streamWriter.Encoding.GetBytes(logMessage)); + + AssertLogFrame(outputStream.ToArray(), actualMsg => + { + // make sure that the message starts with a valid datetime + const string dateFormat = "yyyy-MM-ddTHH:mm:ss.fffZ"; + var dateString = actualMsg.Substring(0, dateFormat.Length); + DateTime.ParseExact(dateString, dateFormat, CultureInfo.InvariantCulture); + + // output should contains request ID + Assert.Contains(requestId, actualMsg); + + // output should end with the actual message + Assert.EndsWith(logMessage, actualMsg); + }); + } + + [Fact] + public void LambdaLoggerShouldWriteToLogLevelLoggerWriter() + { + using var outputStream = new MemoryStream(); + using var streamWriter = FileDescriptorLogFactory.InitializeWriter(outputStream); + + const string logMessage = "hello world\nsomething else on a new line."; + var loggerWriter = new LogLevelLoggerWriter(streamWriter, streamWriter, streamWriter.BaseStream); + loggerWriter.SetLogFormatType(LogFormatType.Unformatted); + + LambdaLogger.Log(streamWriter.Encoding.GetBytes(logMessage)); + + // verify that the log message is directed to LogLevelLoggerWriter + AssertLogFrame(outputStream.ToArray(), m => Assert.Equal(m, logMessage)); + } + + private static void AssertLogFrame(ReadOnlySpan frame, Action messageAssertion) + { + Assert.True(frame.Length >= 8); + + var frameType = BinaryPrimitives.ReadUInt32BigEndian(frame.Slice(0, 4)); + Assert.Equal(frameType, FileDescriptorLogFactory.LambdaTelemetryLogHeaderFrameType); + + var length = BinaryPrimitives.ReadInt32BigEndian(frame.Slice(4, 4)); + var actualMessage = new UTF8Encoding(false, false).GetString(frame.Slice(8, length)); + messageAssertion(actualMessage); + } + } +} +#endif diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestFileStream.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestFileStream.cs index 0b3d3b8fc..223728d91 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestFileStream.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestFileStream.cs @@ -22,6 +22,11 @@ public override void Write(byte[] buffer, int offset, int count) WriteAction(TrimTrailingNullBytes(buffer).Take(count).ToArray(), offset, count); } + public override void Write(ReadOnlySpan buffer) + { + Write(buffer.ToArray(), 0, buffer.Length); + } + private static IEnumerable TrimTrailingNullBytes(IEnumerable buffer) { // Trim trailing null bytes to make testing assertions easier