Initial release
This commit is contained in:
295
src/main/java/com/demo/OrderResource.java
Normal file
295
src/main/java/com/demo/OrderResource.java
Normal file
@@ -0,0 +1,295 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user