Exam Guide: Developer - Associate
ποΈ Domain 4: Troubleshooting And Optimization
π Task 2: Instrument Code For Observability
Observability goes beyond monitoring: it's about understanding the internal state of your system from its external outputs. This task tests your ability to instrument applications with AWS X-Ray for distributed tracing, implement structured logging with correlation IDs, set up CloudWatch alarms and notifications, and build health check endpoints. X-Ray concepts (traces, segments, subsegments, annotations vs metadata), structured logging best practices, and alarm configuration.
πConcepts
Logging vs Monitoring vs Observability
These three concepts build on each other.
| Aspect | Logging | Monitoring | Observability |
|---|---|---|---|
| What It Is | Recording discrete events | Tracking predefined metrics over time | Understanding system behaviour from outputs |
| Question It Answers | What happened? | Is the system healthy? | Why is the system behaving this way? |
| Data Type | Text entries (structured or unstructured) | Numeric time-series data | Logs + metrics + traces combined |
| Proactive vs Reactive | Reactive. Search after an issue | Proactive. Alerts before users notice | Both. Explore unknown unknowns |
| AWS Service(s) | CloudWatch Logs | CloudWatch Metrics, Alarms | X-Ray, CloudWatch Logs Insights, ServiceLens |
Example |
ERROR: DynamoDB timeout on table Orders |
Lambda Duration > 5s for 3 consecutive periods |
Trace shows DynamoDB call took 4.8s due to throttling |
The Three Pillars Of Observability
| Pillar | What It Provides | AWS Service |
|---|---|---|
| Logs | Detailed event records for debugging | CloudWatch Logs |
| Metrics | Numeric measurements for alerting and trending | CloudWatch Metrics |
| Traces | Request flow across distributed services | AWS X-Ray |
Monitoring tells you something is wrong. Observability tells you why. Metrics show a problem (high latency) but you need traces to find the root cause (a specific downstream service is slow)
AWS X-Ray Concepts
π‘ X-Ray is the answer to "How do you find which service is causing latency?"
X-Ray provides distributed tracing. It tracks requests as they flow through your application across multiple services.
| Concept | What It Is | Example |
|---|---|---|
| Trace | The complete journey of a single request through all services | API Gateway β Lambda β DynamoDB β SNS |
| Segment | A unit of work done by a single service | The Lambda function's execution |
| Subsegment | A subdivision of a segment for more detail | A specific DynamoDB call within the Lambda function |
| Trace ID | Unique identifier for the entire trace | 1-5f84c7a1-abcdef1234567890 |
| Sampling | Controls what percentage of requests are traced | Default: first request each second + 5% of additional |
| Service Map | Visual representation of your architecture and request flow | Shows services as nodes with latency and error rates |
| Trace Map | Visual representation of a single trace | Shows the path of one request through services |
X-Ray Annotations vs Metadata
| Feature | Annotations | Metadata |
|---|---|---|
| What It Stores | Key-value pairs (string, number, boolean) | Any JSON-serializable object |
| Indexed | Yes. Searchable in the X-Ray console | No. Not searchable |
| Use For | Filtering and grouping traces | Storing detailed debug data |
| Max Size | 50 annotations per trace | No practical limit |
| Example | userId: "CUST-001", orderType: "premium" |
Full request/response bodies, stack traces |
| Search Syntax | annotation.userId = "CUST-001" |
Cannot search by metadata |
Use annotations for values you want to search and filter by user IDs, order types, environment names.
Use metadata for detailed debugging data you only need when examining a specific trace.
Annotations are indexed and searchable, metadata is not.
π‘"How do you find all traces for a specific user?" The answer is annotations.
X-Ray Sampling Rules
| Setting | Default |
Purpose |
|---|---|---|
| Reservoir |
1 per second |
Guaranteed minimum traces per second |
| Rate |
5% (0.05) |
Percentage of additional requests to trace |
| Service Name |
* (all) |
Which services the rule applies to |
| HTTP Method |
* (all) |
Filter by GET, POST, etc. |
| URL Path |
* (all) |
Filter by specific paths |
| Priority |
10000 (default) |
Lower number = higher priority |
Custom Sampling Rule Example:
- Trace 100% of requests to
/api/orders(high-value endpoint) - Trace 1% of requests to
/api/health(noisy, low-value)
π‘ The default sampling rule traces the first request each second plus 5% of additional requests. This keeps costs manageable for high-traffic applications. You can create custom rules to trace more (or fewer) requests for specific paths or services. Sampling rules are evaluated in priority order. Lower priority number wins.
X-Ray Integration with AWS Services
| Service | How to Enable | What Gets Traced |
|---|---|---|
| Lambda | Toggle "Active tracing" in function configuration | Function execution, SDK calls |
| API Gateway | Enable X-Ray tracing on the stage | API request processing |
| ECS/Fargate | Run X-Ray daemon as sidecar container | Application SDK calls |
| EC2 | Install and run X-Ray daemon | Application SDK calls |
| Elastic Beanstalk | Enable in .ebextensions or console |
Application SDK calls |
| SNS/SQS | Automatic when upstream service is traced | Message propagation |
π‘ For Lambda, enabling "Active tracing" is all you need for basic traces. To add custom subsegments and annotations, you need the X-Ray SDK in your code. The X-Ray daemon runs automatically in Lambda yiu don't need to install it. For EC2 and ECS, you must install and run the daemon yourself.
Structured Logging Best Practices
| Practice | Why It Matters | Example |
|---|---|---|
| Use JSON Format | Machine-parseable, works with Logs Insights | {"level":"ERROR","message":"timeout"} |
| Include Correlation ID | Track requests across services | {"correlationId":"abc-123","service":"orders"} |
| Include Request Context | Debug without searching multiple logs | {"requestId":"req-1","userId":"user-5"} |
| Use Log Levels | Filter by severity | DEBUG, INFO, WARN, ERROR, FATAL |
| Avoid Sensitive Data | Security and compliance | Never log passwords, tokens, PII |
| Include timestamps | Accurate event ordering | ISO 8601 format: 2024-01-15T10:30:00Z
|
Correlation IDs for Distributed Tracing
A Correlation ID is a unique identifier that follows a request across all services:
Client β API Gateway β Lambda A β SQS β Lambda B β DynamoDB
correlationId: "abc-123" flows through every service
| Where to Set It | How |
|---|---|
| API Gateway | Pass X-Correlation-Id header or generate in mapping template |
| Lambda | Read from event headers or generate if missing |
| SQS | Include in message attributes |
| SNS | Include in message attributes |
| DynamoDB | Not applicable (use in logs only) |
π‘ Correlation IDs complement X-Ray traces. X-Ray gives you the visual trace map. Correlation IDs let you search logs across services for the same request. Use both together: add the correlation ID as an X-Ray annotation so you can find the trace from the log and vice versa.
CloudWatch Alarms
| Alarm Component | What It Does | Options |
|---|---|---|
| Metric | What you're monitoring | Any CloudWatch metric (built-in or custom) |
| Statistic | How to aggregate data points | Average, Sum, Min, Max, p99, SampleCount |
| Period | Time window for each data point | 10s, 30s, 60s, 300s, etc. |
| Evaluation periods | How many periods to evaluate | 1-100 |
| Datapoints to alarm | How many periods must breach threshold | 1 to evaluation periods |
| Threshold | The value that triggers the alarm | Any number |
| Comparison | How to compare metric to threshold | >, >=, <, <= |
| Actions | What happens when alarm fires | SNS notification, Auto Scaling, EC2 action, Lambda |
| Missing data | How to treat gaps in data | missing, notBreaching, breaching, ignore |
Alarm States:
| State | Meaning |
|---|---|
| OK | Metric is within the threshold |
| ALARM | Metric has breached the threshold |
| INSUFFICIENT_DATA | Not enough data to determine state |
Composite Alarms
| Feature | Standard Alarm | Composite Alarm |
|---|---|---|
| Based On | A single metric | Multiple alarms combined with AND/OR/NOT |
| Use Case | Monitor one metric | Reduce alarm noise, complex conditions |
| Example | Error rate > 5% | Error rate > 5% AND latency > 2s |
| Actions | SNS, Auto Scaling, EC2, Lambda | SNS, Lambda |
π‘ Composite alarms reduce alert fatigue. Instead of getting separate alerts for high errors and high latency, create a composite alarm that only fires when both conditions are true. Use
ANDfor "all conditions must be true" andORfor "any condition triggers." Composite alarms can suppress actions on child alarms to prevent duplicate notifications.
Health Check Patterns
| Pattern | What It Checks | Response |
|---|---|---|
| Shallow Health Check | Is the service running? | 200 OK (no dependency checks) |
| Deep Health Check | Are all dependencies healthy? | 200 OK with dependency status |
| Liveness Probe | I_s the process alive?_ | 200 OK (minimal check) |
| Readiness Probe | Can the service handle requests? | 200 OK only when fully initialized |
Deep Health Check Response Example:
{
"status": "healthy",
"timestamp": "2026-07-20T10:30:00Z",
"dependencies": {
"dynamodb": {"status": "healthy", "latency": "12ms"},
"sqs": {"status": "healthy", "latency": "8ms"},
"external-api": {"status": "degraded", "latency": "2500ms"}
}
}
π‘Health checks should have their own endpoint (e.g.,
/health) that doesn't require authentication. Shallow checks are fast and cheap: use them for load balancer health checks. Deep checks verify dependencies: use them for monitoring dashboards. Don't make deep health checks too expensive. Cache dependency status for a few seconds.
ποΈ Build An Observable Order Service
Build an Observable Order Service from scratch using the AWS Console:
- A Lambda function with X-Ray active tracing enabled
- Custom X-Ray subsegments and annotations in the code
- Structured JSON logging with correlation IDs
- CloudWatch alarms with SNS email notifications
- A health check endpoint that verifies dependencies
- Traces visible in the X-Ray console service map
Prerequisites
Part I
Create a Lambda Function with X-Ray Tracing
Step 01: Create the Function
Open the Lambda console β Create function
-
Function name:
ObservableOrderService - Runtime: Python 3.13
β Click Create function
Step 02: Enable X-Ray Active Tracing
Go to Configuration β Monitoring and operations tools β Edit
β Under CloudWatch Application Signals and AWS X-Ray β β Lambda services traces
β Click Save
Step 03: Add the X-Ray SDK Layer
β οΈ The instrumented code below uses
aws_xray_sdkfor custom subsegments and annotations. That package is **not in the Lambda runtime (and the AWSSDKPandas layer doesn't include it), so you'll getNo module named 'aws_xray_sdk'unless you add it. Build a layer in CloudShell (no local setup needed):**
mkdir -p xray-layer/python
pip install aws-xray-sdk -t xray-layer/python/
cd xray-layer && zip -r ../xray-sdk-layer.zip python/ && cd ..
β οΈ Download it (Actions β Download file β
xray-sdk-layer.zip), then in the Lambda console:
β Layers β Edit β Add layer β Create a new layer
β Name: aws-xray-sdk
β Source code: Create from a .zip file β Choose file β Upload the zip β Compatible runtimes optional: Python 3.13 β Create β Copy arn
β Open ObservableOrderService β Code β Layers β Edit β Add a layer β Specify an ARN β Paste arn β Verify β Add
π‘ Prefer no layer? You can skip the SDK entirely and rely on active tracing alone It auto-traces the invocation and any boto3/HTTP calls without importing anything. You'd just lose in-code custom subsegments/annotations. If you go that route, remove the
aws_xray_sdkimports andxray_recordercalls from the code below.β οΈ X-Ray permissions: if traces don't appear, attach
AWSXRayDaemonWriteAccessto the function's execution role (Configuration β Permissions β role β Add permissions). Active tracing needsxray:PutTraceSegmentsandxray:PutTelemetryRecords.β οΈ Alternatively, you can use the
aws_xray_sdkby packaging it with your function code. For this tutorial, we'll use the built-in boto3 tracing capabilities.
Step 04: Add Instrumented Code
Replace the function code with:
import json
import time
import boto3
from aws_xray_sdk.core import xray_recorder
from aws_xray_sdk.core import patch_all
patch_all()
def generate_correlation_id(event):
headers = event.get('headers', {}) or {}
return headers.get('X-Correlation-Id',
headers.get('x-correlation-id',
f"gen-{int(time.time() * 1000)}"))
def structured_log(level, message, correlation_id, **kwargs):
print(json.dumps({
"timestamp": time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
"level": level, "message": message,
"correlationId": correlation_id,
"service": "ObservableOrderService", **kwargs
}))
def lambda_handler(event, context):
correlation_id = generate_correlation_id(event)
action = event.get('action', event.get('path', '/process'))
# Facade-segment fix: annotate our OWN subsegment, not the Lambda root.
with xray_recorder.in_subsegment('request-handler') as subsegment:
subsegment.put_annotation('correlationId', correlation_id)
subsegment.put_annotation('action', str(action))
subsegment.put_metadata('event', event, 'request')
structured_log("INFO", "Request received", correlation_id,
action=action, requestId=context.aws_request_id)
if action in ('/health', 'health'):
return health_check(correlation_id)
return process_order(event, correlation_id, context)
def health_check(correlation_id):
with xray_recorder.in_subsegment('HealthCheck') as subsegment:
subsegment.put_annotation('checkType', 'deep')
dependencies = {}
try:
start = time.time()
boto3.client('sts').get_caller_identity()
latency = int((time.time() - start) * 1000)
dependencies['sts'] = {'status': 'healthy', 'latency': f'{latency}ms'}
except Exception as e:
dependencies['sts'] = {'status': 'unhealthy', 'error': str(e)}
overall = 'healthy' if all(d['status'] == 'healthy' for d in dependencies.values()) else 'degraded'
structured_log("INFO", f"Health check: {overall}", correlation_id, dependencies=dependencies)
return {
'statusCode': 200 if overall == 'healthy' else 503,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'status': overall, 'correlationId': correlation_id, 'dependencies': dependencies})
}
def process_order(event, correlation_id, context):
with xray_recorder.in_subsegment('ValidateInput') as subsegment:
order_id = event.get('orderId', f"ORD-{int(time.time())}")
customer_id = event.get('customerId')
subsegment.put_annotation('orderId', order_id)
if not customer_id:
subsegment.put_annotation('validationResult', 'failed')
structured_log("WARN", "Missing customerId", correlation_id, orderId=order_id)
return {
'statusCode': 400,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'customerId is required', 'correlationId': correlation_id})
}
subsegment.put_annotation('validationResult', 'passed')
with xray_recorder.in_subsegment('ProcessBusinessLogic') as subsegment:
subsegment.put_annotation('orderId', order_id)
subsegment.put_annotation('customerId', customer_id)
processing_time = 0.1 + (hash(order_id) % 5) / 10
time.sleep(processing_time)
subsegment.put_metadata('processingDetails',
{'orderId': order_id, 'processingTime': f'{processing_time:.1f}s', 'items': event.get('items', [])}, 'order')
structured_log("INFO", "Order processed successfully", correlation_id,
orderId=order_id, customerId=customer_id, processingTime=f"{processing_time:.1f}s")
return {
'statusCode': 200,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'message': 'Order processed', 'orderId': order_id,
'customerId': customer_id, 'correlationId': correlation_id})
}
β Click Deploy
β οΈ If the
aws_xray_sdkimport fails, you'll need to add it as a Lambda layer. Create a layer with the X-Ray SDK package, or use the simplified version without the SDK (X-Ray will still trace boto3 calls automatically with active tracing enabled).
Part II
Add Custom Subsegments and Annotations
Step 01: Test the Function
β Go to the Test tab
β Create a test event:
{
"action": "process",
"orderId": "ORD-2026-001",
"customerId": "CUST-100",
"items": [{"productId": "PROD-A", "quantity": 2}],
"headers": {"X-Correlation-Id": "test-corr-001"}
}
β Click Test β β οΈ **Verify the response includes the correlation ID
β Run it several more times with different order IDs
Step 02: Test the Health Check
β Create another test event
{
"action": "health"
}
β Click Test β β οΈ Verify the health check response with dependency status
Step 03: Test Validation Failure
Create a test event without customerId
{
"action": "process",
"orderId": "ORD-2026-002"
}
β Click Test β β οΈ verify the 400 response with validation error
Part III
View Traces in the X-Ray Console
Step 01: Open the X-Ray Trace Map
β Open the X-Ray console (or CloudWatch β Trace Map β ObservableOrderService)
β Set the time range to the last 30 minutes
β You should see a service map showing:
- Client β ObservableOrderService (Lambda Context) β ObservableOrderService (Lambda Function)
- If the function called other AWS services, those appear as nodes too
Step 02: Examine Individual Traces
β Click on the ObservableOrderService node in the service map
β Click View traces
β Click on a specific trace to see the timeline:
- The Lambda segment (total execution time)
- Initialization subsegment (cold start, if applicable)
- ValidateInput subsegment (your custom subsegment)
- ProcessBusinessLogic subsegment (your custom subsegment)
- HealthCheck subsegment (for health check requests)
Step 03: Search Traces by Annotation
β Go to Traces
β In the filter expression box, type:
annotation.correlationId = "test-corr-001"
β Click Run query β You should see only the traces with that correlation ID
π‘This is exactly why annotations are indexed and metadata is not. You can search for
annotation.userId = "CUST-100"to find all traces for a specific user. You cannot search by metadata values. Use annotations for the fields you'll want to filter by. Use metadata for the detailed data you'll examine once you've found the right trace.
Part IV
Set Up CloudWatch Alarms with SNS Notifications
Step 01: Create an SNS Topic
β Open the SNS console β Topics β Create topic
β Type: Standard
β Name: OrderServiceAlerts
β Click Create topic
Step 02: Create an Email Subscription
β Click Create subscription
β Protocol: Email
β Endpoint: your email address
β Click Create subscription
β Check your email and confirm the subscription
Step 03: Create an Error Rate Alarm
β Open the CloudWatch console β Alarms β Create alarm
β Click Select metric
β Navigate: Lambda β By Function Name β ObservableOrderService β Errors
β Click Select metric
- Statistic: Sum
- Period: 60 seconds
- Threshold type: Static
- Whenever Errors is: Greater than 0
- Datapoints to alarm: 1 out of 1
β Notification:
-
Alarm state trigger:
In alarm -
SNS topic:
OrderServiceAlerts
β Alarm name: OrderService-Errors
β Click Create alarm
Step 04: Create a Duration Alarm
β Create another alarm:
-
Metric: Lambda β
ObservableOrderServiceβ Duration - Statistic: p95
- Period: 300 seconds (5 minutes)
- Threshold: Greater than 3000 (3 seconds)
- Datapoints to alarm: 2 out of 3
-
SNS topic:
OrderServiceAlerts -
Alarm name:
OrderService-HighLatency
Step 05: Create a Composite Alarm
β Go to Alarms β Create alarm β Create composite alarm
β Alarm rule:
ALARM("OrderService-Errors") AND ALARM("OrderService-HighLatency")
β Notification: OrderServiceAlerts
β Alarm name: OrderService-Critical
β Click Create alarm
β οΈ This composite alarm only fires when both errors AND high latency occur simultaneously indicating a real problem, not just a transient spike.
Part V
Verify the Complete Observability Stack
Generate Traffic and Verify
β Go back to the Lambda function and run various test events:
- Normal orders (several times)
- Health checks
- Validation failures (missing customerId)
β Check CloudWatch Logs:
- Open the log group
/aws/lambda/ObservableOrderService - Verify structured JSON logs with correlation IDs
- Use Logs Insights to query:
parse @message '"correlationId":"*","service":"*"' as corrId, svc
| filter corrId like /test-corr/
| fields @timestamp, @message
| sort @timestamp desc
β Check X-Ray:
- View the trace map. Verify your function appears
- Search traces by annotation
- Examine subsegment timing
β Check CloudWatch Alarms:
- Verify alarms are in OK state
- Trigger errors to test the alarm β check your email for the SNS notification
β Check the Dashboard (optional):
- Create a dashboard combining Lambda metrics, custom metrics, and Logs Insights queries
π‘ A fully observable application has all three pillars working together: structured logs with correlation IDs (searchable in Logs Insights), metrics with alarms (proactive alerting), and traces with annotations (distributed request tracking). The correlation ID ties them all together. From a log entry, you can find the X-Ray trace, and from a trace, you can search the logs.
ποΈ What You Built | π Exam Concepts Recap
| What You Built | Exam Concept |
|---|---|
| Added structured logging with correlation IDs | Tracking a request across services |
| Created custom X-Ray subsegments | Measuring specific code sections for bottlenecks |
| Added X-Ray annotations and metadata | Annotations are indexed/searchable. Metadata is not |
Searched traces by annotation.correlationId |
Filtering traces by indexed fields |
| Enabled active tracing on Lambda | X-Ray daemon runs automatically, no install |
| Created error and latency CloudWatch alarms | Proactive alerting with SNS notifications |
| Built a composite alarm with AND logic | Reducing alert noise by combining conditions |
| Tied logs + metrics + traces via correlation ID | The three pillars of observability working together |
β οΈ Clean Up Protocol
-
Lambda β Delete
ObservableOrderService -
SNS β Delete the
OrderServiceAlertstopic (this also deletes subscriptions) - CloudWatch β Delete all three alarms (Errors, HighLatency, Critical composite)
-
CloudWatch β Delete the log group
/aws/lambda/ObservableOrderService - IAM β Delete the Lambda execution role
Key Takeaways
- Observability = logs + metrics + traces. Monitoring tells you something is wrong. Observability tells you why. X-Ray is the answer when the question asks about finding the root cause of latency in distributed systems.
- X-Ray annotations are indexed and searchable: use them for filtering traces by user ID, order ID, etc. Metadata is not searchable: use it for detailed debugging data.
- X-Ray subsegments let you measure specific parts of your code. Create subsegments for database calls, external API calls, and business logic to identify bottlenecks.
- Correlation IDs track a request across services. Generate one at the entry point and pass it through headers, message attributes, and log entries. Add it as an X-Ray annotation.
- Structured JSON logging is essential for Logs Insights queries. Include level, message, correlation ID, timestamp, and relevant context in every log entry.
-
CloudWatch alarms have three states: OK, ALARM, INSUFFICIENT_DATA. Configure
TreatMissingDatacarefully.notBreachingis usually the right choice for Lambda. - Composite alarms combine multiple alarms with AND/OR logic to reduce alert noise. They only fire when the combined condition is true.
- X-Ray sampling defaults to 1 request/second + 5% of additional requests. Create custom rules to trace more of high-value endpoints and less of health checks.
- Health checks should have shallow (is it running?) and deep (are dependencies healthy?) variants. Use shallow for load balancers, deep for dashboards.
- For Lambda, enable Lambda service traces in the function configuration. The X-Ray daemon runs automatically no installation needed. Use the X-Ray SDK to add custom subsegments and annotations.
Additional Resources
- What is AWS X-Ray?
- AWS X-Ray SDK for Python
- Using Amazon CloudWatch alarms
- Create a composite alarm
- Visualize Lambda function invocations using AWS X-Ray
ποΈ
Top comments (0)