Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support micrometer context-propagation #5577

Open
wants to merge 43 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
faffdd6
Support micrometer context-propagation : Skeleton code
chickenchickenlove Apr 8, 2024
0e1fd49
Merge branch 'main' into 240408-context-propagation
chickenchickenlove Apr 8, 2024
48b4039
Resolve lint error
chickenchickenlove Apr 8, 2024
65aa41b
Add java docs
chickenchickenlove Apr 10, 2024
1c3cda0
Add java docs and modify unit test
chickenchickenlove Apr 10, 2024
8030e8f
Apply comment
chickenchickenlove Apr 10, 2024
46cd890
remove ContextPropagation from boot3-autoconfigure
chickenchickenlove Apr 12, 2024
67a4f14
Move RequestContextAccessor to core module.
chickenchickenlove Apr 12, 2024
ae022ad
remove useless dependency
chickenchickenlove Apr 12, 2024
c633eaa
add dependencies
chickenchickenlove Apr 12, 2024
8710f1d
resolve java doc error
chickenchickenlove Apr 12, 2024
64cb196
fix test fail
chickenchickenlove Apr 12, 2024
9b32927
remove unused imported
chickenchickenlove Apr 12, 2024
6d4762a
Add test cases
chickenchickenlove Apr 12, 2024
4209756
Add test case
chickenchickenlove Apr 13, 2024
747fa36
Update core/src/main/java/com/linecorp/armeria/common/RequestContextA…
chickenchickenlove Apr 17, 2024
83a84b0
Update core/src/main/java/com/linecorp/armeria/common/RequestContextA…
chickenchickenlove Apr 17, 2024
3bdbd3e
Update core/src/main/java/com/linecorp/armeria/common/RequestContextA…
chickenchickenlove Apr 17, 2024
767bd9a
Fix lint error and test case
chickenchickenlove Apr 17, 2024
88b1475
resolve lint error
chickenchickenlove Apr 17, 2024
4a3ebfc
remove RequestContextPropagationHooks.
chickenchickenlove Apr 21, 2024
6a3d4f4
remove init context for Stepverifier.
chickenchickenlove Apr 21, 2024
7d2970d
Add test cases for contextCapture().
chickenchickenlove Apr 21, 2024
24c6f38
Should not ctx in main thread.
chickenchickenlove Apr 21, 2024
4a00814
apply review
chickenchickenlove Apr 22, 2024
f23f670
add @UnstableAPI and remove whitespace.
chickenchickenlove Apr 22, 2024
eb0c71e
Update core/src/test/java/com/linecorp/armeria/common/RequestContextA…
chickenchickenlove Apr 23, 2024
8a46ca5
apply review
chickenchickenlove Apr 23, 2024
aca31f8
Merge branch 'main' into 240408-context-propagation
chickenchickenlove Apr 23, 2024
9cd60e1
apply review
chickenchickenlove Apr 23, 2024
4b5015f
add test case
chickenchickenlove Apr 23, 2024
90f76a7
fix lint error
chickenchickenlove Apr 24, 2024
230a664
Update core/src/main/java/com/linecorp/armeria/internal/common/Reques…
chickenchickenlove Apr 24, 2024
3b9de29
Update core/src/test/java/com/linecorp/armeria/internal/common/Reques…
chickenchickenlove Apr 24, 2024
890a725
apply review
chickenchickenlove Apr 24, 2024
6a3e46e
Update core/src/main/java/com/linecorp/armeria/internal/common/Reques…
chickenchickenlove May 8, 2024
e80117c
apply review
chickenchickenlove May 8, 2024
12a2929
Merge branch 'main' into 240408-context-propagation
trustin Jun 5, 2024
2ca6b1b
Merge branch 'main' into 240408-context-propagation
jrhee17 Jun 17, 2024
79f16af
Update reactor3/src/test/java/com/linecorp/armeria/common/reactor3/Re…
chickenchickenlove Jun 20, 2024
fb2db7d
Update reactor3/src/test/java/com/linecorp/armeria/common/reactor3/Re…
chickenchickenlove Jun 20, 2024
f087912
Update core/src/main/java/com/linecorp/armeria/internal/common/Reques…
chickenchickenlove Jun 20, 2024
1e6cd9b
Add validation callback for context-propagation.
chickenchickenlove Jun 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ dependencies {
optionalApi libs.micrometer.prometheus.legacy
optionalApi libs.dropwizard.metrics.core
optionalApi libs.prometheus.legacy
implementation libs.context.propagation

// Netty
api libs.netty.transport
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with the current implementation since it is analogous to the current RequestContextHooks.

Going forward, I'm thinking:

  • The current generalization allows for multiple request contexts and not just RequestContext
  • We and users are already extensively using the RequestContext#[push|pop] APIs - we probably don't want to make a breaking change on this behavior
  • Maybe RequestContextThreadLocalAccessor positioned between the RequestContext and RequestContextStorage implementation is more realistic.

e.g.

class RequestContext {
...
  RequestContext push() {
    return contextSnapshotFactory.captureAll(key -> key.equals(RequestContext.class)).get(RequestContext.class)
  }
...
}
class RequestContextThreadLocalAccessor {
...
  public RequestContext getValue() {
    return RequestContextUtil.get();
  }
...
}

This way, we could 1) keep compatibility with the current RequestContext API 2) Allow an extension point for other context objects to be passed around 3) integration with the context-propagation API can be more first-class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I'm down.
Also, we can introduce Context interface to make new scope context extension easy for the future.
(For example, RequestContext is per Request, ConnectionContext is per Connection if armeria supports that features in the future. )

Copy link
Member

@trustin trustin Jun 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarify a little bit: We don't want to use Micrometer API to push or pop the context in our core. What we're trying to achieve with the integration is to allow users to access RequestContext (or the context types from other frameworks like Reactor) using Micrometer API.

I agree that we will eventually want per-connection context, which is gonna be quite useful!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What we're trying to achieve with the integration is to allow users to access RequestContext (or the context types from other frameworks like Reactor) using Micrometer API.

I see, I thought the point of integrating micrometer's context-propagation was to introduce ways for users to propagate thread local contexts other than RequestContext when using Armeria server/client.

I'm fine with just letting users access RequestContext via the Micrometer API then.

Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright 2024 LINE Corporation
*
* LINE Corporation licenses this file to you 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 com.linecorp.armeria.internal.common;

import org.reactivestreams.Subscription;

import com.linecorp.armeria.common.RequestContext;
import com.linecorp.armeria.common.RequestContextStorage;

import io.micrometer.context.ContextRegistry;
import io.micrometer.context.ContextSnapshot;
import io.micrometer.context.ContextSnapshot.Scope;
import io.micrometer.context.ThreadLocalAccessor;

/**
* This class works with the
* <a href="https://docs.micrometer.io/context-propagation/reference/index.html">Micrometer
* Context Propagation</a> to keep the {@link RequestContext} during
* <a href="https://github.com/reactor/reactor-core">Reactor</a> operations.
* Get the {@link RequestContextThreadLocalAccessor} to register it to the {@link ContextRegistry}.
* Then, {@link ContextRegistry} will use {@link RequestContextThreadLocalAccessor} to
* propagate context during the
* <a href="https://github.com/reactor/reactor-core">Reactor</a> operations
* so that you can get the context using {@link RequestContext#current()}.
* However, please note that you should include Mono#contextWrite(ContextView) or
* Flux#contextWrite(ContextView) to end of the Reactor codes.
* If not, {@link RequestContext} will not be keep during Reactor Operation.
*/
public final class RequestContextThreadLocalAccessor implements ThreadLocalAccessor<RequestContext> {

private static final Object KEY = RequestContext.class;

/**
* The value which obtained through {@link RequestContextThreadLocalAccessor},
* will be stored in the Context under this {@code KEY}.
* This method will be called by {@link ContextSnapshot} internally.
*/
@Override
public Object key() {
return KEY;
}

/**
* {@link ContextSnapshot} will call this method during the execution
* of lambda functions in {@link ContextSnapshot#wrap(Runnable)},
* as well as during Mono#subscribe(), Flux#subscribe(),
* {@link Subscription#request(long)}, and CoreSubscriber#onSubscribe(Subscription).
* Following these calls, {@link ContextSnapshot#setThreadLocals()} is
* invoked to restore the state of {@link RequestContextStorage}.
* Furthermore, at the end of these methods, {@link Scope#close()} is executed
* to revert the {@link RequestContextStorage} to its original state.
*/
@Override
public RequestContext getValue() {
return RequestContext.currentOrNull();
}

/**
* {@link ContextSnapshot} will call this method during the execution
* of lambda functions in {@link ContextSnapshot#wrap(Runnable)},
* as well as during Mono#subscribe(), Flux#subscribe(),
* {@link Subscription#request(long)}, and CoreSubscriber#onSubscribe(Subscription).
* Following these calls, {@link ContextSnapshot#setThreadLocals()} is
* invoked to restore the state of {@link RequestContextStorage}.
* Furthermore, at the end of these methods, {@link Scope#close()} is executed
* to revert the {@link RequestContextStorage} to its original state.
*/
@Override
@SuppressWarnings("MustBeClosedChecker")
public void setValue(RequestContext value) {
RequestContextUtil.getAndSet(value);
}

/**
* This method will be called at the start of {@link ContextSnapshot.Scope} and
* the end of {@link ContextSnapshot.Scope}. If reactor Context does not
* contains {@link RequestContextThreadLocalAccessor#KEY}, {@link ContextSnapshot} will use
* this method to remove the value from {@link ThreadLocal}.
* Please note that {@link RequestContextUtil#pop()} return {@link AutoCloseable} instance,
* but it is not used in `Try with Resources` syntax. this is because {@link ContextSnapshot.Scope}
* will handle the {@link AutoCloseable} instance returned by {@link RequestContextUtil#pop()}.
*/
@Override
@SuppressWarnings("MustBeClosedChecker")
public void setValue() {
RequestContextUtil.pop();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.linecorp.armeria.internal.common.RequestContextThreadLocalAccessor
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
* Copyright 2024 LINE Corporation
*
* LINE Corporation licenses this file to you 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 com.linecorp.armeria.internal.common;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.util.List;

import org.junit.jupiter.api.Test;

import com.linecorp.armeria.client.ClientRequestContext;
import com.linecorp.armeria.common.HttpMethod;
import com.linecorp.armeria.common.HttpRequest;
import com.linecorp.armeria.common.RequestContext;

import io.micrometer.context.ContextRegistry;
import io.micrometer.context.ContextSnapshot;
import io.micrometer.context.ContextSnapshot.Scope;
import io.micrometer.context.ContextSnapshotFactory;
import io.micrometer.context.ThreadLocalAccessor;

class RequestContextThreadLocalAccessorTest {

@Test
void should_be_loaded_by_SPI() {
final ContextRegistry ctxRegistry = ContextRegistry.getInstance();
final List<ThreadLocalAccessor<?>> threadLocalAccessors = ctxRegistry.getThreadLocalAccessors();

assertThat(threadLocalAccessors.size()).isGreaterThan(1);
assertThat(threadLocalAccessors).hasAtLeastOneElementOfType(RequestContextThreadLocalAccessor.class);
}

@Test
void should_return_expected_key() {
// Given
final RequestContextThreadLocalAccessor reqCtxAccessor = new RequestContextThreadLocalAccessor();
final Object expectedValue = RequestContext.class;

// When
final Object result = reqCtxAccessor.key();

// Then
assertThat(result).isEqualTo(expectedValue);
}

@Test
@SuppressWarnings("MustBeClosedChecker")
void should_success_set() {
// Given
final ClientRequestContext ctx = newContext();
final RequestContextThreadLocalAccessor reqCtxAccessor = new RequestContextThreadLocalAccessor();

// When
reqCtxAccessor.setValue(ctx);

// Then
final RequestContext currentCtx = RequestContext.current();
assertThat(currentCtx).isEqualTo(ctx);

RequestContextUtil.pop();
}

@Test
void should_throw_NPE_when_set_null() {
// Given
final RequestContextThreadLocalAccessor reqCtxAccessor = new RequestContextThreadLocalAccessor();

// When + Then
assertThatThrownBy(() -> reqCtxAccessor.setValue(null))
.isInstanceOf(NullPointerException.class);
}

@Test
void should_be_null_when_setValue() {
// Given
final ClientRequestContext ctx = newContext();
final RequestContextThreadLocalAccessor reqCtxAccessor = new RequestContextThreadLocalAccessor();
reqCtxAccessor.setValue(ctx);

// When
reqCtxAccessor.setValue();

// Then
final RequestContext reqCtx = RequestContext.currentOrNull();
assertThat(reqCtx).isNull();
}

@Test
@SuppressWarnings("MustBeClosedChecker")
void should_be_restore_original_state_when_restore() {
// Given
final RequestContextThreadLocalAccessor reqCtxAccessor = new RequestContextThreadLocalAccessor();
final ClientRequestContext previousCtx = newContext();
final ClientRequestContext currentCtx = newContext();
reqCtxAccessor.setValue(currentCtx);

// When
reqCtxAccessor.restore(previousCtx);

// Then
final RequestContext reqCtx = RequestContext.currentOrNull();
assertThat(reqCtx).isNotNull();
assertThat(reqCtx).isEqualTo(previousCtx);

RequestContextUtil.pop();
}

@Test
void should_be_null_when_restore() {
// Given
final RequestContextThreadLocalAccessor reqCtxAccessor = new RequestContextThreadLocalAccessor();
final ClientRequestContext currentCtx = newContext();
reqCtxAccessor.setValue(currentCtx);

// When
reqCtxAccessor.restore();

// Then
final RequestContext reqCtx = RequestContext.currentOrNull();
assertThat(reqCtx).isNull();
}

@Test
void requestContext_should_exist_inside_scope_and_not_outside() {
// Given
final RequestContextThreadLocalAccessor reqCtxAccessor = new RequestContextThreadLocalAccessor();
final ClientRequestContext currentCtx = newContext();
final ClientRequestContext expectedCtx = currentCtx;
reqCtxAccessor.setValue(currentCtx);

final ContextSnapshotFactory factory = ContextSnapshotFactory.builder()
.clearMissing(true)
.build();
final ContextSnapshot contextSnapshot = factory.captureAll();
reqCtxAccessor.setValue();

// When : contextSnapshot.setThreadLocals()
try (Scope ignored = contextSnapshot.setThreadLocals()) {

// Then : should not
final RequestContext reqCtxInScope = RequestContext.currentOrNull();
assertThat(reqCtxInScope).isNotNull();
assertThat(reqCtxInScope).isSameAs(expectedCtx);
}

// Then
final RequestContext reqCtxOutOfScope = RequestContext.currentOrNull();
assertThat(reqCtxOutOfScope).isNull();
}

static ClientRequestContext newContext() {
return ClientRequestContext.of(HttpRequest.of(HttpMethod.GET, "/"));
}
}
5 changes: 5 additions & 0 deletions dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ caffeine = "2.9.3"
cglib = "3.3.0"
checkerframework = "2.5.6"
checkstyle = "10.3.2"
context-propagation = "1.1.1"
controlplane = "1.0.45"
curator = "5.6.0"
dagger = "2.51.1"
Expand Down Expand Up @@ -315,6 +316,10 @@ version.ref = "checkerframework"
module = "com.puppycrawl.tools:checkstyle"
version.ref = "checkstyle"

[libraries.context-propagation]
module = "io.micrometer:context-propagation"
version.ref = "context-propagation"

[libraries.controlplane-api]
module = "io.envoyproxy.controlplane:api"
version.ref = "controlplane"
Expand Down
1 change: 1 addition & 0 deletions reactor3/build.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
dependencies {
api libs.reactor.core
implementation libs.context.propagation
}
Loading
Loading