Article
In microservices architectures, understanding the flow of requests across service boundaries is crucial for debugging and performance optimization. Jaeger is an open-source, end-to-end distributed tracing system that helps developers monitor and troubleshoot complex transactions in distributed systems. The Jaeger All-in-One distribution packages all Jaeger components into a single, easy-to-run binary, making it perfect for development, testing, and demo environments.
This article will guide you through setting up Jaeger All-in-One and instrumenting your Java applications to provide comprehensive distributed tracing.
What is Jaeger All-in-One?
Jaeger All-in-One is a pre-configured distribution that bundles:
- Jaeger Agent: Receives spans and forwards to collector
- Jaeger Collector: Receives traces from agents and writes to storage
- Jaeger Query: Provides API and UI for retrieving traces
- Jaeger UI: Web interface for visualizing traces
- In-Memory Storage: Embedded storage (not suitable for production)
This all-in-one approach eliminates complex setup, making it ideal for local development and testing.
Quick Start: Running Jaeger All-in-One
Using Docker (Recommended)
docker run -d --name jaeger \ -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \ -p 5775:5775/udp \ -p 6831:6831/udp \ -p 6832:6832/udp \ -p 5778:5778 \ -p 16686:16686 \ -p 14268:14268 \ -p 14250:14250 \ -p 9411:9411 \ jaegertracing/all-in-one:1.54
Using Docker Compose
# docker-compose.yml version: '3.8' services: jaeger: image: jaegertracing/all-in-one:1.54 ports: - "5775:5775/udp" - "6831:6831/udp" - "6832:6832/udp" - "5778:5778" - "16686:16686" # UI - "14268:14268" # Collector endpoint - "14250:14250" # Collector gRPC endpoint - "9411:9411" # Zipkin compatible endpoint environment: - LOG_LEVEL=debug
After running, access the Jaeger UI at: http://localhost:16686
Instrumenting Java Applications with Jaeger
1. Maven Dependencies
Add the necessary dependencies to your pom.xml:
<properties>
<opentelemetry.version>1.34.1</opentelemetry.version>
<opentelemetry-instrumentation.version>2.1.0</opentelemetry-instrumentation.version>
</properties>
<dependencies>
<!-- OpenTelemetry SDK -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<version>${opentelemetry.version}</version>
</dependency>
<!-- OpenTelemetry Exporter for Jaeger -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-jaeger</artifactId>
<version>${opentelemetry.version}</version>
</dependency>
<!-- OpenTelemetry API -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>${opentelemetry.version}</version>
</dependency>
<!-- Automatic instrumentation for common libraries -->
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-spring-boot-starter</artifactId>
<version>${opentelemetry-instrumentation.version}</version>
</dependency>
</dependencies>
2. Configuration Class for OpenTelemetry
Create a configuration class to set up OpenTelemetry with Jaeger exporter:
@Configuration
public class JaegerConfig {
@Value("${spring.application.name:unknown-spring-app}")
private String applicationName;
@Bean
public OpenTelemetry openTelemetry() {
// Jaeger gRPC endpoint
JaegerGrpcSpanExporter jaegerExporter = JaegerGrpcSpanExporter.builder()
.setEndpoint("http://localhost:14250") // Jaeger collector gRPC endpoint
.build();
// Resource definition
Resource resource = Resource.getDefault()
.merge(Resource.builder()
.put(SERVICE_NAME, applicationName)
.put("deployment.environment", "development")
.build());
// SDK setup
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(jaegerExporter).build())
.setResource(resource)
.build();
// OpenTelemetry instance
return OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
.buildAndRegisterGlobal();
}
}
3. Spring Boot Auto-Configuration
For Spring Boot applications, you can use auto-configuration:
application.yml:
spring:
application:
name: order-service
opentelemetry:
service-name: ${spring.application.name}
tracer:
name: ${spring.application.name}-tracer
management:
tracing:
sampling:
probability: 1.0 # Sample 100% of traces for development
logging:
pattern:
level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"
4. Manual Instrumentation Example
Here's how to manually instrument your business logic:
@Service
@Slf4j
public class OrderService {
private final Tracer tracer;
private final PaymentService paymentService;
private final InventoryService inventoryService;
public OrderService(Tracer tracer, PaymentService paymentService,
InventoryService inventoryService) {
this.tracer = tracer;
this.paymentService = paymentService;
this.inventoryService = inventoryService;
}
public Order processOrder(OrderRequest request) {
// Start a new span for the order processing
Span orderSpan = tracer.spanBuilder("processOrder")
.setAttribute("order.id", request.getOrderId())
.setAttribute("order.amount", request.getAmount().doubleValue())
.setAttribute("customer.id", request.getCustomerId())
.startSpan();
try (Scope scope = orderSpan.makeCurrent()) {
log.info("Processing order: {}", request.getOrderId());
// Step 1: Validate order
Span validationSpan = tracer.spanBuilder("validateOrder").startSpan();
try (Scope validationScope = validationSpan.makeCurrent()) {
validateOrder(request);
} finally {
validationSpan.end();
}
// Step 2: Process payment
paymentService.processPayment(request);
// Step 3: Reserve inventory
inventoryService.reserveInventory(request);
// Step 4: Create order record
Order order = createOrderRecord(request);
orderSpan.setStatus(StatusCode.OK);
log.info("Order processed successfully: {}", order.getId());
return order;
} catch (Exception e) {
orderSpan.setStatus(StatusCode.ERROR);
orderSpan.recordException(e);
log.error("Failed to process order: {}", request.getOrderId(), e);
throw e;
} finally {
orderSpan.end();
}
}
private void validateOrder(OrderRequest request) {
// Validation logic
if (request.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Order amount must be positive");
}
}
private Order createOrderRecord(OrderRequest request) {
Span span = tracer.spanBuilder("createOrderRecord").startSpan();
try (Scope scope = span.makeCurrent()) {
// Simulate database operation
Thread.sleep(50);
return new Order(UUID.randomUUID().toString(), request.getCustomerId(),
request.getAmount(), "COMPLETED");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted during order creation", e);
} finally {
span.end();
}
}
}
5. HTTP Client Instrumentation
For HTTP calls between services:
@Component
public class PaymentService {
private final Tracer tracer;
private final RestTemplate restTemplate;
public PaymentService(Tracer tracer, RestTemplate restTemplate) {
this.tracer = tracer;
this.restTemplate = restTemplate;
}
public void processPayment(OrderRequest request) {
Span span = tracer.spanBuilder("processPayment")
.setAttribute("payment.amount", request.getAmount().doubleValue())
.setAttribute("payment.customer", request.getCustomerId())
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Create HTTP headers with tracing context
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// Inject tracing context into headers
TextMapSetter<HttpHeaders> setter = (carrier, key, value) -> {
if (carrier != null) {
carrier.set(key, value);
}
};
GlobalOpenTelemetry.getPropagators().getTextMapPropagator()
.inject(Context.current(), headers, setter);
// Create request entity
PaymentRequest paymentRequest = new PaymentRequest(
request.getOrderId(), request.getAmount(), request.getCustomerId());
HttpEntity<PaymentRequest> entity = new HttpEntity<>(paymentRequest, headers);
// Make HTTP call
ResponseEntity<PaymentResponse> response = restTemplate.exchange(
"http://localhost:8081/api/payments",
HttpMethod.POST,
entity,
PaymentResponse.class
);
if (!response.getStatusCode().is2xxSuccessful()) {
throw new RuntimeException("Payment failed with status: " + response.getStatusCode());
}
span.setStatus(StatusCode.OK);
log.info("Payment processed successfully for order: {}", request.getOrderId());
} catch (Exception e) {
span.setStatus(StatusCode.ERROR);
span.recordException(e);
log.error("Payment processing failed for order: {}", request.getOrderId(), e);
throw e;
} finally {
span.end();
}
}
}
6. HTTP Server Instrumentation (Spring Boot)
For automatic HTTP server instrumentation in Spring Boot:
@RestController
@Slf4j
public class OrderController {
private final OrderService orderService;
private final Tracer tracer;
public OrderController(OrderService orderService, Tracer tracer) {
this.orderService = orderService;
this.tracer = tracer;
}
@PostMapping("/api/orders")
public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request) {
// Current span is automatically created by Spring Boot instrumentation
Span currentSpan = Span.current();
currentSpan.setAttribute("order.currency", "USD");
log.info("Received order creation request: {}", request.getOrderId());
try {
Order order = orderService.processOrder(request);
return ResponseEntity.ok(order);
} catch (Exception e) {
currentSpan.recordException(e);
currentSpan.setStatus(StatusCode.ERROR);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@GetMapping("/api/orders/{orderId}")
public ResponseEntity<Order> getOrder(@PathVariable String orderId) {
// Add custom attributes to the auto-created span
Span.current().setAttribute("order.id", orderId);
log.info("Fetching order: {}", orderId);
// Simulate database lookup
try {
Thread.sleep(100);
Order order = new Order(orderId, "customer-123",
new BigDecimal("99.99"), "COMPLETED");
return ResponseEntity.ok(order);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}
7. Database Instrumentation
For database operations:
@Repository
@Slf4j
public class OrderRepository {
private final Tracer tracer;
private final JdbcTemplate jdbcTemplate;
public OrderRepository(Tracer tracer, JdbcTemplate jdbcTemplate) {
this.tracer = tracer;
this.jdbcTemplate = jdbcTemplate;
}
public Order save(Order order) {
Span span = tracer.spanBuilder("OrderRepository.save")
.setAttribute("db.operation", "insert")
.setAttribute("db.table", "orders")
.setAttribute("db.system", "mysql")
.startSpan();
try (Scope scope = span.makeCurrent()) {
log.info("Saving order to database: {}", order.getId());
// Simulate database operation
String sql = "INSERT INTO orders (id, customer_id, amount, status) VALUES (?, ?, ?, ?)";
jdbcTemplate.update(sql, order.getId(), order.getCustomerId(),
order.getAmount(), order.getStatus());
span.setStatus(StatusCode.OK);
return order;
} catch (Exception e) {
span.setStatus(StatusCode.ERROR);
span.recordException(e);
log.error("Failed to save order: {}", order.getId(), e);
throw e;
} finally {
span.end();
}
}
}
Testing the Setup
1. Generate Some Traffic
@Component
public class TestDataGenerator implements CommandLineRunner {
private final OrderService orderService;
public TestDataGenerator(OrderService orderService) {
this.orderService = orderService;
}
@Override
public void run(String... args) throws Exception {
// Generate test orders
for (int i = 1; i <= 5; i++) {
OrderRequest request = new OrderRequest(
"order-" + i,
"customer-" + i,
new BigDecimal(i * 25.0)
);
try {
orderService.processOrder(request);
Thread.sleep(1000); // Wait between requests
} catch (Exception e) {
log.error("Failed to process test order: {}", request.getOrderId(), e);
}
}
}
}
2. View Traces in Jaeger UI
- Open http://localhost:16686 in your browser
- Select your service from the dropdown
- Click "Find Traces"
- Explore the detailed trace timeline
Production Considerations
While All-in-One is great for development, for production consider:
- Separate Components: Run Jaeger components separately
- Persistent Storage: Use Elasticsearch or Cassandra instead of in-memory storage
- Sampling: Reduce sampling rate (e.g., 0.01 for 1% sampling)
- Kubernetes: Use Jaeger Operator for Kubernetes deployments
Conclusion
Jaeger All-in-One provides a fantastic starting point for implementing distributed tracing in Java applications. With minimal setup and comprehensive OpenTelemetry integration, you can quickly gain visibility into your microservices architecture. The combination of automatic instrumentation for common frameworks and manual instrumentation for business logic gives you complete control over what gets traced.
By following this guide, you can set up a full tracing environment locally and instrument your Java applications to provide crucial insights into request flows, performance bottlenecks, and error propagation across your distributed system.