From 28589d63af7d06b8f891e9313e5a3ed0465624b4 Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov Date: Wed, 7 Aug 2024 18:34:40 -0700 Subject: [PATCH] Add history-tracking to ObservationValidator This change lets you peek into the history of certain interactions with your Observations if an InvalidObservationException is thrown. This includes which method was called on your Observations (start, stop, error, etc) and also the relevant part of the stack trace. InvalidObservationException can provide you the full history through its getHistory method and it also gives you a summary through its toString. --- .../tck/InvalidObservationException.java | 76 ++++++++++++++++++- .../observation/tck/ObservationValidator.java | 38 +++++++++- 2 files changed, 112 insertions(+), 2 deletions(-) diff --git a/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/InvalidObservationException.java b/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/InvalidObservationException.java index 72f0c89392..6caddbd851 100644 --- a/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/InvalidObservationException.java +++ b/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/InvalidObservationException.java @@ -18,6 +18,10 @@ import io.micrometer.observation.Observation; import io.micrometer.observation.Observation.Context; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + /** * A {@link RuntimeException} that can be thrown when an invalid {@link Observation} * detected. @@ -29,13 +33,83 @@ public class InvalidObservationException extends RuntimeException { private final Context context; - InvalidObservationException(String message, Context context) { + private final List history; + + InvalidObservationException(String message, Context context, List history) { super(message); this.context = context; + this.history = history; } public Context getContext() { return context; } + public List getHistory() { + return history; + } + + @Override + public String toString() { + return super.toString() + "\n" + + history.stream().map(HistoryElement::toString).collect(Collectors.joining("\n")); + } + + public static class HistoryElement { + + private final EventName eventName; + + private final StackTraceElement[] stackTrace; + + HistoryElement(EventName eventName) { + this.eventName = eventName; + StackTraceElement[] currentStackTrace = Thread.getAllStackTraces().get(Thread.currentThread()); + this.stackTrace = findRelevantStackTraceElements(currentStackTrace); + } + + private StackTraceElement[] findRelevantStackTraceElements(StackTraceElement[] stackTrace) { + int index = findFirstRelevantStackTraceElementIndex(stackTrace); + if (index == -1) { + return new StackTraceElement[0]; + } + else { + return Arrays.copyOfRange(stackTrace, index, stackTrace.length); + } + } + + private int findFirstRelevantStackTraceElementIndex(StackTraceElement[] stackTrace) { + int index = -1; + for (int i = 0; i < stackTrace.length; i++) { + String className = stackTrace[i].getClassName(); + if (className.equals(Observation.class.getName()) + || className.equals("io.micrometer.observation.SimpleObservation")) { + // the first relevant StackTraceElement is after the last Observation + index = i + 1; + } + } + + return (index >= stackTrace.length) ? -1 : index; + } + + public EventName getEventName() { + return eventName; + } + + public StackTraceElement[] getStackTrace() { + return stackTrace; + } + + @Override + public String toString() { + return eventName + ": " + stackTrace[0]; + } + + } + + public enum EventName { + + START, STOP, ERROR, EVENT, SCOPE_OPEN, SCOPE_CLOSE, SCOPE_RESET + + } + } 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 index 4dc9e68a6d..1ab546e92e 100644 --- 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 @@ -19,7 +19,12 @@ import io.micrometer.observation.Observation.Context; import io.micrometer.observation.Observation.Event; import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.tck.InvalidObservationException.EventName; +import io.micrometer.observation.tck.InvalidObservationException.HistoryElement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.function.Consumer; import java.util.function.Predicate; @@ -52,6 +57,7 @@ class ObservationValidator implements ObservationHandler { @Override public void onStart(Context context) { + addHistoryElement(context, EventName.START); Status status = context.get(Status.class); if (status != null) { consumer.accept(new ValidationResult("Invalid start: Observation has already been started", context)); @@ -63,31 +69,37 @@ public void onStart(Context context) { @Override public void onError(Context context) { + addHistoryElement(context, EventName.ERROR); checkIfObservationWasStartedButNotStopped("Invalid error signal", context); } @Override public void onEvent(Event event, Context context) { + addHistoryElement(context, EventName.EVENT); checkIfObservationWasStartedButNotStopped("Invalid event signal", context); } @Override public void onScopeOpened(Context context) { + addHistoryElement(context, EventName.SCOPE_OPEN); checkIfObservationWasStartedButNotStopped("Invalid scope opening", context); } @Override public void onScopeClosed(Context context) { + addHistoryElement(context, EventName.SCOPE_CLOSE); checkIfObservationWasStartedButNotStopped("Invalid scope closing", context); } @Override public void onScopeReset(Context context) { + addHistoryElement(context, EventName.SCOPE_RESET); checkIfObservationWasStartedButNotStopped("Invalid scope resetting", context); } @Override public void onStop(Context context) { + addHistoryElement(context, EventName.STOP); Status status = checkIfObservationWasStartedButNotStopped("Invalid stop", context); if (status != null) { status.markStopped(); @@ -99,6 +111,14 @@ public boolean supportsContext(Context context) { return supportsContextPredicate.test(context); } + private void addHistoryElement(Context context, EventName eventName) { + if (!context.containsKey(History.class)) { + context.put(History.class, new History()); + } + History history = context.get(History.class); + history.addHistoryElement(eventName); + } + @Nullable private Status checkIfObservationWasStartedButNotStopped(String prefix, Context context) { Status status = context.get(Status.class); @@ -113,7 +133,9 @@ else if (status.isStopped()) { } private static void throwInvalidObservationException(ValidationResult validationResult) { - throw new InvalidObservationException(validationResult.getMessage(), validationResult.getContext()); + History history = validationResult.getContext().getOrDefault(History.class, new History()); + throw new InvalidObservationException(validationResult.getMessage(), validationResult.getContext(), + history.getHistoryElements()); } static class ValidationResult { @@ -156,4 +178,18 @@ void markStopped() { } + static class History { + + private final List historyElements = new ArrayList<>(); + + private void addHistoryElement(EventName eventName) { + historyElements.add(new HistoryElement(eventName)); + } + + List getHistoryElements() { + return Collections.unmodifiableList(historyElements); + } + + } + }