Open
Description
The BufferingApplicationStartup
returns a StartupTimeline
that contains events. Those events have a parent - child relationship (a TimelineEvent
has access to BufferedStartupStep
that can have a parent id).
The problem is that we can't traverse this tree of events in a proper order. We need to build the graph ourselves.
@jonatan-ivanov has prototyped such traversal in the following manner:
@ReadOperation
public void observabilitySnapshot() {
StartupTimeline startupTimeline = this.applicationStartup.getBufferedTimeline();
observeStartupTimeline(startupTimeline);
}
private void observeStartupTimeline(StartupTimeline startupTimeline) {
if (startupTimeline.getEvents().isEmpty()) {
return;
}
// We're building a map of IDs to nodes
Map<Long, Node> stepMap = startupTimeline.getEvents().stream()
.map(Node::new)
.collect(toMap(node -> node.startupStep.getId(), Function.identity()));
// Some startupsteps do not have a parent so we need a root one. It's start will be the timeline's start time and end will be the last event's end time
Node artificalRoot = new Node(new StartupStep() {
@Override
public String getName() {
return "a name";
}
@Override
public long getId() {
return -100;
}
@Override
public Long getParentId() {
return null;
}
@Override
public StartupStep tag(String key, String value) {
return null;
}
@Override
public StartupStep tag(String key, Supplier<String> value) {
return null;
}
@Override
public Tags getTags() {
return null;
}
@Override
public void end() {
}
}, toNanos(startupTimeline.getStartTime()), stepMap.entrySet().stream().max(Map.Entry.comparingByKey()).get().getValue().endTimeNanos);
// we're adding for each node its corresponding children
for (Map.Entry<Long, Node> entry : stepMap.entrySet()) {
Node current = entry.getValue();
Node parent = stepMap.get(current.startupStep.getParentId() != null ? current.startupStep.getParentId() : artificalRoot);
parent = parent != null ? parent : artificalRoot;
parent.children.add(current);
}
visit(artificalRoot);
}
// Recursive node visiting
private void visit(Node node) {
// e.g. generate a span
T t = doSomeWorkBefore(node);
// TODO: add filtering over duration otherwise the graph can blow up (e.g. maybe merge manually various children)
for (Node child : node.children) {
visit(child);
}
// e.g. close a span
doSomeWorkAfter(node, t);
}
static class Node {
private final StartupStep startupStep;
private final long startTimeNanos;
private final long endTimeNanos;
List<Node> children = new ArrayList<>();
Node(StartupTimeline.TimelineEvent timelineEvent) {
this.startupStep = timelineEvent.getStartupStep();
this.startTimeNanos = toNanos(timelineEvent.getStartTime());
this.endTimeNanos = toNanos(timelineEvent.getEndTime());
}
Node(StartupStep startupStep, long startTimeNanos, long endTimeNanos) {
this.startupStep = startupStep;
this.startTimeNanos = startTimeNanos;
this.endTimeNanos = endTimeNanos;
}
private long toNanos(Instant time) {
return TimeUnit.SECONDS.toNanos(time.getEpochSecond()) + time.getNano();
}
}
It would be great if such node traversal was done from within Boot, e.g. the StartupTimeline
would have a method like traverse
that would give us Function<Node, T> beforeVisit
and BiConsumer<Node, T> afterVisit
.