Skip to content

Commit 95735d0

Browse files
committed
Clean up Observation from context after command completion.
We now restore the previous Observation parent after completing the Observation to avoid unintentional nesting of observations. Previously, a command became the parent of an earlier command on the same thread regardless of whether the earlier command has been finished or whether it was active. Observation nesting now considers only commands that are still active restoring their parent after completion to avoid invalid nesting and with that, to not allocate observation graphs that allocate infinite memory.
1 parent e1281a3 commit 95735d0

File tree

3 files changed

+62
-25
lines changed

3 files changed

+62
-25
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MapRequestContext.java

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import java.util.stream.Stream;
2222

2323
import com.mongodb.RequestContext;
24-
import org.jspecify.annotations.Nullable;
2524

2625
/**
2726
* A {@link Map}-based {@link RequestContext}.
@@ -30,32 +29,25 @@
3029
* @author Greg Turnquist
3130
* @since 4.0.0
3231
*/
33-
class MapRequestContext implements RequestContext {
34-
35-
private final Map<Object, Object> map;
32+
record MapRequestContext(Map<Object, Object> map) implements RequestContext {
3633

3734
public MapRequestContext() {
3835
this(new HashMap<>());
3936
}
4037

41-
public MapRequestContext(Map<Object, Object> context) {
42-
this.map = context;
43-
}
44-
4538
@Override
4639
public <T> T get(Object key) {
4740

48-
4941
T value = (T) map.get(key);
50-
if(value != null) {
42+
if (value != null) {
5143
return value;
5244
}
5345
throw new NoSuchElementException("%s is missing".formatted(key));
5446
}
5547

5648
@Override
5749
public boolean hasKey(Object key) {
58-
return map.containsKey(key);
50+
return map.get(key) != null;
5951
}
6052

6153
@Override

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservationCommandListener.java

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,6 @@ public void commandStarted(CommandStartedEvent event) {
113113

114114
Observation parent = observationFromContext(requestContext);
115115

116-
if (log.isDebugEnabled()) {
117-
log.debug("Found the following observation passed from the mongo context [" + parent + "]");
118-
}
119-
120116
MongoHandlerContext observationContext = new MongoHandlerContext(connectionString, event, requestContext);
121117
observationContext.setRemoteServiceName("mongo");
122118

@@ -141,22 +137,20 @@ public void commandStarted(CommandStartedEvent event) {
141137
@Override
142138
public void commandSucceeded(CommandSucceededEvent event) {
143139

144-
doInObservation(event.getRequestContext(), (observation, context) -> {
140+
stopObservation(event.getRequestContext(), (observation, context) -> {
145141

146142
context.setCommandSucceededEvent(event);
147143

148144
if (log.isDebugEnabled()) {
149145
log.debug("Command succeeded - will stop observation [" + observation + "]");
150146
}
151-
152-
observation.stop();
153147
});
154148
}
155149

156150
@Override
157151
public void commandFailed(CommandFailedEvent event) {
158152

159-
doInObservation(event.getRequestContext(), (observation, context) -> {
153+
stopObservation(event.getRequestContext(), (observation, context) -> {
160154

161155
context.setCommandFailedEvent(event);
162156

@@ -165,18 +159,17 @@ public void commandFailed(CommandFailedEvent event) {
165159
}
166160

167161
observation.error(event.getThrowable());
168-
observation.stop();
169162
});
170163
}
171164

172165
/**
173-
* Performs the given action for the {@link Observation} and {@link MongoHandlerContext} if there is an ongoing Mongo
174-
* Observation. Exceptions thrown by the action are relayed to the caller.
166+
* Stops the {@link Observation} after applying {@code action} given {@link MongoHandlerContext} if there is an
167+
* ongoing Mongo Observation. Exceptions thrown by the action are relayed to the caller.
175168
*
176169
* @param requestContext the context to extract the Observation from.
177170
* @param action the action to invoke.
178171
*/
179-
private void doInObservation(@Nullable RequestContext requestContext,
172+
private void stopObservation(@Nullable RequestContext requestContext,
180173
BiConsumer<Observation, MongoHandlerContext> action) {
181174

182175
if (requestContext == null) {
@@ -188,7 +181,18 @@ private void doInObservation(@Nullable RequestContext requestContext,
188181
return;
189182
}
190183

191-
action.accept(observation, context);
184+
try {
185+
action.accept(observation, context);
186+
} finally {
187+
188+
observation.stop();
189+
190+
if (log.isDebugEnabled()) {
191+
log.debug(
192+
"Restoring parent observation [" + observation + "] for Mongo instrumentation and put it in Mongo context");
193+
}
194+
requestContext.put(ObservationThreadLocalAccessor.KEY, observation.getContext().getParentObservation());
195+
}
192196
}
193197

194198
/**
@@ -210,7 +214,7 @@ private void doInObservation(@Nullable RequestContext requestContext,
210214
}
211215

212216
if (log.isDebugEnabled()) {
213-
log.debug("No observation was found - will not create any child observations");
217+
log.debug("No observation was found: Creating a new root observation");
214218
}
215219

216220
return null;

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import io.micrometer.observation.ObservationRegistry;
2828
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
2929

30+
import org.assertj.core.api.Assertions;
3031
import org.bson.BsonDocument;
3132
import org.bson.BsonString;
3233
import org.junit.jupiter.api.BeforeEach;
@@ -251,6 +252,46 @@ public String getName() {
251252
assertThat(meterRegistry).hasMeterWithName("custom.name.active");
252253
}
253254

255+
@Test // GH-5064
256+
void completionRestoresParentObservation() {
257+
258+
// given
259+
Observation parent = Observation.start("name", observationRegistry);
260+
observationRegistry.setCurrentObservationScope(parent.openScope());
261+
RequestContext traceRequestContext = getContext();
262+
263+
// when
264+
listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, 0, null, "database", "insert",
265+
new BsonDocument("collection", new BsonString("user"))));
266+
267+
Assertions.assertThat((Observation) traceRequestContext.get(ObservationThreadLocalAccessor.KEY)).isNotNull()
268+
.isNotEqualTo(parent);
269+
270+
listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, 0, null, "insert", null, null, 0));
271+
272+
Assertions.assertThat((Observation) traceRequestContext.get(ObservationThreadLocalAccessor.KEY)).isEqualTo(parent);
273+
}
274+
275+
@Test // GH-5064
276+
void failureRestoresParentObservation() {
277+
278+
// given
279+
Observation parent = Observation.start("name", observationRegistry);
280+
observationRegistry.setCurrentObservationScope(parent.openScope());
281+
RequestContext traceRequestContext = getContext();
282+
283+
// when
284+
listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, 0, null, "database", "insert",
285+
new BsonDocument("collection", new BsonString("user"))));
286+
287+
Assertions.assertThat((Observation) traceRequestContext.get(ObservationThreadLocalAccessor.KEY)).isNotNull()
288+
.isNotEqualTo(parent);
289+
290+
listener.commandFailed(new CommandFailedEvent(traceRequestContext, 0, 0, null, "insert", null, 0, null));
291+
292+
Assertions.assertThat((Observation) traceRequestContext.get(ObservationThreadLocalAccessor.KEY)).isEqualTo(parent);
293+
}
294+
254295
private RequestContext getContext() {
255296
return ((SynchronousContextProvider) ContextProviderFactory.create(observationRegistry)).getContext();
256297
}

0 commit comments

Comments
 (0)