Skip to content

Commit

Permalink
Add ObservationValidator
Browse files Browse the repository at this point in the history
Closes gh-5239
  • Loading branch information
jonatan-ivanov committed Jul 11, 2024
1 parent ea51e72 commit d365754
Show file tree
Hide file tree
Showing 3 changed files with 300 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -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<Context> {

private static final InternalLogger LOGGER = InternalLoggerFactory.getInstance(ObservationValidator.class);

private final Consumer<ValidationResult> consumer;

private final Predicate<Context> supportsContextPredicate;

ObservationValidator() {
this(validationResult -> LOGGER.warn(validationResult.toString()));
}

ObservationValidator(Consumer<ValidationResult> consumer) {
this(consumer, context -> true);
}

ObservationValidator(Consumer<ValidationResult> consumer, Predicate<Context> 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;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* 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");
}

static class TestConsumer implements Consumer<ValidationResult> {

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();
}

}

}

0 comments on commit d365754

Please sign in to comment.