001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.client.trace;
019
020import io.opentelemetry.api.trace.SpanId;
021import io.opentelemetry.sdk.trace.data.SpanData;
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.HashMap;
025import java.util.LinkedHashMap;
026import java.util.List;
027import java.util.ListIterator;
028import java.util.Map;
029import java.util.Objects;
030import java.util.function.Consumer;
031import java.util.stream.Collectors;
032import org.apache.commons.lang3.builder.ToStringBuilder;
033import org.apache.commons.lang3.builder.ToStringStyle;
034import org.apache.yetus.audience.InterfaceAudience;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038/**
039 * A Rudimentary tool for visualizing a hierarchy of spans. Given a collection of spans, indexes
040 * them from parents to children and prints them out one per line, indented.
041 */
042@InterfaceAudience.Private
043public class StringTraceRenderer {
044  private static final Logger logger = LoggerFactory.getLogger(StringTraceRenderer.class);
045
046  private final List<Node> graphs;
047
048  public StringTraceRenderer(final Collection<SpanData> spans) {
049    final Map<String, Node> spansById = indexSpansById(spans);
050    populateChildren(spansById);
051    graphs = findRoots(spansById);
052  }
053
054  private static Map<String, Node> indexSpansById(final Collection<SpanData> spans) {
055    final Map<String, Node> spansById = new HashMap<>(spans.size());
056    spans.forEach(span -> spansById.put(span.getSpanId(), new Node(span)));
057    return spansById;
058  }
059
060  private static void populateChildren(final Map<String, Node> spansById) {
061    spansById.forEach((spanId, node) -> {
062      final SpanData spanData = node.spanData;
063      final String parentSpanId = spanData.getParentSpanId();
064      if (Objects.equals(parentSpanId, SpanId.getInvalid())) {
065        return;
066      }
067      final Node parentNode = spansById.get(parentSpanId);
068      if (parentNode == null) {
069        logger.warn("Span {} has parent {} that is not found in index, {}", spanId, parentSpanId,
070          spanData);
071        return;
072      }
073      parentNode.children.put(spanId, node);
074    });
075  }
076
077  private static List<Node> findRoots(final Map<String, Node> spansById) {
078    return spansById.values().stream()
079      .filter(node -> Objects.equals(node.spanData.getParentSpanId(), SpanId.getInvalid()))
080      .collect(Collectors.toList());
081  }
082
083  public void render(final Consumer<String> writer) {
084    for (ListIterator<Node> iter = graphs.listIterator(); iter.hasNext();) {
085      final int idx = iter.nextIndex();
086      final Node node = iter.next();
087      render(writer, node, 0, idx == 0);
088    }
089  }
090
091  private static void render(final Consumer<String> writer, final Node node, final int indent,
092    final boolean isFirst) {
093    writer.accept(render(node.spanData, indent, isFirst));
094    final List<Node> children = new ArrayList<>(node.children.values());
095    for (ListIterator<Node> iter = children.listIterator(); iter.hasNext();) {
096      final int idx = iter.nextIndex();
097      final Node child = iter.next();
098      render(writer, child, indent + 2, idx == 0);
099    }
100  }
101
102  private static String render(final SpanData spanData, final int indent, final boolean isFirst) {
103    final StringBuilder sb = new StringBuilder();
104    for (int i = 0; i < indent; i++) {
105      sb.append(' ');
106    }
107
108    return sb.append(isFirst ? "└─ " : "├─ ").append(render(spanData)).toString();
109  }
110
111  private static String render(final SpanData spanData) {
112    return new ToStringBuilder(spanData, ToStringStyle.NO_CLASS_NAME_STYLE)
113      .append("spanId", spanData.getSpanId()).append("name", spanData.getName())
114      .append("hasEnded", spanData.hasEnded()).toString();
115  }
116
117  private static class Node {
118    final SpanData spanData;
119    final LinkedHashMap<String, Node> children;
120
121    Node(final SpanData spanData) {
122      this.spanData = spanData;
123      this.children = new LinkedHashMap<>();
124    }
125  }
126}