Skip to content

Better integration with spring structured logging #2116

@kamilhark

Description

@kamilhark

Since Spring Boot 3.4, there is a way to configure logging directly in spring in a way the output is a json document.
https://spring.io/blog/2024/08/23/structured-logging-in-spring-boot-3-4

When using default sink and writer, the entire logbook json is serialized to a string.
That string (message) is later used in structured logging to populate the message, the final log look like that:

{
  "@timestamp": "2025-06-17T13:57:03.956984242Z",
  "log": {
    "level": "TRACE",
    "logger": "org.zalando.logbook.Logbook"
  },
  "message": "\"duration\": \"267\",\n\"protocol\": \"HTTP\\/1.1\",\n\"status\": \"200\",\"type\": \"response\"",
  "ecs": {
    "version": "8.11"
  },
  ...
}

That means the content of the message will not be recognized as a json in tools like ElasticSearch not will not be indexed correctly.

The solution my team implemented is not to put the whole json as string into message, but to use MDC to populate individual fields from what logbook collected (a map) and keep the message as human readable string (which I think is how it's meant to be)

class StructuredLogbookSink(private val formatter: JsonHttpLogFormatter, private val writer: HttpLogWriter) : Sink {

    override fun write(precorrelation: Precorrelation, request: HttpRequest){
        val msg = "Request: ${request.method} ${request.requestUri}"
        val requestLogMap = formatter.prepare(precorrelation, request)

        withMDC(requestLogMap) {
            writer.write(precorrelation, msg)
        }
    }

    override fun write(correlation: Correlation, request: HttpRequest, response: HttpResponse){
        val msg = "Response: ${request.method} ${request.requestUri} - Status: ${response.status}"
        val responseLogMap = formatter.prepare(correlation, response)

        withMDC(responseLogMap) {
            writer.write(correlation, msg)
        }
    }

    private fun withMDC(map: Map<String, Any>, block: () -> Unit) {
        map.forEach { (key, value) ->
            MDC.put("logbook.$key", value.toString())
        }
        block()
        map.keys.forEach { key ->
            MDC.remove("logbook.$key")
        }
    }
}

Using that sink will result in log like that:

{
  "@timestamp": "2025-06-17T13:57:03.956984242Z",
  "log": {
    "level": "TRACE",
    "logger": "org.zalando.logbook.Logbook"
  },
  "message": "Response: GET http:\/\/localhost:8080\/api\/actuator\/health - Status: 200",
  "logbook": {
    "duration": "267",
    "protocol": "HTTP\/1.1",
    "status": "200",
    "origin": "local",
    "type": "response"
  },
  "ecs": {
    "version": "8.11"
  }
} 

What I don't like about it is that it uses JsonHttpLogFormatter to get the map of log properties even if the json is not used in the end, it's just the method that extracts properties from request/response is there.
Second it's a custom code that we will need to maintain and possible this is something more people will be interested about.

Do you think such a feature could be implemented directly in logbook / spring boot starter?

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions