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

Merged
merged 4 commits into from
Jun 3, 2025
Merged
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
239 changes: 135 additions & 104 deletions Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs
Original file line number Diff line number Diff line change
@@ -1,109 +1,140 @@
using System;
using System;
using System.Collections.Generic;

namespace Microsoft.Extensions.Logging
{
internal class LambdaILogger : ILogger
{
// Private fields
private readonly string _categoryName;
private readonly LambdaLoggerOptions _options;


internal IExternalScopeProvider ScopeProvider { get; set; }

// Constructor
public LambdaILogger(string categoryName, LambdaLoggerOptions options)
{
_categoryName = categoryName;
_options = options;
}

// ILogger methods
public IDisposable BeginScope<TState>(TState state) => ScopeProvider?.Push(state) ?? new NoOpDisposable();

public bool IsEnabled(LogLevel logLevel)
{
return (
_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);
}

private void GetScopeInformation(List<string> logMessageComponents)
{
var scopeProvider = ScopeProvider;

if (_options.IncludeScopes && scopeProvider != null)
{
var initialCount = logMessageComponents.Count;

scopeProvider.ForEachScope((scope, list) =>
{
list.Add(scope.ToString());
}, (logMessageComponents));

if (logMessageComponents.Count > initialCount)
{
logMessageComponents.Add("=>");
}
}
}

// Private classes
private class NoOpDisposable : IDisposable
{
public void Dispose()
{
}
}

}
internal class LambdaILogger : ILogger
{
// Private fields
private readonly string _categoryName;
private readonly LambdaLoggerOptions _options;


internal IExternalScopeProvider ScopeProvider { get; set; }

// Constructor
public LambdaILogger(string categoryName, LambdaLoggerOptions options)
{
_categoryName = categoryName;
_options = options;
}

// ILogger methods
public IDisposable BeginScope<TState>(TState state) => ScopeProvider?.Push(state) ?? new NoOpDisposable();

public bool IsEnabled(LogLevel logLevel)
{
return (
_options.Filter == null ||
_options.Filter(_categoryName, logLevel));
}

/// <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)
{
var scopeProvider = ScopeProvider;

if (_options.IncludeScopes && scopeProvider != null)
{
var initialCount = logMessageComponents.Count;

scopeProvider.ForEachScope((scope, list) =>
{
list.Add(scope.ToString());
}, (logMessageComponents));

if (logMessageComponents.Count > initialCount)
{
logMessageComponents.Add("=>");
}
}
}

// Private classes
private class NoOpDisposable : IDisposable
{
public void Dispose()
{
}
}

}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
Expand All @@ -18,8 +18,8 @@ public class LambdaLoggerOptions
private const string INCLUDE_CATEGORY_KEY = "IncludeCategory";
private const string INCLUDE_NEWLINE_KEY = "IncludeNewline";
private const string INCLUDE_EXCEPTION_KEY = "IncludeException";
private const string INCLUDE_EVENT_ID_KEY = "IncludeEventId";
private const string INCLUDE_SCOPES_KEY = "IncludeScopes";
private const string INCLUDE_EVENT_ID_KEY = "IncludeEventId";
private const string INCLUDE_SCOPES_KEY = "IncludeScopes";
private const string LOG_LEVEL_KEY = "LogLevel";
private const string DEFAULT_CATEGORY = "Default";

Expand Down Expand Up @@ -60,7 +60,7 @@ public class LambdaLoggerOptions
/// </summary>
public bool IncludeScopes { get; set; }

/// <summary>
/// <summary>
/// Function used to filter events based on the log level.
/// Default value is null and will instruct logger to log everything.
/// </summary>
Expand Down
Loading