Skip to content

Commit

Permalink
Merge branch 'feature/structured-logging' of https://github.com/aws/a…
Browse files Browse the repository at this point in the history
…ws-lambda-dotnet into feature/structured-logging
  • Loading branch information
normj committed Sep 4, 2024
2 parents b029e5c + b8dbdb1 commit 1a38727
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,10 @@ public WrapperTextWriter(TextWriter innerWriter, string defaultLogLevel)
{
_minmumLogLevel = LogLevel.Critical;
}
else if (string.Equals(envLogLevel, "warn", StringComparison.InvariantCultureIgnoreCase))
{
_minmumLogLevel = LogLevel.Warning;
}
else if (Enum.TryParse<LogLevel>(envLogLevel, true, out var result))
{
_minmumLogLevel = result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="AWSSDK.IdentityManagement" Version="3.7.100.98" />
<PackageReference Include="AWSSDK.Lambda" Version="3.7.105.17" />
<PackageReference Include="AWSSDK.IdentityManagement" Version="3.7.402.7" />
<PackageReference Include="AWSSDK.Lambda" Version="3.7.402.3" />
<PackageReference Include="AWSSDK.S3" Version="3.7.103.34" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
Expand All @@ -32,7 +32,7 @@
<PackageReference Include="xunit" Version="2.4.2" />

<!-- This needs to be referenced to allow testing via AssumeRole credentials -->
<PackageReference Include="AWSSDK.SecurityToken" Version="3.7.101.32" />
<PackageReference Include="AWSSDK.SecurityToken" Version="3.7.400.13" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,11 @@ protected async Task<InvokeResponse> InvokeFunctionAsync(IAmazonLambda lambdaCli
return await lambdaClient.InvokeAsync(request);
}

protected async Task UpdateHandlerAsync(IAmazonLambda lambdaClient, string handler, Dictionary<string, string> environmentVariables = null)
protected async Task UpdateHandlerAsync(IAmazonLambda lambdaClient, string handler, Dictionary<string, string> environmentVariables = null, RuntimeLogLevel? logLevel = null)
{
if(environmentVariables == null)
await WaitForFunctionToBeReady(lambdaClient);

if (environmentVariables == null)
{
environmentVariables = new Dictionary<string, string>();
}
Expand All @@ -216,8 +218,30 @@ protected async Task UpdateHandlerAsync(IAmazonLambda lambdaClient, string handl
Variables = environmentVariables
}
};

if (logLevel == null)
{
updateFunctionConfigurationRequest.LoggingConfig = new LoggingConfig
{
LogFormat = LogFormat.Text
};
}
else
{
updateFunctionConfigurationRequest.LoggingConfig = new LoggingConfig
{
ApplicationLogLevel = ConvertRuntimeLogLevel(logLevel.Value),
LogFormat = LogFormat.JSON
};
}

await lambdaClient.UpdateFunctionConfigurationAsync(updateFunctionConfigurationRequest);

await WaitForFunctionToBeReady(lambdaClient);
}

private async Task WaitForFunctionToBeReady(IAmazonLambda lambdaClient)
{
// Wait for eventual consistency of function change.
var getConfigurationRequest = new GetFunctionConfigurationRequest { FunctionName = FunctionName };
GetFunctionConfigurationResponse getConfigurationResponse = null;
Expand Down Expand Up @@ -344,5 +368,58 @@ protected class NoDeploymentPackageFoundException : Exception
{

}

private ApplicationLogLevel ConvertRuntimeLogLevel(RuntimeLogLevel runtimeLogLevel)
{
switch (runtimeLogLevel)
{
case RuntimeLogLevel.Trace:
return ApplicationLogLevel.TRACE;
case RuntimeLogLevel.Debug:
return ApplicationLogLevel.DEBUG;
case RuntimeLogLevel.Information:
return ApplicationLogLevel.INFO;
case RuntimeLogLevel.Warning:
return ApplicationLogLevel.WARN;
case RuntimeLogLevel.Error:
return ApplicationLogLevel.ERROR;
case RuntimeLogLevel.Critical:
return ApplicationLogLevel.FATAL;
default:
throw new ArgumentException("Unknown log level: " + runtimeLogLevel);
}
}

public enum RuntimeLogLevel
{
/// <summary>
/// Trace level logging
/// </summary>
Trace = 0,
/// <summary>
/// Debug level logging
/// </summary>
Debug = 1,

/// <summary>
/// Information level logging
/// </summary>
Information = 2,

/// <summary>
/// Warning level logging
/// </summary>
Warning = 3,

/// <summary>
/// Error level logging
/// </summary>
Error = 4,

/// <summary>
/// Critical level logging
/// </summary>
Critical = 5
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -92,7 +93,7 @@ protected virtual async Task TestAllHandlersAsync()
roleAlreadyExisted = await PrepareTestResources(s3Client, lambdaClient, iamClient);

// .NET API to address setting memory constraint was added for .NET 8
if(_targetFramework == TargetFramework.NET8)
if (_targetFramework == TargetFramework.NET8)
{
await RunMaxHeapMemoryCheck(lambdaClient, "GetTotalAvailableMemoryBytes");
await RunWithoutMaxHeapMemoryCheck(lambdaClient, "GetTotalAvailableMemoryBytes");
Expand All @@ -102,8 +103,23 @@ protected virtual async Task TestAllHandlersAsync()
await RunTestExceptionAsync(lambdaClient, "ExceptionNonAsciiCharacterUnwrappedAsync", "", "Exception", "Unhandled exception with non ASCII character: ♂");
await RunTestSuccessAsync(lambdaClient, "UnintendedDisposeTest", "not-used", "UnintendedDisposeTest-SUCCESS");
await RunTestSuccessAsync(lambdaClient, "LoggingStressTest", "not-used", "LoggingStressTest-success");
await RunLoggingTestAsync(lambdaClient, "LoggingTest", null);
await RunLoggingTestAsync(lambdaClient, "LoggingTest", "debug");

await RunJsonLoggingWithUnhandledExceptionAsync(lambdaClient);

await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Trace, LogConfigSource.LambdaAPI);
await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Debug, LogConfigSource.LambdaAPI);
await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Information, LogConfigSource.LambdaAPI);
await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Warning, LogConfigSource.LambdaAPI);
await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Error, LogConfigSource.LambdaAPI);
await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Critical, LogConfigSource.LambdaAPI);
await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Trace, LogConfigSource.DotnetEnvironment);
await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Debug, LogConfigSource.DotnetEnvironment);
await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Information, LogConfigSource.DotnetEnvironment);
await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Warning, LogConfigSource.DotnetEnvironment);
await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Error, LogConfigSource.DotnetEnvironment);
await RunLoggingTestAsync(lambdaClient, "LoggingTest", RuntimeLogLevel.Critical, LogConfigSource.DotnetEnvironment);


await RunUnformattedLoggingTestAsync(lambdaClient, "LoggingTest");

await RunTestSuccessAsync(lambdaClient, "ToUpperAsync", "message", "ToUpperAsync-MESSAGE");
Expand Down Expand Up @@ -136,6 +152,18 @@ protected virtual async Task TestAllHandlersAsync()
}
}

private async Task RunJsonLoggingWithUnhandledExceptionAsync(AmazonLambdaClient lambdaClient)
{
await UpdateHandlerAsync(lambdaClient, "ThrowUnhandledApplicationException", null, RuntimeLogLevel.Information);
var invokeResponse = await InvokeFunctionAsync(lambdaClient, JsonConvert.SerializeObject(""));

var log = System.Text.UTF8Encoding.UTF8.GetString(Convert.FromBase64String(invokeResponse.LogResult));
var exceptionLog = log.Split('\n').FirstOrDefault(x => x.Contains("System.ApplicationException"));

Assert.NotNull(exceptionLog);
Assert.Contains("\"level\":\"Error\"", exceptionLog);
}

private async Task RunMaxHeapMemoryCheck(AmazonLambdaClient lambdaClient, string handler)
{
await UpdateHandlerAsync(lambdaClient, handler);
Expand Down Expand Up @@ -207,37 +235,83 @@ private async Task RunTestExceptionAsync(AmazonLambdaClient lambdaClient, string
}
}

private async Task RunLoggingTestAsync(AmazonLambdaClient lambdaClient, string handler, string logLevel)
// The .NET Lambda runtime has a legacy environment variable for configuring the log level. This enum is used in the test to choose
// whether the legacy environment variable should be set or use the new properties in the update configuration api for setting log level.
enum LogConfigSource { LambdaAPI, DotnetEnvironment}
private async Task RunLoggingTestAsync(AmazonLambdaClient lambdaClient, string handler, RuntimeLogLevel? runtimeLogLevel, LogConfigSource configSource)
{
var environmentVariables = new Dictionary<string, string>();
if(!string.IsNullOrEmpty(logLevel))
if(runtimeLogLevel.HasValue && configSource == LogConfigSource.DotnetEnvironment)
{
environmentVariables["AWS_LAMBDA_HANDLER_LOG_LEVEL"] = logLevel;
environmentVariables["AWS_LAMBDA_HANDLER_LOG_LEVEL"] = runtimeLogLevel.Value.ToString().ToLowerInvariant();
}
await UpdateHandlerAsync(lambdaClient, handler, environmentVariables);
await UpdateHandlerAsync(lambdaClient, handler, environmentVariables, configSource == LogConfigSource.LambdaAPI ? runtimeLogLevel : null);

var invokeResponse = await InvokeFunctionAsync(lambdaClient, JsonConvert.SerializeObject(""));
Assert.True(invokeResponse.HttpStatusCode == System.Net.HttpStatusCode.OK);
Assert.True(invokeResponse.FunctionError == null);

var log = System.Text.UTF8Encoding.UTF8.GetString(Convert.FromBase64String(invokeResponse.LogResult));

Assert.Contains("info\tA information log", log);
Assert.Contains("warn\tA warning log", log);
Assert.Contains("fail\tA error log", log);
Assert.Contains("crit\tA critical log", log);
if (!runtimeLogLevel.HasValue)
runtimeLogLevel = RuntimeLogLevel.Information;

if (runtimeLogLevel <= RuntimeLogLevel.Trace)
{
Assert.Contains("A trace log", log);
}
else
{
Assert.DoesNotContain("A trace log", log);
}

if (runtimeLogLevel <= RuntimeLogLevel.Debug)
{
Assert.Contains("A debug log", log);
}
else
{
Assert.DoesNotContain("A debug log", log);
}

if (runtimeLogLevel <= RuntimeLogLevel.Information)
{
Assert.Contains("A information log", log);
Assert.Contains("A stdout info message", log);
}
else
{
Assert.DoesNotContain("A information log", log);
Assert.DoesNotContain("A stdout info message", log);
}

Assert.Contains("info\tA stdout info message", log);
if (runtimeLogLevel <= RuntimeLogLevel.Warning)
{
Assert.Contains("A warning log", log);
}
else
{
Assert.DoesNotContain("A warning log", log);
}

Assert.Contains("fail\tA stderror error message", log);
if (runtimeLogLevel <= RuntimeLogLevel.Error)
{
Assert.Contains("A error log", log);
Assert.Contains("A stderror error message", log);
}
else
{
Assert.DoesNotContain("A error log", log);
Assert.DoesNotContain("A stderror error message", log);
}

if (string.IsNullOrEmpty(logLevel))
if (runtimeLogLevel <= RuntimeLogLevel.Critical)
{
Assert.DoesNotContain($"a {logLevel} log".ToLower(), log.ToLower());
Assert.Contains("A critical log", log);
}
else
{
Assert.Contains($"a {logLevel} log".ToLower(), log.ToLower());
Assert.DoesNotContain("A critical log", log);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ private static async Task Main(string[] args)
case nameof(GetTimezoneNameAsync):
bootstrap = new LambdaBootstrap(GetTimezoneNameAsync);
break;
case nameof(ThrowUnhandledApplicationException):
handlerWrapper = HandlerWrapper.GetHandlerWrapper((Action)ThrowUnhandledApplicationException);
bootstrap = new LambdaBootstrap(handlerWrapper);
break;
default:
throw new Exception($"Handler {handler} is not supported.");
}
Expand Down Expand Up @@ -374,6 +378,11 @@ private static void AggregateExceptionNotUnwrapped()
throw new AggregateException("AggregateException thrown from a synchronous handler.");
}

private static void ThrowUnhandledApplicationException()
{
throw new ApplicationException("Function fail");
}

private static Task<InvocationResponse> TooLargeResponseBodyAsync(InvocationRequest invocation)
{
return Task.FromResult(GetInvocationResponse(nameof(TooLargeResponseBodyAsync), SevenMBString.Value));
Expand Down

0 comments on commit 1a38727

Please sign in to comment.