Skip to content

Commit 28589d6

Browse files
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.
1 parent 9ffef08 commit 28589d6

File tree

2 files changed

+112
-2
lines changed

2 files changed

+112
-2
lines changed

micrometer-observation-test/src/main/java/io/micrometer/observation/tck/InvalidObservationException.java

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
import io.micrometer.observation.Observation;
1919
import io.micrometer.observation.Observation.Context;
2020

21+
import java.util.Arrays;
22+
import java.util.List;
23+
import java.util.stream.Collectors;
24+
2125
/**
2226
* A {@link RuntimeException} that can be thrown when an invalid {@link Observation}
2327
* detected.
@@ -29,13 +33,83 @@ public class InvalidObservationException extends RuntimeException {
2933

3034
private final Context context;
3135

32-
InvalidObservationException(String message, Context context) {
36+
private final List<HistoryElement> history;
37+
38+
InvalidObservationException(String message, Context context, List<HistoryElement> history) {
3339
super(message);
3440
this.context = context;
41+
this.history = history;
3542
}
3643

3744
public Context getContext() {
3845
return context;
3946
}
4047

48+
public List<HistoryElement> getHistory() {
49+
return history;
50+
}
51+
52+
@Override
53+
public String toString() {
54+
return super.toString() + "\n"
55+
+ history.stream().map(HistoryElement::toString).collect(Collectors.joining("\n"));
56+
}
57+
58+
public static class HistoryElement {
59+
60+
private final EventName eventName;
61+
62+
private final StackTraceElement[] stackTrace;
63+
64+
HistoryElement(EventName eventName) {
65+
this.eventName = eventName;
66+
StackTraceElement[] currentStackTrace = Thread.getAllStackTraces().get(Thread.currentThread());
67+
this.stackTrace = findRelevantStackTraceElements(currentStackTrace);
68+
}
69+
70+
private StackTraceElement[] findRelevantStackTraceElements(StackTraceElement[] stackTrace) {
71+
int index = findFirstRelevantStackTraceElementIndex(stackTrace);
72+
if (index == -1) {
73+
return new StackTraceElement[0];
74+
}
75+
else {
76+
return Arrays.copyOfRange(stackTrace, index, stackTrace.length);
77+
}
78+
}
79+
80+
private int findFirstRelevantStackTraceElementIndex(StackTraceElement[] stackTrace) {
81+
int index = -1;
82+
for (int i = 0; i < stackTrace.length; i++) {
83+
String className = stackTrace[i].getClassName();
84+
if (className.equals(Observation.class.getName())
85+
|| className.equals("io.micrometer.observation.SimpleObservation")) {
86+
// the first relevant StackTraceElement is after the last Observation
87+
index = i + 1;
88+
}
89+
}
90+
91+
return (index >= stackTrace.length) ? -1 : index;
92+
}
93+
94+
public EventName getEventName() {
95+
return eventName;
96+
}
97+
98+
public StackTraceElement[] getStackTrace() {
99+
return stackTrace;
100+
}
101+
102+
@Override
103+
public String toString() {
104+
return eventName + ": " + stackTrace[0];
105+
}
106+
107+
}
108+
109+
public enum EventName {
110+
111+
START, STOP, ERROR, EVENT, SCOPE_OPEN, SCOPE_CLOSE, SCOPE_RESET
112+
113+
}
114+
41115
}

micrometer-observation-test/src/main/java/io/micrometer/observation/tck/ObservationValidator.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@
1919
import io.micrometer.observation.Observation.Context;
2020
import io.micrometer.observation.Observation.Event;
2121
import io.micrometer.observation.ObservationHandler;
22+
import io.micrometer.observation.tck.InvalidObservationException.EventName;
23+
import io.micrometer.observation.tck.InvalidObservationException.HistoryElement;
2224

25+
import java.util.ArrayList;
26+
import java.util.Collections;
27+
import java.util.List;
2328
import java.util.function.Consumer;
2429
import java.util.function.Predicate;
2530

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

5358
@Override
5459
public void onStart(Context context) {
60+
addHistoryElement(context, EventName.START);
5561
Status status = context.get(Status.class);
5662
if (status != null) {
5763
consumer.accept(new ValidationResult("Invalid start: Observation has already been started", context));
@@ -63,31 +69,37 @@ public void onStart(Context context) {
6369

6470
@Override
6571
public void onError(Context context) {
72+
addHistoryElement(context, EventName.ERROR);
6673
checkIfObservationWasStartedButNotStopped("Invalid error signal", context);
6774
}
6875

6976
@Override
7077
public void onEvent(Event event, Context context) {
78+
addHistoryElement(context, EventName.EVENT);
7179
checkIfObservationWasStartedButNotStopped("Invalid event signal", context);
7280
}
7381

7482
@Override
7583
public void onScopeOpened(Context context) {
84+
addHistoryElement(context, EventName.SCOPE_OPEN);
7685
checkIfObservationWasStartedButNotStopped("Invalid scope opening", context);
7786
}
7887

7988
@Override
8089
public void onScopeClosed(Context context) {
90+
addHistoryElement(context, EventName.SCOPE_CLOSE);
8191
checkIfObservationWasStartedButNotStopped("Invalid scope closing", context);
8292
}
8393

8494
@Override
8595
public void onScopeReset(Context context) {
96+
addHistoryElement(context, EventName.SCOPE_RESET);
8697
checkIfObservationWasStartedButNotStopped("Invalid scope resetting", context);
8798
}
8899

89100
@Override
90101
public void onStop(Context context) {
102+
addHistoryElement(context, EventName.STOP);
91103
Status status = checkIfObservationWasStartedButNotStopped("Invalid stop", context);
92104
if (status != null) {
93105
status.markStopped();
@@ -99,6 +111,14 @@ public boolean supportsContext(Context context) {
99111
return supportsContextPredicate.test(context);
100112
}
101113

114+
private void addHistoryElement(Context context, EventName eventName) {
115+
if (!context.containsKey(History.class)) {
116+
context.put(History.class, new History());
117+
}
118+
History history = context.get(History.class);
119+
history.addHistoryElement(eventName);
120+
}
121+
102122
@Nullable
103123
private Status checkIfObservationWasStartedButNotStopped(String prefix, Context context) {
104124
Status status = context.get(Status.class);
@@ -113,7 +133,9 @@ else if (status.isStopped()) {
113133
}
114134

115135
private static void throwInvalidObservationException(ValidationResult validationResult) {
116-
throw new InvalidObservationException(validationResult.getMessage(), validationResult.getContext());
136+
History history = validationResult.getContext().getOrDefault(History.class, new History());
137+
throw new InvalidObservationException(validationResult.getMessage(), validationResult.getContext(),
138+
history.getHistoryElements());
117139
}
118140

119141
static class ValidationResult {
@@ -156,4 +178,18 @@ void markStopped() {
156178

157179
}
158180

181+
static class History {
182+
183+
private final List<HistoryElement> historyElements = new ArrayList<>();
184+
185+
private void addHistoryElement(EventName eventName) {
186+
historyElements.add(new HistoryElement(eventName));
187+
}
188+
189+
List<HistoryElement> getHistoryElements() {
190+
return Collections.unmodifiableList(historyElements);
191+
}
192+
193+
}
194+
159195
}

0 commit comments

Comments
 (0)