Skip to content
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
31 changes: 31 additions & 0 deletions Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,37 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
messageTemplate = formatter.Invoke(state, exception);
}

// Append structured scope key/value pairs to the template and parameters so they
// are emitted as named JSON properties by the Lambda JSON formatter.
if (_options.IncludeScopes && ScopeProvider != null)
{
var scopeEntries = new List<KeyValuePair<string, object>>();
ScopeProvider.ForEachScope((scope, list) =>
{
if (scope is IEnumerable<KeyValuePair<string, object>> scopeKvps)
{
foreach (var kvp in scopeKvps)
{
if (kvp.Key != null && kvp.Key != "{OriginalFormat}")
{
list.Add(kvp);
}
}
}
}, scopeEntries);

if (scopeEntries.Count > 0)
{
var sb = new System.Text.StringBuilder(messageTemplate);
foreach (var entry in scopeEntries)
{
sb.Append($" {{{entry.Key}}}");
parameters.Add(entry.Value);
}
messageTemplate = sb.ToString();
}
}

Amazon.Lambda.Core.LambdaLogger.Log(lambdaLogLevel, exception, messageTemplate, parameters.ToArray());
}
else
Expand Down
22 changes: 22 additions & 0 deletions Libraries/src/Amazon.Lambda.Logging.AspNetCore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,25 @@ using(defaultLogger.BeginScope(awsRequestId))
}
}
```

## Structured scopes in Lambda JSON mode

When the `AWS_LAMBDA_LOG_FORMAT` environment variable is set to `JSON` and `IncludeScopes` is `true`, scope state objects that implement `IEnumerable<KeyValuePair<string, object>>` (such as `Dictionary<string, object>`) will have their key/value entries included as structured parameters in the emitted JSON log entry.

```csharp
var loggerOptions = new LambdaLoggerOptions { IncludeScopes = true };

var scopeProperties = new Dictionary<string, object>
{
{ "RequestId", "abc-123" },
{ "UserId", 42 }
};

using (logger.BeginScope(scopeProperties))
{
logger.LogInformation("Order {OrderId} placed", orderId);
// Emits JSON with RequestId, UserId, and OrderId as structured properties.
}
```

Nested structured scopes are supported. The scope properties are prepended to the parameter list (outermost scope first), followed by the message-template parameters. Non-structured scopes (e.g. plain strings) are silently ignored in JSON mode.
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,191 @@ public void JsonLoggingWithNoOriginalFormat()
}
}

[Fact]
public void JsonLogging_SingleStructuredScope_IncludedInParameters()
{
Environment.SetEnvironmentVariable("AWS_LAMBDA_LOG_FORMAT", "JSON");
try
{
using (var writer = new StringWriter())
{
ConnectLoggingActionToLogger(message => writer.Write(message));

var loggerOptions = new LambdaLoggerOptions { IncludeScopes = true };
var loggerFactory = new TestLoggerFactory().AddLambdaLogger(loggerOptions);
var logger = loggerFactory.CreateLogger("JsonScopeTest");

var scopeProps = new Dictionary<string, object> { { "RequestId", "abc-123" } };
using (logger.BeginScope(scopeProps))
{
logger.LogInformation("User {Name} logged in", "Alice");
}

var text = writer.ToString();
// scope param + 1 message param = 2
Assert.Contains("parameter count: 2", text);
}
}
finally
{
Environment.SetEnvironmentVariable("AWS_LAMBDA_LOG_FORMAT", null);
}
}

[Fact]
public void JsonLogging_NestedStructuredScopes_AllIncludedInParameters()
{
Environment.SetEnvironmentVariable("AWS_LAMBDA_LOG_FORMAT", "JSON");
try
{
using (var writer = new StringWriter())
{
ConnectLoggingActionToLogger(message => writer.Write(message));

var loggerOptions = new LambdaLoggerOptions { IncludeScopes = true };
var loggerFactory = new TestLoggerFactory().AddLambdaLogger(loggerOptions);
var logger = loggerFactory.CreateLogger("JsonScopeTest");

var outerScope = new Dictionary<string, object> { { "TraceId", "trace-1" } };
var innerScope = new Dictionary<string, object> { { "UserId", "user-99" } };
using (logger.BeginScope(outerScope))
{
using (logger.BeginScope(innerScope))
{
logger.LogInformation("Processed {Item}", "order");
}
}

var text = writer.ToString();
// outer (1) + inner (1) + message param (1) = 3
Assert.Contains("parameter count: 3", text);
}
}
finally
{
Environment.SetEnvironmentVariable("AWS_LAMBDA_LOG_FORMAT", null);
}
}

[Fact]
public void JsonLogging_ScopesDisabled_ScopePropertiesNotIncluded()
{
Environment.SetEnvironmentVariable("AWS_LAMBDA_LOG_FORMAT", "JSON");
try
{
using (var writer = new StringWriter())
{
ConnectLoggingActionToLogger(message => writer.Write(message));

var loggerOptions = new LambdaLoggerOptions { IncludeScopes = false };
var loggerFactory = new TestLoggerFactory().AddLambdaLogger(loggerOptions);
var logger = loggerFactory.CreateLogger("JsonScopeTest");

var scopeProps = new Dictionary<string, object> { { "RequestId", "abc-123" } };
using (logger.BeginScope(scopeProps))
{
logger.LogInformation("User {Name} logged in", "Alice");
}

var text = writer.ToString();
// only 1 message param, scope excluded
Assert.Contains("parameter count: 1", text);
}
}
finally
{
Environment.SetEnvironmentVariable("AWS_LAMBDA_LOG_FORMAT", null);
}
}

[Fact]
public void JsonLogging_NoScopes_MessageTemplatePropertiesPreserved()
{
Environment.SetEnvironmentVariable("AWS_LAMBDA_LOG_FORMAT", "JSON");
try
{
using (var writer = new StringWriter())
{
ConnectLoggingActionToLogger(message => writer.Write(message));

var loggerOptions = new LambdaLoggerOptions { IncludeScopes = true };
var loggerFactory = new TestLoggerFactory().AddLambdaLogger(loggerOptions);
var logger = loggerFactory.CreateLogger("JsonScopeTest");

logger.LogInformation("Order {OrderId} placed for {Customer}", 42, "Bob");

var text = writer.ToString();
Assert.Contains("parameter count: 2", text);
Assert.Contains("Order {OrderId} placed for {Customer}", text);
}
}
finally
{
Environment.SetEnvironmentVariable("AWS_LAMBDA_LOG_FORMAT", null);
}
}

[Fact]
public void JsonLogging_ScopeWithNullValue_DoesNotCrash()
{
Environment.SetEnvironmentVariable("AWS_LAMBDA_LOG_FORMAT", "JSON");
try
{
using (var writer = new StringWriter())
{
ConnectLoggingActionToLogger(message => writer.Write(message));

var loggerOptions = new LambdaLoggerOptions { IncludeScopes = true };
var loggerFactory = new TestLoggerFactory().AddLambdaLogger(loggerOptions);
var logger = loggerFactory.CreateLogger("JsonScopeTest");

var scopeProps = new Dictionary<string, object> { { "NullProp", null } };
using (logger.BeginScope(scopeProps))
{
logger.LogInformation("Null scope value test");
}

var text = writer.ToString();
// 1 scope param (null) + 0 message params = 1
Assert.Contains("parameter count: 1", text);
}
}
finally
{
Environment.SetEnvironmentVariable("AWS_LAMBDA_LOG_FORMAT", null);
}
}

[Fact]
public void JsonLogging_NonStructuredScope_DoesNotCrash()
{
Environment.SetEnvironmentVariable("AWS_LAMBDA_LOG_FORMAT", "JSON");
try
{
using (var writer = new StringWriter())
{
ConnectLoggingActionToLogger(message => writer.Write(message));

var loggerOptions = new LambdaLoggerOptions { IncludeScopes = true };
var loggerFactory = new TestLoggerFactory().AddLambdaLogger(loggerOptions);
var logger = loggerFactory.CreateLogger("JsonScopeTest");

using (logger.BeginScope("plain string scope"))
{
logger.LogInformation("Message {Param}", "value");
}

var text = writer.ToString();
// non-structured scope ignored; only 1 message param
Assert.Contains("parameter count: 1", text);
}
}
finally
{
Environment.SetEnvironmentVariable("AWS_LAMBDA_LOG_FORMAT", null);
}
}

private static string GetAppSettingsPath(string fileName)
{
return Path.Combine(APPSETTINGS_DIR, fileName);
Expand Down