A comprehensive distributed tracing visualization system that collects, analyzes, and visualizes trace data from microservices architectures.
Project Overview
This system processes distributed trace data from multiple sources (Jaeger, Zipkin, OpenTelemetry) and provides interactive visualizations including service maps, dependency graphs, and latency analysis.
Architecture
graph TB A[Microservices] --> B[Trace Collectors] B --> C[Trace Storage] C --> D[Trace Processor] D --> E[Visualization Engine] E --> F[Web UI] E --> G[Graph Export] H[Jaeger] --> B I[Zipkin] --> B J[OpenTelemetry] --> B K[Analysis Engine] --> D L[Alerting System] --> E
Project Structure and Dependencies
Maven Dependencies
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <groupId>com.tracing</groupId> <artifactId>trace-visualizer</artifactId> <version>1.0.0</version> <properties> <spring-boot.version>3.1.0</spring-boot.version> <opentelemetry.version>1.28.0</opentelemetry.version> </properties> <dependencies> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- OpenTelemetry --> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-api</artifactId> <version>${opentelemetry.version}</version> </dependency> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-sdk</artifactId> <version>${opentelemetry.version}</version> </dependency> <!-- Graph Visualization --> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-neo4j</artifactId> <version>7.1.0</version> </dependency> <dependency> <groupId>guru.nidi</groupId> <artifactId>graphviz-java</artifactId> <version>0.18.1</version> </dependency> <!-- Database --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>2.2.220</version> </dependency> <!-- Utilities --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.0</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.12.0</version> </dependency> <!-- Testing --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>${spring-boot.version}</version> <scope>test</scope> </dependency> </dependencies> </project> Core Domain Models
package com.tracing.model; import javax.persistence.*; import java.time.Instant; import java.util.*; @Entity @Table(name = "traces") public class Trace { @Id private String traceId; private String name; private Instant startTime; private Instant endTime; private long durationMs; private String status; // SUCCESS, ERROR, TIMEOUT @OneToMany(mappedBy = "trace", cascade = CascadeType.ALL, fetch = FetchType.EAGER) private List<Span> spans = new ArrayList<>(); @ElementCollection @CollectionTable(name = "trace_tags", joinColumns = @JoinColumn(name = "trace_id")) private Map<String, String> tags = new HashMap<>(); // constructors, getters, setters public Trace() {} public Trace(String traceId, String name, Instant startTime) { this.traceId = traceId; this.name = name; this.startTime = startTime; } public void addSpan(Span span) { span.setTrace(this); this.spans.add(span); } public void calculateDuration() { if (spans.isEmpty()) return; this.startTime = spans.stream() .map(Span::getStartTime) .min(Instant::compareTo) .orElse(Instant.now()); this.endTime = spans.stream() .map(span -> span.getStartTime().plusNanos(span.getDurationNs())) .max(Instant::compareTo) .orElse(Instant.now()); this.durationMs = java.time.Duration.between(startTime, endTime).toMillis(); } // Getters and setters public String getTraceId() { return traceId; } public void setTraceId(String traceId) { this.traceId = traceId; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Instant getStartTime() { return startTime; } public void setStartTime(Instant startTime) { this.startTime = startTime; } public Instant getEndTime() { return endTime; } public void setEndTime(Instant endTime) { this.endTime = endTime; } public long getDurationMs() { return durationMs; } public void setDurationMs(long durationMs) { this.durationMs = durationMs; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public List<Span> getSpans() { return spans; } public void setSpans(List<Span> spans) { this.spans = spans; } public Map<String, String> getTags() { return tags; } public void setTags(Map<String, String> tags) { this.tags = tags; } } @Entity @Table(name = "spans") public class Span { @Id private String spanId; private String name; private String serviceName; private Instant startTime; private long durationNs; private String kind; // SERVER, CLIENT, PRODUCER, CONSUMER @ManyToOne @JoinColumn(name = "trace_id") private Trace trace; private String parentSpanId; @ElementCollection @CollectionTable(name = "span_tags", joinColumns = @JoinColumn(name = "span_id")) private Map<String, String> tags = new HashMap<>(); @OneToMany(mappedBy = "span", cascade = CascadeType.ALL) private List<SpanEvent> events = new ArrayList<>(); @OneToMany(mappedBy = "span", cascade = CascadeType.ALL) private List<SpanLink> links = new ArrayList<>(); // constructors, getters, setters public Span() {} public Span(String spanId, String name, String serviceName, Instant startTime) { this.spanId = spanId; this.name = name; this.serviceName = serviceName; this.startTime = startTime; } // Getters and setters public String getSpanId() { return spanId; } public void setSpanId(String spanId) { this.spanId = spanId; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getServiceName() { return serviceName; } public void setServiceName(String serviceName) { this.serviceName = serviceName; } public Instant getStartTime() { return startTime; } public void setStartTime(Instant startTime) { this.startTime = startTime; } public long getDurationNs() { return durationNs; } public void setDurationNs(long durationNs) { this.durationNs = durationNs; } public String getKind() { return kind; } public void setKind(String kind) { this.kind = kind; } public Trace getTrace() { return trace; } public void setTrace(Trace trace) { this.trace = trace; } public String getParentSpanId() { return parentSpanId; } public void setParentSpanId(String parentSpanId) { this.parentSpanId = parentSpanId; } public Map<String, String> getTags() { return tags; } public void setTags(Map<String, String> tags) { this.tags = tags; } public List<SpanEvent> getEvents() { return events; } public void setEvents(List<SpanEvent> events) { this.events = events; } public List<SpanLink> getLinks() { return links; } public void setLinks(List<SpanLink> links) { this.links = links; } } @Entity @Table(name = "span_events") public class SpanEvent { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private Instant timestamp; @ElementCollection @CollectionTable(name = "event_attributes", joinColumns = @JoinColumn(name = "event_id")) private Map<String, String> attributes = new HashMap<>(); @ManyToOne @JoinColumn(name = "span_id") private Span span; // constructors, getters, setters } @Entity @Table(name = "service_dependencies") public class ServiceDependency { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String sourceService; private String targetService; private String operation; private long callCount; private double averageLatency; private double errorRate; private Instant lastUpdated; // constructors, getters, setters public ServiceDependency() {} public ServiceDependency(String sourceService, String targetService, String operation) { this.sourceService = sourceService; this.targetService = targetService; this.operation = operation; this.lastUpdated = Instant.now(); } public void updateMetrics(long callCount, double averageLatency, double errorRate) { this.callCount = callCount; this.averageLatency = averageLatency; this.errorRate = errorRate; this.lastUpdated = Instant.now(); } } Trace Collector Service
package com.tracing.collector; import com.tracing.model.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import java.io.IOException; import java.time.Instant; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @Service public class TraceCollectorService { @Autowired private TraceRepository traceRepository; @Autowired private ServiceDependencyRepository dependencyRepository; @Autowired private VisualizationService visualizationService; private final Set<WebSocketSession> liveSessions = ConcurrentHashMap.newKeySet(); public void processJaegerTrace(Map<String, Object> jaegerData) { try { Trace trace = convertJaegerToTrace(jaegerData); processTrace(trace); } catch (Exception e) { System.err.println("Error processing Jaeger trace: " + e.getMessage()); } } public void processZipkinTrace(List<Map<String, Object>> zipkinSpans) { try { Trace trace = convertZipkinToTrace(zipkinSpans); processTrace(trace); } catch (Exception e) { System.err.println("Error processing Zipkin trace: " + e.getMessage()); } } public void processOpenTelemetryTrace(Map<String, Object> otelData) { try { Trace trace = convertOpenTelemetryToTrace(otelData); processTrace(trace); } catch (Exception e) { System.err.println("Error processing OpenTelemetry trace: " + e.getMessage()); } } private Trace convertJaegerToTrace(Map<String, Object> jaegerData) { String traceId = (String) jaegerData.get("traceID"); List<Map<String, Object>> jaegerSpans = (List<Map<String, Object>>) jaegerData.get("spans"); Trace trace = new Trace(traceId, "jaeger-trace", Instant.now()); for (Map<String, Object> jaegerSpan : jaegerSpans) { Span span = convertJaegerSpan(jaegerSpan); trace.addSpan(span); } trace.calculateDuration(); return trace; } private Span convertJaegerSpan(Map<String, Object> jaegerSpan) { String spanId = (String) jaegerSpan.get("spanID"); String operationName = (String) jaegerSpan.get("operationName"); long startTime = Long.parseLong(jaegerSpan.get("startTime").toString()); long duration = Long.parseLong(jaegerSpan.get("duration").toString()); Span span = new Span(spanId, operationName, "unknown", Instant.ofEpochMicro(startTime)); span.setDurationNs(duration * 1000); // Convert to nanoseconds // Process tags List<Map<String, Object>> tags = (List<Map<String, Object>>) jaegerSpan.get("tags"); if (tags != null) { for (Map<String, Object> tag : tags) { String key = (String) tag.get("key"); String value = tag.get("value").toString(); span.getTags().put(key, value); if ("service.name".equals(key)) { span.setServiceName(value); } } } return span; } private Trace convertZipkinToTrace(List<Map<String, Object>> zipkinSpans) { if (zipkinSpans.isEmpty()) { throw new IllegalArgumentException("Zipkin spans list is empty"); } String traceId = (String) zipkinSpans.get(0).get("traceId"); Trace trace = new Trace(traceId, "zipkin-trace", Instant.now()); for (Map<String, Object> zipkinSpan : zipkinSpans) { Span span = convertZipkinSpan(zipkinSpan); trace.addSpan(span); } trace.calculateDuration(); return trace; } private Span convertZipkinSpan(Map<String, Object> zipkinSpan) { String spanId = (String) zipkinSpan.get("id"); String name = (String) zipkinSpan.get("name"); String serviceName = (String) ((Map<String, Object>) zipkinSpan.get("localEndpoint")).get("serviceName"); long timestamp = Long.parseLong(zipkinSpan.get("timestamp").toString()); long duration = Long.parseLong(zipkinSpan.get("duration").toString()); Span span = new Span(spanId, name, serviceName, Instant.ofEpochMicro(timestamp)); span.setDurationNs(duration * 1000); // Process parent span String parentId = (String) zipkinSpan.get("parentId"); if (parentId != null) { span.setParentSpanId(parentId); } return span; } private Trace convertOpenTelemetryToTrace(Map<String, Object> otelData) { // Implementation for OpenTelemetry format // Similar to above implementations return new Trace(); // Simplified for example } private void processTrace(Trace trace) { // Save to database traceRepository.save(trace); // Update service dependencies updateServiceDependencies(trace); // Send to visualization service visualizationService.processTraceForVisualization(trace); // Notify WebSocket clients notifyLiveViewers(trace); } private void updateServiceDependencies(Trace trace) { Map<String, List<Span>> spansByService = new HashMap<>(); // Group spans by service for (Span span : trace.getSpans()) { spansByService .computeIfAbsent(span.getServiceName(), k -> new ArrayList<>()) .add(span); } // Analyze dependencies between services for (Span span : trace.getSpans()) { if (span.getParentSpanId() != null) { Optional<Span> parentSpan = trace.getSpans().stream() .filter(s -> s.getSpanId().equals(span.getParentSpanId())) .findFirst(); if (parentSpan.isPresent()) { Span parent = parentSpan.get(); if (!parent.getServiceName().equals(span.getServiceName())) { // Found inter-service call String operation = parent.getName() + " -> " + span.getName(); ServiceDependency dependency = dependencyRepository .findBySourceServiceAndTargetServiceAndOperation( parent.getServiceName(), span.getServiceName(), operation) .orElse(new ServiceDependency( parent.getServiceName(), span.getServiceName(), operation)); // Update metrics (simplified) dependency.updateMetrics( dependency.getCallCount() + 1, (dependency.getAverageLatency() + span.getDurationNs() / 1_000_000.0) / 2, dependency.getErrorRate() // Would calculate based on span status ); dependencyRepository.save(dependency); } } } } } public void registerLiveSession(WebSocketSession session) { liveSessions.add(session); } public void unregisterLiveSession(WebSocketSession session) { liveSessions.remove(session); } private void notifyLiveViewers(Trace trace) { String message = createTraceUpdateMessage(trace); liveSessions.forEach(session -> { try { if (session.isOpen()) { session.sendMessage(new TextMessage(message)); } } catch (IOException e) { System.err.println("Error sending WebSocket message: " + e.getMessage()); } }); } private String createTraceUpdateMessage(Trace trace) { Map<String, Object> message = new HashMap<>(); message.put("type", "TRACE_UPDATE"); message.put("traceId", trace.getTraceId()); message.put("serviceCount", trace.getSpans().stream() .map(Span::getServiceName) .distinct() .count()); message.put("durationMs", trace.getDurationMs()); message.put("status", trace.getStatus()); // Convert to JSON return convertToJson(message); } private String convertToJson(Map<String, Object> data) { // Simplified JSON conversion try { return new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(data); } catch (Exception e) { return "{}"; } } } Visualization Engine
package com.tracing.visualization; import com.tracing.model.*; import guru.nidi.graphviz.attribute.*; import guru.nidi.graphviz.engine.*; import guru.nidi.graphviz.model.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.File; import java.io.IOException; import java.util.*; import java.util.stream.Collectors; import static guru.nidi.graphviz.model.Factory.*; @Service public class VisualizationService { @Autowired private ServiceDependencyRepository dependencyRepository; @Autowired private TraceRepository traceRepository; public void generateServiceMap(String outputPath) throws IOException { List<ServiceDependency> dependencies = dependencyRepository.findAll(); MutableGraph graph = mutGraph("Service Map") .setDirected(true) .graphAttrs() .add(Rank.dir(Rank.RankDir.LEFT_TO_RIGHT)); // Create nodes for all services Set<String> services = dependencies.stream() .flatMap(dep -> Arrays.asList(dep.getSourceService(), dep.getTargetService()).stream()) .collect(Collectors.toSet()); Map<String, MutableNode> serviceNodes = new HashMap<>(); for (String service : services) { MutableNode node = mutNode(service) .add(Shape.RECTANGLE, Style.FILLED, Color.LIGHTBLUE, Label.of(service)); serviceNodes.put(service, node); graph.add(node); } // Create edges for dependencies for (ServiceDependency dependency : dependencies) { MutableNode source = serviceNodes.get(dependency.getSourceService()); MutableNode target = serviceNodes.get(dependency.getTargetService()); if (source != null && target != null) { String label = String.format("%s\n%d calls\n%.2fms avg", dependency.getOperation(), dependency.getCallCount(), dependency.getAverageLatency()); graph.add(source.linkTo(target) .with(Label.of(label), Color.BLUE, Style.SOLID)); } } // Generate the visualization Graphviz.fromGraph(graph) .width(1200) .render(Format.SVG) .toFile(new File(outputPath)); } public void generateTraceWaterfall(Trace trace, String outputPath) throws IOException { MutableGraph graph = mutGraph("Trace Waterfall") .setDirected(true) .graphAttrs() .add(Rank.dir(Rank.RankDir.TOP_TO_BOTTOM)); // Sort spans by start time List<Span> sortedSpans = trace.getSpans().stream() .sorted(Comparator.comparing(Span::getStartTime)) .collect(Collectors.toList()); // Create timeline visualization for (Span span : sortedSpans) { String durationMs = String.format("%.2fms", span.getDurationNs() / 1_000_000.0); String label = String.format("%s\\n%s\\n%s", span.getServiceName(), span.getName(), durationMs); MutableNode spanNode = mutNode(span.getSpanId()) .add(Shape.RECTANGLE, Style.FILLED, getSpanColor(span), Label.of(label), Width.of(span.getDurationNs() / 1_000_000.0 / 10)); // Scale width graph.add(spanNode); } // Create parent-child relationships for (Span span : sortedSpans) { if (span.getParentSpanId() != null) { graph.add(mutNode(span.getParentSpanId()) .addLink(mutNode(span.getSpanId()) .with(Style.DASHED, Color.GRAY))); } } Graphviz.fromGraph(graph) .width(1600) .height(800) .render(Format.PNG) .toFile(new File(outputPath)); } private Color getSpanColor(Span span) { // Color coding based on service or status Map<String, Color> serviceColors = Map.of( "user-service", Color.LIGHTBLUE, "order-service", Color.LIGHTGREEN, "payment-service", Color.LIGHTYELLOW, "inventory-service", Color.LIGHTCYAN, "shipping-service", Color.LIGHTGRAY ); return serviceColors.getOrDefault(span.getServiceName(), Color.WHITE); } public String generateMermaidServiceMap() { List<ServiceDependency> dependencies = dependencyRepository.findAll(); StringBuilder mermaid = new StringBuilder(); mermaid.append("graph TB\n"); Set<String> services = new HashSet<>(); for (ServiceDependency dep : dependencies) { services.add(dep.getSourceService()); services.add(dep.getTargetService()); } // Add nodes for (String service : services) { mermaid.append(" ").append(service.replaceAll("[^a-zA-Z0-9]", "")) .append("[").append(service).append("]\n"); } // Add edges for (ServiceDependency dep : dependencies) { String source = dep.getSourceService().replaceAll("[^a-zA-Z0-9]", ""); String target = dep.getTargetService().replaceAll("[^a-zA-Z0-9]", ""); mermaid.append(" ").append(source) .append(" -->|").append(dep.getCallCount()).append(" calls| ") .append(target).append("\n"); } return mermaid.toString(); } public String generateMermaidTraceDiagram(Trace trace) { StringBuilder mermaid = new StringBuilder(); mermaid.append("sequenceDiagram\n"); // Group spans by service and sort by start time Map<String, List<Span>> spansByService = trace.getSpans().stream() .collect(Collectors.groupingBy(Span::getServiceName)); // Add participants spansByService.keySet().forEach(service -> { mermaid.append(" participant ").append(service.replaceAll("[^a-zA-Z0-9]", "")) .append(" as ").append(service).append("\n"); }); // Add interactions List<Span> sortedSpans = trace.getSpans().stream() .sorted(Comparator.comparing(Span::getStartTime)) .collect(Collectors.toList()); for (Span span : sortedSpans) { String service = span.getServiceName().replaceAll("[^a-zA-Z0-9]", ""); if (span.getParentSpanId() != null) { Optional<Span> parent = trace.getSpans().stream() .filter(s -> s.getSpanId().equals(span.getParentSpanId())) .findFirst(); if (parent.isPresent()) { String parentService = parent.get().getServiceName().replaceAll("[^a-zA-Z0-9]", ""); if (!parentService.equals(service)) { mermaid.append(" ").append(parentService) .append("->>+").append(service) .append(": ").append(span.getName()) .append(" (").append(span.getDurationNs() / 1_000_000).append("ms)") .append("\n"); } } } } return mermaid.toString(); } public Map<String, Object> generateTraceAnalytics(Trace trace) { Map<String, Object> analytics = new HashMap<>(); // Basic statistics analytics.put("totalSpans", trace.getSpans().size()); analytics.put("totalDurationMs", trace.getDurationMs()); analytics.put("servicesInvolved", trace.getSpans().stream() .map(Span::getServiceName) .distinct() .count()); // Service-level statistics Map<String, Object> serviceStats = new HashMap<>(); trace.getSpans().stream() .collect(Collectors.groupingBy(Span::getServiceName)) .forEach((service, spans) -> { Map<String, Object> stats = new HashMap<>(); stats.put("spanCount", spans.size()); stats.put("totalTimeMs", spans.stream() .mapToLong(Span::getDurationNs) .sum() / 1_000_000); stats.put("averageTimeMs", spans.stream() .mapToLong(Span::getDurationNs) .average() .orElse(0) / 1_000_000); serviceStats.put(service, stats); }); analytics.put("serviceStatistics", serviceStats); // Critical path analysis List<Span> criticalPath = findCriticalPath(trace); analytics.put("criticalPath", criticalPath.stream() .map(span -> Map.of( "service", span.getServiceName(), "operation", span.getName(), "durationMs", span.getDurationNs() / 1_000_000 )) .collect(Collectors.toList())); return analytics; } private List<Span> findCriticalPath(Trace trace) { // Simplified critical path analysis // In production, this would use more sophisticated algorithms return trace.getSpans().stream() .sorted((a, b) -> Long.compare(b.getDurationNs(), a.getDurationNs())) .limit(5) .collect(Collectors.toList()); } } Web Interface and REST API
package com.tracing.controller; import com.tracing.model.*; import com.tracing.visualization.VisualizationService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Optional; @Controller public class TraceVisualizerController { @Autowired private TraceRepository traceRepository; @Autowired private ServiceDependencyRepository dependencyRepository; @Autowired private VisualizationService visualizationService; @GetMapping("/") public String index(Model model) { model.addAttribute("recentTraces", traceRepository.findTop10ByOrderByStartTimeDesc()); return "index"; } @GetMapping("/traces") public String viewTraces(Model model, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { // Implementation for paginated trace list return "traces"; } @GetMapping("/traces/{traceId}") public String viewTrace(@PathVariable String traceId, Model model) { Optional<Trace> trace = traceRepository.findById(traceId); if (trace.isPresent()) { model.addAttribute("trace", trace.get()); model.addAttribute("mermaidDiagram", visualizationService.generateMermaidTraceDiagram(trace.get())); model.addAttribute("analytics", visualizationService.generateTraceAnalytics(trace.get())); return "trace-detail"; } else { return "error"; } } @GetMapping("/services") public String viewServices(Model model) { List<ServiceDependency> dependencies = dependencyRepository.findAll(); model.addAttribute("dependencies", dependencies); model.addAttribute("mermaidServiceMap", visualizationService.generateMermaidServiceMap()); return "services"; } @GetMapping("/api/traces") @ResponseBody public List<Trace> getTraces(@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "50") int size) { return traceRepository.findAll(); } @GetMapping("/api/traces/{traceId}") @ResponseBody public ResponseEntity<Trace> getTrace(@PathVariable String traceId) { Optional<Trace> trace = traceRepository.findById(traceId); return trace.map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @GetMapping("/api/services/dependencies") @ResponseBody public List<ServiceDependency> getServiceDependencies() { return dependencyRepository.findAll(); } @PostMapping("/api/collect/jaeger") @ResponseBody public ResponseEntity<String> collectJaegerTrace(@RequestBody Map<String, Object> traceData) { // This would be handled by the TraceCollectorService return ResponseEntity.ok("Trace received"); } @GetMapping("/api/visualize/trace/{traceId}/waterfall") public ResponseEntity<String> generateTraceWaterfall(@PathVariable String traceId) { Optional<Trace> trace = traceRepository.findById(traceId); if (trace.isPresent()) { try { String outputPath = "waterfall-" + traceId + ".png"; visualizationService.generateTraceWaterfall(trace.get(), outputPath); return ResponseEntity.ok("Waterfall generated: " + outputPath); } catch (Exception e) { return ResponseEntity.badRequest().body("Error generating waterfall: " + e.getMessage()); } } return ResponseEntity.notFound().build(); } } WebSocket Live Updates
package com.tracing.websocket; import com.tracing.collector.TraceCollectorService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.socket.*; import org.springframework.web.socket.handler.TextWebSocketHandler; @Component public class TraceWebSocketHandler extends TextWebSocketHandler { @Autowired private TraceCollectorService traceCollectorService; @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { traceCollectorService.registerLiveSession(session); session.sendMessage(new TextMessage("{\"type\":\"CONNECTED\",\"message\":\"Live trace updates enabled\"}")); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { traceCollectorService.unregisterLiveSession(session); } @Override public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { // Handle client messages (filtering, subscription changes, etc.) String payload = message.getPayload(); // Process client requests } @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { System.err.println("WebSocket transport error: " + exception.getMessage()); } } Configuration
package com.tracing.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.*; @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new TraceWebSocketHandler(), "/ws/traces") .setAllowedOrigins("*"); } } @Configuration public class OpenTelemetryConfig { // @Bean // public OpenTelemetry openTelemetry() { // return OpenTelemetrySdk.builder() // .setTracerProvider(...) // .build(); // } } HTML Templates (Thymeleaf)
index.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Distributed Tracing Visualizer</title> <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script> <style> .container { max-width: 1400px; margin: 0 auto; padding: 20px; } .nav { background: #f8f9fa; padding: 10px; margin-bottom: 20px; } .card { border: 1px solid #ddd; padding: 15px; margin-bottom: 20px; } .trace-list { max-height: 400px; overflow-y: auto; } .service-map { height: 600px; border: 1px solid #ccc; } .live-updates { background: #f0f8ff; padding: 10px; } </style> </head> <body> <div class="container"> <div class="nav"> <h1>Distributed Tracing Visualizer</h1> <a href="/">Dashboard</a> | <a href="/traces">Traces</a> | <a href="/services">Service Map</a> </div> <div class="row"> <div class="col-md-6"> <div class="card"> <h3>Recent Traces</h3> <div class="trace-list"> <table class="table"> <thead> <tr> <th>Trace ID</th> <th>Duration</th> <th>Services</th> <th>Status</th> </tr> </thead> <tbody> <tr th:each="trace : ${recentTraces}"> <td> <a th:href="@{/traces/{id}(id=${trace.traceId})}" th:text="${trace.traceId.substring(0, 8)}..."></a> </td> <td th:text="${trace.durationMs + 'ms'}"></td> <td th:text="${trace.spans.![serviceName].distinct().size()}"></td> <td th:text="${trace.status}"></td> </tr> </tbody> </table> </div> </div> </div> <div class="col-md-6"> <div class="card"> <h3>Live Trace Updates</h3> <div class="live-updates"> <div id="liveTraceList"></div> </div> </div> </div> </div> <div class="card"> <h3>Service Dependency Map</h3> <div class="mermaid" th:utext="${mermaidServiceMap}"></div> </div> </div> <script> mermaid.initialize({ startOnLoad: true }); // WebSocket for live updates const ws = new WebSocket('ws://' + window.location.host + '/ws/traces'); ws.onmessage = function(event) { const data = JSON.parse(event.data); if (data.type === 'TRACE_UPDATE') { addLiveTrace(data); } }; function addLiveTrace(traceData) { const list = document.getElementById('liveTraceList'); const entry = document.createElement('div'); entry.innerHTML = ` <div class="trace-entry"> <strong>${traceData.traceId}</strong> - ${traceData.serviceCount} services - ${traceData.durationMs}ms - ${traceData.status} </div> `; list.insertBefore(entry, list.firstChild); // Keep only last 10 entries if (list.children.length > 10) { list.removeChild(list.lastChild); } } </script> </body> </html> Usage Example
package com.tracing; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class TraceVisualizerApplication { public static void main(String[] args) { SpringApplication.run(TraceVisualizerApplication.class, args); } } Key Features
- Multi-format Support: Jaeger, Zipkin, and OpenTelemetry trace ingestion
- Real-time Visualization: Live WebSocket updates for incoming traces
- Service Dependency Mapping: Automatic discovery and visualization of service relationships
- Interactive Waterfall Diagrams: Detailed trace timeline visualizations
- Performance Analytics: Critical path analysis and bottleneck identification
- Multiple Output Formats: Graphviz, Mermaid, and interactive web visualizations
- REST API: Programmatic access to trace data and visualizations
This distributed tracing visualizer provides comprehensive insights into microservices architectures, helping teams understand system behavior, identify performance bottlenecks, and troubleshoot issues across service boundaries.