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

λLambda / EC2CWCloudWatchMetrics + LogsCTCloudTrailAPI AuditXRX-RayTracesSNSSNS AlertS3S3 Archivemetrics + logsAPI callstrace segmentsalarm triggerlog archive

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:

ServiceLog group patternAuto-enabled?Contents / key fields
Lambda/aws/lambda/{function-name}Yesstdout/stderr + REPORT lines; built-in fields: @duration, @billedDuration, @initDuration, @maxMemoryUsed, @requestId
API GatewayConfigurable per stageManualExecution 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}ManualFour 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 driverContainer stdout/stderr; one log stream per task ID; set in the task definition's logConfiguration block
VPC Flow LogsConfigurable (or route to S3)ManualSource/dest IP, ports, protocol, bytes, packets, ACCEPT/REJECT; enable per VPC, subnet, or ENI; essential for network security analysis
CloudTrailConfigurable (or route to S3)Opt-in per trailDelivers management events to a log group, enabling real-time alerting via metric filters or EventBridge rules
Route 53 Query LogsConfigurable (e.g. /aws/route53/)ManualDNS 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 LogsS3 bucket (configurable prefix)S3 onlyClient IP, target IP/port, processing times, response code, bytes, user agent; NLB additionally logs TLS handshake and connection metadata
CloudFront Access LogsS3 bucket (standard) / Kinesis (real-time)S3 / KinesisEdge 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 LogsTarget S3 bucket (configurable prefix)S3 onlyRequester, 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

SVCApp ServicesLambda, EC2, ECSCWCloudWatch LogsFHKinesis FirehoseλAlert LambdaS3S3 ArchiveOSOpenSearchstdout / filesall logsERROR filterarchiveindex

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:

fields

Select specific log fields to display. Works on JSON keys and built-in fields: @timestamp, @message, @duration, @requestId, @initDuration.

filter

Keep only events matching a condition. Supports =, !=, <, >, like /regex/, in [...], and ispresent() for optional fields.

stats

Aggregate with count(*), sum(), avg(), min(), max(), pct(field, N). Group results with by field or by bin(1h) for time buckets.

sort

Order results by any field, asc or desc.

limit

Cap the number of results returned.

parse

Extract 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 desc

Cross-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

LambdaCW LogsAnomaly DetectorCW AlarmSNSemit log eventsbatch scan (5 min)score vs. ML baselinescore exceeds thresholdpublish alert

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.

ServiceFree Tier (monthly)After Free Tier
Custom Metrics10 metrics$0.30/metric/month
Alarms10 alarms$0.10/alarm/month
Logs Ingestion5 GB combined*$0.50/GB ingested
Logs StorageIncluded in 5 GB*$0.03/GB/month
Logs Insights QueriesIncluded in 5 GB*$0.005/GB scanned
Contributor Insights1 rule + 1M events$0.50/rule/month
Log Anomaly DetectionNo separate charge - bundled into CloudWatch Logs
X-Ray Traces100,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_data to 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