Skip to content

Commit d365754

Browse files
Add ObservationValidator
Closes gh-5239
1 parent ea51e72 commit d365754

File tree

3 files changed

+300
-1
lines changed

3 files changed

+300
-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: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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+
static class TestConsumer implements Consumer<ValidationResult> {
125+
126+
private final StringBuilder stringBuilder = new StringBuilder();
127+
128+
@Override
129+
public void accept(ValidationResult validationResult) {
130+
stringBuilder.append(validationResult.getMessage()).append("\n");
131+
}
132+
133+
@Override
134+
public String toString() {
135+
return stringBuilder.toString().trim();
136+
}
137+
138+
}
139+
140+
}

0 commit comments

Comments
 (0)