Files
otel-quarkus-demo/src/main/java/com/demo/OrderResource.java
2026-05-29 16:17:38 +02:00

296 lines
12 KiB
Java

package com.demo;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.jboss.logging.Logger;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ThreadLocalRandom;
@Path("/api")
@Produces(MediaType.APPLICATION_JSON)
public class OrderResource {
private static final Logger LOG = Logger.getLogger(OrderResource.class);
@Inject
Tracer tracer;
@Inject
MeterRegistry registry;
@Inject
InventoryService inventoryService;
// In-memory store for demo purposes
private final List<Order> orders = new CopyOnWriteArrayList<>();
// ────── Custom Metrics (registered lazily) ──────
private Counter ordersCounter(String status) {
return Counter.builder("orders")
.description("Total number of orders processed")
.tag("status", status)
.register(registry);
}
private Counter revenueCounter() {
return Counter.builder("orders_amount")
.description("Total order revenue")
.baseUnit("dollars")
.register(registry);
}
private Timer orderProcessingTimer() {
return Timer.builder("order_processing_duration")
.description("Time spent processing an order")
.publishPercentiles(0.5, 0.9, 0.95, 0.99)
.register(registry);
}
private DistributionSummary orderSizeSummary() {
return DistributionSummary.builder("order_item_count")
.description("Number of items per order")
.publishPercentiles(0.5, 0.9)
.register(registry);
}
// ────── Endpoints ──────
@GET
@Path("/health")
public Response health() {
return Response.ok(Map.of("status", "UP", "service", "otel-quarkus-demo")).build();
}
/**
* POST /api/orders — Create and process an order.
* Demonstrates: custom spans, span attributes, custom metrics, error injection, latency simulation.
*/
@POST
@Path("/orders")
@Consumes(MediaType.APPLICATION_JSON)
public Response createOrder(OrderRequest request) {
LOG.infof("Received order request: product=%s, quantity=%d, customerId=%s",
request.product, request.quantity, request.customerId);
// Start a custom parent span for the full order processing
Span orderSpan = tracer.spanBuilder("processOrder")
.setSpanKind(SpanKind.INTERNAL)
.setAttribute("order.product", request.product)
.setAttribute("order.quantity", (long) request.quantity)
.setAttribute("order.customer_id", request.customerId)
.startSpan();
try (Scope scope = orderSpan.makeCurrent()) {
return orderProcessingTimer().record(() -> {
try {
// Step 1: Validate the order
validateOrder(request);
// Step 2: Check inventory
boolean inStock = checkInventory(request.product, request.quantity);
if (!inStock) {
orderSpan.setStatus(StatusCode.ERROR, "Insufficient inventory");
ordersCounter("rejected_no_stock").increment();
LOG.warnf("Order rejected — insufficient inventory for product=%s, requested=%d",
request.product, request.quantity);
return Response.status(Response.Status.CONFLICT)
.entity(Map.of("error", "Insufficient inventory", "product", request.product))
.build();
}
// Step 3: Process payment (simulated)
processPayment(request);
// Step 4: Create the order
Order order = new Order(request.customerId, request.product,
request.quantity, request.unitPrice);
// Simulate processing time based on complexity
String processingType = request.quantity > 5 ? "complex" : "simple";
orderSpan.setAttribute("order.processing_type", processingType);
orderSpan.setAttribute("order.total_price", order.totalPrice);
simulateProcessingDelay(processingType);
// Randomly inject errors (~10% of the time) for troubleshooting demo
if (ThreadLocalRandom.current().nextDouble() < 0.10) {
throw new RuntimeException("Downstream fulfillment service timeout");
}
order.status = "COMPLETED";
orders.add(order);
// Record metrics
ordersCounter("completed").increment();
revenueCounter().increment(order.totalPrice);
orderSizeSummary().record(request.quantity);
// Update inventory
inventoryService.decrementStock(request.product, request.quantity);
LOG.infof("Order %s completed: product=%s, quantity=%d, total=%.2f, type=%s",
order.id, order.product, order.quantity, order.totalPrice, processingType);
orderSpan.setAttribute("order.id", order.id);
orderSpan.setStatus(StatusCode.OK);
return Response.status(Response.Status.CREATED).entity(order).build();
} catch (IllegalArgumentException e) {
orderSpan.setStatus(StatusCode.ERROR, e.getMessage());
orderSpan.recordException(e);
ordersCounter("rejected_validation").increment();
LOG.errorf("Order validation failed: %s", e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", e.getMessage())).build();
} catch (RuntimeException e) {
orderSpan.setStatus(StatusCode.ERROR, e.getMessage());
orderSpan.recordException(e);
ordersCounter("failed").increment();
LOG.errorf(e, "Order processing failed: %s", e.getMessage());
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Order processing failed", "detail", e.getMessage())).build();
}
});
} finally {
orderSpan.end();
}
}
/**
* GET /api/orders — List all orders.
*/
@GET
@Path("/orders")
public Response listOrders() {
LOG.debugf("Listing %d orders", orders.size());
return Response.ok(orders).build();
}
/**
* GET /api/orders/{id} — Get a single order by ID.
*/
@GET
@Path("/orders/{id}")
public Response getOrder(@PathParam("id") String id) {
return orders.stream()
.filter(o -> o.id.equals(id))
.findFirst()
.map(o -> Response.ok(o).build())
.orElseGet(() -> {
LOG.warnf("Order not found: %s", id);
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", "Order not found")).build();
});
}
/**
* GET /api/inventory — Current inventory levels (gauge metrics).
*/
@GET
@Path("/inventory")
public Response getInventory() {
return Response.ok(inventoryService.getInventoryLevels()).build();
}
// ────── Internal Processing Steps (each with its own span) ──────
private void validateOrder(OrderRequest request) {
Span span = tracer.spanBuilder("validateOrder").startSpan();
try (Scope s = span.makeCurrent()) {
if (request.product == null || request.product.isBlank()) {
throw new IllegalArgumentException("Product name is required");
}
if (request.quantity <= 0) {
throw new IllegalArgumentException("Quantity must be positive");
}
if (request.unitPrice <= 0) {
throw new IllegalArgumentException("Unit price must be positive");
}
span.setAttribute("validation.passed", true);
LOG.debugf("Order validation passed for product=%s", request.product);
} finally {
span.end();
}
}
private boolean checkInventory(String product, int quantity) {
Span span = tracer.spanBuilder("checkInventory")
.setAttribute("inventory.product", product)
.setAttribute("inventory.requested_quantity", (long) quantity)
.startSpan();
try (Scope s = span.makeCurrent()) {
// Simulate a small network call delay
Thread.sleep(ThreadLocalRandom.current().nextInt(10, 50));
int available = inventoryService.getStock(product);
span.setAttribute("inventory.available", (long) available);
boolean inStock = available >= quantity;
span.setAttribute("inventory.in_stock", inStock);
LOG.debugf("Inventory check: product=%s, available=%d, requested=%d, inStock=%s",
product, available, quantity, inStock);
return inStock;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
span.end();
}
}
private void processPayment(OrderRequest request) {
Span span = tracer.spanBuilder("processPayment")
.setSpanKind(SpanKind.CLIENT)
.setAttribute("payment.amount", request.quantity * request.unitPrice)
.setAttribute("payment.currency", "USD")
.startSpan();
try (Scope s = span.makeCurrent()) {
// Simulate payment gateway latency
Thread.sleep(ThreadLocalRandom.current().nextInt(20, 150));
span.setAttribute("payment.status", "approved");
LOG.infof("Payment processed: amount=%.2f", request.quantity * request.unitPrice);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
span.end();
}
}
private void simulateProcessingDelay(String type) {
try {
if ("complex".equals(type)) {
Thread.sleep(ThreadLocalRandom.current().nextInt(200, 800));
} else {
Thread.sleep(ThreadLocalRandom.current().nextInt(10, 100));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// ────── Request DTO ──────
public static class OrderRequest {
public String customerId;
public String product;
public int quantity;
public double unitPrice;
}
}