Skip to content

JSON log format pairs ILogger message template placeholders with state values by position, garbling ASP.NET Core hosting logs #2468

Description

@madmox

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).
  2. JsonLogMessageFormatter then pairs template placeholders with arguments strictly by position (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 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:

{
    "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.:

{
    "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:

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:

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugThis issue is a bug.needs-triageThis issue or PR still needs to be triaged.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions