From bc2a2600bc506c640d4b4d0eddf1d24c8a3ba380 Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov Date: Mon, 1 Jul 2024 14:21:10 -0700 Subject: [PATCH] Add ObservationValidator Closes gh-5239 --- .../observation/tck/ObservationValidator.java | 159 +++++++++++++++ .../tck/TestObservationRegistry.java | 2 +- .../tck/ObservationValidatorTests.java | 189 ++++++++++++++++++ 3 files changed, 349 insertions(+), 1 deletion(-) create mode 100644 micrometer-observation-test/src/main/java/io/micrometer/observation/tck/ObservationValidator.java create mode 100644 micrometer-observation-test/src/test/java/io/micrometer/observation/tck/ObservationValidatorTests.java diff --git a/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/ObservationValidator.java b/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/ObservationValidator.java new file mode 100644 index 0000000000..e64dab04c3 --- /dev/null +++ b/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/ObservationValidator.java @@ -0,0 +1,159 @@ +/* + * Copyright 2024 VMware, Inc. + * + * Licensed 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 io.micrometer.observation.tck; + +import io.micrometer.common.lang.Nullable; +import io.micrometer.common.util.internal.logging.InternalLogger; +import io.micrometer.common.util.internal.logging.InternalLoggerFactory; +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.Observation.Event; +import io.micrometer.observation.ObservationHandler; + +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * An {@link ObservationHandler} that validates the order of events of an Observation (for + * example stop should be called after start) and with a validation message and the + * original context, it publishes the events of these invalid scenarios to the + * {@link Consumer} of your choice. + * + * @author Jonatan Ivanov + */ +class ObservationValidator implements ObservationHandler { + + private static final InternalLogger LOGGER = InternalLoggerFactory.getInstance(ObservationValidator.class); + + private final Consumer consumer; + + private final Predicate supportsContextPredicate; + + ObservationValidator() { + this(validationResult -> LOGGER.warn(validationResult.toString())); + } + + ObservationValidator(Consumer consumer) { + this(consumer, context -> true); + } + + ObservationValidator(Consumer consumer, Predicate supportsContextPredicate) { + this.consumer = consumer; + this.supportsContextPredicate = supportsContextPredicate; + } + + @Override + public void onStart(Context context) { + Status status = context.get(Status.class); + if (status != null) { + consumer.accept(new ValidationResult("Invalid start: Observation has already been started", context)); + } + else { + context.put(Status.class, new Status()); + } + } + + @Override + public void onError(Context context) { + checkIfObservationWasStartedButNotStopped("Invalid error signal", context); + } + + @Override + public void onEvent(Event event, Context context) { + checkIfObservationWasStartedButNotStopped("Invalid event signal", context); + } + + @Override + public void onScopeOpened(Context context) { + checkIfObservationWasStartedButNotStopped("Invalid scope opening", context); + } + + @Override + public void onScopeClosed(Context context) { + checkIfObservationWasStartedButNotStopped("Invalid scope closing", context); + } + + @Override + public void onScopeReset(Context context) { + checkIfObservationWasStartedButNotStopped("Invalid scope resetting", context); + } + + @Override + public void onStop(Context context) { + Status status = checkIfObservationWasStartedButNotStopped("Invalid stop", context); + if (status != null) { + status.markStopped(); + } + } + + @Override + public boolean supportsContext(Context context) { + return supportsContextPredicate.test(context); + } + + @Nullable + private Status checkIfObservationWasStartedButNotStopped(String prefix, Context context) { + Status status = context.get(Status.class); + if (status == null) { + consumer.accept(new ValidationResult(prefix + ": Observation has not been started yet", context)); + } + else if (status.isStopped()) { + consumer.accept(new ValidationResult(prefix + ": Observation has already been stopped", context)); + } + + return status; + } + + static class ValidationResult { + + private final String message; + + private final Context context; + + ValidationResult(String message, Context context) { + this.message = message; + this.context = context; + } + + String getMessage() { + return message; + } + + Context getContext() { + return context; + } + + @Override + public String toString() { + return getMessage() + " - " + getContext(); + } + + } + + static class Status { + + private boolean stopped = false; + + boolean isStopped() { + return stopped; + } + + void markStopped() { + stopped = true; + } + + } + +} diff --git a/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/TestObservationRegistry.java b/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/TestObservationRegistry.java index 6dc38bdd1c..c631cd1b66 100644 --- a/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/TestObservationRegistry.java +++ b/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/TestObservationRegistry.java @@ -38,7 +38,7 @@ public final class TestObservationRegistry implements ObservationRegistry { private final StoringObservationHandler handler = new StoringObservationHandler(); private TestObservationRegistry() { - observationConfig().observationHandler(this.handler); + observationConfig().observationHandler(this.handler).observationHandler(new ObservationValidator()); } /** diff --git a/micrometer-observation-test/src/test/java/io/micrometer/observation/tck/ObservationValidatorTests.java b/micrometer-observation-test/src/test/java/io/micrometer/observation/tck/ObservationValidatorTests.java new file mode 100644 index 0000000000..e62ccf69ca --- /dev/null +++ b/micrometer-observation-test/src/test/java/io/micrometer/observation/tck/ObservationValidatorTests.java @@ -0,0 +1,189 @@ +/* + * Copyright 2024 VMware, Inc. + * + * Licensed 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 io.micrometer.observation.tck; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.Observation.Event; +import io.micrometer.observation.Observation.Scope; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.tck.ObservationValidator.ValidationResult; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.function.Consumer; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ObservationValidator}. + * + * @author Jonatan Ivanov + */ +class ObservationValidatorTests { + + private TestConsumer testConsumer; + + private ObservationRegistry registry; + + @BeforeEach + void setUp() { + testConsumer = new TestConsumer(); + registry = ObservationRegistry.create(); + registry.observationConfig().observationHandler(new ObservationValidator(testConsumer)); + } + + @Test + void doubleStartShouldBeInvalid() { + Observation.start("test", registry).start(); + assertThat(testConsumer.toString()).isEqualTo("Invalid start: Observation has already been started"); + } + + @Test + void stopBeforeStartShouldBeInvalid() { + Observation.createNotStarted("test", registry).stop(); + assertThat(testConsumer.toString()).isEqualTo("Invalid stop: Observation has not been started yet"); + } + + @Test + void errorBeforeStartShouldBeInvalid() { + Observation.createNotStarted("test", registry).error(new RuntimeException()); + assertThat(testConsumer.toString()).isEqualTo("Invalid error signal: Observation has not been started yet"); + } + + @Test + void eventBeforeStartShouldBeInvalid() { + Observation.createNotStarted("test", registry).event(Event.of("test")); + assertThat(testConsumer.toString()).isEqualTo("Invalid event signal: Observation has not been started yet"); + } + + @Test + void scopeBeforeStartShouldBeInvalid() { + Scope scope = Observation.createNotStarted("test", registry).openScope(); + scope.reset(); + scope.close(); + assertThat(testConsumer.toString()).isEqualTo("Invalid scope opening: Observation has not been started yet\n" + + "Invalid scope resetting: Observation has not been started yet\n" + + "Invalid scope closing: Observation has not been started yet"); + } + + @Test + void observeAfterStartShouldBeInvalid() { + Observation.start("test", registry).observe(() -> ""); + assertThat(testConsumer.toString()).isEqualTo("Invalid start: Observation has already been started"); + } + + @Test + void doubleStopShouldBeInvalid() { + Observation observation = Observation.start("test", registry); + observation.stop(); + observation.stop(); + assertThat(testConsumer.toString()).isEqualTo("Invalid stop: Observation has already been stopped"); + } + + @Test + void errorAfterStopShouldBeInvalid() { + Observation observation = Observation.start("test", registry); + observation.stop(); + observation.error(new RuntimeException()); + assertThat(testConsumer.toString()).isEqualTo("Invalid error signal: Observation has already been stopped"); + } + + @Test + void eventAfterStopShouldBeInvalid() { + Observation observation = Observation.start("test", registry); + observation.stop(); + observation.event(Event.of("test")); + assertThat(testConsumer.toString()).isEqualTo("Invalid event signal: Observation has already been stopped"); + } + + @Test + void scopeAfterStopShouldBeInvalid() { + Observation observation = Observation.start("test", registry); + observation.stop(); + Scope scope = observation.openScope(); + scope.reset(); + scope.close(); + assertThat(testConsumer.toString()).isEqualTo("Invalid scope opening: Observation has already been stopped\n" + + "Invalid scope resetting: Observation has already been stopped\n" + + "Invalid scope closing: Observation has already been stopped"); + } + + @Test + void startEventStopShouldBeValid() { + Observation.start("test", registry).event(Event.of("test")).stop(); + assertThat(testConsumer.toString()).isEmpty(); + } + + @Test + void startEventErrorStopShouldBeValid() { + Observation.start("test", registry).event(Event.of("test")).error(new RuntimeException()).stop(); + assertThat(testConsumer.toString()).isEmpty(); + } + + @Test + void startErrorEventStopShouldBeValid() { + Observation.start("test", registry).error(new RuntimeException()).event(Event.of("test")).stop(); + assertThat(testConsumer.toString()).isEmpty(); + } + + @Test + void startScopeEventStopShouldBeValid() { + Observation observation = Observation.start("test", registry); + observation.openScope().close(); + observation.event(Event.of("test")); + observation.stop(); + assertThat(testConsumer.toString()).isEmpty(); + } + + @Test + void startScopeEventErrorStopShouldBeValid() { + Observation observation = Observation.start("test", registry); + Scope scope = observation.openScope(); + observation.event(Event.of("test")); + observation.error(new RuntimeException()); + scope.close(); + observation.stop(); + assertThat(testConsumer.toString()).isEmpty(); + } + + @Test + void startScopeErrorEventStopShouldBeValid() { + Observation observation = Observation.start("test", registry); + Scope scope = observation.openScope(); + observation.error(new RuntimeException()); + observation.event(Event.of("test")); + scope.close(); + observation.stop(); + assertThat(testConsumer.toString()).isEmpty(); + } + + static class TestConsumer implements Consumer { + + private final StringBuilder stringBuilder = new StringBuilder(); + + @Override + public void accept(ValidationResult validationResult) { + stringBuilder.append(validationResult.getMessage()).append("\n"); + } + + @Override + public String toString() { + return stringBuilder.toString().trim(); + } + + } + +}