Monitoring & Observability
CloudWatch, CloudTrail, and X-Ray give you full visibility into what your systems are doing in production: metrics, logs, traces, anomaly detection, and contributor analysis.
Introduction
The three pillars of observability are metrics, logs, and traces. AWS provides managed services for each: CloudWatch handles metrics and logs, X-Ray provides distributed tracing, and CloudTrail records every API call made in your account. Beyond the basics, CloudWatch Logs Insights lets you query logs with a SQL-like language, ML-based anomaly detection flags unusual patterns automatically, and Contributor Insights reveals which specific entities (IPs, users, functions) are driving operational problems.
Quick Navigation
1. Metrics & Alarms
Custom metrics, threshold alarms, ASG actions
2. CloudWatch Logs
Hierarchy, managed log sources, Agent, Insights queries, subscription filters
3. CloudTrail
API audit trail, security forensics, compliance
4. X-Ray Tracing
Distributed traces, latency segments, SDK instrumentation
5. Log Anomaly Detection
ML baselines, anomaly scores, pattern suppression
6. Contributor Insights
Top contributors to ops problems, DDoS detection
AWS Observability Stack
Every AWS service emits metrics to CloudWatch; CloudTrail captures every API call for audit
1. CloudWatch Metrics and Alarms
Every AWS service automatically publishes metrics to CloudWatch. EC2 publishes CPU, network, and disk I/O. Lambda publishes invocation count, duration, errors, and throttles. RDS publishes connections, read/write IOPS, and free storage. You can also publish custom metrics from application code.
Alarms watch a metric and trigger an action, send an SNS notification, scale an ASG, or stop an EC2 instance, when a threshold is breached:
# Create an alarm: alert when EC2 CPU > 80% for 5 minutes aws cloudwatch put-metric-alarm \ --alarm-name "HighCPU-web-server" \ --alarm-description "CPU above 80% for 5 min" \ --metric-name CPUUtilization \ --namespace AWS/EC2 \ --statistic Average \ --period 300 \ --threshold 80 \ --comparison-operator GreaterThanOrEqualToThreshold \ --evaluation-periods 1 \ --dimensions Name=InstanceId,Value=i-0123456789abcdef0 \ --alarm-actions arn:aws:sns:us-east-1:123456789012:ops-alerts # List alarms currently in ALARM state aws cloudwatch describe-alarms --state-value ALARM
Publish custom business metrics from Python:
import boto3
cw = boto3.client("cloudwatch")
def record_order_latency(duration_ms: float, service: str) -> None:
cw.put_metric_data(
Namespace="MyApp/Orders",
MetricData=[{
"MetricName": "ProcessingLatency",
"Dimensions": [{"Name": "Service", "Value": service}],
"Value": duration_ms,
"Unit": "Milliseconds",
}]
)
import time
start = time.time()
process_order(order_id)
record_order_latency((time.time() - start) * 1000, "OrderService")2. CloudWatch Logs - Deep Dive
Log Groups, Streams, and Events
CloudWatch Logs organizes data in a three-level hierarchy:
Log Group
Logical container for one application or service. Example: /aws/lambda/payment-service or /myapp/production. Retention policies and subscription filters are set at this level.
Log Stream
Sequence of events from a single source: one EC2 instance, one Lambda execution environment, or one container. Lambda creates a new stream per invocation environment automatically.
Log Event
A single timestamped message. Lambda stdout/stderr is captured automatically. EC2 and on-premises servers require the CloudWatch Agent to ship log files.
AWS Managed Log Sources
Several AWS services write to CloudWatch Logs automatically, or with a one-time configuration toggle. Each uses a predictable log group naming pattern, knowing it lets you navigate straight to the right group in the console or target it precisely in a Logs Insights query:
| Service | Log group pattern | Auto-enabled? | Contents / key fields |
|---|---|---|---|
| Lambda | /aws/lambda/{function-name} | Yes | stdout/stderr + REPORT lines; built-in fields: @duration, @billedDuration, @initDuration, @maxMemoryUsed, @requestId |
| API Gateway | Configurable per stage | Manual | Execution logs (full request/response detail) and access logs (custom format); each type is enabled separately per stage |
| RDS / Aurora | /aws/rds/instance/{identifier}/{log-type} | Manual | Four types per engine: general, slowquery, error, audit - enable per log type in the DB parameter group |
| ECS / Fargate | /ecs/{task-def-family}/{container-name} | awslogs driver | Container stdout/stderr; one log stream per task ID; set in the task definition's logConfiguration block |
| VPC Flow Logs | Configurable (or route to S3) | Manual | Source/dest IP, ports, protocol, bytes, packets, ACCEPT/REJECT; enable per VPC, subnet, or ENI; essential for network security analysis |
| CloudTrail | Configurable (or route to S3) | Opt-in per trail | Delivers management events to a log group, enabling real-time alerting via metric filters or EventBridge rules |
| Route 53 Query Logs | Configurable (e.g. /aws/route53/) | Manual | DNS query name, type, response code, resolver IP; Public Hosted Zone logs ship to CloudWatch Logs; Resolver query logs also support S3 and Kinesis Firehose as destinations |
| The following access logs route to S3, not CloudWatch Logs, query them with Amazon Athena or download directly | |||
| ALB / NLB Access Logs | S3 bucket (configurable prefix) | S3 only | Client IP, target IP/port, processing times, response code, bytes, user agent; NLB additionally logs TLS handshake and connection metadata |
| CloudFront Access Logs | S3 bucket (standard) / Kinesis (real-time) | S3 / Kinesis | Edge cache hit/miss, viewer IP, URI, response code, bytes, referrer; real-time logs add latency fields and route to Kinesis Data Streams for near-instant analysis |
| S3 Server Access Logs | Target S3 bucket (configurable prefix) | S3 only | Requester, bucket, key, operation, response code, bytes, error code; enable per source bucket in Properties - useful for audit and cost attribution of S3 traffic |
Lambda built-in fields: Lambda's REPORT line is automatically parsed into queryable @-prefixed fields. @initDuration only appears on cold starts, making filter ispresent(@initDuration) the standard way to count cold starts in Logs Insights. @billedDuration is rounded up to the nearest millisecond and is what Lambda actually charges you for.
CloudWatch Agent and Structured Logging
The CloudWatch Agent is a daemon you install on EC2 or on-premises servers. It reads log files and ships them to CloudWatch Logs, and also collects OS-level metrics (memory, disk) that CloudWatch does not capture natively. Configure it with a JSON file:
# Install the agent on Amazon Linux 2:
# sudo yum install -y amazon-cloudwatch-agent
# Config: /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json
{
"logs": {
"logs_collected": {
"files": {
"collect_list": [
{
"file_path": "/var/log/myapp/app.log",
"log_group_name": "/myapp/production",
"log_stream_name": "{instance_id}/app",
"timestamp_format": "%Y-%m-%dT%H:%M:%S"
},
{
"file_path": "/var/log/nginx/access.log",
"log_group_name": "/myapp/nginx",
"log_stream_name": "{instance_id}/nginx"
}
]
}
}
},
"metrics": {
"metrics_collected": {
"mem": { "measurement": ["mem_used_percent"] },
"disk": { "measurement": ["disk_used_percent"] }
}
}
}Emit structured JSON from application code so Logs Insights can filter and aggregate on individual fields rather than parsing raw text:
import json
import time
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def handler(event, context):
start = time.time()
result = process_order(event["orderId"])
# Emit structured JSON - every field becomes queryable in Logs Insights
logger.info(json.dumps({
"event": "order_processed",
"orderId": event["orderId"],
"userId": event["userId"],
"amount": result["amount"],
"duration_ms": round((time.time() - start) * 1000),
"status": "success",
}))
return result
# In Logs Insights you can now query individual fields:
# fields @timestamp, orderId, duration_ms, status
# | filter status = "success"
# | stats avg(duration_ms) as avg_ms by bin(5m)Subscription Filters and Archival
A subscription filter routes log events in real time to another AWS service. Use this to archive logs to S3, trigger Lambda functions on specific patterns, or feed logs into OpenSearch for interactive dashboards:
CloudWatch Logs Routing
Subscription filters fan out logs to S3 (compliance), Lambda (real-time alerts), or OpenSearch (dashboards)
import boto3
logs = boto3.client("logs")
# Route ALL logs to S3 via Kinesis Firehose (compliance archival)
# roleArn is required for Firehose destinations: CloudWatch Logs assumes this
# role to call firehose:PutRecordBatch. Lambda destinations use resource policies
# instead, so no roleArn is needed for the Lambda filter below.
logs.put_subscription_filter(
logGroupName="/myapp/production",
filterName="all-logs-to-s3",
filterPattern="", # empty pattern matches every log event
destinationArn="arn:aws:firehose:us-east-1:123456789012:deliverystream/logs-to-s3",
roleArn="arn:aws:iam::123456789012:role/CloudWatchLogsToFirehoseRole",
)
# Route only ERROR logs to a Lambda for real-time alerting
logs.put_subscription_filter(
logGroupName="/myapp/production",
filterName="errors-to-lambda",
filterPattern='{ $.status = "error" }',
destinationArn="arn:aws:lambda:us-east-1:123456789012:function:error-alerter",
)
# Set a 90-day retention policy on the log group
logs.put_retention_policy(
logGroupName="/myapp/production",
retentionInDays=90,
)Metric Filters
Turn log patterns into CloudWatch metrics without any code changes. Count occurrences of "ERROR" in a log group and create an alarm on the resulting metric. Useful for surfacing errors from applications that only write to stdout.
Retention Policies
CloudWatch Logs retains data indefinitely by default. Set a retention period (7, 14, 30, 90 days, etc.) on every log group to control costs. Pair with a subscription filter to S3 for long-term compliance storage.
CloudWatch Logs Insights - Query Language
Logs Insights is an interactive query engine built into CloudWatch. Queries run against one or more log groups and return results in seconds. The language has six core commands:
fieldsSelect specific log fields to display. Works on JSON keys and built-in fields: @timestamp, @message, @duration, @requestId, @initDuration.
filterKeep only events matching a condition. Supports =, !=, <, >, like /regex/, in [...], and ispresent() for optional fields.
statsAggregate with count(*), sum(), avg(), min(), max(), pct(field, N). Group results with by field or by bin(1h) for time buckets.
sortOrder results by any field, asc or desc.
limitCap the number of results returned.
parseExtract fields from unstructured text using glob patterns (* wildcards) or regex named capture groups (?<field>...).
Practical examples - run these in the CloudWatch console or via the CLI:
# 1. Top 10 slowest Lambda invocations
fields @timestamp, @duration, @requestId
| sort @duration desc
| limit 10
# 2. Count errors by error code (structured JSON logs)
fields event, errorCode
| filter status = "error"
| stats count(*) as total by errorCode
| sort total desc
# 3. p99 and average latency per endpoint
fields path, duration_ms
| filter ispresent(duration_ms)
| stats pct(duration_ms, 99) as p99_ms,
avg(duration_ms) as avg_ms,
count(*) as requests by path
| sort p99_ms desc
# 4. Lambda cold start frequency per hour
filter ispresent(@initDuration)
| stats count(*) as cold_starts,
avg(@initDuration) as avg_cold_start_ms
by bin(1h)
# 5. Compare exception counts across multiple services
# (select multiple log groups in the console before running)
fields @logStream, @message
| filter @message like /Exception/
| stats count(*) as exceptions by @logStream
| sort exceptions descCross-log-group queries: In the console, select multiple log groups before running a query to compare error rates or latency across services side by side. Queries can be saved and pinned to CloudWatch Dashboards for ongoing monitoring.
3. CloudTrail - API Audit Log
CloudTrail records every API call made to AWS - through the console, CLI, SDK, or another AWS service. Each event includes who made the call, from which IP, at what time, with what parameters, and whether it succeeded.
Security forensics - who deleted that S3 bucket? Who changed the security group?
Compliance - SOC 2, PCI-DSS, and HIPAA all require API audit logs
Anomaly detection - alert on unusual API calls via EventBridge rules
Enable CloudTrail immediately on every new AWS account. Create a trail that logs to S3 with log file validation enabled. Events are free for 90 days in Event History; creating a trail archives them permanently to S3 at very low cost.
4. X-Ray - Distributed Tracing
X-Ray traces requests through distributed systems. When a request enters via API Gateway, passes through Lambda, queries DynamoDB, and calls an external API, X-Ray shows a timeline with the latency of each segment. This pinpoints performance bottlenecks across service boundaries.
Enable it in Lambda by toggling Active Tracing in the function configuration. For EC2 or ECS, install the X-Ray daemon and instrument your code with the X-Ray SDK:
import boto3
from aws_xray_sdk.core import xray_recorder, patch_all
patch_all() # auto-instruments boto3, requests, psycopg2, etc.
@xray_recorder.capture("process_order")
def process_order(order_id: str):
dynamodb = boto3.resource("dynamodb")
# X-Ray automatically traces this DynamoDB call as a sub-segment
return dynamodb.Table("Orders").get_item(Key={"orderId": order_id})5. Log Anomaly Detection
CloudWatch Logs Anomaly Detection uses machine learning to build a baseline of your log patterns. It continuously scores incoming events against that baseline and flags patterns that deviate significantly, without any manual threshold configuration. This catches problems you would not think to write an alarm for.
How it works
- AWS clusters log lines into patterns - recurring templates like "Request failed with status *"
- A baseline frequency is learned for each pattern over time
- When a pattern's frequency spikes or drops, an anomaly score is assigned
- High-scoring anomalies appear in the console and can trigger CloudWatch Alarms
Suppression
Scheduled health checks, batch jobs, and deployment events all cause temporary log pattern shifts. Suppress known patterns so they never surface again.
Suppression can be time-limited (e.g., during a deployment window) or permanent for patterns you know are benign.
Anomaly Detection Flow
The detector scans patterns on each evaluation window; the alarm fires only when the score crosses your threshold
import boto3
logs = boto3.client("logs")
# Create an anomaly detector - AWS builds an ML baseline from your log patterns
response = logs.create_log_anomaly_detector(
logGroupArnList=[
"arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/payment-service"
],
detectorName="payment-service-anomalies",
evaluationFrequency="FIVE_MIN",
anomalyVisibilityTime=14, # retain detected anomalies for 14 days
)
detector_arn = response["anomalyDetectorArn"]
# List active anomalies (after the detector has collected enough data)
result = logs.list_anomalies(
anomalyDetectorArn=detector_arn,
suppressionState="UNSUPPRESSED",
)
for anomaly in result["anomalies"]:
print(f"Pattern: {anomaly['patternString']}")
print(f"Score: {anomaly['anomalyScore']}")
# Suppress a known noisy pattern permanently
# (useful for scheduled health checks or routine deployment logs)
if result["anomalies"]:
logs.update_anomaly(
anomalyDetectorArn=detector_arn,
anomalyId=result["anomalies"][0]["anomalyId"],
suppressionType="INFINITE",
)6. Contributor Insights
Anomaly detection tells you that something changed. Contributor Insights tells you who or what is causing it. You define a rule that groups log events by one or more fields and ranks the top-N contributors by count or sum. Results update in real time as new log events arrive.
DDoS Early Warning
Rank source IPs by 4xx/5xx count. A surge from one IP appears at the top before your rate limiter triggers.
Noisy Tenant Diagnosis
In multi-tenant SaaS, rank by account ID to find which customer is consuming disproportionate API capacity.
Cost Attribution
Rank Lambda functions by invocation count or duration to identify the biggest cost drivers in a shared account.
Error Source Triage
Rank by user agent, endpoint, or region to find which combination generates the most exceptions.
Rules are JSON objects that specify the log group, log format, which field to aggregate on, and optional filters. Here is a rule that surfaces the top IPs generating 5xx errors:
import json
import boto3
from datetime import datetime, timedelta, timezone
cw = boto3.client("cloudwatch")
# Rule: find the top IP addresses generating 5xx HTTP errors
# Requires ALB access logs delivered to CloudWatch Logs in JSON format
rule_body = json.dumps({
"Schema": {"Name": "CloudWatchLogRule", "Version": 1},
"AggregateOn": "Count",
"Contribution": {
"Filters": [
{"Match": "$.status", "GreaterThan": 499}
],
"Keys": ["$.clientIp"],
},
"LogFormat": "JSON",
"LogGroupNames": ["/alb/access-logs"],
})
cw.put_insight_rule(
RuleName="top-5xx-source-ips",
RuleState="ENABLED",
RuleDefinition=rule_body,
)
# Query the top 10 contributors over the past hour
now = datetime.now(timezone.utc)
report = cw.get_insight_rule_report(
RuleName="top-5xx-source-ips",
StartTime=now - timedelta(hours=1),
EndTime=now,
Period=3600,
MaxContributorCount=10,
)
for contributor in report["Contributors"]:
ip = contributor["Keys"][0]
error_count = contributor["ApproximateAggregateValue"]
print(f"{ip}: {error_count:.0f} errors")Anomaly Detection vs. Contributor Insights: Use them together. Anomaly Detection fires the alarm ("error rate spiked"). Contributor Insights answers the follow-up question ("which IP or endpoint caused the spike"). Contributor Insights also works with DynamoDB table-level metrics, not just CloudWatch Logs.
Pricing Overview
Prices are for us-east-1; other regions may differ slightly.
| Service | Free Tier (monthly) | After Free Tier |
|---|---|---|
| Custom Metrics | 10 metrics | $0.30/metric/month |
| Alarms | 10 alarms | $0.10/alarm/month |
| Logs Ingestion | 5 GB combined* | $0.50/GB ingested |
| Logs Storage | Included in 5 GB* | $0.03/GB/month |
| Logs Insights Queries | Included in 5 GB* | $0.005/GB scanned |
| Contributor Insights | 1 rule + 1M events | $0.50/rule/month |
| Log Anomaly Detection | No separate charge - bundled into CloudWatch Logs | |
| X-Ray Traces | 100,000 traces | $5.00/million traces recorded |
| CloudTrail (first trail) | Management events free - you only pay standard S3 rates for log storage | |
| CloudTrail (additional trails) | - | $2.00/100K management events |
* The 5 GB CloudWatch Logs free tier is a combined quota across ingestion, storage, and Insights query data scanned. At scale, Logs ingestion ($0.50/GB) is the biggest cost driver, set retention policies on every log group and prefer narrow Logs Insights queries (use filter before stats) to minimize scanned data.
Key Takeaways
- CloudWatch Metrics - every AWS service publishes metrics automatically; create alarms to trigger SNS, ASG scaling, or EC2 actions
- Custom metrics - use
put_metric_datato push business metrics (order latency, payment failures) alongside infrastructure metrics - Logs hierarchy - Log Groups contain Log Streams; Lambda ships automatically, EC2 needs the CloudWatch Agent; emit structured JSON to make fields queryable in Insights
- Subscription Filters - route logs in real time to S3 via Firehose, Lambda for alerting, or OpenSearch for dashboards; set retention policies to control cost
- Logs Insights - use fields, filter, stats, sort, limit, parse;
pct(field, N)for percentile latency,bin(1h)for time-series aggregation - CloudTrail - enable immediately on every account; answers "who did what and when" for security and compliance
- X-Ray - distributed tracing that pinpoints latency across service boundaries; enable on Lambda with one toggle
- Log Anomaly Detection - ML baseline on log patterns catches unexpected behavior without manual threshold tuning; suppress known noisy patterns
- Contributor Insights - ranks top-N entities (IPs, accounts, functions) driving operational problems; pairs with anomaly detection for root cause analysis