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