Another option that might be a little more involved but could be more beneficial is to create a custom attribute containing a StringBuilder or some form of logging context.
Every predicate would then append a message to the value stored in this custom attribute instead of individually logging its own messages and would not need to override toString(). The challenge would be you need a new handler whose responsibility is to actually log out the message at the end. This gives a benefit where each request will only have a single request logging context logged as a single log entry. Otherwise, if you have concurrent requests running it could be challenging to pick apart the full context of the request you are interested in since its logs will be interleaved with many other logs. It would also be nice to make this functionality have timing and allow all handlers to opt-in.
Jersey handles this logging in a unique way where the trace context is sent back as an HTTP header for your request and looks like this.
1 $ curl -i http://localhost:9998/ALL/root/sub-resource-locator/sub-resource-method -H content-type:application/x-jersey-test --data '-=#[LKR]#=-' -H X-Jersey-Tracing-Threshold:SUMMARY -H accept:application/x-jersey-test -X POST
2
3 X-Jersey-Tracing-000: START [ ---- / ---- ms | ---- %] baseUri=[http://localhost:9998/ALL/] requestUri=[http://localhost:9998/ALL/root/sub-resource-locator/sub-resource-method] method=[POST] authScheme=[n/a] accept=[application/x-jersey-test] accept-encoding=n/a accept-charset=n/a accept-language=n/a content-type=[application/x-jersey-test] content-length=[11]
4 X-Jersey-Tracing-001: PRE-MATCH [ 0.01 / 0.68 ms | 0.01 %] PreMatchRequest summary: 2 filters
5 X-Jersey-Tracing-002: MATCH [ 8.44 / 9.15 ms | 4.59 %] RequestMatching summary
6 X-Jersey-Tracing-003: REQ-FILTER [ 0.01 / 9.20 ms | 0.00 %] Request summary: 2 filters
7 X-Jersey-Tracing-004: RI [86.14 / 95.49 ms | 46.87 %] ReadFrom summary: 3 interceptors
8 X-Jersey-Tracing-005: INVOKE [ 0.04 / 95.70 ms | 0.02 %] Resource [org.glassfish.jersey.tests.integration.tracing.SubResource @901a4f3] method=[public org.glassfish.jersey.tests.integration.tracing.Message org.glassfish.jersey.tests.integration.tracing.SubResource.postSub(org.glassfish.jersey.tests.integration.tracing.Message)]
9 X-Jersey-Tracing-006: RESP-FILTER [ 0.01 / 96.55 ms | 0.00 %] Response summary: 2 filters
10 X-Jersey-Tracing-007: WI [85.95 / 183.69 ms | 46.77 %] WriteTo summary: 4 interceptors
11 X-Jersey-Tracing-008: FINISHED [ ---- / 183.79 ms | ---- %] Response status: 200/SUCCESSFUL|OK
There is also a more verbose mode that gives more detail on every filter invoked.
Jersey has a much more rigid set of interfaces which makes it a little easier to automatically generate this type of logging. Undertow offers much more flexibility but that means figuring out how to properly append logs will be a much more manual process.