Skip to content

Lambda Logging improvements #2062

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .autover/changes/16cc3c6a-8fa0-410b-ba57-74221abb086a.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"Projects": [
{
"Name": "Amazon.Lambda.RuntimeSupport",
"Type": "Patch",
"ChangelogMessages": [
"Add support for parameterized logging method with exception to global logger LambdaLogger in Amazon.Lambda.Core"
]
},
{
"Name": "Amazon.Lambda.Core",
"Type": "Minor",
"ChangelogMessages": [
"Added log level version of the static logging functions on Amazon.Lambda.Core.LambdaLogger"
]
}
{
"Name": "Amazon.Lambda.AspNetCoreServer",
"Type": "Patch",
"ChangelogMessages": [
"Update Amazon.Lambda.Logging.AspNetCore dependency"
]
},
{
"Name": "Amazon.Lambda.AspNetCoreServer.Hosting",
"Type": "Patch",
"ChangelogMessages": [
"Update Amazon.Lambda.Logging.AspNetCore dependency"
]
},
{
"Name": "Amazon.Lambda.Logging.AspNetCore",
"Type": "Major",
"ChangelogMessages": [
"Add support Lambda log levels",
"Change build target from .NET Standard 2.0 to .NET 6 and NET 8 to match Amazon.Lambda.AspNetCoreServer"
]
},
{
"Name": "Amazon.Lambda.TestUtilities",
"Type": "Major",
"ChangelogMessages": [
"Update Amazon.Lambda.TestUtitlies to have implementation of the newer logging methods",
"Change build target from .NET Standard 2.0 to .NET 6 and NET 8 to match Amazon.Lambda.AspNetCoreServer"
]
}
]
}
41 changes: 39 additions & 2 deletions Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public static void Log(string message)
// value with an Action that directs the logging into its logging system.
#pragma warning disable IDE0044 // Add readonly modifier
private static Action<string, string, object[]> _loggingWithLevelAction = LogWithLevelToConsole;
private static Action<string, Exception, string, object[]> _loggingWithLevelAndExceptionAction = LogWithLevelAndExceptionToConsole;
#pragma warning restore IDE0044 // Add readonly modifier

// Logs message to console
Expand All @@ -65,6 +66,15 @@ private static void LogWithLevelToConsole(string level, string message, params o
Console.WriteLine(sb.ToString());
}

private static void LogWithLevelAndExceptionToConsole(string level, Exception exception, string message, params object[] args)
{
// Formatting here is not important, it is used for debugging Amazon.Lambda.Core only.
// In a real scenario Amazon.Lambda.RuntimeSupport will change the value of _loggingWithLevelAction
// to an Action inside it's logging system to handle the real formatting.
LogWithLevelToConsole(level, message, args);
Console.WriteLine(exception);
}

private const string ParameterizedPreviewMessage =
"This method has been mark as preview till the Lambda .NET Managed runtime has been updated with the backing implementation of this method. " +
"It is possible to use this method while in preview if the Lambda function is deployed as an executable and uses the latest version of Amazon.Lambda.RuntimeSupport.";
Expand All @@ -78,7 +88,6 @@ private static void LogWithLevelToConsole(string level, string message, params o
/// <param name="level">The log level of the message</param>
/// <param name="message">Message to log. The message may have format arguments.</param>
/// <param name="args">Arguments to format the message with.</param>
[RequiresPreviewFeatures(ParameterizedPreviewMessage)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these no longer needed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the implementation in Amazon.Lambda.RuntimeSupport of these methods have been deployed to the managed runtimes. That were marked as preview till that deployment was done.

public static void Log(string level, string message, params object[] args)
{
_loggingWithLevelAction(level, message, args);
Expand All @@ -93,8 +102,36 @@ public static void Log(string level, string message, params object[] args)
/// <param name="level">The log level of the message</param>
/// <param name="message">Message to log. The message may have format arguments.</param>
/// <param name="args">Arguments to format the message with.</param>
[RequiresPreviewFeatures(ParameterizedPreviewMessage)]
public static void Log(LogLevel level, string message, params object[] args) => Log(level.ToString(), message, args);

/// <summary>
/// Logs a message to AWS CloudWatch Logs.
///
/// Logging will not be done:
/// If the role provided to the function does not have sufficient permissions.
/// </summary>
/// <param name="level">The log level of the message</param>
/// <param name="exception">Exception to include with the logging.</param>
/// <param name="message">Message to log. The message may have format arguments.</param>
/// <param name="args">Arguments to format the message with.</param>
[RequiresPreviewFeatures(ParameterizedPreviewMessage)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this here but not in others?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this PR adds the implementation of these methods in Amazon.Lambda.RuntimeSupport and we will need to wait till that change in Amazon.Lambda.RuntimeSupport gets deployed to managed runtime. Ideally I should have added these at the same time I did the versions that didn't take an Exception. That was a miss on my part that I'm fixing.

public static void Log(string level, Exception exception, string message, params object[] args)
{
_loggingWithLevelAndExceptionAction(level, exception, message, args);
}

/// <summary>
/// Logs a message to AWS CloudWatch Logs.
///
/// Logging will not be done:
/// If the role provided to the function does not have sufficient permissions.
/// </summary>
/// <param name="level">The log level of the message</param>
/// <param name="exception">Exception to include with the logging.</param>
/// <param name="message">Message to log. The message may have format arguments.</param>
/// <param name="args">Arguments to format the message with.</param>
[RequiresPreviewFeatures(ParameterizedPreviewMessage)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same question

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answered above

public static void Log(LogLevel level, Exception exception, string message, params object[] args) => Log(level.ToString(), exception, message, args);
#endif
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<PropertyGroup>
<Description>Amazon Lambda .NET Core support - Logging ASP.NET Core package.</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to add support for net6? Since we are doing a major version, might as well go all the way to net8

Copy link
Member Author

@normj normj May 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what our usage is still on .NET 6 and I'm not ready to make that call for this update. Also this change would spiral with having to remove .NET 6 from the Amazon.Lambda.AspNetCoreServer and Amazon.Lambda.AspNetCoreServer.Hosting.

<AssemblyTitle>Amazon.Lambda.Logging.AspNetCore</AssemblyTitle>
<Version>3.1.1</Version>
<AssemblyName>Amazon.Lambda.Logging.AspNetCore</AssemblyName>
Expand Down
129 changes: 80 additions & 49 deletions Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;

namespace Microsoft.Extensions.Logging
Expand Down Expand Up @@ -28,54 +28,85 @@ public bool IsEnabled(LogLevel logLevel)
_options.Filter == null ||
_options.Filter(_categoryName, logLevel));
}

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (formatter == null)
{
throw new ArgumentNullException(nameof(formatter));
}

if (!IsEnabled(logLevel))
{
return;
}

// Format of the logged text, optional components are in {}
// {[LogLevel] }{ => Scopes : }{Category: }{EventId: }MessageText {Exception}{\n}

var components = new List<string>(4);
if (_options.IncludeLogLevel)
{
components.Add($"[{logLevel}]");
}

GetScopeInformation(components);

if (_options.IncludeCategory)
{
components.Add($"{_categoryName}:");
}
if (_options.IncludeEventId)
{
components.Add($"[{eventId}]:");
}

var text = formatter.Invoke(state, exception);
components.Add(text);

if (_options.IncludeException)
{
components.Add($"{exception}");
}
if (_options.IncludeNewline)
{
components.Add(Environment.NewLine);
}

var finalText = string.Join(" ", components);
Amazon.Lambda.Core.LambdaLogger.Log(finalText);
}

/// <summary>
/// The Log method called by the ILogger framework to log message to logger's target. In the Lambda case the formatted logging will be
/// sent to the Amazon.Lambda.Core.LambdaLogger's Log method.
/// </summary>
/// <typeparam name="TState"></typeparam>
/// <param name="logLevel"></param>
/// <param name="eventId"></param>
/// <param name="state"></param>
/// <param name="exception"></param>
/// <param name="formatter"></param>
/// <exception cref="ArgumentNullException"></exception>
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add documentation to this method

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

{
if (formatter == null)
{
throw new ArgumentNullException(nameof(formatter));
}

if (!IsEnabled(logLevel))
{
return;
}

var components = new List<string>(4);
if (_options.IncludeLogLevel)
{
components.Add($"[{logLevel}]");
}

GetScopeInformation(components);

if (_options.IncludeCategory)
{
components.Add($"{_categoryName}:");
}
if (_options.IncludeEventId)
{
components.Add($"[{eventId}]:");
}

var text = formatter.Invoke(state, exception);
components.Add(text);

if (_options.IncludeException)
{
components.Add($"{exception}");
}
if (_options.IncludeNewline)
{
components.Add(Environment.NewLine);
}

var finalText = string.Join(" ", components);

var lambdaLogLevel = ConvertLogLevel(logLevel);
Amazon.Lambda.Core.LambdaLogger.Log(lambdaLogLevel, finalText);
}

private static Amazon.Lambda.Core.LogLevel ConvertLogLevel(LogLevel logLevel)
{
switch (logLevel)
{
case LogLevel.Trace:
return Amazon.Lambda.Core.LogLevel.Trace;
case LogLevel.Debug:
return Amazon.Lambda.Core.LogLevel.Debug;
case LogLevel.Information:
return Amazon.Lambda.Core.LogLevel.Information;
case LogLevel.Warning:
return Amazon.Lambda.Core.LogLevel.Warning;
case LogLevel.Error:
return Amazon.Lambda.Core.LogLevel.Error;
case LogLevel.Critical:
return Amazon.Lambda.Core.LogLevel.Critical;
default:
return Amazon.Lambda.Core.LogLevel.Information;
}
}

private void GetScopeInformation(List<string> logMessageComponents)
{
Expand Down
100 changes: 100 additions & 0 deletions Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ internal class UserCodeLoader
{
private const string UserInvokeException = "An exception occurred while invoking customer handler.";
private const string LambdaLoggingActionFieldName = "_loggingAction";
private const string LambdaLoggingWithLevelActionFieldName = "_loggingWithLevelAction";
private const string LambdaLoggingWithLevelAndExceptionActionFieldName = "_loggingWithLevelAndExceptionAction";

internal const string LambdaCoreAssemblyName = "Amazon.Lambda.Core";

Expand Down Expand Up @@ -148,6 +150,13 @@ public void Invoke(Stream lambdaData, ILambdaContext lambdaContext, Stream outSt
_invokeDelegate(lambdaData, lambdaContext, outStream);
}

/// <summary>
/// Sets the backing logger action field in Amazon.Logging.Core to redirect logs into Amazon.Lambda.RuntimeSupport.
/// </summary>
/// <param name="coreAssembly"></param>
/// <param name="customerLoggingAction"></param>
/// <param name="internalLogger"></param>
/// <exception cref="ArgumentNullException"></exception>
internal static void SetCustomerLoggerLogAction(Assembly coreAssembly, Action<string> customerLoggingAction, InternalLogger internalLogger)
{
if (coreAssembly == null)
Expand Down Expand Up @@ -186,6 +195,97 @@ internal static void SetCustomerLoggerLogAction(Assembly coreAssembly, Action<st
}
}

/// <summary>
/// Sets the backing logger action field in Amazon.Logging.Core to redirect logs into Amazon.Lambda.RuntimeSupport.
/// </summary>
/// <param name="coreAssembly"></param>
/// <param name="loggingWithLevelAction"></param>
/// <param name="internalLogger"></param>
/// <exception cref="ArgumentNullException"></exception>

internal static void SetCustomerLoggerLogAction(Assembly coreAssembly, Action<string, string, object[]> loggingWithLevelAction, InternalLogger internalLogger)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add documentation for this method

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

{
if (coreAssembly == null)
{
throw new ArgumentNullException(nameof(coreAssembly));
}

if (loggingWithLevelAction == null)
{
throw new ArgumentNullException(nameof(loggingWithLevelAction));
}

internalLogger.LogDebug($"UCL : Retrieving type '{Types.LambdaLoggerTypeName}'");
var lambdaILoggerType = coreAssembly.GetType(Types.LambdaLoggerTypeName);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isnt this library NativeAOT compatible? Why are you using reflection here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UserCodeLoader is not Native AOT complaint but it is only used in class library programming model and you use executable programming model for Native AOT. At the top of the UserCodeLoader file is the RequiresUnreferencedCode attribute to say this code shouldn't be used for Native AOT.

if (lambdaILoggerType == null)
{
throw LambdaExceptions.ValidationException(Errors.UserCodeLoader.Internal.UnableToLocateType, Types.LambdaLoggerTypeName);
}

internalLogger.LogDebug($"UCL : Retrieving field '{LambdaLoggingWithLevelActionFieldName}'");
var loggingActionField = lambdaILoggerType.GetTypeInfo().GetField(LambdaLoggingWithLevelActionFieldName, BindingFlags.NonPublic | BindingFlags.Static);
if (loggingActionField == null)
{
throw LambdaExceptions.ValidationException(Errors.UserCodeLoader.Internal.UnableToRetrieveField, LambdaLoggingWithLevelActionFieldName, Types.LambdaLoggerTypeName);
}

internalLogger.LogDebug($"UCL : Setting field '{LambdaLoggingWithLevelActionFieldName}'");
try
{
loggingActionField.SetValue(null, loggingWithLevelAction);
}
catch (Exception e)
{
throw LambdaExceptions.ValidationException(e, Errors.UserCodeLoader.Internal.UnableToSetField,
Types.LambdaLoggerTypeName, LambdaLoggingWithLevelActionFieldName);
}
}

/// <summary>
/// Sets the backing logger action field in Amazon.Logging.Core to redirect logs into Amazon.Lambda.RuntimeSupport.
/// </summary>
/// <param name="coreAssembly"></param>
/// <param name="loggingWithAndExceptionLevelAction"></param>
/// <param name="internalLogger"></param>
/// <exception cref="ArgumentNullException"></exception>
internal static void SetCustomerLoggerLogAction(Assembly coreAssembly, Action<string, Exception, string, object[]> loggingWithAndExceptionLevelAction, InternalLogger internalLogger)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add docs for this method

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

{
if (coreAssembly == null)
{
throw new ArgumentNullException(nameof(coreAssembly));
}

if (loggingWithAndExceptionLevelAction == null)
{
throw new ArgumentNullException(nameof(loggingWithAndExceptionLevelAction));
}

internalLogger.LogDebug($"UCL : Retrieving type '{Types.LambdaLoggerTypeName}'");
var lambdaILoggerType = coreAssembly.GetType(Types.LambdaLoggerTypeName);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same question abotut reflection

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answered above

if (lambdaILoggerType == null)
{
throw LambdaExceptions.ValidationException(Errors.UserCodeLoader.Internal.UnableToLocateType, Types.LambdaLoggerTypeName);
}

internalLogger.LogDebug($"UCL : Retrieving field '{LambdaLoggingWithLevelAndExceptionActionFieldName}'");
var loggingActionField = lambdaILoggerType.GetTypeInfo().GetField(LambdaLoggingWithLevelAndExceptionActionFieldName, BindingFlags.NonPublic | BindingFlags.Static);
if (loggingActionField == null)
{
throw LambdaExceptions.ValidationException(Errors.UserCodeLoader.Internal.UnableToRetrieveField, LambdaLoggingWithLevelAndExceptionActionFieldName, Types.LambdaLoggerTypeName);
}

internalLogger.LogDebug($"UCL : Setting field '{LambdaLoggingWithLevelAndExceptionActionFieldName}'");
try
{
loggingActionField.SetValue(null, loggingWithAndExceptionLevelAction);
}
catch (Exception e)
{
throw LambdaExceptions.ValidationException(e, Errors.UserCodeLoader.Internal.UnableToSetField,
Types.LambdaLoggerTypeName, LambdaLoggingWithLevelAndExceptionActionFieldName);
}
}

/// <summary>
/// Constructs customer-specified serializer, specified either on the method,
/// the assembly, or not specified at all.
Expand Down
Loading