Skip to content

Commit 4f21e1e

Browse files
Add ObservationValidator (#5300)
This is intended to be used with the TestObservationRegistry for verifying that instrumentation does not call Observation methods in unexpected orders. This is done as part of TestObservationRegistry so that such instrumentation issues can be caught in unit tests without the cost of validation being paid in production code paths, which was a conscious choice in writing the implementation of Observation related code. Closes gh-5239
1 parent ea51e72 commit 4f21e1e

File tree

3 files changed

+349
-1
lines changed

3 files changed

+349
-1
lines changed
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright 2024 VMware, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micrometer.observation.tck;
17+
18+
import io.micrometer.common.lang.Nullable;
19+
import io.micrometer.common.util.internal.logging.InternalLogger;
20+
import io.micrometer.common.util.internal.logging.InternalLoggerFactory;
21+
import io.micrometer.observation.Observation.Context;
22+
import io.micrometer.observation.Observation.Event;
23+
import io.micrometer.observation.ObservationHandler;
24+
25+
import java.util.function.Consumer;
26+
import java.util.function.Predicate;
27+
28+
/**
29+
* An {@link ObservationHandler} that validates the order of events of an Observation (for
30+
* example stop should be called after start) and with a validation message and the
31+
* original context, it publishes the events of these invalid scenarios to the
32+
* {@link Consumer} of your choice.
33+
*
34+
* @author Jonatan Ivanov
35+
*/
36+
class ObservationValidator implements ObservationHandler<Context> {
37+
38+
private static final InternalLogger LOGGER = InternalLoggerFactory.getInstance(ObservationValidator.class);
39+
40+
private final Consumer<ValidationResult> consumer;
41+
42+
private final Predicate<Context> supportsContextPredicate;
43+
44+
ObservationValidator() {
45+
this(validationResult -> LOGGER.warn(validationResult.toString()));
46+
}
47+
48+
ObservationValidator(Consumer<ValidationResult> consumer) {
49+
this(consumer, context -> true);
50+
}
51+
52+
ObservationValidator(Consumer<ValidationResult> consumer, Predicate<Context> supportsContextPredicate) {
53+
this.consumer = consumer;
54+
this.supportsContextPredicate = supportsContextPredicate;
55+
}
56+
57+
@Override
58+
public void onStart(Context context) {
59+
Status status = context.get(Status.class);
60+
if (status != null) {
61+
consumer.accept(new ValidationResult("Invalid start: Observation has already been started", context));
62+
}
63+
else {
64+
context.put(Status.class, new Status());
65+
}
66+
}
67+
68+
@Override
69+
public void onError(Context context) {
70+
checkIfObservationWasStartedButNotStopped("Invalid error signal", context);
71+
}
72+
73+
@Override
74+
public void onEvent(Event event, Context context) {
75+
checkIfObservationWasStartedButNotStopped("Invalid event signal", context);
76+
}
77+
78+
@Override
79+
public void onScopeOpened(Context context) {
80+
checkIfObservationWasStartedButNotStopped("Invalid scope opening", context);
81+
}
82+
83+
@Override
84+
public void onScopeClosed(Context context) {
85+
checkIfObservationWasStartedButNotStopped("Invalid scope closing", context);
86+
}
87+
88+
@Override
89+
public void onScopeReset(Context context) {
90+
checkIfObservationWasStartedButNotStopped("Invalid scope resetting", context);
91+
}
92+
93+
@Override
94+
public void onStop(Context context) {
95+
Status status = checkIfObservationWasStartedButNotStopped("Invalid stop", context);
96+
if (status != null) {
97+
status.markStopped();
98+
}
99+
}
100+
101+
@Override
102+
public boolean supportsContext(Context context) {
103+
return supportsContextPredicate.test(context);
104+
}
105+
106+
@Nullable
107+
private Status checkIfObservationWasStartedButNotStopped(String prefix, Context context) {
108+
Status status = context.get(Status.class);
109+
if (status == null) {
110+
consumer.accept(new ValidationResult(prefix + ": Observation has not been started yet", context));
111+
}
112+
else if (status.isStopped()) {
113+
consumer.accept(new ValidationResult(prefix + ": Observation has already been stopped", context));
114+
}
115+
116+
return status;
117+
}
118+
119+
static class ValidationResult {
120+
121+
private final String message;
122+
123+
private final Context context;
124+
125+
ValidationResult(String message, Context context) {
126+
this.message = message;
127+
this.context = context;
128+
}
129+
130+
String getMessage() {
131+
return message;
132+
}
133+
134+
Context getContext() {
135+
return context;
136+
}
137+
138+
@Override
139+
public String toString() {
140+
return getMessage() + " - " + getContext();
141+
}
142+
143+
}
144+
145+
static class Status {
146+
147+
private boolean stopped = false;
148+
149+
boolean isStopped() {
150+
return stopped;
151+
}
152+
153+
void markStopped() {
154+
stopped = true;
155+
}
156+
157+
}
158+
159+
}

micrometer-observation-test/src/main/java/io/micrometer/observation/tck/TestObservationRegistry.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public final class TestObservationRegistry implements ObservationRegistry {
3838
private final StoringObservationHandler handler = new StoringObservationHandler();
3939

4040
private TestObservationRegistry() {
41-
observationConfig().observationHandler(this.handler);
41+
observationConfig().observationHandler(this.handler).observationHandler(new ObservationValidator());
4242
}
4343

4444
/**
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/*
2+
* Copyright 2024 VMware, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micrometer.observation.tck;
17+
18+
import io.micrometer.observation.Observation;
19+
import io.micrometer.observation.Observation.Event;
20+
import io.micrometer.observation.Observation.Scope;
21+
import io.micrometer.observation.ObservationRegistry;
22+
import io.micrometer.observation.tck.ObservationValidator.ValidationResult;
23+
import org.junit.jupiter.api.BeforeEach;
24+
import org.junit.jupiter.api.Test;
25+
26+
import java.util.function.Consumer;
27+
28+
import static org.assertj.core.api.Assertions.assertThat;
29+
30+
/**
31+
* Tests for {@link ObservationValidator}.
32+
*
33+
* @author Jonatan Ivanov
34+
*/
35+
class ObservationValidatorTests {
36+
37+
private TestConsumer testConsumer;
38+
39+
private ObservationRegistry registry;
40+
41+
@BeforeEach
42+
void setUp() {
43+
testConsumer = new TestConsumer();
44+
registry = ObservationRegistry.create();
45+
registry.observationConfig().observationHandler(new ObservationValidator(testConsumer));
46+
}
47+
48+
@Test
49+
void doubleStartShouldBeInvalid() {
50+
Observation.start("test", registry).start();
51+
assertThat(testConsumer.toString()).isEqualTo("Invalid start: Observation has already been started");
52+
}
53+
54+
@Test
55+
void stopBeforeStartShouldBeInvalid() {
56+
Observation.createNotStarted("test", registry).stop();
57+
assertThat(testConsumer.toString()).isEqualTo("Invalid stop: Observation has not been started yet");
58+
}
59+
60+
@Test
61+
void errorBeforeStartShouldBeInvalid() {
62+
Observation.createNotStarted("test", registry).error(new RuntimeException());
63+
assertThat(testConsumer.toString()).isEqualTo("Invalid error signal: Observation has not been started yet");
64+
}
65+
66+
@Test
67+
void eventBeforeStartShouldBeInvalid() {
68+
Observation.createNotStarted("test", registry).event(Event.of("test"));
69+
assertThat(testConsumer.toString()).isEqualTo("Invalid event signal: Observation has not been started yet");
70+
}
71+
72+
@Test
73+
void scopeBeforeStartShouldBeInvalid() {
74+
Scope scope = Observation.createNotStarted("test", registry).openScope();
75+
scope.reset();
76+
scope.close();
77+
assertThat(testConsumer.toString()).isEqualTo("Invalid scope opening: Observation has not been started yet\n"
78+
+ "Invalid scope resetting: Observation has not been started yet\n"
79+
+ "Invalid scope closing: Observation has not been started yet");
80+
}
81+
82+
@Test
83+
void observeAfterStartShouldBeInvalid() {
84+
Observation.start("test", registry).observe(() -> "");
85+
assertThat(testConsumer.toString()).isEqualTo("Invalid start: Observation has already been started");
86+
}
87+
88+
@Test
89+
void doubleStopShouldBeInvalid() {
90+
Observation observation = Observation.start("test", registry);
91+
observation.stop();
92+
observation.stop();
93+
assertThat(testConsumer.toString()).isEqualTo("Invalid stop: Observation has already been stopped");
94+
}
95+
96+
@Test
97+
void errorAfterStopShouldBeInvalid() {
98+
Observation observation = Observation.start("test", registry);
99+
observation.stop();
100+
observation.error(new RuntimeException());
101+
assertThat(testConsumer.toString()).isEqualTo("Invalid error signal: Observation has already been stopped");
102+
}
103+
104+
@Test
105+
void eventAfterStopShouldBeInvalid() {
106+
Observation observation = Observation.start("test", registry);
107+
observation.stop();
108+
observation.event(Event.of("test"));
109+
assertThat(testConsumer.toString()).isEqualTo("Invalid event signal: Observation has already been stopped");
110+
}
111+
112+
@Test
113+
void scopeAfterStopShouldBeInvalid() {
114+
Observation observation = Observation.start("test", registry);
115+
observation.stop();
116+
Scope scope = observation.openScope();
117+
scope.reset();
118+
scope.close();
119+
assertThat(testConsumer.toString()).isEqualTo("Invalid scope opening: Observation has already been stopped\n"
120+
+ "Invalid scope resetting: Observation has already been stopped\n"
121+
+ "Invalid scope closing: Observation has already been stopped");
122+
}
123+
124+
@Test
125+
void startEventStopShouldBeValid() {
126+
Observation.start("test", registry).event(Event.of("test")).stop();
127+
assertThat(testConsumer.toString()).isEmpty();
128+
}
129+
130+
@Test
131+
void startEventErrorStopShouldBeValid() {
132+
Observation.start("test", registry).event(Event.of("test")).error(new RuntimeException()).stop();
133+
assertThat(testConsumer.toString()).isEmpty();
134+
}
135+
136+
@Test
137+
void startErrorEventStopShouldBeValid() {
138+
Observation.start("test", registry).error(new RuntimeException()).event(Event.of("test")).stop();
139+
assertThat(testConsumer.toString()).isEmpty();
140+
}
141+
142+
@Test
143+
void startScopeEventStopShouldBeValid() {
144+
Observation observation = Observation.start("test", registry);
145+
observation.openScope().close();
146+
observation.event(Event.of("test"));
147+
observation.stop();
148+
assertThat(testConsumer.toString()).isEmpty();
149+
}
150+
151+
@Test
152+
void startScopeEventErrorStopShouldBeValid() {
153+
Observation observation = Observation.start("test", registry);
154+
Scope scope = observation.openScope();
155+
observation.event(Event.of("test"));
156+
observation.error(new RuntimeException());
157+
scope.close();
158+
observation.stop();
159+
assertThat(testConsumer.toString()).isEmpty();
160+
}
161+
162+
@Test
163+
void startScopeErrorEventStopShouldBeValid() {
164+
Observation observation = Observation.start("test", registry);
165+
Scope scope = observation.openScope();
166+
observation.error(new RuntimeException());
167+
observation.event(Event.of("test"));
168+
scope.close();
169+
observation.stop();
170+
assertThat(testConsumer.toString()).isEmpty();
171+
}
172+
173+
static class TestConsumer implements Consumer<ValidationResult> {
174+
175+
private final StringBuilder stringBuilder = new StringBuilder();
176+
177+
@Override
178+
public void accept(ValidationResult validationResult) {
179+
stringBuilder.append(validationResult.getMessage()).append("\n");
180+
}
181+
182+
@Override
183+
public String toString() {
184+
return stringBuilder.toString().trim();
185+
}
186+
187+
}
188+
189+
}

0 commit comments

Comments
 (0)