Skip to content

Commit 82b8e0c

Browse files
committed
2 parents 8d3a410 + 8507eaf commit 82b8e0c

File tree

4 files changed

+257
-11
lines changed

4 files changed

+257
-11
lines changed

Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
<PropertyGroup>
66
<TargetFrameworks>netstandard2.0;net5.0;net6.0</TargetFrameworks>
7-
<VersionPrefix>1.8.1</VersionPrefix>
7+
<VersionPrefix>1.8.2</VersionPrefix>
88
<Description>Provides a bootstrap and Lambda Runtime API Client to help you to develop custom .NET Core Lambda Runtimes.</Description>
9-
<VersionPrefix>1.8.1</VersionPrefix>
9+
<VersionPrefix>1.8.2</VersionPrefix>
1010
<Description>Provides a bootstrap and Lambda Runtime API Client to help you to develop custom .NET Core Lambda Runtimes.</Description>
1111
<AssemblyTitle>Amazon.Lambda.RuntimeSupport</AssemblyTitle>
1212
<AssemblyName>Amazon.Lambda.RuntimeSupport</AssemblyName>

Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/FileDescriptorLogStream.cs

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,41 @@ public static class FileDescriptorLogFactory
1515
{
1616
private readonly static ConcurrentDictionary<string, StreamWriter> _writers = new ConcurrentDictionary<string, StreamWriter>();
1717

18+
// max cloudwatch log event size, 256k - 26 bytes of overhead.
19+
internal const int MaxCloudWatchLogEventSize = 256 * 1024 - 26;
20+
internal const int LambdaTelemetryLogHeaderLength = 8;
21+
internal const uint LambdaTelemetryLogHeaderFrameType = 0xa55a0001;
22+
1823
/// <summary>
1924
/// Get the StreamWriter for the particular file descriptor ID. If the same ID is passed the same StreamWriter instance is returned.
2025
/// </summary>
2126
/// <param name="fileDescriptorId"></param>
2227
/// <returns></returns>
2328
public static StreamWriter GetWriter(string fileDescriptorId)
29+
{
30+
var writer = _writers.GetOrAdd(fileDescriptorId,
31+
(x) => {
32+
SafeFileHandle handle = new SafeFileHandle(new IntPtr(int.Parse(fileDescriptorId)), false);
33+
return InitializeWriter(new FileStream(handle, FileAccess.Write));
34+
});
35+
return writer;
36+
}
37+
38+
/// <summary>
39+
/// Initialize a StreamWriter for the given Stream.
40+
/// This method is internal as it is tested in Amazon.RuntimeSupport.Tests
41+
/// </summary>
42+
/// <param name="fileDescriptorStream"></param>
43+
/// <returns></returns>
44+
internal static StreamWriter InitializeWriter(Stream fileDescriptorStream)
2445
{
2546
// AutoFlush must be turned out otherwise the StreamWriter might not send the data to the stream before the Lambda function completes.
2647
// Set the buffer size to the same max size as CloudWatch Logs records.
27-
var writer = _writers.GetOrAdd(fileDescriptorId, (x) => new StreamWriter(new FileDescriptorLogStream(fileDescriptorId), Encoding.UTF8, 256 * 1024) { AutoFlush = true });
28-
return writer;
48+
// Encoder has encoderShouldEmitUTF8Identifier = false as Lambda FD will assume UTF-8 so there is no need to emit an extra log entry.
49+
// In fact this extra log entry is cast to UTF-8 and results in an empty log entry which will be rejected by CloudWatch Logs.
50+
return new StreamWriter(new FileDescriptorLogStream(fileDescriptorStream),
51+
new UTF8Encoding(false), MaxCloudWatchLogEventSize)
52+
{ AutoFlush = true };
2953
}
3054

3155
/// <summary>
@@ -40,16 +64,14 @@ public static StreamWriter GetWriter(string fileDescriptorId)
4064
/// </summary>
4165
private class FileDescriptorLogStream : Stream
4266
{
43-
private const uint FRAME_TYPE = 0xa55a0001;
4467
private readonly Stream _fileDescriptorStream;
4568
private readonly byte[] _frameTypeBytes;
4669

47-
public FileDescriptorLogStream(string fileDescriptorId)
70+
public FileDescriptorLogStream(Stream logStream)
4871
{
49-
SafeFileHandle handle = new SafeFileHandle(new IntPtr(int.Parse(fileDescriptorId)), false);
50-
_fileDescriptorStream = new FileStream(handle, FileAccess.Write);
72+
_fileDescriptorStream = logStream;
5173

52-
_frameTypeBytes = BitConverter.GetBytes(FRAME_TYPE);
74+
_frameTypeBytes = BitConverter.GetBytes(LambdaTelemetryLogHeaderFrameType);
5375
if (BitConverter.IsLittleEndian)
5476
{
5577
Array.Reverse(_frameTypeBytes);
@@ -69,7 +91,7 @@ public override void Write(byte[] buffer, int offset, int count)
6991
Array.Reverse(messageLengthBytes);
7092
}
7193

72-
var typeAndLength = ArrayPool<byte>.Shared.Rent(8);
94+
var typeAndLength = ArrayPool<byte>.Shared.Rent(LambdaTelemetryLogHeaderLength);
7395
try
7496
{
7597
Buffer.BlockCopy(_frameTypeBytes, 0, typeAndLength, 0, 4);
@@ -113,4 +135,4 @@ public override void SetLength(long value)
113135
#endregion
114136
}
115137
}
116-
}
138+
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Text;
6+
using Amazon.Lambda.RuntimeSupport.Helpers;
7+
using Amazon.Lambda.RuntimeSupport.UnitTests.TestHelpers;
8+
using Xunit;
9+
10+
namespace Amazon.Lambda.RuntimeSupport.UnitTests
11+
{
12+
public class FileDescriptorLogStreamTests
13+
{
14+
private const int HeaderLength = FileDescriptorLogFactory.LambdaTelemetryLogHeaderLength;
15+
private const int LogEntryMaxLength = FileDescriptorLogFactory.MaxCloudWatchLogEventSize;
16+
17+
private static readonly byte[] ExpectedMagicBytes =
18+
{
19+
0xA5, 0x5A, 0x00, 0x01
20+
};
21+
22+
[Fact]
23+
public void MultilineLoggingInSingleLogEntryWithTlvFormat()
24+
{
25+
var logs = new List<byte[]>();
26+
var offsets = new List<int>();
27+
var counts = new List<int>();
28+
var stream = new TestFileStream((log, offset, count) =>
29+
{
30+
logs.Add(log);
31+
offsets.Add(offset);
32+
counts.Add(count);
33+
});
34+
TextWriter writer = FileDescriptorLogFactory.InitializeWriter(stream);
35+
// assert that initializing the stream does not result in UTF-8 preamble log entry
36+
Assert.Equal(0, counts.Count);
37+
Assert.Equal(0, offsets.Count);
38+
Assert.Equal(0, logs.Count);
39+
40+
const string logMessage = "hello world\nsomething else on a new line.";
41+
int logMessageLength = logMessage.Length;
42+
writer.Write(logMessage);
43+
44+
Assert.Equal(2, offsets.Count);
45+
int headerLogEntryOffset = offsets[0];
46+
int consoleLogEntryOffset = offsets[1];
47+
Assert.Equal(0, headerLogEntryOffset);
48+
Assert.Equal(0, consoleLogEntryOffset);
49+
50+
Assert.Equal(2, counts.Count);
51+
int headerLogEntrySize = counts[0];
52+
int consoleLogEntrySize = counts[1];
53+
Assert.Equal(HeaderLength, headerLogEntrySize);
54+
Assert.Equal(logMessageLength, consoleLogEntrySize);
55+
56+
Assert.Equal(2, logs.Count);
57+
byte[] headerLogEntry = logs[0];
58+
byte[] consoleLogEntry = logs[1];
59+
Assert.Equal(HeaderLength, headerLogEntry.Length);
60+
Assert.Equal(logMessageLength, consoleLogEntry.Length);
61+
62+
byte[] expectedLengthBytes =
63+
{
64+
0x00, 0x00, 0x00, 0x29
65+
};
66+
AssertHeaderBytes(headerLogEntry, expectedLengthBytes);
67+
Assert.Equal(logMessage, Encoding.UTF8.GetString(consoleLogEntry));
68+
}
69+
70+
[Fact]
71+
public void MaxSizeProducesOneLogFrame()
72+
{
73+
var logs = new List<byte[]>();
74+
var offsets = new List<int>();
75+
var counts = new List<int>();
76+
var stream = new TestFileStream((log, offset, count) =>
77+
{
78+
logs.Add(log);
79+
offsets.Add(offset);
80+
counts.Add(count);
81+
});
82+
TextWriter writer = FileDescriptorLogFactory.InitializeWriter(stream);
83+
// assert that initializing the stream does not result in UTF-8 preamble log entry
84+
Assert.Equal(0, counts.Count);
85+
Assert.Equal(0, offsets.Count);
86+
Assert.Equal(0, logs.Count);
87+
88+
string logMessage = new string('a', LogEntryMaxLength - 1) + "b";
89+
writer.Write(logMessage);
90+
91+
Assert.Equal(2, offsets.Count);
92+
int headerLogEntryOffset = offsets[0];
93+
int consoleLogEntryOffset = offsets[1];
94+
Assert.Equal(0, headerLogEntryOffset);
95+
Assert.Equal(0, consoleLogEntryOffset);
96+
97+
Assert.Equal(2, counts.Count);
98+
int headerLogEntrySize = counts[0];
99+
int consoleLogEntrySize = counts[1];
100+
Assert.Equal(HeaderLength, headerLogEntrySize);
101+
Assert.Equal(LogEntryMaxLength, consoleLogEntrySize);
102+
103+
Assert.Equal(2, logs.Count);
104+
byte[] headerLogEntry = logs[0];
105+
byte[] consoleLogEntry = logs[1];
106+
Assert.Equal(HeaderLength, headerLogEntry.Length);
107+
Assert.Equal(LogEntryMaxLength, consoleLogEntry.Length);
108+
109+
byte[] expectedLengthBytes =
110+
{
111+
0x00, 0x03, 0xFF, 0xE6
112+
};
113+
AssertHeaderBytes(headerLogEntry, expectedLengthBytes);
114+
Assert.Equal(logMessage, Encoding.UTF8.GetString(consoleLogEntry));
115+
}
116+
117+
[Fact]
118+
public void LogEntryAboveMaxSizeProducesMultipleLogFrames()
119+
{
120+
var logs = new List<byte[]>();
121+
var offsets = new List<int>();
122+
var counts = new List<int>();
123+
var stream = new TestFileStream((log, offset, count) =>
124+
{
125+
logs.Add(log);
126+
offsets.Add(offset);
127+
counts.Add(count);
128+
});
129+
TextWriter writer = FileDescriptorLogFactory.InitializeWriter(stream);
130+
// assert that initializing the stream does not result in UTF-8 preamble log entry
131+
Assert.Equal(0, counts.Count);
132+
Assert.Equal(0, offsets.Count);
133+
Assert.Equal(0, logs.Count);
134+
135+
string logMessage = new string('a', LogEntryMaxLength) + "b";
136+
writer.Write(logMessage);
137+
138+
Assert.Equal(4, offsets.Count);
139+
int headerLogEntryOffset = offsets[0];
140+
int consoleLogEntryOffset = offsets[1];
141+
int headerLogSecondEntryOffset = offsets[2];
142+
int consoleLogSecondEntryOffset = offsets[3];
143+
Assert.Equal(0, headerLogEntryOffset);
144+
Assert.Equal(0, consoleLogEntryOffset);
145+
Assert.Equal(0, headerLogSecondEntryOffset);
146+
Assert.Equal(0, consoleLogSecondEntryOffset);
147+
148+
Assert.Equal(4, counts.Count);
149+
int headerLogEntrySize = counts[0];
150+
int consoleLogEntrySize = counts[1];
151+
int headerLogSecondEntrySize = counts[2];
152+
int consoleLogSecondEntrySize = counts[3];
153+
Assert.Equal(HeaderLength, headerLogEntrySize);
154+
Assert.Equal(LogEntryMaxLength, consoleLogEntrySize);
155+
Assert.Equal(HeaderLength, headerLogSecondEntrySize);
156+
Assert.Equal(1, consoleLogSecondEntrySize);
157+
158+
Assert.Equal(4, logs.Count);
159+
byte[] headerLogEntry = logs[0];
160+
byte[] consoleLogEntry = logs[1];
161+
byte[] headerLogSecondEntry = logs[2];
162+
byte[] consoleLogSecondEntry = logs[3];
163+
Assert.Equal(HeaderLength, headerLogEntry.Length);
164+
Assert.Equal(LogEntryMaxLength, consoleLogEntry.Length);
165+
Assert.Equal(HeaderLength, headerLogSecondEntry.Length);
166+
Assert.Equal(1, consoleLogSecondEntry.Length);
167+
168+
byte[] expectedLengthBytes =
169+
{
170+
0x00, 0x03, 0xFF, 0xE6
171+
};
172+
AssertHeaderBytes(headerLogEntry, expectedLengthBytes);
173+
174+
byte[] expectedLengthBytesSecondEntry =
175+
{
176+
0x00, 0x00, 0x00, 0x01
177+
};
178+
AssertHeaderBytes(headerLogSecondEntry, expectedLengthBytesSecondEntry);
179+
string expectedLogEntry = logMessage.Substring(0, LogEntryMaxLength);
180+
string expectedSecondLogEntry = logMessage.Substring(LogEntryMaxLength);
181+
Assert.Equal(expectedLogEntry, Encoding.UTF8.GetString(consoleLogEntry));
182+
Assert.Equal(expectedSecondLogEntry, Encoding.UTF8.GetString(consoleLogSecondEntry));
183+
}
184+
185+
private static void AssertHeaderBytes(byte[] buf, byte[] expectedLengthBytes)
186+
{
187+
byte[] actualHeaderMagicBytes = buf.Take(4).ToArray();
188+
byte[] actualHeaderLengthBytes = buf.TakeLast(4).ToArray();
189+
Assert.Equal(ExpectedMagicBytes, actualHeaderMagicBytes);
190+
Assert.Equal(expectedLengthBytes, actualHeaderLengthBytes);
191+
}
192+
}
193+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
6+
namespace Amazon.Lambda.RuntimeSupport.UnitTests.TestHelpers
7+
{
8+
public class TestFileStream : FileStream
9+
{
10+
private Action<byte[], int, int> WriteAction { get; }
11+
12+
public TestFileStream(Action<byte[], int, int> writeAction)
13+
: base(Path.GetTempFileName(), FileMode.Append, FileAccess.Write)
14+
{
15+
WriteAction = writeAction;
16+
}
17+
18+
public override bool CanWrite => true;
19+
20+
public override void Write(byte[] buffer, int offset, int count)
21+
{
22+
WriteAction(TrimTrailingNullBytes(buffer).Take(count).ToArray(), offset, count);
23+
}
24+
25+
private static IEnumerable<byte> TrimTrailingNullBytes(IEnumerable<byte> buffer)
26+
{
27+
// Trim trailing null bytes to make testing assertions easier
28+
return buffer.Reverse().SkipWhile(x => x == 0).Reverse();
29+
}
30+
}
31+
}

0 commit comments

Comments
 (0)