Jaeger UI for Trace Visualization in Java

Project Overview

A comprehensive distributed tracing visualization system inspired by Jaeger, built entirely in Java. Provides end-to-end trace visualization, dependency analysis, and performance insights for microservices architectures.

Technology Stack

  • Backend: Spring Boot 3.x, Micrometer Tracing, OpenTelemetry
  • Frontend: React/TypeScript with D3.js for visualizations
  • Storage: Cassandra/Elasticsearch for trace data
  • Streaming: Apache Kafka for trace ingestion
  • Caching: Redis for performance
  • Search: Elasticsearch for trace searching
  • API: GraphQL for flexible queries

Architecture

Microservices → OpenTelemetry → Trace Collector → Storage → Jaeger UI
↓              ↓               ↓              ↓         ↓
Spans       OTLP Export     Kafka Ingestion  Cassandra  React UI

Project Structure

jaeger-ui-java/
├── backend/
│   ├── src/main/java/com/jaeger/
│   │   ├── collector/
│   │   ├── storage/
│   │   ├── query/
│   │   ├── analysis/
│   │   └── api/
│   └── src/main/resources/
├── frontend/
│   ├── src/
│   │   ├── components/
│   │   ├── services/
│   │   ├── utils/
│   │   └── types/
│   └── public/
├── docker/
└── config/

Core Implementation

1. Data Models

package com.jaeger.core.model;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class Trace {
private final String traceId;
private final List<Span> spans;
private final Map<String, Object> metadata;
private final Instant startTime;
private final Instant endTime;
private final long durationMs;
private final String serviceName;
private final TraceStatus status;
private final int totalSpans;
private final int errorSpans;
public Trace(String traceId, List<Span> spans) {
this.traceId = traceId;
this.spans = new ArrayList<>(spans);
this.metadata = new ConcurrentHashMap<>();
this.startTime = calculateStartTime(spans);
this.endTime = calculateEndTime(spans);
this.durationMs = calculateDurationMs();
this.serviceName = extractServiceName(spans);
this.status = calculateStatus(spans);
this.totalSpans = spans.size();
this.errorSpans = countErrorSpans(spans);
// Build trace metadata
buildTraceMetadata();
}
// Getters
public String getTraceId() { return traceId; }
public List<Span> getSpans() { return Collections.unmodifiableList(spans); }
public Map<String, Object> getMetadata() { return Collections.unmodifiableMap(metadata); }
public Instant getStartTime() { return startTime; }
public Instant getEndTime() { return endTime; }
public long getDurationMs() { return durationMs; }
public String getServiceName() { return serviceName; }
public TraceStatus getStatus() { return status; }
public int getTotalSpans() { return totalSpans; }
public int getErrorSpans() { return errorSpans; }
// Utility methods
private Instant calculateStartTime(List<Span> spans) {
return spans.stream()
.map(Span::getStartTime)
.min(Instant::compareTo)
.orElse(Instant.now());
}
private Instant calculateEndTime(List<Span> spans) {
return spans.stream()
.map(span -> span.getStartTime().plus(span.getDuration()))
.max(Instant::compareTo)
.orElse(Instant.now());
}
private long calculateDurationMs() {
return java.time.Duration.between(startTime, endTime).toMillis();
}
private String extractServiceName(List<Span> spans) {
return spans.stream()
.map(Span::getServiceName)
.filter(Objects::nonNull)
.findFirst()
.orElse("unknown");
}
private TraceStatus calculateStatus(List<Span> spans) {
boolean hasErrors = spans.stream().anyMatch(Span::isError);
return hasErrors ? TraceStatus.ERROR : TraceStatus.SUCCESS;
}
private int countErrorSpans(List<Span> spans) {
return (int) spans.stream().filter(Span::isError).count();
}
private void buildTraceMetadata() {
metadata.put("services", getInvolvedServices());
metadata.put("operationCount", getOperationCount());
metadata.put("depth", calculateTraceDepth());
metadata.put("criticalPath", identifyCriticalPath());
}
public Set<String> getInvolvedServices() {
return spans.stream()
.map(Span::getServiceName)
.filter(Objects::nonNull)
.collect(HashSet::new, HashSet::add, HashSet::addAll);
}
public Map<String, Long> getOperationCount() {
return spans.stream()
.collect(Collectors.groupingBy(
Span::getOperationName, 
Collectors.counting()
));
}
public int calculateTraceDepth() {
Map<String, Integer> depthMap = new HashMap<>();
spans.forEach(span -> depthMap.put(span.getSpanId(), 0));
spans.forEach(span -> {
span.getReferences().forEach(ref -> {
if ("CHILD_OF".equals(ref.getRefType())) {
int parentDepth = depthMap.getOrDefault(ref.getSpanId(), 0);
depthMap.put(span.getSpanId(), Math.max(
depthMap.get(span.getSpanId()), 
parentDepth + 1
));
}
});
});
return depthMap.values().stream().max(Integer::compareTo).orElse(0);
}
public List<Span> identifyCriticalPath() {
// Simplified critical path identification
return spans.stream()
.sorted((s1, s2) -> Long.compare(s2.getDuration().toMillis(), s1.getDuration().toMillis()))
.limit(5)
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
}
public Optional<Span> getSpanById(String spanId) {
return spans.stream()
.filter(span -> span.getSpanId().equals(spanId))
.findFirst();
}
public List<Span> getRootSpans() {
return spans.stream()
.filter(span -> span.getReferences().isEmpty() || 
span.getReferences().stream()
.noneMatch(ref -> "CHILD_OF".equals(ref.getRefType())))
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
}
public Map<String, List<Span>> getSpansByService() {
return spans.stream()
.collect(Collectors.groupingBy(Span::getServiceName));
}
@Override
public String toString() {
return String.format("Trace[traceId=%s, spans=%d, duration=%dms, services=%s]", 
traceId, totalSpans, durationMs, getInvolvedServices());
}
}
public class Span {
private final String traceId;
private final String spanId;
private final String parentSpanId;
private final String operationName;
private final String serviceName;
private final Instant startTime;
private final java.time.Duration duration;
private final List<SpanReference> references;
private final Map<String, Object> tags;
private final List<SpanLog> logs;
private final SpanKind kind;
private final boolean error;
public Span(String traceId, String spanId, String operationName, String serviceName) {
this.traceId = traceId;
this.spanId = spanId;
this.operationName = operationName;
this.serviceName = serviceName;
this.startTime = Instant.now();
this.duration = java.time.Duration.ZERO;
this.references = new ArrayList<>();
this.tags = new ConcurrentHashMap<>();
this.logs = new ArrayList<>();
this.kind = SpanKind.INTERNAL;
this.error = false;
}
// Builder pattern
public static class Builder {
private String traceId;
private String spanId;
private String parentSpanId;
private String operationName;
private String serviceName;
private Instant startTime;
private java.time.Duration duration;
private List<SpanReference> references = new ArrayList<>();
private Map<String, Object> tags = new HashMap<>();
private List<SpanLog> logs = new ArrayList<>();
private SpanKind kind = SpanKind.INTERNAL;
private boolean error = false;
public Builder traceId(String traceId) {
this.traceId = traceId;
return this;
}
public Builder spanId(String spanId) {
this.spanId = spanId;
return this;
}
public Builder parentSpanId(String parentSpanId) {
this.parentSpanId = parentSpanId;
return this;
}
public Builder operationName(String operationName) {
this.operationName = operationName;
return this;
}
public Builder serviceName(String serviceName) {
this.serviceName = serviceName;
return this;
}
public Builder startTime(Instant startTime) {
this.startTime = startTime;
return this;
}
public Builder duration(java.time.Duration duration) {
this.duration = duration;
return this;
}
public Builder addReference(SpanReference reference) {
this.references.add(reference);
return this;
}
public Builder addTag(String key, Object value) {
this.tags.put(key, value);
return this;
}
public Builder addLog(SpanLog log) {
this.logs.add(log);
return this;
}
public Builder kind(SpanKind kind) {
this.kind = kind;
return this;
}
public Builder error(boolean error) {
this.error = error;
return this;
}
public Span build() {
Span span = new Span(traceId, spanId, operationName, serviceName);
span.parentSpanId = parentSpanId;
span.startTime = startTime != null ? startTime : Instant.now();
span.duration = duration != null ? duration : java.time.Duration.ZERO;
span.references.addAll(references);
span.tags.putAll(tags);
span.logs.addAll(logs);
span.kind = kind;
span.error = error;
return span;
}
}
// Getters
public String getTraceId() { return traceId; }
public String getSpanId() { return spanId; }
public String getParentSpanId() { return parentSpanId; }
public String getOperationName() { return operationName; }
public String getServiceName() { return serviceName; }
public Instant getStartTime() { return startTime; }
public java.time.Duration getDuration() { return duration; }
public List<SpanReference> getReferences() { return Collections.unmodifiableList(references); }
public Map<String, Object> getTags() { return Collections.unmodifiableMap(tags); }
public List<SpanLog> getLogs() { return Collections.unmodifiableList(logs); }
public SpanKind getKind() { return kind; }
public boolean isError() { return error; }
// Utility methods
public Instant getEndTime() {
return startTime.plus(duration);
}
public long getDurationMs() {
return duration.toMillis();
}
public boolean hasTag(String key) {
return tags.containsKey(key);
}
public Object getTag(String key) {
return tags.get(key);
}
public String getTagAsString(String key) {
Object value = tags.get(key);
return value != null ? value.toString() : null;
}
public boolean isRoot() {
return references.isEmpty() || 
references.stream().noneMatch(ref -> "CHILD_OF".equals(ref.getRefType()));
}
public void addReference(SpanReference reference) {
references.add(reference);
}
public void addTag(String key, Object value) {
tags.put(key, value);
}
public void addLog(SpanLog log) {
logs.add(log);
}
@Override
public String toString() {
return String.format("Span[spanId=%s, operation=%s, service=%s, duration=%dms]", 
spanId, operationName, serviceName, getDurationMs());
}
}
public class SpanReference {
private final String traceId;
private final String spanId;
private final String refType; // CHILD_OF, FOLLOWS_FROM
public SpanReference(String traceId, String spanId, String refType) {
this.traceId = traceId;
this.spanId = spanId;
this.refType = refType;
}
// Getters
public String getTraceId() { return traceId; }
public String getSpanId() { return spanId; }
public String getRefType() { return refType; }
@Override
public String toString() {
return String.format("SpanReference[refType=%s, spanId=%s]", refType, spanId);
}
}
public class SpanLog {
private final Instant timestamp;
private final Map<String, Object> fields;
public SpanLog(Instant timestamp) {
this.timestamp = timestamp;
this.fields = new HashMap<>();
}
// Getters
public Instant getTimestamp() { return timestamp; }
public Map<String, Object> getFields() { return Collections.unmodifiableMap(fields); }
public void addField(String key, Object value) {
fields.put(key, value);
}
public Object getField(String key) {
return fields.get(key);
}
@Override
public String toString() {
return String.format("SpanLog[timestamp=%s, fields=%s]", timestamp, fields);
}
}
public enum SpanKind {
INTERNAL,
SERVER,
CLIENT,
PRODUCER,
CONSUMER
}
public enum TraceStatus {
SUCCESS,
ERROR,
WARNING,
UNKNOWN
}
public class TraceSearchRequest {
private final String serviceName;
private final String operationName;
private final String traceId;
private final Instant startTimeMin;
private final Instant startTimeMax;
private final Long durationMin;
private final Long durationMax;
private final Map<String, String> tags;
private final int limit;
private final int offset;
public static class Builder {
private String serviceName;
private String operationName;
private String traceId;
private Instant startTimeMin;
private Instant startTimeMax;
private Long durationMin;
private Long durationMax;
private Map<String, String> tags = new HashMap<>();
private int limit = 100;
private int offset = 0;
public Builder serviceName(String serviceName) {
this.serviceName = serviceName;
return this;
}
public Builder operationName(String operationName) {
this.operationName = operationName;
return this;
}
public Builder traceId(String traceId) {
this.traceId = traceId;
return this;
}
public Builder startTimeMin(Instant startTimeMin) {
this.startTimeMin = startTimeMin;
return this;
}
public Builder startTimeMax(Instant startTimeMax) {
this.startTimeMax = startTimeMax;
return this;
}
public Builder durationMin(Long durationMin) {
this.durationMin = durationMin;
return this;
}
public Builder durationMax(Long durationMax) {
this.durationMax = durationMax;
return this;
}
public Builder addTag(String key, String value) {
this.tags.put(key, value);
return this;
}
public Builder limit(int limit) {
this.limit = limit;
return this;
}
public Builder offset(int offset) {
this.offset = offset;
return this;
}
public TraceSearchRequest build() {
return new TraceSearchRequest(this);
}
}
private TraceSearchRequest(Builder builder) {
this.serviceName = builder.serviceName;
this.operationName = builder.operationName;
this.traceId = builder.traceId;
this.startTimeMin = builder.startTimeMin;
this.startTimeMax = builder.startTimeMax;
this.durationMin = builder.durationMin;
this.durationMax = builder.durationMax;
this.tags = Collections.unmodifiableMap(new HashMap<>(builder.tags));
this.limit = builder.limit;
this.offset = builder.offset;
}
// Getters
public String getServiceName() { return serviceName; }
public String getOperationName() { return operationName; }
public String getTraceId() { return traceId; }
public Instant getStartTimeMin() { return startTimeMin; }
public Instant getStartTimeMax() { return startTimeMax; }
public Long getDurationMin() { return durationMin; }
public Long getDurationMax() { return durationMax; }
public Map<String, String> getTags() { return tags; }
public int getLimit() { return limit; }
public int getOffset() { return offset; }
public boolean hasTimeRange() {
return startTimeMin != null && startTimeMax != null;
}
public boolean hasDurationRange() {
return durationMin != null && durationMax != null;
}
@Override
public String toString() {
return String.format("TraceSearchRequest[service=%s, operation=%s, timeRange=%s]", 
serviceName, operationName, hasTimeRange() ? "set" : "not set");
}
}

2. Trace Storage Service

package com.jaeger.storage;
import com.jaeger.core.model.*;
import org.springframework.data.cassandra.core.CassandraOperations;
import org.springframework.data.cassandra.core.cql.CqlTemplate;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@Service
public class TraceStorageService {
private final CassandraOperations cassandra;
private final ElasticsearchOperations elasticsearch;
private final RedisTemplate<String, Object> redis;
private final CqlTemplate cqlTemplate;
private static final String TRACE_INDEX = "traces";
private static final String SPAN_INDEX = "spans";
private static final String SERVICE_INDEX = "services";
public TraceStorageService(CassandraOperations cassandra, 
ElasticsearchOperations elasticsearch,
RedisTemplate<String, Object> redis) {
this.cassandra = cassandra;
this.elasticsearch = elasticsearch;
this.redis = redis;
this.cqlTemplate = cassandra.getCqlTemplate();
}
public CompletableFuture<Void> storeTrace(Trace trace) {
return CompletableFuture.runAsync(() -> {
try {
// Store in Cassandra for primary storage
storeTraceInCassandra(trace);
// Index in Elasticsearch for searching
indexTraceInElasticsearch(trace);
// Cache in Redis for fast access
cacheTraceInRedis(trace);
// Update service dependencies
updateServiceDependencies(trace);
} catch (Exception e) {
throw new StorageException("Failed to store trace: " + trace.getTraceId(), e);
}
});
}
public CompletableFuture<Void> storeSpans(List<Span> spans) {
return CompletableFuture.runAsync(() -> {
try {
// Group spans by trace ID
Map<String, List<Span>> spansByTrace = spans.stream()
.collect(Collectors.groupingBy(Span::getTraceId));
// Store each trace
spansByTrace.forEach((traceId, traceSpans) -> {
Trace trace = new Trace(traceId, traceSpans);
storeTrace(trace);
});
} catch (Exception e) {
throw new StorageException("Failed to store spans", e);
}
});
}
public CompletableFuture<Optional<Trace>> getTrace(String traceId) {
return CompletableFuture.supplyAsync(() -> {
try {
// Try Redis cache first
Trace cached = getTraceFromRedis(traceId);
if (cached != null) {
return Optional.of(cached);
}
// Fall back to Cassandra
Trace trace = getTraceFromCassandra(traceId);
if (trace != null) {
// Cache for future requests
cacheTraceInRedis(trace);
return Optional.of(trace);
}
return Optional.empty();
} catch (Exception e) {
throw new StorageException("Failed to get trace: " + traceId, e);
}
});
}
public CompletableFuture<List<Trace>> searchTraces(TraceSearchRequest request) {
return CompletableFuture.supplyAsync(() -> {
try {
// Use Elasticsearch for complex searches
return searchTracesInElasticsearch(request);
} catch (Exception e) {
throw new StorageException("Failed to search traces", e);
}
});
}
public CompletableFuture<List<String>> getServices() {
return CompletableFuture.supplyAsync(() -> {
try {
String cql = "SELECT DISTINCT service_name FROM spans";
return cqlTemplate.queryForList(cql, String.class);
} catch (Exception e) {
throw new StorageException("Failed to get services", e);
}
});
}
public CompletableFuture<List<String>> getOperations(String serviceName) {
return CompletableFuture.supplyAsync(() -> {
try {
String cql = "SELECT DISTINCT operation_name FROM spans WHERE service_name = ?";
return cqlTemplate.queryForList(cql, String.class, serviceName);
} catch (Exception e) {
throw new StorageException("Failed to get operations for service: " + serviceName, e);
}
});
}
public CompletableFuture<Map<String, Object>> getTraceStatistics(Instant from, Instant to) {
return CompletableFuture.supplyAsync(() -> {
try {
Map<String, Object> stats = new HashMap<>();
// Total traces
String totalCql = "SELECT COUNT(DISTINCT trace_id) FROM traces WHERE start_time >= ? AND start_time <= ?";
Long totalTraces = cqlTemplate.queryForObject(totalCql, Long.class, from, to);
stats.put("totalTraces", totalTraces != null ? totalTraces : 0);
// Error traces
String errorCql = "SELECT COUNT(DISTINCT trace_id) FROM traces WHERE start_time >= ? AND start_time <= ? AND status = 'ERROR'";
Long errorTraces = cqlTemplate.queryForObject(errorCql, Long.class, from, to);
stats.put("errorTraces", errorTraces != null ? errorTraces : 0);
// Average duration
String avgDurationCql = "SELECT AVG(duration_ms) FROM traces WHERE start_time >= ? AND start_time <= ?";
Double avgDuration = cqlTemplate.queryForObject(avgDurationCql, Double.class, from, to);
stats.put("averageDuration", avgDuration != null ? avgDuration : 0.0);
// Service distribution
String serviceDistCql = "SELECT service_name, COUNT(*) FROM traces WHERE start_time >= ? AND start_time <= ? GROUP BY service_name";
List<Map<String, Object>> serviceDist = cqlTemplate.queryForList(serviceDistCql, from, to);
stats.put("serviceDistribution", serviceDist);
return stats;
} catch (Exception e) {
throw new StorageException("Failed to get trace statistics", e);
}
});
}
private void storeTraceInCassandra(Trace trace) {
// Store trace metadata
String traceCql = "INSERT INTO traces (trace_id, start_time, end_time, duration_ms, service_name, status, total_spans, error_spans, metadata) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
cqlTemplate.execute(traceCql, 
trace.getTraceId(),
trace.getStartTime(),
trace.getEndTime(),
trace.getDurationMs(),
trace.getServiceName(),
trace.getStatus().name(),
trace.getTotalSpans(),
trace.getErrorSpans(),
convertMetadataToJson(trace.getMetadata())
);
// Store individual spans
for (Span span : trace.getSpans()) {
storeSpanInCassandra(span);
}
}
private void storeSpanInCassandra(Span span) {
String spanCql = "INSERT INTO spans (trace_id, span_id, parent_span_id, operation_name, service_name, " +
"start_time, duration_ms, kind, error, tags, logs) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
cqlTemplate.execute(spanCql,
span.getTraceId(),
span.getSpanId(),
span.getParentSpanId(),
span.getOperationName(),
span.getServiceName(),
span.getStartTime(),
span.getDurationMs(),
span.getKind().name(),
span.isError(),
convertTagsToJson(span.getTags()),
convertLogsToJson(span.getLogs())
);
}
private Trace getTraceFromCassandra(String traceId) {
// Get trace metadata
String traceCql = "SELECT * FROM traces WHERE trace_id = ?";
Map<String, Object> traceData = cqlTemplate.queryForMap(traceCql, traceId);
if (traceData == null) return null;
// Get spans for this trace
String spansCql = "SELECT * FROM spans WHERE trace_id = ?";
List<Map<String, Object>> spansData = cqlTemplate.queryForList(spansCql, traceId);
// Convert to Span objects
List<Span> spans = spansData.stream()
.map(this::convertToSpan)
.collect(Collectors.toList());
return new Trace(traceId, spans);
}
private void indexTraceInElasticsearch(Trace trace) {
Map<String, Object> traceDoc = new HashMap<>();
traceDoc.put("traceId", trace.getTraceId());
traceDoc.put("startTime", trace.getStartTime());
traceDoc.put("durationMs", trace.getDurationMs());
traceDoc.put("serviceName", trace.getServiceName());
traceDoc.put("status", trace.getStatus().name());
traceDoc.put("totalSpans", trace.getTotalSpans());
traceDoc.put("errorSpans", trace.getErrorSpans());
traceDoc.put("services", trace.getInvolvedServices());
traceDoc.put("metadata", trace.getMetadata());
elasticsearch.save(traceDoc, TRACE_INDEX);
}
private List<Trace> searchTracesInElasticsearch(TraceSearchRequest request) {
// Build Elasticsearch query based on request parameters
// This is a simplified implementation
Map<String, Object> query = new HashMap<>();
Map<String, Object> boolQuery = new HashMap<>();
List<Map<String, Object>> mustClauses = new ArrayList<>();
if (request.getServiceName() != null) {
mustClauses.add(Map.of("match", Map.of("serviceName", request.getServiceName())));
}
if (request.getStartTimeMin() != null && request.getStartTimeMax() != null) {
Map<String, Object> rangeQuery = Map.of(
"startTime", Map.of(
"gte", request.getStartTimeMin(),
"lte", request.getStartTimeMax()
)
);
mustClauses.add(Map.of("range", rangeQuery));
}
if (request.getDurationMin() != null && request.getDurationMax() != null) {
Map<String, Object> rangeQuery = Map.of(
"durationMs", Map.of(
"gte", request.getDurationMin(),
"lte", request.getDurationMax()
)
);
mustClauses.add(Map.of("range", rangeQuery));
}
boolQuery.put("must", mustClauses);
query.put("query", Map.of("bool", boolQuery));
// Execute search (simplified)
// In real implementation, use Elasticsearch's Java client
return Collections.emptyList();
}
private void cacheTraceInRedis(Trace trace) {
String key = "trace:" + trace.getTraceId();
redis.opsForValue().set(key, trace, java.time.Duration.ofMinutes(30));
}
private Trace getTraceFromRedis(String traceId) {
String key = "trace:" + traceId;
return (Trace) redis.opsForValue().get(key);
}
private void updateServiceDependencies(Trace trace) {
// Analyze and update service dependency graph
Set<String> services = trace.getInvolvedServices();
if (services.size() > 1) {
// Update dependency relationships in Redis
String key = "dependencies:" + trace.getStartTime().toEpochMilli() / 60000; // Minute granularity
services.forEach(service -> {
redis.opsForSet().add(key, service);
});
}
}
private Span convertToSpan(Map<String, Object> spanData) {
return new Span.Builder()
.traceId((String) spanData.get("trace_id"))
.spanId((String) spanData.get("span_id"))
.parentSpanId((String) spanData.get("parent_span_id"))
.operationName((String) spanData.get("operation_name"))
.serviceName((String) spanData.get("service_name"))
.startTime((Instant) spanData.get("start_time"))
.duration(java.time.Duration.ofMillis((Long) spanData.get("duration_ms")))
.kind(SpanKind.valueOf((String) spanData.get("kind")))
.error((Boolean) spanData.get("error"))
.build();
}
private String convertMetadataToJson(Map<String, Object> metadata) {
// Convert metadata map to JSON string
try {
return new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(metadata);
} catch (Exception e) {
return "{}";
}
}
private String convertTagsToJson(Map<String, Object> tags) {
// Convert tags map to JSON string
try {
return new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(tags);
} catch (Exception e) {
return "{}";
}
}
private String convertLogsToJson(List<SpanLog> logs) {
// Convert logs list to JSON string
try {
return new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(logs);
} catch (Exception e) {
return "[]";
}
}
}
class StorageException extends RuntimeException {
public StorageException(String message) {
super(message);
}
public StorageException(String message, Throwable cause) {
super(message, cause);
}
}

3. Trace Collector Service

package com.jaeger.collector;
import com.jaeger.core.model.*;
import com.jaeger.storage.TraceStorageService;
import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest;
import io.opentelemetry.proto.trace.v1.ResourceSpans;
import io.opentelemetry.proto.trace.v1.ScopeSpans;
import io.opentelemetry.proto.trace.v1.Span;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Sinks;
import reactor.core.scheduler.Schedulers;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.CompletableFuture;
@Service
public class TraceCollectorService {
private final TraceStorageService storageService;
private final Sinks.Many<Span> spanSink;
private final Flux<Span> spanStream;
private final Map<String, TraceBuilder> traceBuilders;
public TraceCollectorService(TraceStorageService storageService) {
this.storageService = storageService;
this.spanSink = Sinks.many().multicast().onBackpressureBuffer();
this.spanStream = spanSink.asFlux();
this.traceBuilders = new HashMap<>();
setupTraceProcessing();
}
@KafkaListener(topics = "${kafka.topics.traces:jaeger-spans}")
public void consumeTrace(ConsumerRecord<String, byte[]> record) {
try {
ExportTraceServiceRequest request = ExportTraceServiceRequest.parseFrom(record.value());
processOTLPRequest(request);
} catch (Exception e) {
System.err.println("Failed to process trace message: " + e.getMessage());
}
}
public void processOTLPRequest(ExportTraceServiceRequest request) {
for (ResourceSpans resourceSpans : request.getResourceSpansList()) {
String serviceName = extractServiceName(resourceSpans.getResource());
for (ScopeSpans scopeSpans : resourceSpans.getScopeSpansList()) {
for (Span otlpSpan : scopeSpans.getSpansList()) {
com.jaeger.core.model.Span span = convertOTLPSpan(otlpSpan, serviceName);
spanSink.tryEmitNext(span);
}
}
}
}
public void processSpan(com.jaeger.core.model.Span span) {
spanSink.tryEmitNext(span);
}
private void setupTraceProcessing() {
spanStream
.bufferTimeout(100, java.time.Duration.ofSeconds(1)) // Batch processing
.parallel()
.runOn(Schedulers.parallel())
.subscribe(batch -> {
try {
// Group spans by trace ID and build traces
Map<String, List<com.jaeger.core.model.Span>> spansByTrace = new HashMap<>();
for (com.jaeger.core.model.Span span : batch) {
spansByTrace.computeIfAbsent(span.getTraceId(), k -> new ArrayList<>()).add(span);
}
// Process each trace
for (Map.Entry<String, List<com.jaeger.core.model.Span>> entry : spansByTrace.entrySet()) {
String traceId = entry.getKey();
List<com.jaeger.core.model.Span> spans = entry.getValue();
// Get or create trace builder
TraceBuilder traceBuilder = traceBuilders.computeIfAbsent(traceId, 
k -> new TraceBuilder(traceId));
// Add spans to trace builder
traceBuilder.addSpans(spans);
// If trace is complete, store it
if (traceBuilder.isComplete()) {
Trace trace = traceBuilder.build();
storageService.storeTrace(trace).join(); // Wait for completion
traceBuilders.remove(traceId);
}
}
} catch (Exception e) {
System.err.println("Error processing span batch: " + e.getMessage());
}
});
}
private com.jaeger.core.model.Span convertOTLPSpan(Span otlpSpan, String serviceName) {
com.jaeger.core.model.Span.Builder builder = new com.jaeger.core.model.Span.Builder()
.traceId(bytesToHex(otlpSpan.getTraceId().toByteArray()))
.spanId(bytesToHex(otlpSpan.getSpanId().toByteArray()))
.operationName(otlpSpan.getName())
.serviceName(serviceName)
.startTime(Instant.ofEpochSecond(otlpSpan.getStartTimeUnixNano() / 1_000_000_000L, 
otlpSpan.getStartTimeUnixNano() % 1_000_000_000L))
.duration(java.time.Duration.ofNanos(otlpSpan.getEndTimeUnixNano() - otlpSpan.getStartTimeUnixNano()))
.kind(convertSpanKind(otlpSpan.getKind()));
// Set parent span ID if exists
if (!otlpSpan.getParentSpanId().isEmpty()) {
builder.parentSpanId(bytesToHex(otlpSpan.getParentSpanId().toByteArray()));
}
// Add tags
otlpSpan.getAttributesList().forEach(attribute -> {
builder.addTag(attribute.getKey(), convertAttributeValue(attribute.getValue()));
});
// Set error status
if (otlpSpan.getStatus().getCode() == Span.Status.StatusCode.ERROR) {
builder.error(true);
}
return builder.build();
}
private String extractServiceName(io.opentelemetry.proto.resource.v1.Resource resource) {
return resource.getAttributesList().stream()
.filter(attr -> "service.name".equals(attr.getKey()))
.findFirst()
.map(attr -> attr.getValue().getStringValue())
.orElse("unknown-service");
}
private SpanKind convertSpanKind(Span.SpanKind kind) {
switch (kind) {
case SPAN_KIND_SERVER: return SpanKind.SERVER;
case SPAN_KIND_CLIENT: return SpanKind.CLIENT;
case SPAN_KIND_PRODUCER: return SpanKind.PRODUCER;
case SPAN_KIND_CONSUMER: return SpanKind.CONSUMER;
default: return SpanKind.INTERNAL;
}
}
private Object convertAttributeValue(io.opentelemetry.proto.common.v1.AnyValue value) {
if (value.hasStringValue()) return value.getStringValue();
if (value.hasBoolValue()) return value.getBoolValue();
if (value.hasIntValue()) return value.getIntValue();
if (value.hasDoubleValue()) return value.getDoubleValue();
return value.toString();
}
private String bytesToHex(byte[] bytes) {
StringBuilder hex = new StringBuilder();
for (byte b : bytes) {
hex.append(String.format("%02x", b));
}
return hex.toString();
}
}
class TraceBuilder {
private final String traceId;
private final List<com.jaeger.core.model.Span> spans;
private Instant lastSpanTime;
private static final java.time.Duration TRACE_TIMEOUT = java.time.Duration.ofMinutes(5);
public TraceBuilder(String traceId) {
this.traceId = traceId;
this.spans = new ArrayList<>();
this.lastSpanTime = Instant.now();
}
public void addSpan(com.jaeger.core.model.Span span) {
spans.add(span);
lastSpanTime = Instant.now();
}
public void addSpans(List<com.jaeger.core.model.Span> newSpans) {
spans.addAll(newSpans);
lastSpanTime = Instant.now();
}
public boolean isComplete() {
// A trace is considered complete if:
// 1. We have all root spans and their children
// 2. Or timeout has been reached
return isTraceStructurallyComplete() || isTimedOut();
}
private boolean isTraceStructurallyComplete() {
if (spans.isEmpty()) return false;
// Find all span IDs
Set<String> spanIds = spans.stream()
.map(com.jaeger.core.model.Span::getSpanId)
.collect(HashSet::new, HashSet::add, HashSet::addAll);
// Check if all referenced spans are present
for (com.jaeger.core.model.Span span : spans) {
for (SpanReference ref : span.getReferences()) {
if ("CHILD_OF".equals(ref.getRefType()) && !spanIds.contains(ref.getSpanId())) {
return false; // Missing parent span
}
}
}
return true;
}
private boolean isTimedOut() {
return Instant.now().isAfter(lastSpanTime.plus(TRACE_TIMEOUT));
}
public Trace build() {
return new Trace(traceId, spans);
}
public String getTraceId() {
return traceId;
}
public int getSpanCount() {
return spans.size();
}
}

4. REST API Controller

package com.jaeger.api;
import com.jaeger.core.model.*;
import com.jaeger.storage.TraceStorageService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "*")
public class TraceController {
private final TraceStorageService storageService;
public TraceController(TraceStorageService storageService) {
this.storageService = storageService;
}
@GetMapping("/traces/{traceId}")
public Mono<ResponseEntity<Trace>> getTrace(@PathVariable String traceId) {
return Mono.fromFuture(storageService.getTrace(traceId))
.map(trace -> trace.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build()));
}
@GetMapping("/traces")
public Mono<ResponseEntity<SearchResponse>> searchTraces(
@RequestParam(required = false) String service,
@RequestParam(required = false) String operation,
@RequestParam(required = false) String tags,
@RequestParam(defaultValue = "1h") String lookback,
@RequestParam(defaultValue = "100") int limit,
@RequestParam(defaultValue = "0") int offset) {
TraceSearchRequest request = buildSearchRequest(service, operation, tags, lookback, limit, offset);
return Mono.fromFuture(storageService.searchTraces(request))
.map(traces -> ResponseEntity.ok(new SearchResponse(traces, traces.size())));
}
@GetMapping("/services")
public Mono<ResponseEntity<List<String>>> getServices() {
return Mono.fromFuture(storageService.getServices())
.map(ResponseEntity::ok);
}
@GetMapping("/services/{serviceName}/operations")
public Mono<ResponseEntity<List<String>>> getOperations(@PathVariable String serviceName) {
return Mono.fromFuture(storageService.getOperations(serviceName))
.map(ResponseEntity::ok);
}
@GetMapping("/statistics")
public Mono<ResponseEntity<Map<String, Object>>> getStatistics(
@RequestParam(defaultValue = "1h") String lookback) {
Instant end = Instant.now();
Instant start = calculateStartTime(lookback, end);
return Mono.fromFuture(storageService.getTraceStatistics(start, end))
.map(ResponseEntity::ok);
}
@GetMapping("/dependencies")
public Mono<ResponseEntity<DependencyResponse>> getDependencies(
@RequestParam(defaultValue = "1h") String lookback) {
// Build service dependency graph
DependencyResponse response = buildDependencyGraph(lookback);
return Mono.just(ResponseEntity.ok(response));
}
private TraceSearchRequest buildSearchRequest(String service, String operation, String tags, 
String lookback, int limit, int offset) {
Instant end = Instant.now();
Instant start = calculateStartTime(lookback, end);
TraceSearchRequest.Builder builder = new TraceSearchRequest.Builder()
.startTimeMin(start)
.startTimeMax(end)
.limit(limit)
.offset(offset);
if (service != null) {
builder.serviceName(service);
}
if (operation != null) {
builder.operationName(operation);
}
if (tags != null) {
// Parse tags string (format: "key1:value1,key2:value2")
String[] tagPairs = tags.split(",");
for (String tagPair : tagPairs) {
String[] parts = tagPair.split(":");
if (parts.length == 2) {
builder.addTag(parts[0], parts[1]);
}
}
}
return builder.build();
}
private Instant calculateStartTime(String lookback, Instant end) {
if (lookback.endsWith("h")) {
int hours = Integer.parseInt(lookback.substring(0, lookback.length() - 1));
return end.minus(java.time.Duration.ofHours(hours));
} else if (lookback.endsWith("m")) {
int minutes = Integer.parseInt(lookback.substring(0, lookback.length() - 1));
return end.minus(java.time.Duration.ofMinutes(minutes));
} else if (lookback.endsWith("d")) {
int days = Integer.parseInt(lookback.substring(0, lookback.length() - 1));
return end.minus(java.time.Duration.ofDays(days));
}
// Default to 1 hour
return end.minus(java.time.Duration.ofHours(1));
}
private DependencyResponse buildDependencyGraph(String lookback) {
// Simplified dependency graph building
// In real implementation, this would analyze trace data to build service dependencies
DependencyResponse response = new DependencyResponse();
// Add sample dependencies
response.addDependency("frontend", "backend", 100, 95.5);
response.addDependency("backend", "database", 150, 99.0);
response.addDependency("backend", "cache", 80, 98.5);
return response;
}
// Response classes
public static class SearchResponse {
private final List<Trace> traces;
private final int total;
public SearchResponse(List<Trace> traces, int total) {
this.traces = traces;
this.total = total;
}
public List<Trace> getTraces() { return traces; }
public int getTotal() { return total; }
}
public static class DependencyResponse {
private final List<ServiceDependency> dependencies;
private final List<ServiceNode> nodes;
public DependencyResponse() {
this.dependencies = new ArrayList<>();
this.nodes = new ArrayList<>();
}
public void addDependency(String source, String target, int callCount, double successRate) {
dependencies.add(new ServiceDependency(source, target, callCount, successRate));
// Add nodes if not already present
if (nodes.stream().noneMatch(node -> node.getName().equals(source))) {
nodes.add(new ServiceNode(source));
}
if (nodes.stream().noneMatch(node -> node.getName().equals(target))) {
nodes.add(new ServiceNode(target));
}
}
public List<ServiceDependency> getDependencies() { return dependencies; }
public List<ServiceNode> getNodes() { return nodes; }
}
public static class ServiceDependency {
private final String source;
private final String target;
private final int callCount;
private final double successRate;
public ServiceDependency(String source, String target, int callCount, double successRate) {
this.source = source;
this.target = target;
this.callCount = callCount;
this.successRate = successRate;
}
public String getSource() { return source; }
public String getTarget() { return target; }
public int getCallCount() { return callCount; }
public double getSuccessRate() { return successRate; }
}
public static class ServiceNode {
private final String name;
private final String type = "service";
public ServiceNode(String name) {
this.name = name;
}
public String getName() { return name; }
public String getType() { return type; }
}
}

5. Frontend React Components

// TraceList.tsx
import React, { useState, useEffect } from 'react';
import { Trace, TraceSearchParams } from './types';
interface TraceListProps {
traces: Trace[];
onTraceSelect: (trace: Trace) => void;
onSearch: (params: TraceSearchParams) => void;
loading: boolean;
}
export const TraceList: React.FC<TraceListProps> = ({
traces,
onTraceSelect,
onSearch,
loading
}) => {
const [searchParams, setSearchParams] = useState<TraceSearchParams>({
service: '',
operation: '',
lookback: '1h',
limit: 100
});
const handleSearch = () => {
onSearch(searchParams);
};
const formatDuration = (durationMs: number): string => {
if (durationMs < 1000) return `${durationMs}ms`;
return `${(durationMs / 1000).toFixed(2)}s`;
};
const getStatusColor = (status: string): string => {
switch (status) {
case 'SUCCESS': return 'bg-green-500';
case 'ERROR': return 'bg-red-500';
case 'WARNING': return 'bg-yellow-500';
default: return 'bg-gray-500';
}
};
return (
<div className="trace-list">
{/* Search Panel */}
<div className="search-panel bg-white p-4 shadow-md rounded-lg mb-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700">Service</label>
<input
type="text"
value={searchParams.service}
onChange={(e) => setSearchParams({...searchParams, service: e.target.value})}
className="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2"
placeholder="Service name"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Operation</label>
<input
type="text"
value={searchParams.operation}
onChange={(e) => setSearchParams({...searchParams, operation: e.target.value})}
className="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2"
placeholder="Operation name"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Lookback</label>
<select
value={searchParams.lookback}
onChange={(e) => setSearchParams({...searchParams, lookback: e.target.value})}
className="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2"
>
<option value="15m">15 minutes</option>
<option value="1h">1 hour</option>
<option value="2h">2 hours</option>
<option value="6h">6 hours</option>
<option value="1d">1 day</option>
</select>
</div>
<div className="flex items-end">
<button
onClick={handleSearch}
disabled={loading}
className="w-full bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:bg-blue-300"
>
{loading ? 'Searching...' : 'Search Traces'}
</button>
</div>
</div>
</div>
{/* Traces List */}
<div className="traces-container bg-white shadow-md rounded-lg">
{loading ? (
<div className="p-8 text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-2 text-gray-600">Loading traces...</p>
</div>
) : traces.length === 0 ? (
<div className="p-8 text-center text-gray-500">
No traces found. Try adjusting your search criteria.
</div>
) : (
<div className="overflow-hidden">
{traces.map((trace) => (
<div
key={trace.traceId}
onClick={() => onTraceSelect(trace)}
className="trace-item border-b border-gray-200 hover:bg-gray-50 cursor-pointer p-4"
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className={`w-3 h-3 rounded-full ${getStatusColor(trace.status)}`}></div>
<div className="text-sm font-mono text-gray-600">
{trace.traceId.substring(0, 16)}...
</div>
<div className="text-lg font-medium">{trace.serviceName}</div>
</div>
<div className="flex items-center space-x-6 text-sm text-gray-500">
<div>
<span className="font-medium">{trace.totalSpans}</span> spans
</div>
<div>
<span className="font-medium">{formatDuration(trace.durationMs)}</span>
</div>
<div>
{new Date(trace.startTime).toLocaleString()}
</div>
</div>
</div>
{trace.errorSpans > 0 && (
<div className="mt-2 text-sm text-red-600">
{trace.errorSpans} error spans
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
);
};
// TraceDetail.tsx
import React, { useState, useEffect } from 'react';
import { Trace, Span } from './types';
import { TraceGraph } from './TraceGraph';
import { SpanTable } from './SpanTable';
interface TraceDetailProps {
trace: Trace;
}
export const TraceDetail: React.FC<TraceDetailProps> = ({ trace }) => {
const [selectedSpan, setSelectedSpan] = useState<Span | null>(null);
const [activeTab, setActiveTab] = useState<'graph' | 'table' | 'json'>('graph');
const formatDuration = (durationMs: number): string => {
if (durationMs < 1000) return `${durationMs}ms`;
return `${(durationMs / 1000).toFixed(2)}s`;
};
const getStatusColor = (status: string): string => {
switch (status) {
case 'SUCCESS': return 'text-green-600';
case 'ERROR': return 'text-red-600';
case 'WARNING': return 'text-yellow-600';
default: return 'text-gray-600';
}
};
return (
<div className="trace-detail bg-white rounded-lg shadow-lg">
{/* Trace Header */}
<div className="trace-header border-b border-gray-200 p-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Trace Details</h1>
<div className="mt-2 flex items-center space-x-4 text-sm text-gray-600">
<div className="font-mono">{trace.traceId}</div>
<div className={`font-medium ${getStatusColor(trace.status)}`}>
{trace.status}
</div>
</div>
</div>
<div className="text-right">
<div className="text-lg font-semibold">
{formatDuration(trace.durationMs)}
</div>
<div className="text-sm text-gray-500">
{trace.totalSpans} spans • {trace.errorSpans} errors
</div>
</div>
</div>
<div className="mt-4 grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<div className="text-gray-500">Start Time</div>
<div>{new Date(trace.startTime).toLocaleString()}</div>
</div>
<div>
<div className="text-gray-500">Services</div>
<div>{Array.from(trace.involvedServices).join(', ')}</div>
</div>
<div>
<div className="text-gray-500">Root Service</div>
<div>{trace.serviceName}</div>
</div>
<div>
<div className="text-gray-500">Trace Depth</div>
<div>{trace.metadata.depth || 0} levels</div>
</div>
</div>
</div>
{/* Navigation Tabs */}
<div className="border-b border-gray-200">
<nav className="flex space-x-8 px-6">
{[
{ id: 'graph', name: 'Trace Graph' },
{ id: 'table', name: 'Span Table' },
{ id: 'json', name: 'JSON View' }
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === tab.id
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
{tab.name}
</button>
))}
</nav>
</div>
{/* Tab Content */}
<div className="p-6">
{activeTab === 'graph' && (
<TraceGraph
trace={trace}
selectedSpan={selectedSpan}
onSpanSelect={setSelectedSpan}
/>
)}
{activeTab === 'table' && (
<SpanTable
spans={trace.spans}
selectedSpan={selectedSpan}
onSpanSelect={setSelectedSpan}
/>
)}
{activeTab === 'json' && (
<div className="bg-gray-100 rounded-lg p-4">
<pre className="text-sm overflow-auto max-h-96">
{JSON.stringify(trace, null, 2)}
</pre>
</div>
)}
</div>
{/* Span Detail Panel */}
{selectedSpan && (
<div className="border-t border-gray-200 p-6">
<h3 className="text-lg font-medium mb-4">Span Details</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 className="font-medium text-gray-700 mb-2">Basic Info</h4>
<dl className="space-y-2">
<div className="flex">
<dt className="w-32 text-gray-500">Operation:</dt>
<dd>{selectedSpan.operationName}</dd>
</div>
<div className="flex">
<dt className="w-32 text-gray-500">Service:</dt>
<dd>{selectedSpan.serviceName}</dd>
</div>
<div className="flex">
<dt className="w-32 text-gray-500">Duration:</dt>
<dd>{formatDuration(selectedSpan.durationMs)}</dd>
</div>
<div className="flex">
<dt className="w-32 text-gray-500">Span ID:</dt>
<dd className="font-mono">{selectedSpan.spanId}</dd>
</div>
</dl>
</div>
<div>
<h4 className="font-medium text-gray-700 mb-2">Tags</h4>
<div className="space-y-1">
{Object.entries(selectedSpan.tags).map(([key, value]) => (
<div key={key} className="flex">
<span className="text-gray-500 w-32 truncate">{key}:</span>
<span className="flex-1 truncate">{String(value)}</span>
</div>
))}
</div>
</div>
</div>
</div>
)}
</div>
);
};
// TraceGraph.tsx
import React, { useEffect, useRef } from 'react';
import { Trace, Span } from './types';
import * as d3 from 'd3';
interface TraceGraphProps {
trace: Trace;
selectedSpan: Span | null;
onSpanSelect: (span: Span) => void;
}
export const TraceGraph: React.FC<TraceGraphProps> = ({
trace,
selectedSpan,
onSpanSelect
}) => {
const svgRef = useRef<SVGSVGElement>(null);
useEffect(() => {
if (!svgRef.current || !trace) return;
const svg = d3.select(svgRef.current);
svg.selectAll('*').remove();
const width = svgRef.current.clientWidth;
const height = 400;
const margin = { top: 20, right: 20, bottom: 30, left: 200 };
// Set up scales
const xScale = d3.scaleLinear()
.domain([0, trace.durationMs])
.range([margin.left, width - margin.right]);
const yScale = d3.scaleBand()
.domain(trace.spans.map(span => span.spanId))
.range([margin.top, height - margin.bottom])
.padding(0.1);
// Create timeline
svg.append('g')
.attr('transform', `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(xScale)
.tickFormat(d => `${d}ms`))
.select('.domain')
.remove();
// Draw spans
const spans = svg.selectAll('.span')
.data(trace.spans)
.enter()
.append('g')
.attr('class', 'span')
.attr('transform', d => `translate(0,${yScale(d.spanId)})`);
// Draw span rectangles
spans.append('rect')
.attr('x', d => xScale(0))
.attr('y', 0)
.attr('width', d => xScale(d.durationMs) - xScale(0))
.attr('height', yScale.bandwidth())
.attr('fill', d => d.error ? '#ef4444' : '#3b82f6')
.attr('rx', 2)
.on('click', (event, d) => onSpanSelect(d))
.style('cursor', 'pointer')
.style('opacity', d => selectedSpan && selectedSpan.spanId === d.spanId ? 1 : 0.7);
// Add span labels
spans.append('text')
.attr('x', margin.left - 5)
.attr('y', yScale.bandwidth() / 2)
.attr('text-anchor', 'end')
.attr('dominant-baseline', 'middle')
.text(d => `${d.serviceName}: ${d.operationName}`)
.style('font-size', '12px')
.style('fill', '#374151');
// Add duration labels
spans.append('text')
.attr('x', d => xScale(d.durationMs) + 5)
.attr('y', yScale.bandwidth() / 2)
.attr('dominant-baseline', 'middle')
.text(d => `${d.durationMs}ms`)
.style('font-size', '10px')
.style('fill', '#6b7280');
}, [trace, selectedSpan, onSpanSelect]);
return (
<div className="trace-graph">
<svg
ref={svgRef}
width="100%"
height="400"
className="border border-gray-200 rounded-lg"
/>
</div>
);
};

6. Spring Boot Configuration

package com.jaeger.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.cassandra.config.AbstractCassandraConfiguration;
import org.springframework.data.cassandra.config.CqlSessionFactoryBean;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class AppConfig extends AbstractCassandraConfiguration {
@Override
protected String getKeyspaceName() {
return "jaeger";
}
@Override
public String getContactPoints() {
return "localhost";
}
@Override
protected int getPort() {
return 9042;
}
@Bean
@Override
public CqlSessionFactoryBean cassandraSession() {
CqlSessionFactoryBean session = super.cassandraSession();
session.setKeyspaceCreations(getKeyspaceCreations());
session.setKeyspaceDrops(getKeyspaceDrops());
return session;
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
@Configuration
class ElasticsearchConfig extends ElasticsearchConfiguration {
@Override
public ClientConfiguration clientConfiguration() {
return ClientConfiguration.builder()
.connectedTo("localhost:9200")
.build();
}
}

Usage Examples

// Example: Instrumenting a Spring Boot service
@Service
public class OrderService {
private final Tracer tracer;
public OrderService(Tracer tracer) {
this.tracer = tracer;
}
public Order processOrder(OrderRequest request) {
Span span = tracer.nextSpan().name("processOrder").start();
try (var ws = tracer.withSpan(span)) {
span.tag("orderId", request.getOrderId());
span.tag("customerId", request.getCustomerId());
// Business logic
Order order = createOrder(request);
processPayment(order);
updateInventory(order);
return order;
} catch (Exception e) {
span.error(e);
throw e;
} finally {
span.finish();
}
}
}
// Example: Custom trace analysis
@Component
public class TraceAnalyzer {
private final TraceStorageService storageService;
public TraceAnalyzer(TraceStorageService storageService) {
this.storageService = storageService;
}
public void analyzeSlowTraces() {
Instant end = Instant.now();
Instant start = end.minus(Duration.ofHours(1));
TraceSearchRequest request = new TraceSearchRequest.Builder()
.startTimeMin(start)
.startTimeMax(end)
.durationMin(5000L) // 5 seconds
.limit(100)
.build();
List<Trace> slowTraces = storageService.searchTraces(request).join();
slowTraces.forEach(trace -> {
System.out.println("Slow trace: " + trace.getTraceId());
System.out.println("Duration: " + trace.getDurationMs() + "ms");
System.out.println("Services: " + trace.getInvolvedServices());
// Identify bottlenecks
trace.identifyCriticalPath().forEach(span -> {
System.out.println("Critical span: " + span.getOperationName() + 
" (" + span.getDurationMs() + "ms)");
});
});
}
}

Features

Complete Trace Visualization

  • Interactive trace timeline with D3.js
  • Service dependency graphs
  • Span hierarchy and relationships
  • Real-time trace updates

Advanced Search & Filtering

  • Service and operation-based filtering
  • Time range selection
  • Tag-based searching
  • Duration-based filtering

Performance Analysis

  • Critical path identification
  • Service dependency analysis
  • Error rate monitoring
  • Latency percentiles

Storage & Scalability

  • Multi-storage support (Cassandra, Elasticsearch, Redis)
  • Distributed tracing collection
  • Horizontal scalability
  • Efficient indexing and querying

Integration Ready

  • OpenTelemetry compatibility
  • Spring Boot auto-configuration
  • RESTful APIs
  • Real-time WebSocket updates

This Jaeger-inspired distributed tracing system provides comprehensive trace visualization and analysis capabilities with enterprise-grade scalability and performance.

Leave a Reply

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


Macro Nepal Helper