Skip to content

Fix ConfigureStructuredLogging options dropped due to duplicate formatters (#2350) - #2579

Merged
normj merged 5 commits into
masterfrom
normj-aws-lambda-dotnet-2350
Sep 25, 2026
Merged

normj merged 5 commits into
masterfrom
normj-aws-lambda-dotnet-2350

Conversation

@normj

@normj normj commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Issue

Fixes #2350

LambdaLogger.ConfigureStructuredLogging had no effect for functions using JSON log format: a customer-supplied JsonSerializerOptions (for example one carrying NodaTime converters) was silently dropped, so a destructured value such as {@ScheduledDate} serialized as "ScheduledDate":{} instead of using the custom converter.

Root cause

LogLevelLoggerWriter.Initialize creates two WrapperTextWriter instances — one for stdout and one for stderr. When JSON log format is enabled, each WrapperTextWriter constructed its own JsonLogMessageFormatter.

Each JsonLogMessageFormatter registers the customer's ConfigureStructuredLogging callback through a single static setter on Amazon.Lambda.Core.LambdaLogger (SetConfigureStructuredLoggingAction). Because there are two formatters, the second construction (stderr) overwrote the first (stdout). When the customer called ConfigureStructuredLogging, the options were delivered only to the stderr formatter, while context.Logger.Log* writes go through the stdout formatter — which still had default options. The custom converter therefore never ran and the value serialized to {}.

This is not an assembly-identity or timing problem: a single Amazon.Lambda.Core is loaded (from the deployment bundle, via the customer deps.json), and Core already replays options configured before the callback is wired. The defect was purely that the wrong (duplicate) formatter instance received the callback.

Fix

Share a single formatter between the two writers. LogLevelLoggerWriter.Initialize now builds the stdout writer first and passes its formatter to the stderr writer via a new optional sharedFormatter parameter on the WrapperTextWriter constructor. Both writers resolve the same log format from the same environment, so a single formatter is correct for both, and the single ConfigureStructuredLogging callback registration now targets the same instance that formats stdout output.

Verification

  • New regression test SharedFormatterStructuredLoggingTests exercises the two-writer path with a converter registered only via OverrideSerializerOptions on a value type that otherwise serializes to {}. It fails without the fix and passes with it. The test injects IEnvironmentVariables so it does not mutate process-wide state.
  • A StructuredLogging xUnit collection now serializes the tests that mutate the process-wide LambdaLogger structured-logging static.
  • Full Amazon.Lambda.RuntimeSupport unit suite passes (313/313) on net8.0; clean build across net8.0/net9.0/net10.0 with 0 warnings (TreatWarningsAsErrors).
  • End-to-end: built the managed-runtime container base image from this branch (LambdaRuntimeDockerfiles), packaged a .NET 10 class-library function that reproduces the reporter's exact scenario, and deployed it to AWS Lambda (us-west-2, JSON log format). CloudWatch confirms "scheduledDate":"2026-09-01T15:40:00Z" (converter applied) where before the fix it was "scheduledDate":{}.

…anaged runtime (#2350)

In the class library programming model the managed runtime hosts a bundled
Amazon.Lambda.RuntimeSupport that is compiled against its own Amazon.Lambda.Core,
while the customer's Amazon.Lambda.Core is loaded separately by UserCodeLoader.
The compile-time SetConfigureStructuredLoggingAction wiring therefore registered
the formatter callback on the wrong LambdaLogger, so LambdaLogger.ConfigureStructuredLogging
had no effect (custom JsonSerializerOptions were dropped and objects rendered as {}).

Wire the structured logging callback into the customer's Amazon.Lambda.Core via
reflection, mirroring the existing SetCustomerLoggerLogAction pattern:
- Add ConfigureCallbackInCore(Assembly, Action) reflection overload
- Track JsonLogMessageFormatter instances and add WireStructuredLoggingCallbacksToCustomerCore
- Invoke it from UserCodeLoader when the customer's Amazon.Lambda.Core loads

The executable / custom runtime path is unchanged. Adds regression tests.
@normj
normj requested review from a team as code owners September 23, 2026 00:15
@normj
normj marked this pull request as draft September 23, 2026 23:19
@normj normj changed the title Fix ConfigureStructuredLogging no-op for class library functions on managed runtime (#2350) Fix ConfigureStructuredLogging options dropped due to duplicate formatters (#2350) Sep 24, 2026
@normj
normj requested a lite review from Copilot September 24, 2026 03:07
@normj
normj marked this pull request as ready for review September 24, 2026 03:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟢 Approval recommended

No unresolved blocking issues were identified.

Review effort: Lite
Findings: None

What changed in this PR

Fixes structured logging options being dropped by sharing one JSON formatter between stdout and stderr.

Changes:

  • Shares formatter instances across both writers.
  • Adds regression coverage for custom converters.
  • Serializes structured-logging tests.
  • Adds patch changelog metadata.
File Summary
Libraries/​test/​Amazon.Lambda.RuntimeSupport.Tests/​Amazon.Lambda.RuntimeSupport.UnitTests/​StructuredLoggingCollection.cs Serializes structured-logging tests.
Libraries/​test/​Amazon.Lambda.RuntimeSupport.Tests/​Amazon.Lambda.RuntimeSupport.UnitTests/​SharedFormatterStructuredLoggingTests.cs Adds regression coverage for custom serializer options.
Libraries/​test/​Amazon.Lambda.RuntimeSupport.Tests/​Amazon.Lambda.RuntimeSupport.UnitTests/​LogMessageFormatterTests.cs Assigns formatter tests to the serialized collection.
Libraries/​src/​Amazon.Lambda.RuntimeSupport/​Helpers/​ConsoleLoggerWriter.cs Shares the formatter between output writers.
.autover/​changes/​b2f4a6c1-2350-4e8d-9a3c-configstructuredlog.json Documents the patch release change.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Move formatter creation out of WrapperTextWriter and into
LogLevelLoggerWriter.Initialize, then pass the single instance
explicitly into both the stdout and stderr WrapperTextWriter
constructors. The constructor now takes a required ILogMessageFormatter
(no null default) and no longer resolves log format or builds a
formatter internally, so both writers are symmetric and there is no
sibling instance borrowing inner state.

- Add CreateLogMessageFormatter helper and lift LogFormatType enum +
  LOG_FORMAT env-var resolution up to LogLevelLoggerWriter.
- Remove the now-unused _logFormatType field.
- No behavior change: both writers still share one formatter so the
  customer's ConfigureStructuredLogging options reach the stdout
  formatter. SharedFormatterStructuredLoggingTests still passes and
  still fails when formatters are not shared.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

Address culture-dependent test output and isolate all tests that can overwrite the static logging callback.

Review effort: Lite
Findings: None

Previously missed (1)

In code that hasn't changed since last review

Medium severity Isolate tests that mutate the static LambdaLogger callback

Libraries/​test/​Amazon.Lambda.RuntimeSupport.Tests/​Amazon.Lambda.RuntimeSupport.UnitTests/​StructuredLoggingCollection.cs:13

This collection does not isolate these tests from classes in other xUnit collections. LambdaBootstrapMultiConcurrencyTests.WorkerPoolInitializingLog_EmissionGatedByJsonLogFormat constructs LogLevelLoggerWriter with AWS_LAMBDA_HANDLER_LOG_FORMAT=Json (and therefore registers another static LambdaLogger callback) but is not in this collection. If it runs between the regression test's writer construction and ConfigureStructuredLogging, it can overwrite the callback and make the test intermittently fail; include all JSON formatter-constructing tests in the same non-parallel collection or otherwise isolate the static state.

… culture (#2350)

- Add LambdaBootstrapMultiConcurrencyTests to the StructuredLogging
  serial collection. Its WorkerPoolInitializingLog_EmissionGatedByJsonLogFormat
  test builds a real LogLevelLoggerWriter (via TestMultiConcurrencyRuntimeApiClient)
  under JSON log format, which registers a JsonLogMessageFormatter as the
  static LambdaLogger structured-logging callback target. Running it in
  parallel with SharedFormatterStructuredLoggingTests could overwrite the
  callback and cause intermittent failures.
- Format the regression test's Instant converter output with
  CultureInfo.InvariantCulture so the asserted string is culture-independent.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟢 Approval recommended

No unresolved issues were identified that would block approval.

Review effort: Lite
Findings: None

@normj
normj requested a review from peterrsongg September 25, 2026 16:25
@normj
normj merged commit dc1e5a1 into master Sep 25, 2026
9 checks passed
@normj
normj deleted the normj-aws-lambda-dotnet-2350 branch September 25, 2026 21:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow custom JsonSerializerOptions (and converters) in JsonLogMessageFormatter

4 participants