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.

cc @jonatan-ivanov @shakuzen @bclozel