JSON log format pairs ILogger message template placeholders with state values by position, garbling ASP.NET Core hosting logs
- Dominant language
- C#
- Stars
- 1.7k
- Forks
- 503
- Avg merge
- 1d 5h
- Merged PRs (30d)
- 18
Description
### Describe the bug
When a function uses `LoggingConfig.LogFormat: JSON` and bridges `Microsoft.Extensions.Logging.ILogger` to the Lambda logging API via `Amazon.Lambda.Logging.AspNetCore`, structured log events can come out with their JSON properties holding the **wrong values** (each property shifted to a neighboring placeholder's value).
Root cause, as far as I can tell, is a combination of two behaviors:
1. `LambdaILogger` extracts the message template from `{OriginalFormat}` and collects the state values **in state enumeration order** ([LambdaILogger.cs](https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs)).
2. `JsonLogMessageFormatter` then pairs template placeholders with arguments **strictly by position** ([JsonLogMessageFormatter.cs](https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/JsonLogMessageFormatter.cs)).
The `Microsoft.Extensions.Logging` contract is that placeholders are resolved **by name** against the state key/value pairs; nothing guarantees that state enumeration order matches placeholder order. The most prominent violator ships in ASP.NET Core itself: [`HostingRequestStartingLog`](https://github.com/dotnet/aspnetcore/blob/main/src/Hosting/Hosting/src/Internal/HostingRequestStartingLog.cs) enumerates its state as `Protocol, Method, ContentType, ContentLength, Scheme, Host, PathBase, Path, QueryString`, while its template is:
```
Request starting {Protocol} {Method} {Scheme}://{Host}{PathBase}{Path}{QueryString} - {ContentType} {ContentLength}
```
So every `Request starting` log line of every ASP.NET Core app running on Lambda with JSON log format is silently garbled from the third placeholder on.
### Regression Issue
- [ ] Select this option if this issue appears to be a regression.
### Expected Behavior
Template placeholders resolved by name, as with every other `Microsoft.Extensions.Logging` sink:
```json
{
"message": "Request starting HTTP/1.1 DELETE https://api.example.com/events/123?preserve=true - application/json 0",
"Method": "DELETE",
"Scheme": "https",
"Host": "api.example.com",
"Path": "/events/123",
"QueryString": "?preserve=true",
"ContentType": "application/json",
"ContentLength": 0
}
```
### Current Behavior
Actual CloudWatch log record (values anonymized). Note `Scheme` holding the content type, `Host` holding the content length, `Path` holding the host, `ContentType` holding the path, etc.:
```json
{
"timestamp": "2026-07-04T22:43:57.529Z",
"level": "Information",
"requestId": "188cbe50-087d-4465-acf4-d4a6067ef0da",
"message": "Request starting {Protocol} DELETE application/json://0httpsapi.example.com - /events/123 ?preserve=true",
"Method": "DELETE",
"Scheme": "application/json",
"Host": 0,
"PathBase": "https",
"Path": "api.example.com",
"QueryString": "",
"ContentType": "/events/123",
"ContentLength": "?preserve=true"
}
```
Each placeholder received the state value at the same index instead of the value with the matching name (`{Scheme}` ← `ContentType`, `{Host}` ← `ContentLength`, `{PathBase}` ← `Scheme`, ...). The rendered `message` is garbled the same way.
### Reproduction Steps
Minimal ASP.NET Core app:
```csharp
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAWSLambdaHosting(LambdaEventSource.RestApi);
builder.Logging.ClearProviders();
builder.Logging.AddLambdaLogger();
var app = builder.Build();
app.MapGet("/", () => "ok");
app.Run();
```
Deploy behind API Gateway with:
```yaml
LoggingConfig:
LogFormat: JSON
```
Invoke any endpoint and inspect the `Request starting` record in CloudWatch: the `Scheme`, `Host`, `PathBase`, `Path`, `QueryString`, `ContentType` and `ContentLength` properties are shifted as shown above.
### Possible Solution
Either:
- In `LambdaILogger` (JSON branch): parse the placeholder names from `{OriginalFormat}` and emit the arguments **in placeholder order**, resolving each one by key against the state; or
- In `JsonLogMessageFormatter`: when arguments come from an `ILogger` state, match them to placeholders by name instead of by position.
### Additional Information/Context
Any `ILogger` event whose state enumeration order differs from its template placeholder order is affected — ASP.NET Core hosting logs are just the guaranteed-out-of-the-box case. Logs written through the standard `LoggerExtensions`/`LoggerMessage` paths happen to be unaffected because `FormattedLogValues` keeps both orders aligned, which is probably why this went unnoticed.
### AWS .NET SDK and/or Package version used
Amazon.Lambda.AspNetCoreServer 10.1.1
Amazon.Lambda.AspNetCoreServer.Hosting 2.1.0
Amazon.Lambda.Logging.AspNetCore 5.0.0
Amazon.Lambda.Core 3.1.0
### Targeted .NET Platform
.NET 10 (managed `dotnet10` runtime)
### Operating System and version
Amazon Linux 2023 (Lambda managed runtime)
Contributor guide
Research direction
Read Libraries/src/Amazon.Lambda.Logging.AspNetCore/LambdaILogger.cs and Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/JsonLogMessageFormatter.cs, then run the minimal ASP.NET Core reproduction with JSON logging. Done means the Request starting record resolves each JSON property by its placeholder name and the rendered message is no longer shifted.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- aws, csharp
- Domain
- backend, observability-sre
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 70/100