From 87757af611fd2c2fe5b375d6c850abcb66ae79ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 03:07:15 +0000 Subject: [PATCH 1/2] Initial plan From b0300649e85fd6e5e3524880999d8f4709e5cb43 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 03:13:50 +0000 Subject: [PATCH 2/2] feat: Add structured scope support to Lambda JSON logging When AWS_LAMBDA_LOG_FORMAT=JSON and IncludeScopes=true, scope state objects implementing IEnumerable> have their key/value entries appended to the message template (as {key} placeholders) and the parameters array, so they are emitted as named JSON properties by the Lambda JSON formatter. Non-structured scopes are silently ignored. Existing behavior (scopes disabled, text-format logging, no scopes) is unchanged. Closes aws/aws-lambda-dotnet#2122 Co-authored-by: Lanayx <3329606+Lanayx@users.noreply.github.com> --- .../LambdaILogger.cs | 31 +++ .../README.md | 22 +++ .../LoggingTests.cs | 185 ++++++++++++++++++ 3 files changed, 238 insertions(+) diff --git a/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs b/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs index 2898a8472..ee5e5dcc1 100644 --- a/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs +++ b/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs @@ -75,6 +75,37 @@ public void Log(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>(); + ScopeProvider.ForEachScope((scope, list) => + { + if (scope is IEnumerable> 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 diff --git a/Libraries/src/Amazon.Lambda.Logging.AspNetCore/README.md b/Libraries/src/Amazon.Lambda.Logging.AspNetCore/README.md index 6435cd39b..90a45845b 100644 --- a/Libraries/src/Amazon.Lambda.Logging.AspNetCore/README.md +++ b/Libraries/src/Amazon.Lambda.Logging.AspNetCore/README.md @@ -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>` (such as `Dictionary`) 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 +{ + { "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. diff --git a/Libraries/test/Amazon.Lambda.Logging.AspNetCore.Tests/LoggingTests.cs b/Libraries/test/Amazon.Lambda.Logging.AspNetCore.Tests/LoggingTests.cs index e9997cdf3..1c04b5152 100644 --- a/Libraries/test/Amazon.Lambda.Logging.AspNetCore.Tests/LoggingTests.cs +++ b/Libraries/test/Amazon.Lambda.Logging.AspNetCore.Tests/LoggingTests.cs @@ -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 { { "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 { { "TraceId", "trace-1" } }; + var innerScope = new Dictionary { { "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 { { "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 { { "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);