You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 ownJsonLogMessageFormatter.
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
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
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue
Fixes #2350
LambdaLogger.ConfigureStructuredLogginghad no effect for functions using JSON log format: a customer-suppliedJsonSerializerOptions(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.Initializecreates twoWrapperTextWriterinstances — one for stdout and one for stderr. When JSON log format is enabled, eachWrapperTextWriterconstructed its ownJsonLogMessageFormatter.Each
JsonLogMessageFormatterregisters the customer'sConfigureStructuredLoggingcallback through a single static setter onAmazon.Lambda.Core.LambdaLogger(SetConfigureStructuredLoggingAction). Because there are two formatters, the second construction (stderr) overwrote the first (stdout). When the customer calledConfigureStructuredLogging, the options were delivered only to the stderr formatter, whilecontext.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.Coreis loaded (from the deployment bundle, via the customerdeps.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.Initializenow builds the stdout writer first and passes its formatter to the stderr writer via a new optionalsharedFormatterparameter on theWrapperTextWriterconstructor. Both writers resolve the same log format from the same environment, so a single formatter is correct for both, and the singleConfigureStructuredLoggingcallback registration now targets the same instance that formats stdout output.Verification
SharedFormatterStructuredLoggingTestsexercises the two-writer path with a converter registered only viaOverrideSerializerOptionson a value type that otherwise serializes to{}. It fails without the fix and passes with it. The test injectsIEnvironmentVariablesso it does not mutate process-wide state.StructuredLoggingxUnit collection now serializes the tests that mutate the process-wideLambdaLoggerstructured-logging static.Amazon.Lambda.RuntimeSupportunit suite passes (313/313) on net8.0; clean build across net8.0/net9.0/net10.0 with 0 warnings (TreatWarningsAsErrors).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":{}.