Introduction to Lumigo
Lumigo is a serverless monitoring and debugging platform that provides distributed tracing, performance monitoring, and troubleshooting capabilities for serverless applications. It automatically instruments AWS Lambda functions and provides end-to-end visibility across your serverless architecture.
Key Features
- Automatic Distributed Tracing: Track requests across Lambda functions and AWS services
- Performance Monitoring: Identify bottlenecks and cold starts
- Debugging Tools: Real-time debugging and error analysis
- AWS Service Integration: Automatic instrumentation for DynamoDB, S3, SQS, SNS, etc.
- Security Insights: Detect misconfigurations and security issues
Implementation Guide
Dependencies
Add to your pom.xml:
<properties>
<aws.lambda.java.version>1.2.3</aws.lambda.java.version>
<lumigo.version>1.0.15</lumigo.version>
<aws.java.sdk.version>2.20.56</aws.java.sdk.version>
</properties>
<dependencies>
<!-- AWS Lambda Java Core -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-core</artifactId>
<version>${aws.lambda.java.version}</version>
</dependency>
<!-- Lumigo Tracer -->
<dependency>
<groupId>io.lumigo</groupId>
<artifactId>lumigo-java-tracer</artifactId>
<version>${lumigo.version}</version>
</dependency>
<!-- AWS SDK v2 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>dynamodb</artifactId>
<version>${aws.java.sdk.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>${aws.java.sdk.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sqs</artifactId>
<version>${aws.java.sdk.version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
</dependencies>
Basic Lumigo Setup
Environment Configuration
package com.example.lumigo;
import java.util.HashMap;
import java.util.Map;
public class LumigoConfig {
// Lumigo environment variables
public static final String LUMIGO_TOKEN = "LUMIGO_TRACER_TOKEN";
public static final String LUMIGO_DEBUG = "LUMIGO_DEBUG";
public static final String LUMIGO_TAGS = "LUMIGO_TAGS";
public static final String AWS_REGION = "AWS_REGION";
public static Map<String, String> getLumigoConfig() {
Map<String, String> config = new HashMap<>();
// Required: Lumigo tracer token
config.put(LUMIGO_TOKEN, System.getenv(LUMIGO_TOKEN));
// Optional: Enable debug mode
config.put(LUMIGO_DEBUG, "true");
// Optional: Add custom tags
config.put(LUMIGO_TAGS, "environment=production,team=backend");
return config;
}
public static void validateConfig() {
String token = System.getenv(LUMIGO_TOKEN);
if (token == null || token.trim().isEmpty()) {
throw new IllegalStateException("LUMIGO_TRACER_TOKEN environment variable is required");
}
}
public static String getAwsRegion() {
return System.getenv(AWS_REGION) != null ?
System.getenv(AWS_REGION) : "us-east-1";
}
}
Basic Lambda Handler with Lumigo
package com.example.lumigo;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.LambdaLogger;
import io.lumigo.core.Lumigo;
import io.lumigo.core.utils.LumigoLoggingUtils;
import java.util.Map;
public class BasicLumigoHandler implements RequestHandler<Map<String, Object>, Map<String, Object>> {
static {
// Initialize Lumigo tracer
Lumigo.getInstance().init();
}
@Override
public Map<String, Object> handleRequest(Map<String, Object> input, Context context) {
LambdaLogger logger = context.getLogger();
try {
// Log with Lumigo context
LumigoLoggingUtils.info("Starting request processing",
Map.of("requestId", context.getAwsRequestId(),
"functionName", context.getFunctionName()));
// Add custom business data to trace
Lumigo.getInstance().addExecutionTag("customerId",
input.getOrDefault("customerId", "unknown").toString());
Lumigo.getInstance().addExecutionTag("operation", "processOrder");
// Business logic
Map<String, Object> result = processBusinessLogic(input, context);
LumigoLoggingUtils.info("Request completed successfully");
return result;
} catch (Exception e) {
// Errors are automatically captured by Lumigo
LumigoLoggingUtils.error("Error processing request", e);
throw e;
}
}
private Map<String, Object> processBusinessLogic(Map<String, Object> input, Context context) {
// Simulate business logic
Map<String, Object> result = new java.util.HashMap<>();
result.put("status", "success");
result.put("processedAt", java.time.Instant.now().toString());
result.put("requestId", context.getAwsRequestId());
// Add custom span for business logic
Lumigo.getInstance().startSpan("businessLogic");
try {
// Simulate work
Thread.sleep(100);
// Add custom metrics
Lumigo.getInstance().addExecutionTag("processingTime", "100ms");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Processing interrupted", e);
} finally {
Lumigo.getInstance().stopSpan();
}
return result;
}
}
Advanced Lumigo Implementation
Custom Lumigo Tracer Wrapper
package com.example.lumigo;
import io.lumigo.core.Lumigo;
import io.lumigo.core.utils.LumigoLoggingUtils;
import software.amazon.awssdk.core.SdkRequest;
import software.amazon.awssdk.core.SdkResponse;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
public class LumigoTracer {
private static final Lumigo lumigo = Lumigo.getInstance();
public static void init() {
LumigoConfig.validateConfig();
lumigo.init();
}
public static <T> T traceOperation(String operationName, Supplier<T> operation) {
return traceOperation(operationName, Map.of(), operation);
}
public static <T> T traceOperation(String operationName, Map<String, Object> tags,
Supplier<T> operation) {
lumigo.startSpan(operationName);
try {
// Add custom tags to the span
tags.forEach((key, value) ->
lumigo.addSpanTag(key, value.toString()));
T result = operation.get();
lumigo.addSpanTag("status", "success");
return result;
} catch (Exception e) {
lumigo.addSpanTag("status", "error");
lumigo.addSpanTag("error", e.getMessage());
throw e;
} finally {
lumigo.stopSpan();
}
}
public static void traceAsyncOperation(String operationName, Runnable operation) {
traceAsyncOperation(operationName, Map.of(), operation);
}
public static void traceAsyncOperation(String operationName, Map<String, Object> tags,
Runnable operation) {
lumigo.startSpan(operationName);
try {
tags.forEach((key, value) ->
lumigo.addSpanTag(key, value.toString()));
operation.run();
lumigo.addSpanTag("status", "success");
} catch (Exception e) {
lumigo.addSpanTag("status", "error");
lumigo.addSpanTag("error", e.getMessage());
throw e;
} finally {
lumigo.stopSpan();
}
}
public static <T extends SdkRequest> T wrapRequest(T request, String serviceName) {
// Add tracing headers to AWS SDK requests
return lumigo.injectSpanToAwsSdkRequest(request, serviceName);
}
public static <T extends SdkResponse> T wrapResponse(T response, String operation) {
// Capture response metadata
if (response != null) {
lumigo.addSpanTag("aws.requestId", response.responseMetadata().requestId());
lumigo.addSpanTag("aws.operation", operation);
}
return response;
}
public static void addCustomMetric(String name, Number value) {
lumigo.addExecutionTag("metric." + name, value.toString());
}
public static void addBusinessContext(String key, Object value) {
lumigo.addExecutionTag("business." + key, value.toString());
}
public static void recordColdStart() {
lumigo.addExecutionTag("coldStart", "true");
}
public static void recordWarmStart() {
lumigo.addExecutionTag("coldStart", "false");
}
}
AWS Service Integration
package com.example.lumigo.aws;
import com.example.lumigo.LumigoTracer;
import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
import software.amazon.awssdk.core.SdkRequest;
import software.amazon.awssdk.core.SdkResponse;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.*;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.services.sqs.SqsClient;
import software.amazon.awssdk.services.sqs.model.*;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
public class AWSServiceWrapper {
private final DynamoDbClient dynamoDbClient;
private final S3Client s3Client;
private final SqsClient sqsClient;
public AWSServiceWrapper() {
this.dynamoDbClient = DynamoDbClient.builder()
.region(Region.of(System.getenv("AWS_REGION")))
.credentialsProvider(EnvironmentVariableCredentialsProvider.create())
.build();
this.s3Client = S3Client.builder()
.region(Region.of(System.getenv("AWS_REGION")))
.credentialsProvider(EnvironmentVariableCredentialsProvider.create())
.build();
this.sqsClient = SqsClient.builder()
.region(Region.of(System.getenv("AWS_REGION")))
.credentialsProvider(EnvironmentVariableCredentialsProvider.create())
.build();
}
// DynamoDB Operations
public CompletableFuture<GetItemResponse> getItemAsync(String tableName, Map<String, AttributeValue> key) {
return LumigoTracer.traceOperation("DynamoDB.GetItem",
Map.of("tableName", tableName, "key", key.toString()),
() -> {
GetItemRequest request = GetItemRequest.builder()
.tableName(tableName)
.key(key)
.build();
// Wrap request for tracing
GetItemRequest tracedRequest = LumigoTracer.wrapRequest(request, "dynamodb");
return dynamoDbClient.getItem(tracedRequest);
});
}
public PutItemResponse putItem(String tableName, Map<String, AttributeValue> item) {
return LumigoTracer.traceOperation("DynamoDB.PutItem",
Map.of("tableName", tableName, "itemSize", item.size()),
() -> {
PutItemRequest request = PutItemRequest.builder()
.tableName(tableName)
.item(item)
.build();
PutItemRequest tracedRequest = LumigoTracer.wrapRequest(request, "dynamodb");
PutItemResponse response = dynamoDbClient.putItem(tracedRequest);
return LumigoTracer.wrapResponse(response, "PutItem");
});
}
// S3 Operations
public GetObjectResponse getObject(String bucketName, String key) {
return LumigoTracer.traceOperation("S3.GetObject",
Map.of("bucketName", bucketName, "key", key),
() -> {
GetObjectRequest request = GetObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build();
GetObjectRequest tracedRequest = LumigoTracer.wrapRequest(request, "s3");
GetObjectResponse response = s3Client.getObject(tracedRequest);
return LumigoTracer.wrapResponse(response, "GetObject");
});
}
public PutObjectResponse putObject(String bucketName, String key, String content) {
return LumigoTracer.traceOperation("S3.PutObject",
Map.of("bucketName", bucketName, "key", key, "contentLength", content.length()),
() -> {
PutObjectRequest request = PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build();
PutObjectRequest tracedRequest = LumigoTracer.wrapRequest(request, "s3");
// Note: You'd need to handle the actual content streaming
PutObjectResponse response = s3Client.putObject(tracedRequest,
software.amazon.awssdk.core.sync.RequestBody.fromString(content));
return LumigoTracer.wrapResponse(response, "PutObject");
});
}
// SQS Operations
public SendMessageResponse sendMessage(String queueUrl, String messageBody) {
return LumigoTracer.traceOperation("SQS.SendMessage",
Map.of("queueUrl", queueUrl, "messageSize", messageBody.length()),
() -> {
SendMessageRequest request = SendMessageRequest.builder()
.queueUrl(queueUrl)
.messageBody(messageBody)
.build();
SendMessageRequest tracedRequest = LumigoTracer.wrapRequest(request, "sqs");
SendMessageResponse response = sqsClient.sendMessage(tracedRequest);
LumigoTracer.addCustomMetric("sqs.messageId", response.messageId());
return LumigoTracer.wrapResponse(response, "SendMessage");
});
}
public ReceiveMessageResponse receiveMessages(String queueUrl) {
return LumigoTracer.traceOperation("SQS.ReceiveMessage",
Map.of("queueUrl", queueUrl),
() -> {
ReceiveMessageRequest request = ReceiveMessageRequest.builder()
.queueUrl(queueUrl)
.maxNumberOfMessages(10)
.build();
ReceiveMessageRequest tracedRequest = LumigoTracer.wrapRequest(request, "sqs");
ReceiveMessageResponse response = sqsClient.receiveMessage(tracedRequest);
LumigoTracer.addCustomMetric("sqs.messagesReceived", response.messages().size());
return LumigoTracer.wrapResponse(response, "ReceiveMessage");
});
}
public void close() {
dynamoDbClient.close();
s3Client.close();
sqsClient.close();
}
}
Complete Serverless Application
Order Processing Lambda
package com.example.lumigo.handlers;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.example.lumigo.LumigoTracer;
import com.example.lumigo.aws.AWSServiceWrapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.Map;
public class OrderProcessingHandler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
private final AWSServiceWrapper awsServices;
private final ObjectMapper objectMapper;
private static boolean isColdStart = true;
public OrderProcessingHandler() {
// Initialize Lumigo
LumigoTracer.init();
this.awsServices = new AWSServiceWrapper();
this.objectMapper = new ObjectMapper();
// Record cold start
if (isColdStart) {
LumigoTracer.recordColdStart();
isColdStart = false;
} else {
LumigoTracer.recordWarmStart();
}
}
@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
return LumigoTracer.traceOperation("OrderProcessing",
Map.of("httpMethod", input.getHttpMethod(),
"path", input.getPath(),
"requestId", context.getAwsRequestId()),
() -> {
try {
// Parse and validate input
OrderRequest orderRequest = parseOrderRequest(input);
// Add business context
LumigoTracer.addBusinessContext("orderId", orderRequest.getOrderId());
LumigoTracer.addBusinessContext("customerId", orderRequest.getCustomerId());
LumigoTracer.addBusinessContext("totalAmount", orderRequest.getTotalAmount());
// Process the order
OrderResponse orderResponse = processOrder(orderRequest, context);
// Return successful response
return createSuccessResponse(orderResponse);
} catch (ValidationException e) {
LumigoTracer.addCustomMetric("validationErrors", 1);
return createErrorResponse(400, e.getMessage());
} catch (PaymentException e) {
LumigoTracer.addCustomMetric("paymentErrors", 1);
return createErrorResponse(402, e.getMessage());
} catch (Exception e) {
LumigoTracer.addCustomMetric("unhandledErrors", 1);
return createErrorResponse(500, "Internal server error");
}
});
}
private OrderRequest parseOrderRequest(APIGatewayProxyRequestEvent input) throws ValidationException {
try {
if (input.getBody() == null) {
throw new ValidationException("Request body is required");
}
OrderRequest request = objectMapper.readValue(input.getBody(), OrderRequest.class);
// Validate required fields
if (request.getCustomerId() == null || request.getCustomerId().trim().isEmpty()) {
throw new ValidationException("customerId is required");
}
if (request.getItems() == null || request.getItems().isEmpty()) {
throw new ValidationException("At least one item is required");
}
return request;
} catch (Exception e) {
throw new ValidationException("Invalid request format: " + e.getMessage());
}
}
private OrderResponse processOrder(OrderRequest orderRequest, Context context) {
return LumigoTracer.traceOperation("ProcessOrderBusinessLogic",
Map.of("orderId", orderRequest.getOrderId(),
"itemCount", orderRequest.getItems().size()),
() -> {
// Step 1: Validate inventory
validateInventory(orderRequest);
// Step 2: Process payment
processPayment(orderRequest);
// Step 3: Create order record
createOrderRecord(orderRequest);
// Step 4: Update inventory
updateInventory(orderRequest);
// Step 5: Send confirmation
sendConfirmation(orderRequest);
return new OrderResponse(
orderRequest.getOrderId(),
"COMPLETED",
"Order processed successfully",
context.getAwsRequestId()
);
});
}
private void validateInventory(OrderRequest orderRequest) {
LumigoTracer.traceOperation("ValidateInventory", () -> {
// Simulate inventory validation
for (OrderItem item : orderRequest.getItems()) {
// Check if item is available
boolean available = checkItemAvailability(item.getProductId(), item.getQuantity());
if (!available) {
throw new ValidationException("Product " + item.getProductId() + " is out of stock");
}
}
LumigoTracer.addCustomMetric("inventoryChecks", orderRequest.getItems().size());
});
}
private void processPayment(OrderRequest orderRequest) {
LumigoTracer.traceOperation("ProcessPayment",
Map.of("amount", orderRequest.getTotalAmount(),
"paymentMethod", orderRequest.getPaymentMethod()),
() -> {
// Simulate payment processing
boolean paymentSuccess = simulatePaymentProcessing(orderRequest);
if (!paymentSuccess) {
throw new PaymentException("Payment processing failed");
}
LumigoTracer.addCustomMetric("paymentAmount", orderRequest.getTotalAmount());
});
}
private void createOrderRecord(OrderRequest orderRequest) {
LumigoTracer.traceOperation("CreateOrderRecord", () -> {
try {
// Save order to DynamoDB
Map<String, software.amazon.awssdk.services.dynamodb.model.AttributeValue> item =
createDynamoDBItem(orderRequest);
awsServices.putItem("orders-table", item);
LumigoTracer.addCustomMetric("orderCreated", 1);
} catch (Exception e) {
throw new RuntimeException("Failed to create order record", e);
}
});
}
private void updateInventory(OrderRequest orderRequest) {
LumigoTracer.traceOperation("UpdateInventory", () -> {
// Simulate inventory update
for (OrderItem item : orderRequest.getItems()) {
updateItemInventory(item.getProductId(), item.getQuantity());
}
// Send SQS message for inventory updates
String message = createInventoryUpdateMessage(orderRequest);
awsServices.sendMessage("inventory-updates-queue", message);
});
}
private void sendConfirmation(OrderRequest orderRequest) {
LumigoTracer.traceOperation("SendConfirmation", () -> {
// Send SNS notification or email
String notification = createConfirmationMessage(orderRequest);
// Implementation would use SNS or SES
});
}
// Helper methods
private boolean checkItemAvailability(String productId, int quantity) {
// Simulate inventory check
return true;
}
private boolean simulatePaymentProcessing(OrderRequest orderRequest) {
// Simulate payment processing
return orderRequest.getTotalAmount() < 1000; // Reject large amounts for demo
}
private void updateItemInventory(String productId, int quantity) {
// Simulate inventory update
}
private Map<String, software.amazon.awssdk.services.dynamodb.model.AttributeValue> createDynamoDBItem(OrderRequest orderRequest) {
Map<String, software.amazon.awssdk.services.dynamodb.model.AttributeValue> item = new HashMap<>();
item.put("orderId", software.amazon.awssdk.services.dynamodb.model.AttributeValue.builder()
.s(orderRequest.getOrderId()).build());
item.put("customerId", software.amazon.awssdk.services.dynamodb.model.AttributeValue.builder()
.s(orderRequest.getCustomerId()).build());
item.put("totalAmount", software.amazon.awssdk.services.dynamodb.model.AttributeValue.builder()
.n(String.valueOf(orderRequest.getTotalAmount())).build());
return item;
}
private String createInventoryUpdateMessage(OrderRequest orderRequest) {
try {
Map<String, Object> message = new HashMap<>();
message.put("orderId", orderRequest.getOrderId());
message.put("items", orderRequest.getItems());
message.put("timestamp", System.currentTimeMillis());
return objectMapper.writeValueAsString(message);
} catch (Exception e) {
throw new RuntimeException("Failed to create inventory update message", e);
}
}
private String createConfirmationMessage(OrderRequest orderRequest) {
return String.format("Order %s confirmed for customer %s",
orderRequest.getOrderId(), orderRequest.getCustomerId());
}
private APIGatewayProxyResponseEvent createSuccessResponse(OrderResponse orderResponse) {
try {
String body = objectMapper.writeValueAsString(orderResponse);
return new APIGatewayProxyResponseEvent()
.withStatusCode(200)
.withBody(body)
.withHeaders(Map.of("Content-Type", "application/json"));
} catch (Exception e) {
throw new RuntimeException("Failed to create success response", e);
}
}
private APIGatewayProxyResponseEvent createErrorResponse(int statusCode, String message) {
try {
Map<String, String> errorResponse = Map.of(
"error", message,
"timestamp", java.time.Instant.now().toString()
);
String body = objectMapper.writeValueAsString(errorResponse);
return new APIGatewayProxyResponseEvent()
.withStatusCode(statusCode)
.withBody(body)
.withHeaders(Map.of("Content-Type", "application/json"));
} catch (Exception e) {
throw new RuntimeException("Failed to create error response", e);
}
}
// Exception classes
public static class ValidationException extends RuntimeException {
public ValidationException(String message) { super(message); }
}
public static class PaymentException extends RuntimeException {
public PaymentException(String message) { super(message); }
}
}
// Data classes
class OrderRequest {
private String orderId;
private String customerId;
private double totalAmount;
private String paymentMethod;
private java.util.List<OrderItem> items;
// Constructors, getters, and setters
public OrderRequest() {}
public String getOrderId() { return orderId; }
public void setOrderId(String orderId) { this.orderId = orderId; }
public String getCustomerId() { return customerId; }
public void setCustomerId(String customerId) { this.customerId = customerId; }
public double getTotalAmount() { return totalAmount; }
public void setTotalAmount(double totalAmount) { this.totalAmount = totalAmount; }
public String getPaymentMethod() { return paymentMethod; }
public void setPaymentMethod(String paymentMethod) { this.paymentMethod = paymentMethod; }
public java.util.List<OrderItem> getItems() { return items; }
public void setItems(java.util.List<OrderItem> items) { this.items = items; }
}
class OrderItem {
private String productId;
private String productName;
private int quantity;
private double price;
// Constructors, getters, and setters
public OrderItem() {}
public String getProductId() { return productId; }
public void setProductId(String productId) { this.productId = productId; }
public String getProductName() { return productName; }
public void setProductName(String productName) { this.productName = productName; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
}
class OrderResponse {
private String orderId;
private String status;
private String message;
private String requestId;
public OrderResponse(String orderId, String status, String message, String requestId) {
this.orderId = orderId;
this.status = status;
this.message = message;
this.requestId = requestId;
}
// Getters and setters
public String getOrderId() { return orderId; }
public void setOrderId(String orderId) { this.orderId = orderId; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getRequestId() { return requestId; }
public void setRequestId(String requestId) { this.requestId = requestId; }
}
SAM Template Configuration
template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Parameters:
LumigoTracerToken:
Type: String
Description: Lumigo tracer token
NoEcho: true
Globals:
Function:
Timeout: 30
MemorySize: 512
Runtime: java11
Environment:
Variables:
LUMIGO_TRACER_TOKEN: !Ref LumigoTracerToken
LUMIGO_DEBUG: "true"
POWERTOOLS_SERVICE_NAME: order-service
Resources:
OrderProcessingFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: order-processing
CodeUri: target/order-service.jar
Handler: com.example.lumigo.handlers.OrderProcessingHandler::handleRequest
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref OrdersTable
- SQSSendMessagePolicy:
QueueName: !GetRef InventoryUpdatesQueue
- S3ReadPolicy:
BucketName: !Ref ConfigBucket
Events:
ApiEvent:
Type: Api
Properties:
Path: /orders
Method: post
Layers:
- !Sub "arn:aws:lambda:${AWS::Region}:114300393969:layer:lumigo-java-tracer:32"
OrdersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: orders-table
AttributeDefinitions:
- AttributeName: orderId
AttributeType: S
KeySchema:
- AttributeName: orderId
KeyType: HASH
BillingMode: PAY_PER_REQUEST
InventoryUpdatesQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: inventory-updates-queue
ConfigBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub "order-service-config-${AWS::AccountId}"
Outputs:
OrderProcessingApi:
Description: "API Gateway endpoint URL for Order Processing function"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/orders"
OrderProcessingFunction:
Description: "Order Processing Lambda Function ARN"
Value: !GetAtt OrderProcessingFunction.Arn
Performance Monitoring
Custom Metrics and Monitoring
package com.example.lumigo.monitoring;
import com.example.lumigo.LumigoTracer;
import java.util.concurrent.atomic.AtomicLong;
public class PerformanceMonitor {
private static final AtomicLong totalRequests = new AtomicLong(0);
private static final AtomicLong successfulRequests = new AtomicLong(0);
private static final AtomicLong failedRequests = new AtomicLong(0);
private static final AtomicLong totalProcessingTime = new AtomicLong(0);
public static void recordRequestStart() {
totalRequests.incrementAndGet();
}
public static void recordRequestSuccess(long processingTime) {
successfulRequests.incrementAndGet();
totalProcessingTime.addAndGet(processingTime);
// Send custom metrics to Lumigo
LumigoTracer.addCustomMetric("requests.successful", 1);
LumigoTracer.addCustomMetric("processing.time", processingTime);
}
public static void recordRequestFailure(String errorType) {
failedRequests.incrementAndGet();
LumigoTracer.addCustomMetric("requests.failed", 1);
LumigoTracer.addCustomMetric("error." + errorType, 1);
}
public static void recordColdStart() {
LumigoTracer.addCustomMetric("coldStarts", 1);
}
public static void recordMemoryUsage() {
Runtime runtime = Runtime.getRuntime();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
long maxMemory = runtime.maxMemory();
LumigoTracer.addCustomMetric("memory.used", usedMemory);
LumigoTracer.addCustomMetric("memory.max", maxMemory);
LumigoTracer.addCustomMetric("memory.percent", (usedMemory * 100) / maxMemory);
}
public static void calculateAndEmitMetrics() {
long total = totalRequests.get();
long successful = successfulRequests.get();
long failed = failedRequests.get();
if (total > 0) {
double successRate = (successful * 100.0) / total;
double averageProcessingTime = totalProcessingTime.get() / successful;
LumigoTracer.addCustomMetric("success.rate", successRate);
LumigoTracer.addCustomMetric("avg.processing.time", averageProcessingTime);
}
}
}
Testing with Lumigo
Unit Tests
package com.example.lumigo;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class LumigoTracerTest {
@Mock
private io.lumigo.core.Lumigo lumigo;
@Test
void testTraceOperationSuccess() {
AtomicBoolean operationExecuted = new AtomicBoolean(false);
String result = LumigoTracer.traceOperation("testOperation",
Map.of("testKey", "testValue"),
() -> {
operationExecuted.set(true);
return "success";
});
assertTrue(operationExecuted.get());
assertEquals("success", result);
}
@Test
void testTraceOperationException() {
assertThrows(RuntimeException.class, () -> {
LumigoTracer.traceOperation("testOperation", () -> {
throw new RuntimeException("Test exception");
});
});
}
@Test
void testAddCustomMetric() {
// This would test metric addition
// Implementation depends on actual Lumigo API
}
}
class OrderProcessingHandlerTest {
@Test
void testOrderProcessingSuccess() {
// Test successful order processing
// Mock AWS services and verify Lumigo tracing calls
}
@Test
void testOrderProcessingValidationError() {
// Test validation errors
// Verify appropriate Lumigo tags are set
}
@Test
void testColdStartTracking() {
// Test cold start detection and tracking
}
}
Best Practices
1. Environment-specific Configuration
package com.example.lumigo.config;
import java.util.Map;
public class EnvironmentConfig {
public enum Environment {
DEV, STAGING, PRODUCTION
}
public static Environment getCurrentEnvironment() {
String env = System.getenv("ENVIRONMENT");
if ("production".equalsIgnoreCase(env)) {
return Environment.PRODUCTION;
} else if ("staging".equalsIgnoreCase(env)) {
return Environment.STAGING;
} else {
return Environment.DEV;
}
}
public static Map<String, String> getLumigoConfig() {
Environment env = getCurrentEnvironment();
Map<String, String> config = Map.of(
"LUMIGO_TRACER_TOKEN", System.getenv("LUMIGO_TRACER_TOKEN"),
"LUMIGO_DEBUG", env == Environment.PRODUCTION ? "false" : "true",
"LUMIGO_TAGS", getEnvironmentTags(env)
);
return config;
}
private static String getEnvironmentTags(Environment env) {
return String.format("environment=%s,version=%s",
env.name().toLowerCase(),
System.getenv("APP_VERSION") != null ? System.getenv("APP_VERSION") : "unknown");
}
}
2. Error Handling and Retry Logic
package com.example.lumigo.resilience;
import com.example.lumigo.LumigoTracer;
import java.util.function.Supplier;
public class RetryHandler {
public static <T> T withRetry(Supplier<T> operation, String operationName, int maxRetries) {
int attempt = 0;
Exception lastException = null;
while (attempt <= maxRetries) {
try {
return LumigoTracer.traceOperation(
operationName + (attempt > 0 ? "-retry-" + attempt : ""),
Map.of("attempt", attempt, "maxRetries", maxRetries),
operation);
} catch (Exception e) {
lastException = e;
attempt++;
if (attempt <= maxRetries) {
LumigoTracer.addCustomMetric("retries." + operationName, 1);
try {
Thread.sleep(calculateBackoff(attempt));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("Retry interrupted", ie);
}
}
}
}
throw new RuntimeException("Operation failed after " + maxRetries + " retries", lastException);
}
private static long calculateBackoff(int attempt) {
return Math.min(1000 * (long) Math.pow(2, attempt), 30000); // Exponential backoff, max 30s
}
}
Conclusion
This comprehensive Lumigo implementation for Java serverless applications provides:
- Automatic distributed tracing across Lambda functions and AWS services
- Custom business context for better debugging and monitoring
- Performance monitoring with custom metrics
- Error tracking and resilience patterns
- Environment-specific configurations for different deployment stages
Key benefits include:
- End-to-end visibility across your serverless architecture
- Reduced debugging time with detailed traces and context
- Performance optimization insights
- Production-ready error handling and monitoring
- AWS service integration with automatic instrumentation
The implementation follows serverless best practices and provides a solid foundation for building observable, maintainable serverless applications in Java.