Distributed Tracing Visualizer in Java

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

  1. Multi-format Support: Jaeger, Zipkin, and OpenTelemetry trace ingestion
  2. Real-time Visualization: Live WebSocket updates for incoming traces
  3. Service Dependency Mapping: Automatic discovery and visualization of service relationships
  4. Interactive Waterfall Diagrams: Detailed trace timeline visualizations
  5. Performance Analytics: Critical path analysis and bottleneck identification
  6. Multiple Output Formats: Graphviz, Mermaid, and interactive web visualizations
  7. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper