Skip to content

Commit

Permalink
doc: Context propagation documentation (micronaut-projects#9554)
Browse files Browse the repository at this point in the history
  • Loading branch information
dstepanov committed Jul 21, 2023
1 parent c559193 commit cc2f760
Show file tree
Hide file tree
Showing 19 changed files with 422 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/
package io.micronaut.core.async.propagation;

import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.Experimental;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.propagation.PropagatedContext;
import io.micronaut.core.propagation.PropagatedContextElement;
Expand All @@ -31,7 +31,7 @@
* @author Denis Stepanov
* @since 4.0.0
*/
@Internal
@Experimental
public final class ReactorPropagation {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.bind.annotation.Bindable;
import io.micronaut.core.io.buffer.ByteBuffer;
import io.micronaut.core.propagation.MutablePropagatedContext;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.MutableHttpRequest;
Expand Down Expand Up @@ -55,7 +56,8 @@ public final class FilterVisitor implements TypeElementVisitor<Object, Object> {
MutableHttpResponse.class,
FilterContinuation.class,
Optional.class,
Throwable.class
Throwable.class,
MutablePropagatedContext.class
);
private static final Set<String> PERMITTED_BINDING_ANNOTATIONS = Set.of(
Body.class.getName(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
* parameter</li>
* <li>A {@code @}{@link Body} parameter of type {@code byte[]}, {@link String} or
* {@link ByteBuffer}. Only supported for some HTTP server implementations.</li>
* <li>A {@link io.micronaut.core.propagation.MutablePropagatedContext} to modify the propagated context</li>
* </ul>
*
* The return value may be:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
* an error status code, between each filter.</b></li>
* <li>A {@code @}{@link Header}, {@code @}{@link QueryValue} or {@code @}{@link CookieValue}
* parameter</li>
* <li>A {@link io.micronaut.core.propagation.MutablePropagatedContext} to modify the propagated context</li>
* </ul>
*
* The return value may be:
Expand Down
4 changes: 4 additions & 0 deletions src/main/docs/guide/appendix/breaks.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,7 @@ Micronaut Framework 4 changes `@CrossOrigin` behavior to match configuration-ba
==== `@EachBean` requires a `@Named qualifier

`@EachBean` throws a "multiple possible bean candidates found" exception if any parent bean lacks a name qualifier.

==== Manual Context Propagation

In Micronaut Framework 4, users need to extend the <<contextPropagation, propagation context>> manually.
23 changes: 23 additions & 0 deletions src/main/docs/guide/contextPropagation.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
The new Propagation Context API aims to simplify reactor instrumentation, avoid thread-local usage, and integrate idiomatically with Kotlin Coroutines.

The api:core.propagation.PropagatedContext[] object represents the context propagation. We designed the context propagation API for immutability. It consists of multiple elements of type api:core.propagation.PropagatedContextElement[].

Each element represents a particular state that needs to be propagated. There is a special element that can be used to update and restore the thread-local value api:core.propagation.ThreadPropagatedContextElement[]:

.Example of MDC propagated element implementing `ThreadPropagatedContextElement`
[source,java]
----
include::{testsuitejava}/propagation/MdcPropagationContext.java[tags="class"]
----
<1> The class has the MDC state passed in the contractor
<2> The context update sets the MDC state as the current one
<3> The previous state is captured to be restored
<3> The previous state is restored

In this example, the propagated context element implements the setting of the MDC context and it restores it after.

NOTE: The api:core.propagation.ThreadPropagatedContextElement[] is inspired by Kotlin Coroutines propagation API element `kotlinx.coroutines.ThreadContextElement`

snippet::io.micronaut.docs.propagation.MdcService[tags="createUser", indent=0, title="MDC propagation example"]

IMPORTANT: In the previous versions of Micronaut Framework, we would capture the context to propagate, this is not the case since Micronaut Framework 4. We always require for the context to be modified manually and repropagated.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Modifying the propagated context is a common scenario. Usually, you want to extend the context to include the request-related values.

To use a non-reactive HTTP filter API, you need to add a method parameter api:core.propagation.MutablePropagatedContext[] and modify the propagated context elements by adding or removing the existing ones:

.Example of adding a new MDC propagated context element
[source,java]
----
include::{testsuitejava}/propagation/MdcFilter.java[tags="class", indent=0]
----

The next filter in the chain will have the new propagated context available. Any of the thread-local context elements will be set for the next filter or the controller method invocation.

To use the legacy reactive HTTP filters, simply modify and propagate the context bound to the following chain invocation:

.Example of adding a new MDC propagated context element for the reactive filter:
[source,java]
----
include::{testsuitejava}/propagation/MdcLegacyFilter.java[tags="class", indent=0]
----
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Since Micronaut Framework version 4, https://projectreactor.io[Project Reactor] integration no longer captures the state automatically. Micronaut Framework users need to extend the propagation context manually.

Before version 4, Micronaut Framework required the instrumentation of every reactive operator to capture the current state to propagate it. It added an unwanted overhead and forced us to maintain complicated Reactor operators' instrumentation.

Since 3.5.0, Reactor-Core embeds support for the `io.micrometer:context-propagation` SPI. This allows to achieve the same thread-local propagation by including the https://micrometer.io/docs/contextPropagation[Micrometer Context Propagation] dependency.

The framework automatically adds the `PropagatedContext` to Project Reactor's context for interceptors and the HTTP filters. You can access it via the utility class api:core.async.propagation.ReactorPropagation[].

NOTE: api:core.async.propagation.ReactorPropagation[] is an experimental class and might change in the future.

It is possible to use https://micrometer.io/docs/contextPropagation[Micrometer Context Propagation], which Reactor supports for propagation and restoring the thread-local context.

To enable it, include the dependency:

dependency:context-propagation[groupId="io.micrometer",scope="compile"]

After that, all the thread-local propagated elements can restore their thread-local value.

NOTE: The thread-local values are read-only. To modify them, the `PropagatedContext` instance needs to be changed and put into the Reactor's context.

If you have Micrometer Context Propagation on the classpath but don't want to use it, apply the following configuration:

.Disable Micrometer Context Propagation in Reactor
[configuration]
----
reactor:
enable-automatic-context-propagation: false
----
3 changes: 3 additions & 0 deletions src/main/docs/guide/introduction/whatsNew.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ See <<filtermethods, Filter Methods>>

<<javanetClient, Additional implementation of the HTTP Client based on Java HTTP Client>>

==== Context Propagation API

Micronaut Framework 4 introduces a new <<contextPropagation, Context Propagation API>>, which aims to simplify reactor instrumentation, avoid thread-local usage, and integrate idiomatically with Kotlin Coroutines.

=== Breaking Changes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ dependency:org.jetbrains.kotlinx:kotlinx-coroutines-reactor[]

For more detailed information on how to use the library you can find at the official link:https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/-reactor-context/index.html[documentation].

NOTE: Since Micronaut framework 4, we recommend using the latest <<contextPropagation, Context Propagation API>>. The api:core.propagation.ThreadPropagatedContextElement[] is inspired by Kotlin Coroutines propagation API element `kotlinx.coroutines.ThreadContextElement` and acts similarly by restoring thread locals.

Following example shows how to propagate Reactor context from the HTTP filter to the controller's coroutine:

snippet::io.micronaut.docs.reactor.ReactorContextPropagationSpec[tags="simplefilter", indent=0, title="Simple filter which writes into Reactor's context"]
Expand All @@ -14,4 +16,4 @@ snippet::io.micronaut.docs.reactor.ReactorContextPropagationSpec[tags="readctx",

It's possible to use coroutines Reactor integration to create a filter using a suspended function:

snippet::io.micronaut.docs.reactor.ReactorContextPropagationSpec[tags="suspendfilter", indent=0, title="Suspended function filter which writes into Reactor's context"]
snippet::io.micronaut.docs.reactor.ReactorContextPropagationSpec[tags="suspendfilter", indent=0, title="Suspended function filter which writes into Reactor's context"]
4 changes: 4 additions & 0 deletions src/main/docs/guide/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ httpClient:
clientHttp2: HTTP/2 Support
clientHttp3: HTTP/3 Support
clientSample: HTTP Client Sample
contextPropagation:
title: Context propagation
reactorContextPropagation: Reactor context propagation
httpFilterContextPropagation: HTTP filters context propagation
cloud:
title: Cloud Native Features
cloudConfiguration:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.micronaut.docs.ioc.beans

import io.micronaut.core.annotation.Introspected

// tag::class[]
@Introspected(accessKind = [Introspected.AccessKind.FIELD])
class User(
val name: String // <1>
) {
var age = 18 // <2>
}
// end::class[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.micronaut.docs.propagation;

import io.micronaut.context.annotation.Requires;
import io.micronaut.core.propagation.MutablePropagatedContext;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.annotation.RequestFilter;
import io.micronaut.http.annotation.ServerFilter;
import org.slf4j.MDC;

import static io.micronaut.http.annotation.Filter.MATCH_ALL_PATTERN;

@Requires(property = "mdc.example.filter.enabled")
// tag::class[]
@ServerFilter(MATCH_ALL_PATTERN)
public class MdcFilter {

@RequestFilter
public void myRequestFilter(HttpRequest<?> request, MutablePropagatedContext mutablePropagatedContext) {
try {
String trackingId = request.getHeaders().get("X-TrackingId");
MDC.put("trackingId", trackingId);
mutablePropagatedContext.add(new MdcPropagationContext());
} finally {
MDC.remove("trackingId");
}
}

}
// end::class[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2017-2020 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.docs.propagation;

import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.client.HttpClient;
import io.micronaut.runtime.server.EmbeddedServer;
import org.junit.Test;
import org.slf4j.MDC;

import java.util.Map;
import java.util.UUID;

import static org.junit.Assert.assertEquals;

public class MdcFilterSpec {

@Test
public void testFilterSpec() {
try (EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer.class, Map.of("mdc.example.filter.enabled", true))) {
try (HttpClient client = HttpClient.create(embeddedServer.getURL())) {

String tracingId = UUID.randomUUID().toString();
HttpRequest<Object> request = HttpRequest
.GET("/mdc/test")
.header("X-TrackingId", tracingId);
assertEquals(client.toBlocking().retrieve(request), tracingId);
}
}
}

@Controller("/mdc")
@Requires(property = "mdc.example.filter.enabled")
static class MDCController {

@Get(value = "/test", produces = MediaType.TEXT_PLAIN)
String test() {
return MDC.get("trackingId");
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.micronaut.docs.propagation;

import io.micronaut.context.annotation.Requires;
import io.micronaut.context.propagation.slf4j.MdcPropagationContext;
import io.micronaut.core.propagation.PropagatedContext;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.annotation.Filter;
import io.micronaut.http.filter.HttpServerFilter;
import io.micronaut.http.filter.ServerFilterChain;
import org.reactivestreams.Publisher;
import org.slf4j.MDC;

import static io.micronaut.http.annotation.Filter.MATCH_ALL_PATTERN;

@Requires(property = "mdc.example.legacy.filter.enabled")
// tag::class[]
@Filter(MATCH_ALL_PATTERN)
public class MdcLegacyFilter implements HttpServerFilter {

@Override
public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request,
ServerFilterChain chain) {
try {
String trackingId = request.getHeaders().get("X-TrackingId");
MDC.put("trackingId", trackingId);
try (PropagatedContext.Scope ignore = PropagatedContext.get().plus(new MdcPropagationContext()).propagate()) {
return chain.proceed(request);
}
} finally {
MDC.remove("trackingId");
}
}

}
// end::class[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2017-2020 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.docs.propagation;

import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.client.HttpClient;
import io.micronaut.runtime.server.EmbeddedServer;
import org.junit.Test;
import org.slf4j.MDC;

import java.util.Map;
import java.util.UUID;

import static org.junit.Assert.assertEquals;

public class MdcLegacyFilterSpec {

@Test
public void testFilterSpec() {
try (EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer.class, Map.of("mdc.example.legacy.filter.enabled", true))) {
try (HttpClient client = HttpClient.create(embeddedServer.getURL())) {

String tracingId = UUID.randomUUID().toString();
HttpRequest<Object> request = HttpRequest
.GET("/mdc/test")
.header("X-TrackingId", tracingId);
assertEquals(client.toBlocking().retrieve(request), tracingId);
}
}
}

@Controller("/mdc")
@Requires(property = "mdc.example.legacy.filter.enabled")
static class MDCController {

@Get(value = "/test", produces = MediaType.TEXT_PLAIN)
String test() {
return MDC.get("trackingId");
}

}
}
Loading

0 comments on commit cc2f760

Please sign in to comment.