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 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; } }