Skip to content

Commit bb82f86

Browse files
Improves the ThreadLocalAccessor story of continuing scopes (#3731)
The Scope#makeCurrent method is called e.g. via ObservationThreadLocalAccessor#restore(Observation). In that case, we're calling ObservationThreadLocalAccessor#reset() first, and we're closing all the scopes, HOWEVER those are called on the Observation scope that was present there in thread local at the time of calling the method, NOT on the scope that we want to make current (that one can contain some leftovers from previous scope openings like creation of e.g. Brave scope in the TracingContext that is there inside the Observation's Context. When we want to go back to the enclosing scope and want to make that scope current we need to be sure that there are no remaining scoped objects inside Observation's context. This is why BEFORE rebuilding the scope structure we need to notify the handlers to clear them first (again this is a separate scope to the one that was cleared by the reset method) via calling ObservationHandler#onScopeReset(Context). When we reset a scope, we don't want to close it thus we don't want to remove any enclosing scopes. With this logic we reset any existing scopes on reset by calling the handler + when we make an enclosing scope current we will remove it from the list of enclosing scopes.
1 parent 2f91b70 commit bb82f86

File tree

6 files changed

+117
-23
lines changed

6 files changed

+117
-23
lines changed

micrometer-observation/src/main/java/io/micrometer/observation/NoopObservation.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@ public void close() {
130130
public void reset() {
131131
}
132132

133+
@Override
134+
public void makeCurrent() {
135+
136+
}
137+
133138
}
134139

135140
}

micrometer-observation/src/main/java/io/micrometer/observation/Observation.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -768,14 +768,29 @@ interface Scope extends AutoCloseable {
768768
void close();
769769

770770
/**
771-
* Clears the current scope and notifies the handlers that the scope was reset.
771+
* Resets the current scope. The effect of calling this method should be clearing
772+
* all related thread local entries.
773+
*
772774
* You don't need to call this method in most of the cases. Please only call this
773775
* method if you know what you are doing and your use-case demands the usage of
774776
* it.
775777
* @since 1.10.4
776778
*/
777779
void reset();
778780

781+
/**
782+
* This method assumes that all previous scopes got {@link #reset()}. That means
783+
* that in thread locals there are no more entries, and now we can make this scope
784+
* current.
785+
*
786+
* Making this scope current can lead to additional work such as injecting
787+
* variables to MDC. You don't need to call this method in most of the cases.
788+
* Please only call this method if you know what you are doing and your use-case
789+
* demands the usage of it.
790+
* @since 1.10.6
791+
*/
792+
void makeCurrent();
793+
779794
/**
780795
* Checks whether this {@link Scope} is no-op.
781796
* @return {@code true} when this is a no-op scope

micrometer-observation/src/main/java/io/micrometer/observation/ObservationHandler.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@
1515
*/
1616
package io.micrometer.observation;
1717

18-
import java.util.Arrays;
18+
import io.micrometer.observation.Observation.Context;
19+
1920
import java.util.ArrayList;
21+
import java.util.Arrays;
2022
import java.util.List;
2123
import java.util.Optional;
2224
import java.util.function.Consumer;
2325

24-
import io.micrometer.observation.Observation.Context;
25-
2626
/**
2727
* Handler for an {@link Observation}. Hooks in to the lifecycle of an observation.
2828
* Example of handler implementations can create metrics, spans or logs.
@@ -73,7 +73,7 @@ default void onScopeClosed(T context) {
7373

7474
/**
7575
* Reacts to resetting of scopes. If your handler uses a {@link ThreadLocal} value,
76-
* this method should clear that {@link ThreadLocal}.
76+
* this method should clear that {@link ThreadLocal} or any other scoped variable.
7777
* @param context an {@link Observation.Context}
7878
* @since 1.10.4
7979
*/

micrometer-observation/src/main/java/io/micrometer/observation/ObservationView.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package io.micrometer.observation;
1717

18+
import io.micrometer.common.lang.Nullable;
1819
import io.micrometer.observation.Observation.ContextView;
1920

2021
/**
@@ -30,4 +31,14 @@ public interface ObservationView {
3031
*/
3132
ContextView getContextView();
3233

34+
/**
35+
* Returns the last scope attached to this {@link ObservationView} in this thread.
36+
* @return scope for this {@link ObservationView}, {@code null} if there was no scope
37+
* @since 1.10.6
38+
*/
39+
@Nullable
40+
default Observation.Scope getEnclosingScope() {
41+
return Observation.Scope.NOOP;
42+
}
43+
3344
}

micrometer-observation/src/main/java/io/micrometer/observation/SimpleObservation.java

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import io.micrometer.common.KeyValue;
1919
import io.micrometer.common.lang.Nullable;
2020
import io.micrometer.common.util.StringUtils;
21+
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
2122

2223
import java.util.ArrayDeque;
2324
import java.util.Collection;
@@ -46,6 +47,8 @@ class SimpleObservation implements Observation {
4647

4748
private final Collection<ObservationFilter> filters;
4849

50+
private final ThreadLocal<Deque<Scope>> enclosingScopes = ThreadLocal.withInitial(ArrayDeque::new);
51+
4952
SimpleObservation(@Nullable String name, ObservationRegistry registry, Context context) {
5053
this.registry = registry;
5154
this.context = context;
@@ -186,15 +189,31 @@ public void stop() {
186189
}
187190

188191
notifyOnObservationStopped(modifiedContext);
192+
this.enclosingScopes.remove();
189193
}
190194

191195
@Override
192196
public Scope openScope() {
197+
Deque<Scope> scopes = enclosingScopes.get();
198+
Scope currentScope = registry.getCurrentObservationScope();
199+
if (currentScope != null) {
200+
scopes.addFirst(currentScope);
201+
}
193202
Scope scope = new SimpleScope(this.registry, this);
194203
notifyOnScopeOpened();
195204
return scope;
196205
}
197206

207+
@Nullable
208+
@Override
209+
public Scope getEnclosingScope() {
210+
Deque<Scope> scopes = enclosingScopes.get();
211+
if (!scopes.isEmpty()) {
212+
return scopes.getFirst();
213+
}
214+
return null;
215+
}
216+
198217
@Override
199218
public String toString() {
200219
return "{" + "name=" + this.context.getName() + "(" + this.context.getContextualName() + ")" + ", error="
@@ -228,6 +247,11 @@ private void notifyOnScopeClosed() {
228247
this.handlers.descendingIterator().forEachRemaining(handler -> handler.onScopeClosed(this.context));
229248
}
230249

250+
@SuppressWarnings("unchecked")
251+
private void notifyOnScopeMakeCurrent() {
252+
this.handlers.forEach(handler -> handler.onScopeOpened(this.context));
253+
}
254+
231255
@SuppressWarnings("unchecked")
232256
private void notifyOnScopeReset() {
233257
this.handlers.forEach(handler -> handler.onScopeReset(this.context));
@@ -263,14 +287,67 @@ public Observation getCurrentObservation() {
263287

264288
@Override
265289
public void close() {
290+
Deque<Scope> enclosingScopes = this.currentObservation.enclosingScopes.get();
291+
// If we're closing a scope then we have to remove an enclosing scope from the
292+
// deque
293+
if (!enclosingScopes.isEmpty()) {
294+
enclosingScopes.removeFirst();
295+
}
266296
this.registry.setCurrentObservationScope(previousObservationScope);
267297
this.currentObservation.notifyOnScopeClosed();
268298
}
269299

270300
@Override
271301
public void reset() {
272302
this.registry.setCurrentObservationScope(null);
303+
SimpleScope scope = this;
304+
while (scope != null) {
305+
// We don't want to remove any enclosing scopes when resetting
306+
// we just want to remove any scopes if they are present (that's why we're
307+
// not calling scope#close)
308+
this.registry.setCurrentObservationScope(scope.previousObservationScope);
309+
scope.currentObservation.notifyOnScopeReset();
310+
SimpleScope simpleScope = scope;
311+
scope = (SimpleScope) simpleScope.previousObservationScope;
312+
}
313+
}
314+
315+
/**
316+
* This method is called e.g. via
317+
* {@link ObservationThreadLocalAccessor#restore(Observation)}. In that case,
318+
* we're calling {@link ObservationThreadLocalAccessor#reset()} first, and we're
319+
* closing all the scopes, HOWEVER those are called on the Observation scope that
320+
* was present there in thread local at the time of calling the method, NOT on the
321+
* scope that we want to make current (that one can contain some leftovers from
322+
* previous scope openings like creation of e.g. Brave scope in the TracingContext
323+
* that is there inside the Observation's Context.
324+
*
325+
* When we want to go back to the enclosing scope and want to make that scope
326+
* current we need to be sure that there are no remaining scoped objects inside
327+
* Observation's context. This is why BEFORE rebuilding the scope structure we
328+
* need to notify the handlers to clear them first (again this is a separate scope
329+
* to the one that was cleared by the reset method) via calling
330+
* {@link ObservationHandler#onScopeReset(Context)}.
331+
*/
332+
@Override
333+
public void makeCurrent() {
273334
this.currentObservation.notifyOnScopeReset();
335+
// When we make an enclosing scope current we must remove it from the top of
336+
// the
337+
// deque of enclosing scopes (since it will no longer be enclosing)
338+
Deque<Scope> scopeDeque = this.currentObservation.enclosingScopes.get();
339+
if (!scopeDeque.isEmpty()) {
340+
scopeDeque.removeFirst();
341+
}
342+
Deque<SimpleScope> scopes = new ArrayDeque<>();
343+
SimpleScope scope = this;
344+
while (scope != null) {
345+
scopes.addFirst(scope);
346+
scope = (SimpleScope) scope.previousObservationScope;
347+
}
348+
for (SimpleScope simpleScope : scopes) {
349+
simpleScope.currentObservation.notifyOnScopeMakeCurrent();
350+
}
274351
}
275352

276353
}

micrometer-observation/src/main/java/io/micrometer/observation/contextpropagation/ObservationThreadLocalAccessor.java

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ public class ObservationThreadLocalAccessor implements ThreadLocalAccessor<Obser
3232
*/
3333
public static final String KEY = "micrometer.observation";
3434

35-
private static final String SCOPE_KEY = KEY + ".scope";
36-
3735
private static final ObservationRegistry observationRegistry = ObservationRegistry.create();
3836

3937
@Override
@@ -43,15 +41,7 @@ public Object key() {
4341

4442
@Override
4543
public Observation getValue() {
46-
Observation.Scope scope = observationRegistry.getCurrentObservationScope();
47-
if (scope != null) {
48-
Observation observation = scope.getCurrentObservation();
49-
observation.getContext().put(SCOPE_KEY, scope);
50-
return observation;
51-
}
52-
else {
53-
return null;
54-
}
44+
return observationRegistry.getCurrentObservation();
5545
}
5646

5747
@Override
@@ -72,14 +62,10 @@ public void reset() {
7262
@Override
7363
public void restore(Observation value) {
7464
reset();
75-
Observation.Scope observationScope = value.getContext().get(SCOPE_KEY);
76-
if (observationScope != null) {
77-
// We close the previous scope - it will put its parent as current and call
78-
// all handlers.
79-
observationScope.close();
65+
Observation.Scope enclosingScope = value.getEnclosingScope();
66+
if (enclosingScope != null) {
67+
enclosingScope.makeCurrent();
8068
}
81-
// We open the previous scope again, however this time in TL we have the whole
82-
// hierarchy of scopes re-attached via handlers.
8369
setValue(value);
8470
}
8571

0 commit comments

Comments
 (0)