The Trinity Beast Infrastructure — Unified Logs & Observability

Centralized Logging Architecture & Operational Intelligence
October 2026 Region: us-east-2 1 Unified Bucket 20 Log Groups 90-Day Retention < $0.50/mo

1. The Problem — Why This Was Needed

Before June 4, 2026, the Trinity Beast Infrastructure had a logging blind spot. Application code logged to CloudWatch — that part worked. But the surrounding infrastructure was largely invisible:

❌ Before — Scattered & Dark
  • ALB access logs: disabled — no record of individual HTTP requests to the API
  • CloudFront access logs: disabled — no record of website visitor requests
  • WAF request logs: disabled — only sampled CloudWatch metrics (block counts per rule, no request detail)
  • S3 access logs: disabled — no audit of who accessed deployed files
  • Aurora server logs: not exported — slow queries only visible via RDS console download
  • Valkey slow/engine logs: not exported — no visibility into cache performance issues
  • CloudWatch retention: infinite on 18 log groups — unbounded cost growth
  • No single place to look — context switching between console, S3, CloudWatch, API calls
✅ After — Unified & Complete
  • ALB logs: every API request → S3 (latency, status, target, client IP)
  • CloudFront logs: every website hit → S3 (edge location, cache status, bytes)
  • WAF logs: every API request → S3 (full headers, rule match, action, country)
  • S3 access logs: every object operation → S3 (requester, operation, key)
  • Aurora logs: slow queries + errors → CloudWatch (instantly searchable)
  • Valkey logs: slow commands + engine events → CloudWatch
  • All 20 log groups: explicit 90-day retention (controlled cost)
  • One S3 bucket, one lifecycle policy, one search path
The core insight: You cannot learn from a system you cannot observe. Every HTTP request, every database query, every cache miss, every WAF decision — these are the signals that reveal performance bottlenecks, security threats, usage patterns, and optimization opportunities. Without centralized logging, these signals evaporate the moment they occur. With it, they become searchable institutional memory.

What We Couldn't Do Before

What We Can Do Now

2. Architecture — The Unified Logging Model

The logging architecture splits into three tiers based on query pattern and retention needs:

Diagram 2.1 — Unified Logging Architecture — Three-Tier Model
flowchart TB
    %% Sources
    subgraph sources["Log Sources"]
        direction LR
        ALB["ALB
Trinity-Beast-TCP-ALB"] CF["CloudFront
E110PRKEIYQVLL"] WAF["WAF
trinity-beast-api-waf"] S3SRC["S3 Website
trinity-beast-website-east2"] ECS["ECS Containers
Main · Mirror · LRS · Webhook"] LAMBDA["Lambda Functions
10 functions"] AURORA["Aurora PostgreSQL
Writer + Reader"] VALKEY["Valkey 9.0 Serverless"] VPC["VPC Flow Logs
2 VPCs"] CT["CloudTrail
Multi-region"] end %% Tier 1 - S3 subgraph tier1["TIER 1 — S3: Long-Term Archive + Bulk Query"] direction TB BUCKET["aws-waf-logs-trinity-beast"] WAF_PREFIX["AWSLogs/.../WAFLogs/
JSON · gzipped · every 5 min"] ALB_PREFIX["alb/
space-delimited · gzipped · every 5 min"] CF_PREFIX["cloudfront/
tab-delimited · gzipped · every few min"] S3_PREFIX["s3-access/
space-delimited · best effort"] LIFECYCLE["Lifecycle: Standard 0-90d → Glacier IR 90-365d → Delete"] end %% Tier 2 - CloudWatch subgraph tier2["TIER 2 — CloudWatch: Real-Time + Instant Search"] direction TB CW_ECS["/aws/ecs/trinity-beast
4 containers · 1 group · UME self-identifies"] CW_LAMBDA["/aws/lambda/*
10 function log groups (us-east-2)"] CW_AURORA["/aws/rds/cluster/.../postgresql
Slow queries · Lock waits · Errors"] CW_VALKEY["/aws/elasticache/.../slow-log + engine-log
Slow commands · Engine events"] CW_VPC["/aws/vpc/trinity-beast-flowlogs
Both VPCs · Accept/Reject"] CW_CT["/aws/cloudtrail/trinity-beast
30-day window · S3 has full archive"] CW_INSIGHTS["Container Insights + RDSOSMetrics"] RETENTION["All groups: 90-day retention"] end %% Tier 3 - Application State subgraph tier3["TIER 3 — Valkey + Aurora: Application-Level State"] direction TB VK_OPS["autoops:actions:log · threats:daily
Autonomous actions · Threat summaries"] VK_HONEY["honeypot:log · honeypot:ip:*
Trap hits · Per-IP forensics"] VK_TX["tx:job:* · tx:history
Translation job progress"] VK_REPORT["report:text:* · docs:session:log
Daily reports · Session history"] AU_EVENTS["translation_job_events
usage_logs · support_tickets"] AU_CRON["cron.job_run_details
pg_cron execution history"] end %% Connections - Tier 1 WAF -->|"full request logs"| WAF_PREFIX ALB -->|"access logs"| ALB_PREFIX CF -->|"standard logs"| CF_PREFIX S3SRC -->|"server access"| S3_PREFIX WAF_PREFIX --> BUCKET ALB_PREFIX --> BUCKET CF_PREFIX --> BUCKET S3_PREFIX --> BUCKET BUCKET --> LIFECYCLE %% Connections - Tier 2 ECS -->|"stdout/stderr"| CW_ECS LAMBDA -->|"execution logs"| CW_LAMBDA AURORA -->|"pg logs export"| CW_AURORA VALKEY -->|"slow + engine"| CW_VALKEY VPC -->|"flow logs"| CW_VPC CT -->|"API events"| CW_CT ECS -->|"metrics"| CW_INSIGHTS AURORA -->|"OS metrics"| CW_INSIGHTS %% Connections - Tier 3 ECS -->|"app writes"| VK_OPS ECS -->|"honeypot hits"| VK_HONEY ECS -->|"job state"| VK_TX ECS -->|"reports"| VK_REPORT ECS -->|"audit rows"| AU_EVENTS AURORA -->|"cron results"| AU_CRON %% Also CloudTrail to S3 CT -->|"archive"| BUCKET %% Styling classDef s3Style fill:#1a365d,stroke:#60a5fa,color:#e2e8f0 classDef cwStyle fill:#14532d,stroke:#10b981,color:#e2e8f0 classDef appStyle fill:#4c1d95,stroke:#a78bfa,color:#e2e8f0 classDef sourceStyle fill:#1e293b,stroke:#FF9900,color:#e2e8f0 classDef bucketStyle fill:#0f172a,stroke:#FF9900,color:#FF9900 class WAF_PREFIX,ALB_PREFIX,CF_PREFIX,S3_PREFIX,LIFECYCLE s3Style class CW_ECS,CW_LAMBDA,CW_AURORA,CW_VALKEY,CW_VPC,CW_CT,CW_INSIGHTS,RETENTION cwStyle class VK_OPS,VK_HONEY,VK_TX,VK_REPORT,AU_EVENTS,AU_CRON appStyle class ALB,CF,WAF,S3SRC,ECS,LAMBDA,AURORA,VALKEY,VPC,CT sourceStyle class BUCKET bucketStyle

Request Lifecycle — What Gets Logged Where

A single API request generates log entries across multiple tiers. This diagram traces a request from edge to database, showing where each log event lands:

Diagram 2.2 — Request Lifecycle — Log Entry Points Across Tiers
sequenceDiagram
    participant Client as Client (Browser/SDK)
    participant CF as CloudFront
    participant WAF as WAF
    participant ALB as ALB
    participant ECS as ECS Container
    participant Aurora as Aurora
    participant Valkey as Valkey
    participant S3Log as S3 Logs Bucket

    Note over Client,S3Log: A single GET /price?asset=BTC request

    Client->>CF: HTTPS request
    Note right of CF: 📝 CloudFront log → S3
cloudfront/ prefix
(edge, IP, URI, cache hit/miss) CF->>WAF: Forward to origin Note right of WAF: 📝 WAF log → S3
AWSLogs/.../WAFLogs/
(IP, headers, rule match, ALLOW) WAF->>ALB: Passed rules Note right of ALB: 📝 ALB access log → S3
alb/ prefix
(latency, status, target IP) ALB->>ECS: Route to healthy target Note right of ECS: 📝 Container log → CloudWatch
/aws/ecs/trinity-beast
(UME: cluster_node, endpoint, api_key_id) ECS->>Valkey: Check price cache Note right of Valkey: (no log unless slow command) ECS->>Aurora: Query if cache miss Note right of Aurora: 📝 If >1s → PostgreSQL log
/aws/rds/.../postgresql ECS-->>Client: UME Response (200) Note over Client,S3Log: Result: 4-6 log entries for ONE request across 3-4 destinations

Design Decisions

Why one S3 bucket for all log types?

Single lifecycle policy. Single IAM scope. Single place to point Athena. One less thing to remember. The bucket is named aws-waf-logs-trinity-beast because WAF enforces a naming convention (aws-waf-logs-* prefix required). Rather than fight it, we made the WAF-compliant name the home for everything.

Why WAF logs on S3 instead of CloudWatch?

Cost. CloudWatch Logs ingestion is $0.50/GB. S3 Standard is $0.023/GB/month for storage + $0 for delivery. At ~50 MB/day of WAF logs, S3 saves ~$0.70/month vs CloudWatch. More importantly, S3 logs are Athena-queryable — we can run SQL across millions of WAF events without building a pipeline.

Why container logs don't need separate groups

All four LPO containers (Main, Mirror, LRS, Webhook) run the same binary and log the same way. Every structured log line carries UME fields: cluster_node (which container), agent_profile_arn (which actor), endpoint (what path), ip_address (who called). There is zero ambiguity about origin. Keeping them in one log group means one search covers the entire cluster — no need to check each node separately when hunting for an error or tracing a request.

Why keep CloudWatch for application logs?

Instant search. When a Lambda errors or a container crashes, you need the answer in seconds — not minutes downloading from S3. CloudWatch's filter-log-events gives sub-second search across application logs. The tradeoff is worth it: application logs are low-volume and high-urgency; infrastructure logs are high-volume and low-urgency.

3. S3 Unified Logs Bucket

Bucket Name
aws-waf-logs-trinity-beast
Region
us-east-2
Lifecycle
365d
Encryption
SSE-S3
ACL Mode
BucketOwnerPreferred
Log Sources
4

3.1 Prefix Structure

PrefixSourceFormatDelivery
AWSLogs/211998422884/WAFLogs/us-east-2/trinity-beast-api-waf/WAF (ALB)JSON (gzipped), one event per lineEvery 5 minutes
alb/AWSLogs/211998422884/elasticloadbalancing/us-east-2/ALBSpace-delimited (gzipped)Every 5 minutes
cloudfront/CloudFrontTab-delimited (gzipped), W3C extended formatEvery few minutes
s3-access/S3 Server AccessSpace-delimited (not compressed)Best-effort (minutes)

3.2 What Each Log Contains

WAF Logs (richest security signal)

ALB Access Logs

CloudFront Standard Logs

S3 Server Access Logs

3.3 Bucket Policy

The bucket policy grants write access to five AWS service principals, verified live against get-bucket-policy:

PrincipalAccessPath
delivery.logs.amazonaws.comPutObject + GetBucketAcl/ListBucket (bucket-level)AWSLogs/211998422884/* (WAF)
logdelivery.elasticloadbalancing.amazonaws.comPutObjectalb/AWSLogs/211998422884/*
arn:aws:iam::033677994240:rootPutObjectalb/AWSLogs/211998422884/* (ALB, us-east-2 ELB account)
logging.s3.amazonaws.comPutObjects3-access/*

Not yet explained: a sixth statement grants ses.amazonaws.com PutObject on inbound-email/* in this bucket. The prefix holds zero objects, and Section 9 documents the live inbound-email path as landing in a separate us-east-1 bucket (tbi-support-inbound) instead. This statement predates that design or was never cleaned up — flagged here rather than removed, since it costs nothing to leave and its origin isn't confirmed.

3.4 Lifecycle Rules — How Long Each Log Is Kept

AgeStorage ClassCost/GB/MonthAccess Speed
0–90 daysS3 Standard$0.023Instant (milliseconds)
90–365 daysGlacier Instant Retrieval$0.004Instant (milliseconds)
>365 daysDeleted——

3.5 Delivery Freshness — Configuration Is Not Delivery

A feed's configuration says nothing about whether it is actually delivering — get-logging-configuration returning a destination is not evidence, only object recency is. Every S3-delivered feed here is checked for freshness against a per-feed threshold: WAF and ALB at 30 minutes, CloudFront at 360 minutes, S3 access at 720 minutes. WAF full-request logs specifically deliver every 5 minutes and are expected to show a newest object well inside that 30-minute window; anything older is a stale-feed signal, not a quiet-traffic one.

A second, region-locked bucket also exists: aws-waf-logs-trinity-beast-use1 in us-east-1, created as the future home for anything that must be delivered in that region (principally CloudFront WAF logs, if that logging is ever enabled). It is empty by design today and deliberately excluded from the freshness check for that reason — an empty prefix would otherwise false-alarm as stale.

4. CloudWatch Log Groups (Application Logs) & Alarms

All application-level logs live in CloudWatch for instant search. Every group has explicit 90-day retention — no infinite growth.

4.1 ECS Container Logs

Already unified. All four LPO/LRS/Webhook containers share a single log group (/aws/ecs/trinity-beast). There is no need to separate them — every log line self-identifies via the Unified Messaging Envelope (UME): cluster_node tells you which container (BeastMain, BeastMirror, BeastLRS, BeastWebhook), agent_profile_arn tells you which actor produced it, and endpoint tells you what was being served. Stream prefixes exist for AWS's benefit (task routing), but for searching, you filter on UME fields — not stream names.
ServiceLog GroupStream PrefixUME cluster_node
BeastMain/aws/ecs/trinity-beastmain/BeastMain
BeastMirror/aws/ecs/trinity-beastmirror/BeastMirror
BeastLRS/aws/ecs/trinity-beastlrs/BeastLRS
BeastWebhook/aws/ecs/trinity-beastwebhook/BeastWebhook
BeastTranslate/ecs/tbi-translate-workerservice/tbi-translate-worker/—
BeastReconciler/aws/ecs/trinity-beast-syncsync/trinity-beast-sync-job/—

The translate worker and sync job have separate log groups because they are different binaries (Python and Go respectively) with different lifecycles — they are not LPO containers. But the four LPO/LRS nodes are the same binary running in parallel, differentiated only by SERVER_TYPE and CLUSTER_NODE environment variables. UME makes separation unnecessary — search across all four at once and filter by the fields that matter.

Diagram 4.1 — Container Log Unification via UME Self-Identification
flowchart LR
    subgraph containers["4 Containers — Same Binary, Same Log Group"]
        M["BeastMain
APP_REPORT_SERVER"] R["BeastMirror
APP_REPORT_SERVER"] L["BeastLRS
APP_REPORT_SERVER"] W["BeastWebhook
WEBHOOK_SERVER"] end subgraph loggroup["/aws/ecs/trinity-beast"] LOG["Unified Stream
All 4 containers interleaved"] end subgraph ume["UME Fields — Self-Identification"] CN["cluster_node:
BeastMain | BeastMirror | BeastLRS | BeastWebhook"] AP["agent_profile_arn:
tbi | webhook-engine | rhema | ..."] EP["endpoint:
/price | /reports | /admin/..."] IP["ip_address:
client source IP"] end M --> LOG R --> LOG L --> LOG W --> LOG LOG --> CN LOG --> AP LOG --> EP LOG --> IP classDef containerStyle fill:#1e293b,stroke:#FF9900,color:#e2e8f0 classDef logStyle fill:#14532d,stroke:#10b981,color:#e2e8f0 classDef umeStyle fill:#1e1b4b,stroke:#a78bfa,color:#e2e8f0 class M,R,L,W containerStyle class LOG logStyle class CN,AP,EP,IP umeStyle
# Search ALL containers at once — UME self-reports which node answered
aws logs filter-log-events --log-group-name "/aws/ecs/trinity-beast" \
  --start-time $(date -v-1H +%s)000 --filter-pattern "BeastMain" \
  --region us-east-2 --query 'events[*].message' --output json

# Find errors across all 4 nodes simultaneously
aws logs filter-log-events --log-group-name "/aws/ecs/trinity-beast" \
  --start-time $(date -v-1H +%s)000 --filter-pattern "ERROR" \
  --region us-east-2 --query 'events[*].message' --output json

# Filter by agent actor (e.g., only webhook-engine responses)
aws logs filter-log-events --log-group-name "/aws/ecs/trinity-beast" \
  --start-time $(date -v-1H +%s)000 --filter-pattern "webhook-engine" \
  --region us-east-2 --query 'events[*].message' --output json

4.2 Lambda Function Logs

FunctionLog GroupPurpose
trinity-beast-receipt/aws/lambda/trinity-beast-receiptStripe receipt processing
trinity-beast-queued-writer/aws/lambda/trinity-beast-queued-writerSQS → Aurora batch inserts
tbi-ops-notify/aws/lambda/tbi-ops-notifyFormatted SES notifications
tbi-ops-self-heal/aws/lambda/tbi-ops-self-healECS task restart automation
tbi-ops-waf-action/aws/lambda/tbi-ops-waf-actionWAF rule management
tbi-ops-honeypot-processor/aws/lambda/tbi-ops-honeypot-processorHoneypot queue → WAF blocks
tbi-ops-bedrock-analyze/aws/lambda/tbi-ops-bedrock-analyzeAI threat correlation
tbi-rhema-support/aws/lambda/tbi-rhema-supportAI support assistant
tbi-ops-digest/aws/lambda/tbi-ops-digestDaily/weekly operational digest
trinity-beast-email-sender/aws/lambda/trinity-beast-email-senderOutbound receipt/notification email delivery
tbi-support-inbound/aws/lambda/tbi-support-inbound (us-east-1)Inbound customer email replies — us-east-1 because SES inbound receiving is unavailable in us-east-2
Corrected. This table previously listed five legacy translation Lambdas (tbi-translate-deploy, tbi-translate-finalize, tbi-translate-init, tbi-translate-batch-prepare, tbi-translate-batch-submit) as "legacy, no new writes." All five functions and their orphaned log groups have since been deleted outright — an orphaned log group is indistinguishable from a live one at a glance, and leaving them listed made the inventory harder to trust rather than easier. It also omitted trinity-beast-email-sender (live since 2026-07-24) and tbi-support-inbound (us-east-1). Exactly 10 Lambda functions exist in us-east-2, plus tbi-support-inbound in us-east-1 — 11 total.

4.3 Infrastructure Logs

Log GroupContentRetention
/aws/cloudtrail/trinity-beastAll AWS API calls (multi-region)30 days (S3 archive is indefinite)
/aws/vpc/trinity-beast-flowlogsNetwork traffic for both VPCs90 days
/aws/ecs/containerinsights/.../performanceECS CPU, memory, network per task90 days
RDSOSMetricsAurora OS-level metrics (every 30s)90 days
/aws/rds/cluster/trinity-beast-aurora-cluster/postgresqlSlow queries (>1s), lock waits, errors90 days
/aws/elasticache/trinity-beast-cache/slow-logValkey commands exceeding threshold90 days
/aws/elasticache/trinity-beast-cache/engine-logValkey engine events (startup, failover)90 days

4.4 CloudWatch Alarms

Alarms are not a log group — each maintains its own state history — but they are the mechanism that turns everything logged above into an actual notification, so the current count belongs here. 37 alarms in us-east-2 (verified live via describe-alarms), plus 1 in us-east-1 for CloudFront-specific metrics (region-locked, since AWS/CloudFront metrics are only published in us-east-1) — 38 account-wide. The steady state reported by the security dashboard is 35/35 OK, which deliberately excludes the 2 ECS AlarmLow scale-in alarms from that count by convention (a scale-in alarm being in its "not currently scaling in" state is not itself a health signal worth surfacing).

CategoryCountExamples
Infrastructure (ALB/NLB/Aurora/ECS/ElastiCache/S3, target-group health, coverage, webhook-test-receiver, application-log metric filters)25Trinity-Beast-ALB-UnhealthyTargets, Trinity-Beast-Aurora-CPU-High
Security / API4TrinityBeast-API-4xx-Spike, TrinityBeast-WAF-HighBlockRate
Anomaly detection (ML-based)0All four retired 2026-07-30/2026-08-26 — flapped against genuinely healthy/bimodal metrics; not rebuilt
Other — not authored/maintained by us directly82 CloudTrail metric-filter alarms + 6 TargetTracking-service/trinity-beast-fargate-cluster/* (AlarmHigh + AlarmLow × main/mirror/lrs)
us-east-1 (region-locked)1Trinity-Beast-CloudFront-5XX-High
Do not delete the 6 TargetTracking-service/... alarms. ECS Application Auto Scaling creates them automatically (AlarmHigh + AlarmLow for each of the main/mirror/LRS services) and they directly drive the scaling policies — removing one breaks that service's auto-scaling, not just its monitoring.

All alarms publish to SNS topic tbi-ops-notifications, routed through the tbi-ops-notify Lambda for formatted email. See CloudWatch Dashboard & Alarm Notifications for the full per-alarm inventory, thresholds, and response playbook — this section exists here only so the alarm count travels with the rest of the observability inventory rather than living in one document alone.

5. Valkey Operational Logs

Beyond CloudWatch export, the application writes structured operational data directly to Valkey as keys. These function as real-time logs for the AutoOps system.

Key PatternTypeWhat It RecordsTTL
autoops:actions:logSorted SetEvery autonomous action (self-heals, WAF blocks, notifications)Permanent
autoops:threats:dailyString (JSON)Today's AI-generated threat summaryOverwritten daily
honeypot:logSorted SetChronological log of all honeypot trap hitsPermanent
honeypot:ip:<ip>HashPer-IP: first_seen, last_seen, hit_count, pathsPermanent
tx:job:<id>HashTranslation job state (live progress per pair)7 days
report:text:YYYY-MM-DDStringPlain-text daily report for newsletter30 days
docs:session:logListSession close entries (date, session, summary)Permanent

6. Aurora Database Logs

6.1 PostgreSQL Server Logs (CloudWatch)

Enabled June 4, 2026. Aurora exports slow queries and errors to CloudWatch automatically. The instance parameter group controls what gets logged:

ParameterValueEffect
log_min_duration_statement1000Log queries taking longer than 1 second
log_temp_files0Log all temp file usage (sorts spilling to disk)
log_lock_waits1Log lock wait events
track_io_timingonInclude I/O timing in EXPLAIN output
idle_in_transaction_session_timeout300000Kill idle-in-transaction sessions after 5 min

6.2 pg_cron Execution History

Scheduled job execution history lives in Aurora itself — not CloudWatch. Query via:

SELECT r.jobid, j.jobname, r.start_time, r.end_time, r.status, r.return_message
FROM cron.job_run_details r JOIN cron.job j ON r.jobid = j.jobid
ORDER BY r.start_time DESC LIMIT 20;

6.3 Application Audit Tables

TablePurposeRetention
translation_job_eventsEvery state transition for every translation jobPermanent
usage_logsEvery API call (raw request log)Permanent in Aurora; cached 93 days in Valkey for LRS report serving (a separate retention policy on a separate store — see the warning below)
report_usage_logsLRS report generation eventsPurged after 180 days
support_ticketsSupport ticket history1095 days (3 years)
support_threadsChat thread historyClaimed: 90 days · Anon: 72 hours
webhook_delivery_logEvery webhook push write — one row per push per transport (JSONB assets array). ~7.5M rows (moves nightly with the purge — order of magnitude is the stable fact, not the exact count), largest table in the database by an order of magnitude. Never query unbounded — see §6.4.33 days — batched procedure purge_webhook_delivery_log() via pg_cron, not a bare DELETE
webhook_delivery_dailyDaily rollup of the above (per subscription × day × transport) — what the dashboard's Delivery Reports actually readPermanent (tiny — ~100 KB even with millions of raw rows behind it)
kiro_decisionsStructured decision index — what was chosen, why, and what it supersededPermanent — never purged
cron.job_run_detailspg_cron execution results~7 days (pg_cron internal cleanup)
Two different 93/33-day numbers exist for two different stores — don't conflate them. usage_logs keeps a 93-day rolling cache in Valkey (rebuilt nightly by BeastReconciler on recovery) purely for LRS report read speed — Aurora itself keeps every row permanently. webhook_delivery_log keeps a 33-day retention in Aurora itself (raised from 21 days on 2026-08-20, cut from an original 93 days on 2026-08-19) — there is no separate cache layer for it; the daily rollup in webhook_delivery_daily is the durable long-term record. Same-looking numbers, unrelated systems.

6.4 webhook_delivery_log — read this before querying it

This is a log store that lives in Aurora rather than CloudWatch, and it behaves like a high-volume log, not a normal table. At ~7.5M rows and ~2.2 GB (91% of the 2.4 GB database, measured live — the exact figures move nightly with the 33-day purge), it is the large majority of the entire database.

⛔ Every query against this table must be bounded — by rows or by an indexed time range. An unbounded COUNT(*) times out; use pg_class.reltuples for a row estimate instead. DISTINCT ... ORDER BY ... LIMIT n cannot stop early and will read a subscription's entire history — this is exactly what broke the dashboard's Delivery Log panel once (3.4M+ rows scanned to return 50). Use the index below and aggregate in application code, not in SQL.
-- ✅ Bounded — newest 200 rows for one subscription, off the composite index
SELECT id, created_at, asset, status, delivery_method
FROM webhook_delivery_log
WHERE subscription_id = 'sub_...'
ORDER BY created_at DESC
LIMIT 200;

-- ✅ Bounded — an indexed time range instead of a row cap
SELECT count(*) FILTER (WHERE status = 'failed') AS failed
FROM webhook_delivery_log
WHERE subscription_id = 'sub_...'
  AND created_at >= now() - INTERVAL '24 hours';

-- ❌ Never — no LIMIT and no time range, will scan the whole 9M-row table
-- SELECT * FROM webhook_delivery_log WHERE subscription_id = 'sub_...' ORDER BY created_at DESC;

Index to use: idx_webhook_delivery_sub_created (subscription_id, created_at DESC). For a full row estimate without scanning, use SELECT reltuples FROM pg_class WHERE relname = 'webhook_delivery_log' rather than COUNT(*).

Retention & the purge procedure

Purged nightly by pg_cron job tbi-purge-webhook-delivery-log (2:30 AM daily) at 33-day retention, via a batched stored procedure — not a bare DELETE:

CALL purge_webhook_delivery_log(p_retain_days := 33, p_batch_size := 30000, p_max_batches := 300);

The procedure commits every 30,000 rows so a retention cut never becomes one enormous transaction, one large WAL burst, and one dead-tuple cliff. A DO block cannot do this — it runs inside a single transaction and cannot COMMIT mid-loop; only a procedure can (PostgreSQL 11+; this cluster runs 17.7).

The rollup — refreshed every 15 minutes by pg_cron, plus nightly by BeastReconciler

webhook_delivery_daily is refreshed by refresh_webhook_delivery_rollup() on two independent schedules: pg_cron job tbi-webhook-delivery-rollup runs every 15 minutes (trailing 2-day window, all subscriptions), and BeastReconciler's nightly sync job runs it again on a trailing 3-day window as a wider self-healing net with no HTTP deadline. A dashboard "Refresh Now" button is a today-only on-demand convenience layered on top — it is not the only path and was never the sole refresh mechanism.

⛔ Never pass a sub-day interval to refresh_webhook_delivery_rollup(). Its p_since parameter is floored to the start of its America/New_York day internally, and the function upserts whole-day rows. Recomputing a day from a partial window overwrites that day's counts with the partial figures — it does not merge with what is already there.

The budget you are working inside is 5 seconds, not the database

Aurora's statement_timeout is 0 — unbounded, no server-side cutoff. The real ceiling for anything served to a browser is http_write_timeout_seconds = 5 (an application parameter). Dashboard handlers run on the request context, so a query that runs past 5 seconds gets its context cancelled and dies — not because the database refused it.

When a dashboard panel says "unable to load" and CloudWatch logs are clean, suspect the 5-second write deadline — not a database timeout. Aurora will happily keep running a query for much longer than 5 seconds; the browser-facing HTTP handler is what gives up first, and a handler that returns a generic error without logging makes this look like total silence. By contrast, /admin/sql and /admin/sql-batch pass no request context, so a long statement fired through them can outlive the HTTP response entirely — useful for long DDL, but it means the HTTP outcome tells you nothing about whether the statement ran. Poll for the actual result instead of inferring from the response.

7. Retention & Lifecycle Policy

The rule: 90 days for CloudWatch (multiples-of-3 convention: 3 × 30). 365 days for S3 with Glacier transition at 90 days. No infinite-retention log groups exist. Every byte has a known expiration.
TierRetentionStorageApplies To
CloudWatch (Application)90 daysCloudWatch LogsAll 10 Lambda (us-east-2) + 3 ECS groups
CloudWatch (Infrastructure)90 daysCloudWatch LogsVPC Flow Logs, Container Insights, RDSOSMetrics, Aurora PG, Valkey
CloudWatch (CloudTrail)30 daysCloudWatch LogsAPI audit (real-time search); S3 archive handles long-term
S3 (Standard)0–90 daysS3 StandardWAF, ALB, CloudFront, S3 access logs
S3 (Glacier IR)90–365 daysGlacier Instant RetrievalSame — automatic transition
S3 (CloudTrail)IndefiniteS3 StandardCloudTrail raw archive (no lifecycle rule)
Valkey (report cache)93 daysIn-memoryusage_logs cache for LRS report serving — a read-speed cache, not the source of truth (Aurora keeps it permanently)
Valkey (operational state)7–30 daysIn-memoryTranslation job progress, session reports (TTL-managed)
Aurora (webhook_delivery_log)33 daysAurora storageBatched procedure via pg_cron (see §6.4) — a distinct, shorter retention from every other Aurora table below
Aurora (everything else)PermanentAurora storageusage_logs, webhook_delivery_daily rollup, kiro_decisions, translation job events (support tickets/threads/report_usage_logs have their own shorter cutoffs — see §6.3)

8. Querying & Searching Logs

8.1 CloudWatch — Instant Search

# Find errors in any Lambda (last hour)
aws logs filter-log-events --log-group-name "/aws/lambda/tbi-ops-notify" \
  --start-time $(date -v-1H +%s)000 --filter-pattern "ERROR" \
  --region us-east-2 --query 'events[*].message' --output json

# Slow Aurora queries (last 24h)
aws logs filter-log-events \
  --log-group-name "/aws/rds/cluster/trinity-beast-aurora-cluster/postgresql" \
  --start-time $(date -v-24H +%s)000 --filter-pattern "duration" \
  --region us-east-2 --query 'events[*].message' --output json

# ECS container output for sync job (latest run)
STREAM=$(aws logs describe-log-streams --log-group-name "/aws/ecs/trinity-beast-sync" \
  --order-by LastEventTime --descending --limit 1 \
  --region us-east-2 --query 'logStreams[0].logStreamName' --output text)
aws logs get-log-events --log-group-name "/aws/ecs/trinity-beast-sync" \
  --log-stream-name "$STREAM" --limit 30 \
  --region us-east-2 --query 'events[*].message' --output json

8.2 Aurora — Bounded Queries Only

webhook_delivery_log is ~7.5M rows and the large majority of the database. Every query against it must be bounded by rows or by an indexed time range — see §6.4 for the full explanation, the index to use, and worked bounded/unbounded examples.

-- Newest 200 rows for one subscription (bounded by LIMIT, off idx_webhook_delivery_sub_created)
SELECT id, created_at, asset, status, delivery_method
FROM webhook_delivery_log
WHERE subscription_id = 'sub_...'
ORDER BY created_at DESC
LIMIT 200;

-- Row estimate without a full-table scan
SELECT reltuples FROM pg_class WHERE relname = 'webhook_delivery_log';

-- pg_cron execution history (job run detail)
SELECT r.jobid, j.jobname, r.start_time, r.end_time, r.status, r.return_message
FROM cron.job_run_details r JOIN cron.job j ON r.jobid = j.jobid
ORDER BY r.start_time DESC LIMIT 20;
A rolling 24-hour total that is rising is not evidence an attack is happening right now. The window still contains a finished event — a metric that stayed elevated for a full 24 hours after a scanner stopped has been read, incorrectly, as "still climbing." To answer "is this happening right now," bucket raw events by time (5-15 minute buckets) and look at the trend within the window, not the cumulative total.
ALB RequestCount and TargetResponseTime do NOT include health checks. Health checks are load-balancer-to-target probes, not routed client requests, and are not counted in either metric. Do not read a quiet RequestCount period as "at least the health-check floor" — actual daily request counts pre-launch have dipped below the theoretical health-check volume, which only makes sense once you know health checks aren't in that number at all.

8.3 S3 — Download & Analyze

# WAF: Get recent blocked requests
latest=$(aws s3 ls "s3://aws-waf-logs-trinity-beast/AWSLogs/211998422884/WAFLogs/\
us-east-2/trinity-beast-api-waf/$(date -u +%Y/%m/%d/%H)/" --region us-east-2 | tail -1 | awk '{print $4}')
aws s3 cp "s3://aws-waf-logs-trinity-beast/AWSLogs/211998422884/WAFLogs/\
us-east-2/trinity-beast-api-waf/$(date -u +%Y/%m/%d/%H)/$latest" - \
  --region us-east-2 | gunzip | jq -c 'select(.action=="BLOCK") | {ip: .httpRequest.clientIp, uri: .httpRequest.uri, rule: .terminatingRuleId}'

# ALB: Check latency for a specific endpoint
aws s3 cp "s3://aws-waf-logs-trinity-beast/alb/AWSLogs/211998422884/\
elasticloadbalancing/us-east-2/$(date +%Y/%m/%d)/<file>.log.gz" - \
  --region us-east-2 | gunzip | awk '{print $6, $13}' | grep "/price"

# CloudFront: Top requested URIs
aws s3 cp "s3://aws-waf-logs-trinity-beast/cloudfront/<file>.gz" - \
  --region us-east-2 | gunzip | awk '{print $8}' | sort | uniq -c | sort -rn | head -20

8.4 Valkey — Operational State

# Recent autonomous actions
curl -s -X POST -H "X-Admin-Key: $ADMIN_KEY" -H "Content-Type: application/json" \
  -d '{"command":"ZREVRANGEBYSCORE autoops:actions:log +inf -inf LIMIT 0 10"}' \
  "$LPO_BASE/admin/valkey"

# Today's threat assessment
curl -s -X POST -H "X-Admin-Key: $ADMIN_KEY" -H "Content-Type: application/json" \
  -d '{"command":"GET autoops:threats:daily"}' "$LPO_BASE/admin/valkey"

# Honeypot activity (last 10 hits)
curl -s -X POST -H "X-Admin-Key: $ADMIN_KEY" -H "Content-Type: application/json" \
  -d '{"command":"ZREVRANGEBYSCORE honeypot:log +inf -inf LIMIT 0 10"}' \
  "$LPO_BASE/admin/valkey"

9. Quick Reference — Where Do I Look?

I Want To See...Go To
What an ECS container printedCloudWatch: /aws/ecs/trinity-beast (filter by stream prefix)
Why a Lambda failedCloudWatch: /aws/lambda/<function-name> (filter for ERROR)
Every HTTP request to the APIS3: aws-waf-logs-trinity-beast/AWSLogs/.../WAFLogs/
Which WAF rule blocked whatS3: WAF logs → filter for action: "BLOCK"
Per-request latency to the APIS3: aws-waf-logs-trinity-beast/alb/
Every website visitor requestS3: aws-waf-logs-trinity-beast/cloudfront/
Who accessed S3 objectsS3: aws-waf-logs-trinity-beast/s3-access/
Network traffic patternsCloudWatch: /aws/vpc/trinity-beast-flowlogs
Slow database queriesCloudWatch: /aws/rds/cluster/.../postgresql
Slow Valkey commandsCloudWatch: /aws/elasticache/.../slow-log
Who called what AWS APICloudWatch: /aws/cloudtrail/trinity-beast
Historical AWS API callsS3: aws-cloudtrail-logs-211998422884-879cb71c
What AutoOps didValkey: autoops:actions:log
Honeypot hitsValkey: honeypot:log
Translation job traceAurora: translation_job_events table, plus the translation log codes in 9.1
pg_cron job resultsAurora: cron.job_run_details table
What a webhook subscriber was actually sentAurora: webhook_delivery_log — bounded queries only, see §6.4 (33-day retention)
Webhook delivery rate / asset coverage per dayAurora: webhook_delivery_daily rollup (NOT the raw log) — refreshed every 15 min by pg_cron, plus nightly by BeastReconciler
Current CloudWatch alarm count / stateSee §4.4, or aws cloudwatch describe-alarms --region us-east-2
Session history & decisionsS3: daily-reports/tbi-ops-* or ~/daily-reports/

9.1 Translation Log Codes

The October 2026 translation release gave every money and start decision its own searchable code, so you can follow a single quote from fetch to refund with one filter. The codes go to the existing container log groups; nothing new was added to watch. LPO codes are in /aws/ecs/trinity-beast and worker codes in /ecs/tbi-translate-worker (section 4). Anything marked + SNS also reaches the ops topic, because it means money and state disagree and a person should look. The Aurora side of the same story is translation_job_events.

StageCodeLevelWhat it tells you
Quote (LPO)QUOTE-FETCH-BLOCKEDINFOA document URL pointed at a private or internal address and was refused before any fetch (the SSRF guard).
Quote (LPO)QUOTE-CONTENT-UPDATEDINFOAt acceptance the page had changed but the price had not, so the job went ahead with a notice.
Quote (LPO)QUOTE-PRICE-HELDINFOThe re-check at acceptance produced a different price, and the customer was charged the lower of the two (logged with quoted= repriced= charged=). This is what stops a Claude Opus quote from re-pricing in a loop.
Quote (LPO)QUOTE-REQUOTEDINFOThe price or a rule changed at acceptance; a new quote was made and accept answered 409 tbts_quote_requoted.
Accept and start (LPO)QUOTE-ACCEPT-IN-PROGRESSINFOA second accept of the same quote arrived while the first held the claim.
Accept and start (LPO)QUOTE-ACCEPT-CLAIM-FAILEDERRORThe accept claim could not be written, so nothing was charged.
Accept and start (LPO)QUOTE-ACCEPT-RECORD-FAILEDERROR + SNSThe charge succeeded but recording it did not. Needs a human.
Accept and start (LPO)QUOTE-ENQUEUE-REFUSEDERRORThe job could not be queued and nothing was sent, so the quote goes to the start-failure path.
Accept and start (LPO)QUOTE-START-RECOVEREDWARNA start that looked lost was found and finished.
Accept and start (LPO)QUOTE-START-AMBIGUOUSWARNThe start call did not say clearly whether the job started, so accept checked the job record before deciding.
Accept and start (LPO)QUOTE-START-MARK-FAILEDERROR + SNSA start failure could not be recorded. Needs a human.
Accept and start (LPO)TX-SUBMIT-ENQUEUE-FAILEDERRORSending a language's queue message failed.
Accept and start (LPO)TX-SUBMIT-ENQUEUE-FAILED-MARKERRORMarking that failed send on the job also failed.
Accept and start (LPO)TX-FIRST-ENQUEUED-WRITE-FAILEDERRORfirst_enqueued_at could not be stamped after a successful send.
Accept and start (LPO)TX-UNSENT-EVENTS-WRITE-FAILEDERROR + SNSThe lang_failed enqueue-failed events for unsent languages could not be written.
Accept and start (LPO)TX-START-REFUND-CANCELLEDWARNA paid job that never started was cancelled and refunded in full.
Accept and start (LPO)TX-START-REFUND-AND-JOBERROR + SNSA refund and a running job both exist for one quote. Needs a human.
Accept and start (LPO)TX-START-UNSTARTED-CANCELLEDWARNAn unstarted job was cancelled, but its Valkey state could not be updated. Aurora holds the correct state.
Refunds and finalize (LPO)TRANSLATE-FAILED-REFUNDINFO (ERROR on a Stripe failure)One automatic failed-pair refund, logged as quote= from= to= amount=.
Refunds and finalize (LPO)TX-FINALIZE-CATCHUPWARNThe 15-minute sweeper finished a customer job nobody had finalized.
Refunds and finalize (LPO)TX-FINALIZE-CATCHUP-DEFERREDWARNThe catch-up stopped for this pass and left the remaining jobs (remaining=) for the next one.
Refunds and finalize (LPO)TX-FINALIZE-CATCHUP-REPEATWARNThe same job needed the catch-up again, a sign that something keeps it from finishing.
Refunds and finalize (LPO)TX-FINALIZE-CATCHUP-FAILEDERRORThe catch-up could not finalize the job.
Language catalog (LPO)TXLANGS-DEFAULT-MODEL-INVALIDWARNA catalog row's default_model is not a known agent, so that value is ignored.
Language catalog (LPO)TXLANGS-REFRESH-EMPTYWARNThe 15-minute catalog refresh came back empty, so the container kept the copy it had.
WorkerLANG-RUNTIME-FALLBACKWARNLogged as lang= source=: the language's runtime row came from the generated snapshot or the defaults instead of Aurora. The pair still runs.
WorkerLANG-RUNTIME-RELOAD-FAILEDWARNRefreshing the 15-minute runtime cache failed; the last good row is kept.
WorkerMODEL-UNKNOWNWARNLogged as lang= model=: the message named an agent the worker does not know, so it used the standard agent, never the previous job's.
WorkerCANCEL-STATE-UNKNOWNERRORBefore publishing a customer pair the worker could not confirm the job was still live, so it refused to publish (the pair fails closed as cancel-state-unknown).

A quick search: filter /aws/ecs/trinity-beast for the quote id to see every LPO decision about it in order, then filter /ecs/tbi-translate-worker for its job id to see each language run.

10. Cost Analysis

Total Monthly Cost
< $0.50
Daily Volume
~78 MB
Annual Storage
~28 GB
S3 Delivery Cost
$0
SourceDestinationDaily VolumeMonthly Cost
WAF full request logsS3~50 MB~$0.04
ALB access logsS3~5 MB< $0.01
CloudFront logsS3~15 MB~$0.01
S3 access logsS3~2 MB< $0.01
Aurora PostgreSQL logsCloudWatch~5 MB~$0.08
Valkey slow/engine logsCloudWatch~1 MB~$0.02
Total< $0.50/month
Why so cheap? S3 log delivery from AWS services (WAF, ALB, CloudFront, S3) has zero delivery cost — you only pay for storage. At ~72 MB/day to S3, annual storage is ~26 GB × $0.023/GB = $0.60/year for the first 90 days, then Glacier IR at $0.004/GB for months 3-12. CloudWatch ingestion ($0.50/GB) is the only real cost driver, and we only send low-volume logs there (Aurora + Valkey = ~6 MB/day = ~$0.10/month).

11. Athena — SQL Analytics Across All Logs

Amazon Athena is a serverless SQL engine that queries data directly in S3 — no database to provision, no data to move, no ETL. Point it at the log files, define a schema, and run standard SQL. Deployed June 4, 2026.

Workgroup
trinity-beast-analytics
Database
tbi_logs
Tables
5
Saved Queries
9
Cost per Query
~$0.01
Query Results
s3://.../athena-results/

11.1 Tables

TableSourcePartitioned ByKey Fields
waf_logsWAF full request logsyear / month / day / houraction, httprequest.clientip, httprequest.uri, httprequest.country, terminatingruleid, labels
alb_logsALB access logsyear / month / dayrequest_url, elb_status_code, target_processing_time, client_ip, user_agent
cloudfront_logsCloudFront standard logsdate columncs_uri_stem, c_ip, sc_status, x_edge_location, x_edge_result_type, time_taken
s3_access_logsS3 server access logs—operation, key, remote_ip, http_status, requester
cloudtrail_logsCloudTrail API eventsaccount / region / year / month / dayeventname, eventsource, sourceipaddress, useridentity, errorcode
Partition projection — all date-partitioned tables use Athena partition projection. This means new data is queryable the instant it arrives in S3. No manual MSCK REPAIR TABLE, no Glue crawlers, no partition maintenance. The schema tells Athena where to look based on the date path pattern.

11.2 Saved Queries (Ready to Run)

Nine pre-built queries are saved in the trinity-beast-analytics workgroup. Run them from the Athena console or CLI — just click "Saved queries" in the console.

Query NameWhat It Answers
WAF: Blocks by Country (24h)Top countries generating blocked requests today, grouped by WAF rule
WAF: Top Blocked IPs (24h)IPs with the most blocks — identify repeat attackers and which rules caught them
WAF: Request Volume by Endpoint (24h)Traffic distribution across API endpoints — which paths get the most hits
WAF: Trace IP (forensics)Every request from a specific IP: time, action, URI, method, country. Replace THE_IP with the target
CloudTrail: API Calls by Service (24h)Top AWS API calls grouped by service — spot unusual activity patterns
CloudTrail: Error Events (24h)All API calls that returned errors today — permission denials, throttles, service errors
CloudFront: Top Requested Pages (24h)Most popular website pages by request count, bytes served, and average load time
CloudFront: Traffic by Country (24h)Website visitors by country using viewer IP geolocation — audience geography and traffic patterns
Cross-Source: IP CorrelationFind IPs that hit BOTH the API (WAF) and website (CloudFront) — reconnaissance detection via JOIN

11.3 Example Queries

Security — Who got blocked and why?

-- Top 30 blocked IPs with the rules that caught them
SELECT httprequest.clientip AS ip,
       httprequest.country AS country,
       count(*) AS blocks,
       array_agg(DISTINCT terminatingruleid) AS rules
FROM tbi_logs.waf_logs
WHERE action = 'BLOCK'
  AND year = '2026' AND month = '06' AND day = '04'
GROUP BY httprequest.clientip, httprequest.country
ORDER BY blocks DESC
LIMIT 30;

Performance — P95 latency by endpoint

-- API endpoint latency percentiles from ALB logs
SELECT request_url,
       count(*) AS requests,
       approx_percentile(target_processing_time, 0.5) AS p50,
       approx_percentile(target_processing_time, 0.95) AS p95,
       approx_percentile(target_processing_time, 0.99) AS p99
FROM tbi_logs.alb_logs
WHERE year = '2026' AND month = '06' AND day = '04'
  AND request_url LIKE '%/price%'
GROUP BY request_url
ORDER BY requests DESC;

Cross-Source — Reconnaissance detection

-- IPs that hit both the API AND the website (probing behavior)
SELECT w.ip, w.waf_requests, w.blocks, cf.cf_requests
FROM (
  SELECT httprequest.clientip AS ip,
         count(*) AS waf_requests,
         sum(CASE WHEN action='BLOCK' THEN 1 ELSE 0 END) AS blocks
  FROM tbi_logs.waf_logs
  WHERE year = '2026' AND month = '06' AND day = '04'
  GROUP BY httprequest.clientip
) w
JOIN (
  SELECT c_ip AS ip, count(*) AS cf_requests
  FROM tbi_logs.cloudfront_logs
  WHERE date = DATE '2026-06-04'
  GROUP BY c_ip
) cf ON w.ip = cf.ip
ORDER BY w.blocks DESC, w.waf_requests DESC
LIMIT 20;

Forensics — Full trace of a suspicious IP

-- Everything a specific IP did across all API endpoints today
SELECT from_unixtime(timestamp/1000) AS time,
       action,
       httprequest.uri,
       httprequest.httpmethod,
       terminatingruleid,
       httprequest.country
FROM tbi_logs.waf_logs
WHERE httprequest.clientip = '45.148.10.51'
  AND year = '2026' AND month = '06'
ORDER BY timestamp DESC
LIMIT 100;

Infrastructure — What AWS APIs are being called?

-- Top AWS API calls in our account today (spot automation vs manual)
SELECT eventsource, eventname, sourceipaddress, count(*) AS calls
FROM tbi_logs.cloudtrail_logs
WHERE account = '211998422884' AND region = 'us-east-2'
  AND year = '2026' AND month = '06' AND day = '04'
GROUP BY eventsource, eventname, sourceipaddress
ORDER BY calls DESC
LIMIT 30;

11.4 How to Use

From the AWS Console

Open Athena → select workgroup trinity-beast-analytics → database tbi_logs → click "Saved queries" for the 9 pre-built queries, or write your own SQL in the editor. Results appear in seconds.

From the CLI

Submit a query and fetch results:

# Run a query
QID=$(aws athena start-query-execution \
  --query-string "SELECT action, count(*) FROM tbi_logs.waf_logs WHERE year='2026' AND month='06' AND day='04' GROUP BY action" \
  --work-group trinity-beast-analytics \
  --region us-east-2 --query 'QueryExecutionId' --output text)

# Wait for completion (typically 1-3 seconds)
aws athena get-query-execution --query-execution-id $QID \
  --region us-east-2 --query 'QueryExecution.Status.State' --output text

# Get results
aws athena get-query-results --query-execution-id $QID \
  --region us-east-2 --output json | jq '.ResultSet.Rows[] | .Data | map(.VarCharValue) | join(" | ")'

11.5 Cost & Limits

DimensionValue
Price per query$5 per TB scanned (minimum 10 MB charge per query)
Current daily log volume~78 MB/day → ~2.4 GB/month
Cost of scanning full month~$0.012
Query cost limit (workgroup)1 GB per query (safety guardrail)
Partition benefitQuerying one day scans only that day's folder — not the entire bucket
Compression benefitGzipped files = less bytes scanned = lower cost per query
Cost optimization tip: Always include partition filters (year, month, day) in your WHERE clause. Without them, Athena scans ALL data in the table. With them, it only reads the specific folders you need — reducing cost and execution time by orders of magnitude.

11.6 Pattern Recognition — What Athena Reveals

The real power isn't in individual queries — it's in combining signals across log sources to surface patterns invisible to any single source. These are the questions Athena answers that nothing else in the stack can:

PatternHow to DetectSources
Coordinated reconnaissanceSame IP appears in CloudFront (scouting docs), WAF (probing API), and gets blocked. The cross-source correlation query surfaces these instantly.WAF + CloudFront JOIN
Latency regressionALB logs contain per-request target_processing_time. Compute P95/P99 by endpoint across days to spot creeping degradation before users notice.ALB logs, GROUP BY day
Geographic traffic shiftsCloudFront logs include edge location + viewer country. A sudden spike from an unusual region could signal DDoS ramp-up, bot farm, or a new market discovering the product.CloudFront logs
Rate limit evasionWAF logs show high-volume IPs that stay just below the 2000/5min threshold. They're not blocked but they're up to something — distributed scraping across multiple keys.WAF logs, windowed aggregation
Incident timeline reconstruction"What happened between 2:15 and 2:45 AM?" — a single query across WAF + CloudTrail + ALB shows: who called what, from where, what succeeded, what failed, in chronological order.WAF + CloudTrail + ALB
Bot fingerprintingWAF logs include JA3 TLS fingerprints. Group blocked requests by fingerprint to identify bot frameworks vs. legitimate browsers — the same fingerprint across 50 IPs is a botnet.WAF logs, JA3 field
Infrastructure drift detectionCloudTrail error events grouped by service — sudden permission denials or throttling reveals IAM policy drift, resource limits, or configuration changes you didn't make.CloudTrail errors
Content popularity evolutionCloudFront logs show which docs/pages are hot, which are dead, and how the distribution shifts over time. Informs what to translate first, what to promote, where to invest.CloudFront logs, daily aggregation
The multiplier effect: Each table alone is useful. JOINing them is where the real intelligence lives. An IP that appears in only one source is noise. An IP that appears in WAF blocks, CloudFront requests, AND CloudTrail (trying to call AWS APIs from our VPC's NAT gateway IP) is a story — and Athena tells it in one query.

11.7 Future Enhancements