Skip to content

Commit

Permalink
Add history-tracking to ObservationValidator (#5370)
Browse files Browse the repository at this point in the history
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.
This is helpful when the instrumentation is complex and it is not apparent where a previous call to Observation happened that needs to be fixed.

InvalidObservationException can provide you the full history through its
getHistory method and it also gives you a summary through its toString.
  • Loading branch information
jonatan-ivanov authored Aug 8, 2024
1 parent 9ffef08 commit 3d26472
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -29,13 +33,83 @@ public class InvalidObservationException extends RuntimeException {

private final Context context;

InvalidObservationException(String message, Context context) {
private final List<HistoryElement> history;

InvalidObservationException(String message, Context context, List<HistoryElement> history) {
super(message);
this.context = context;
this.history = history;
}

public Context getContext() {
return context;
}

public List<HistoryElement> 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

}

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

Expand Down Expand Up @@ -52,6 +57,7 @@ class ObservationValidator implements ObservationHandler<Context> {

@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));
Expand All @@ -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();
Expand All @@ -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);
Expand All @@ -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 {
Expand Down Expand Up @@ -156,4 +178,18 @@ void markStopped() {

}

static class History {

private final List<HistoryElement> historyElements = new ArrayList<>();

private void addHistoryElement(EventName eventName) {
historyElements.add(new HistoryElement(eventName));
}

List<HistoryElement> getHistoryElements() {
return Collections.unmodifiableList(historyElements);
}

}

}

0 comments on commit 3d26472

Please sign in to comment.