The new structured logging support is great and provides all the necessary building blocks to create custom JSON log formats via JsonWriterStructuredLogFormatter.

However, JsonValueWriter.writeString() escapes / as \/ in JSON, which causes issues when targeting platforms like Google Cloud Logging. GCP expects specific root-level keys such as:

{
"logging.googleapis.com/spanId": "abc",
"logging.googleapis.com/trace": "projects/my-project/traces/123"
}

But the current implementation renders them as:

{
"logging.googleapis.com\/spanId": "abc",
"logging.googleapis.com\/trace": "projects\/my-project\/traces\/123"
}

example impl

public class GCPStructuredLogFormatter extends JsonWriterStructuredLogFormatter<ILoggingEvent> {

    GCPStructuredLogFormatter(StackTracePrinter stackTracePrinter,
                              ContextPairs contextPairs,
                              ThrowableProxyConverter throwableProxyConverter,
                              StructuredLoggingJsonMembersCustomizer<?> customizer) {
        super(GCPStructuredLogFormatter::jsonMembers, customizer);
    }

    private static void jsonMembers(JsonWriter.Members<ILoggingEvent> members) {
        members.add("timestamp", ILoggingEvent::getInstant).as(GCPStructuredLogFormatter::asTimestamp);
        members.add("message", ILoggingEvent::getFormattedMessage);
        members.add("logger_name", ILoggingEvent::getLoggerName);
        members.add("thread_name", ILoggingEvent::getThreadName);
        members.add("level", ILoggingEvent::getLevel);
        members.add("logging.googleapis.com/spanId", (e) -> "abc");
        members.add("logging.googleapis.com/trace", (e) -> "projects/my-project/traces/123");
    }

    private static String asTimestamp(Instant instant) {
        OffsetDateTime offsetDateTime = OffsetDateTime.ofInstant(instant, ZoneId.systemDefault());
        return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(offsetDateTime);
    }

}

This prevents GCP from recognizing the structured fields.

Request: Add a way to opt out of escaping / in JSON keys when using structured logging.

This would avoid having to reimplement JSON rendering logic entirely and allow users to fully benefit from the structured logging infrastructure provided by Spring Boot.

Comment From: mhalbritter

I wonder why we escape / in the first place. The RFC reads:

An object structure is represented as a pair of curly brackets surrounding zero or more name/value pairs (or members). A name is a string.

And a string is:

All Unicode characters may be placed within the quotation marks, except for the characters that MUST be escaped: quotation mark, reverse solidus, and the control characters (U+0000 through U+001F).

Comment From: nosan

Map<String, String> values = new LinkedHashMap<>();
values.put("/", "/");
values.put("NUL", "NUL");  \u0000
values.put("'", "'");
values.put("\"", "\"");
values.put("\n", "\n");
values.put("\\", "\\");
values.put("\b", "\b");
values.put("\f", "\f");
values.put("\r", "\r");
values.put("\t", "\t");



Jackson
{"/":"/","\u0000":"\u0000","'":"'","\"":"\"","\n":"\n","\\":"\\","\b":"\b","\f":"\f","\r":"\r","\t":"\t"}

Gson
{"/":"/","\u0000":"\u0000","'":"'","\"":"\"","\n":"\n","\\":"\\","\b":"\b","\f":"\f","\r":"\r","\t":"\t"}

JSON API (Jakarta)
{"/":"/","\u0000":"\u0000","'":"'","\"":"\"","\n":"\n","\\":"\\","\b":"\b","\f":"\f","\r":"\r","\t":"\t"}

Spring Boot
{"\/":"\/","\u0000":"\u0000","'":"'","\"":"\"","\n":"\n","\\":"\\","\b":"\b","\f":"\f","\r":"\r","\t":"\t"}

Looks like only Spring Boot escapes / , other JSON libraries don't do that.

Comment From: mhalbritter

Thanks for testing, @nosan. I think we stop escaping the /. @philwebb do you agree?

Comment From: philwebb

Yes, I think this is a bug if we don't align with the other libraries.