The Trinity Beast Infrastructure (TBI) uses Amazon CloudWatch as its centralized monitoring and alerting platform. This guide documents every dashboard, alarm, log group, and notification channel deployed across the system.
Before we walk the inventory, one idea worth carrying with you through the whole guide. Monitoring is not the same thing as knowing. An alarm is a small program you wrote once and then stopped looking at, and like any program it can be correct, or wrong, or — most dangerously — silently pointed at nothing at all while reporting that everything is fine. We learned this the hard way on July 30, 2026, and rather than quietly fix it we wrote the whole lesson down in Section 11: Alarm Hygiene. If you read only one section of this guide, read that one. The tables below tell you what exists; Section 11 teaches you how to keep trusting them.
describe-alarms listing, not incremented by hand from a prior count.tbi-ops-notifications routes all alerts through the tbi-ops-notify Lambda for formatted HTML email delivery via SEStbi-support-inbound in us-east-1, forced there by SES inbound-email regional availability), Aurora PostgreSQL, Valkey slow/engine logs, VPC Flow Logs, CloudTrail, Container Insights, and RDS OS Metrics — see the companion Unified Logs & Observability document for the current full inventory and count, which changes more often than this summary line and is tracked there as the source of truthaws-waf-logs-trinity-beast) for high-volume infrastructure logs — WAF, ALB, CloudFront, and S3 access logs with 365-day lifecycle. See Unified Logs & ObservabilityTrinityBeast/LPO and TrinityBeast/LRS namespacesFour CloudWatch dashboards provide layered visibility — from real-time application metrics to executive cost summaries.
| Dashboard | Purpose |
|---|---|
Trinity-Beast-Application-Dashboard |
Primary ops dashboard — LPO, LRS, AWS infra, Lambda, logs |
Trinity-Beast-Master-Dashboard |
Comprehensive view across all services |
Trinity-Beast-Cost-Dashboard |
Live cost intelligence — resource utilization metrics that drive spend, cost-context tables, links to Cost Explorer |
The Trinity-Beast-Application-Dashboard is the primary operational dashboard. It contains widgets organized into six sections covering every layer of the stack.
| Widget | Type |
|---|---|
| LPO Requests (per minute) | Metric — line graph |
| Cache Hit Rate (%) | Metric — gauge / number |
| Avg Latency (ms) | Metric — line graph |
| Cache Hits vs Misses | Metric — stacked area |
| Requests by Asset | Metric — bar chart |
| Requests by Source (Exchange) | Metric — bar chart |
| Errors & Source Failovers | Metric — line graph |
| Widget | Type |
|---|---|
| LRS Total Requests | Metric — line graph |
| LRS Avg Latency (ms) | Metric — line graph |
| LRS Output Format Usage | Metric — bar chart |
| LRS Errors | Metric — line graph |
| Widget | Type |
|---|---|
| ECS CPU Utilization (%) | Metric — line graph |
| ECS Memory Utilization (%) | Metric — line graph |
| ALB Response Time & Errors | Metric — line graph |
| ElastiCache ECPU & Cache Hit Rate | Metric — line graph |
| ElastiCache Storage (BytesUsedForCache) | Metric — gauge / number |
| Aurora Serverless Capacity (ACU) | Metric — line graph |
| Widget | Type |
|---|---|
| LPO — Main Service Logs | Log query |
| LRS — Report Service Logs | Log query |
| Mirror Service Logs | Log query |
| Sync Job Logs | Log query |
| Widget | Type |
|---|---|
| Lambda Invocations | Metric — line graph |
| Lambda Errors | Metric — line graph |
| Lambda Duration (ms) | Metric — line graph |
| Throttles & Concurrency | Metric — line graph |
| Receipts by Handler Type | Log widget |
| Recent Receipts — Handler Detail | Log widget |
| Receipt Lambda Logs | Log query |
| Widget | Type |
|---|---|
| CloudTrail — Errors & Access Denied | Log query |
| CloudTrail — ECS & Infrastructure Changes | Log query |
| VPC Flow Logs — Rejected Traffic (Trinity VPC) | Log query |
One dedicated cost dashboard provides financial visibility into the Trinity Beast Infrastructure spend.
Live cost intelligence dashboard that combines resource utilization metrics with cost context. Rather than embedding stale dollar figures, it shows the metrics that drive spend — ECS CPU/Memory utilization across all 4 services, Aurora ACU + utilization (with min/max annotations), ElastiCache Serverless ECPU/storage, Lambda invocations and duration for all 8 functions, Aurora connections, and NAT Gateway/EC2 data transfer. Two cost-context tables explain the monthly baseline by component (with unit costs) and identify the cost levers you can control. Direct links to Cost Explorer, Budgets, and Savings Plans for exact dollar figures.
Replaces: The previous static Trinity-Beast-Cost-Executive-Dashboard and Trinity-Beast-Cost-Detailed-Dashboard (deleted 2026-05-30) — both were 100% hardcoded markdown text frozen at April 2026 figures, referencing services no longer in use.
31 static-threshold alarms in us-east-2 monitor critical infrastructure metrics (25 infrastructure + 6 security/API — see the category breakdown at the end of this section), plus 1 in us-east-1 for CloudFront (region-locked, since AWS/CloudFront metrics are only published there). All of them publish to the tbi-ops-notifications SNS topic — the us-east-1 alarm bridges across regions via SNS→Lambda to reach the same tbi-ops-notify Lambda for formatted HTML email delivery. Every alarm state change also reaches the tbi-ops-alarm-trigger EventBridge rule, which drives automated self-healing.
Two things are worth understanding before you read the tables, because they shape how you should interpret every row.
First, a threshold is a judgement, not a fact. When you see > 80% on a CPU alarm, that number was chosen by a person who was reasoning about how much headroom this system needs. It deserves to be revisited as the system changes. Several thresholds in these tables were set when the infrastructure looked quite different — the ElastiCache alarms, for instance, were rebuilt entirely after the July 2 Serverless migration, because the old metrics stopped existing.
Second, the TreatMissingData column matters more than it looks. It answers the question "what should this alarm conclude when no data arrives at all?" and the answer is genuinely different depending on what you are watching. Look at the pattern in these tables and you will see three deliberate choices:
| Setting | Means | Use it when |
|---|---|---|
notBreaching |
No data is treated as healthy | Absence genuinely is good news — no 5xx errors means no errors happened. Used on the error-count and WAF alarms. |
breaching |
No data is treated as a problem | The metric should always report, and silence itself is the emergency. Used on the service-count alarms: if RunningTaskCount stops reporting, something is badly wrong. |
missing |
No data leaves the alarm in INSUFFICIENT_DATA |
You want silence to be visible rather than interpreted. Used on the ALB and Aurora health alarms so a broken alarm announces itself instead of pretending to be fine. |
Why that third row exists. Trinity-Beast-ALB-UnhealthyTargets spent three and a half months displaying a comfortable green OK while pointed at a load balancer that had been deleted. It was set to notBreaching, so "no data" rendered as "no problem." Nobody was careless — the ALB was replaced during an upgrade and the alarm simply was not revisited. We changed it to missing on July 30, 2026 so that if it ever goes blind again it will say so out loud. The full story, and the tooling we built so it cannot happen quietly again, is in Section 11.
| Alarm Name | Metric | Namespace | Threshold | Period | Eval Periods | State |
|---|---|---|---|---|---|---|
Trinity-Beast-ALB-UnhealthyTargets |
UnHealthyHostCount | AWS/ApplicationELB |
>= 1 | 60s |
3 | OK |
Trinity-Beast-NLB-UnhealthyTargets |
UnHealthyHostCount | AWS/NetworkELB |
>= 1 | 60s |
3 | OK |
Read the dimensions, not just the alarm name. Each of these alarms watches one specific target group, not every target group on their load balancer — a distinction that mattered enough to have once been a real gap. ALB-UnhealthyTargets watches LoadBalancer=app/Trinity-Beast-TCP-ALB/fafe772c40914b09 and TargetGroup=targetgroup/trinity-beast-fargate-group/f411aca46b262516. NLB-UnhealthyTargets watches Trinity-Beast-UDP-2679-TG.
As of 2026-07-30 session 2 every remaining target group on both load balancers has its own dedicated alarm — see Target Groups (7 Alarms) below for the full set (LRS-TargetGroup-Unhealthy, the two PrivateLink-*-Unhealthy alarms, UDP-2680-Unhealthy, and the two webhook test-receiver alarms added 2026-08-07). This section's original framing — "neither gap is an emergency, but worth knowing you've chosen it" — is exactly the lesson that produced those additions: once the gaps were named, they were closed the same session.
Every target group behind every load balancer in the account now has its own UnHealthyHostCount alarm — closing the exact gap the previous section describes. All seven follow the same shape: Maximum statistic, 60-second period, 3 evaluation periods, threshold >= 1, TreatMissingData: missing (a healthy target group always publishes a real 0, so silence can only mean the alarm itself is blind — see Section 11).
| Alarm Name | Target Group | Load Balancer | Added |
|---|---|---|---|
Trinity-Beast-LRS-TargetGroup-Unhealthy | trinity-beast-lrs-9090 | TCP-ALB | 2026-07-30 |
Trinity-Beast-UDP-2680-Unhealthy | Trinity-Beast-UDP-2680-TG | UDP-NLB | 2026-07-30 |
Trinity-Beast-PrivateLink-8080-Unhealthy | trinity-beast-pl-tcp-8080 | TCP-NLB (internal) | 2026-07-30 |
Trinity-Beast-PrivateLink-9090-Unhealthy | trinity-beast-pl-tcp-9090 | TCP-NLB (internal) | 2026-07-30 |
Trinity-Beast-Webhook-Test-8090-Unhealthy | trinity-beast-webhook-test-tg | TCP-ALB (host-header rule) | 2026-08-07 |
Trinity-Beast-UDP-2681-Unhealthy | Trinity-Beast-UDP-2681-TG | UDP-NLB | 2026-08-07 |
The PrivateLink pair was the one that mattered most. Before 2026-07-30, both target groups behind the internal PrivateLink NLB held six stale ip-type targets from long-dead ECS tasks — every one unhealthy, zero working backends — while the VPC endpoint service serving the Partner tier still advertised Available. No alarm covered either group, so nothing surfaced it. The root cause was that neither group was declared in an ECS service's loadBalancers config, so nothing ever re-registered them when tasks were replaced. Both groups are now attached to main-service and mirror-service the same way the primary target groups are, so ECS self-maintains them going forward.
| Alarm Name | Metric | Namespace | Threshold | Period | Eval Periods | State | Notes |
|---|---|---|---|---|---|---|---|
Trinity-Beast-ECS-CPU-High |
CPUUtilization | AWS/ECS (main-service) |
> 80% | 300s |
2 | OK | — |
Trinity-Beast-ECS-CPU-High-Mirror |
CPUUtilization | AWS/ECS (mirror-service) |
> 80% | 300s |
2 | OK | — |
Trinity-Beast-ECS-CPU-High-LRS |
CPUUtilization | AWS/ECS (lrs-service) |
> 80% | 300s |
2 | OK | — |
Trinity-Beast-Main-Service-Count-Low |
RunningTaskCount | ECS/ContainerInsights (main) |
< 1 | 300s |
2 | OK | TreatMissing: breaching |
Trinity-Beast-Mirror-Service-Count-Low |
RunningTaskCount | ECS/ContainerInsights (mirror) |
< 1 | 300s |
2 | OK | TreatMissing: breaching |
Trinity-Beast-LRS-Service-Count-Low |
RunningTaskCount | ECS/ContainerInsights (lrs) |
< 1 | 300s |
2 | OK | TreatMissing: breaching |
Every alarm above this point was found by checking whether an existing alarm points at a real resource. That kind of check is structurally unable to see a resource that was never alarmed on at all. A coverage check that instead enumerates what should be watched and diffs against what is found these three gaps on its first run — including one on a paying product.
| Alarm Name | Metric | Resource | Why It Mattered |
|---|---|---|---|
Trinity-Beast-Webhook-Service-Count-Low | RunningTaskCount < 1 | trinity-beast-webhook-service | Webhook Push is a paying product ($30–$420/mo tiers). It could have dropped to zero tasks and stopped delivering prices with no signal at all. |
Trinity-Beast-Translate-Worker-Count-Low | RunningTaskCount < 1 | tbi-translate-worker-service | Translation jobs would sit in SQS undrained with no alarm. |
Trinity-Beast-Translation-DLQ-NotEmpty | ApproximateNumberOfMessagesVisible > 0 | trinity-beast-translation-queue-dlq | Messages here are already-failed, paid customer translation jobs, discarded silently at the queue's retention window. |
The DLQ alarm is the one deliberate exception to the missing convention in this doc. It uses notBreaching, and correctly so: an idle, empty SQS queue publishes zero datapoints, not a zero value — confirmed by direct observation (0 datapoints over a 3-hour window with nothing in the queue). missing would park this alarm in permanent INSUFFICIENT_DATA, which would show up on every audit run and teach you to ignore exactly the signal this section exists to protect. The rule that generalizes: ask what the metric does when everything is fine. If the answer is "publishes a real zero," use missing. If the answer is "publishes nothing at all," notBreaching is correct. This alarm also has no OK-action — a DLQ draining back to zero is not a recovery worth an email.
Both alarms below watch a CloudWatch Logs metric filter on /aws/ecs/trinity-beast, rather than a metric AWS already publishes. Each filter pattern was validated against real log samples via test-metric-filter before the alarm was created, specifically to confirm it matches the failure line and does not match an adjacent, intentional fallback line that merely looks similar.
| Alarm Name | Filter Pattern | Threshold | TreatMissingData |
|---|---|---|---|
Trinity-Beast-ParamLoad-AuroraFallback-Recurring | "Load started (Aurora)" | >= 3 in 1 hour | notBreaching |
Trinity-Beast-Startup-MigrationFailure | ?"migration failed" ?"seed failed" | >= 1 in 1 hour | notBreaching |
Same pattern, opposite threshold — and that difference is deliberate, not an inconsistency to fix. A single Aurora-fallback occurrence is normal, designed resilience behavior (Valkey can have a genuine transient blip, and falling back to Aurora when it does is correct), so alarming on every one would fire on ordinary hiccups and get tuned out within a week — the same cry-wolf failure documented for the flapping anomaly alarms elsewhere in this doc. Three within one hour is a low bar that ordinary blips essentially never cross, while a real regression (every container on every cold start taking the fallback path) crosses it immediately. A startup migration failing, by contrast, has no equivalent benign case — it should either succeed or genuinely need attention, so the threshold is 1, not 3.
Both alarms were built the same session, immediately after reading full container startup logs end-to-end rather than trusting a clean services-stable rollout status. Both underlying bugs — a parameter-load ordering defect and a migration writing to a nonexistent column — had been present on every single container boot for weeks, logging an error and continuing, with nothing watching for the pattern.
| Alarm Name | Metric | Namespace | Threshold | Period | Eval Periods | State |
|---|---|---|---|---|---|---|
Trinity-Beast-Aurora-CPU-High |
CPUUtilization | AWS/RDS (trinity-beast-aurora-cluster) |
> 80% | 300s |
2 | OK |
Trinity-Beast-Aurora-Connections-High |
DatabaseConnections | AWS/RDS (trinity-beast-aurora-cluster) |
> 250 | 300s |
2 | OK |
Where 250 comes from. The cluster parameter group sets max_connections = 800, so 250 is roughly 31% of the ceiling — early enough to investigate a connection leak while there is still plenty of room, late enough that normal operation never touches it. At rest the cluster holds about 4 connections, so this alarm has an enormous amount of quiet headroom. That is intentional: a connections alarm should fire when something is wrong, not when something is merely busy.
This alarm was pointed at a cluster identifier that does not exist (cpmp-backend-aurora) until July 30, 2026, and so had never once evaluated real data. Its TreatMissingData is now missing rather than the default, so a future dimension mistake surfaces as INSUFFICIENT_DATA instead of hiding.
A migration changes your metrics, not just your bill. These three alarms replaced the original node-based set (CPUUtilization, DatabaseMemoryUsagePercentage, CurrConnections) after the Serverless migration on July 2, 2026. This is a genuinely useful thing to internalize: when you move to a serverless flavour of a service, the old metrics do not get worse — they cease to exist. There are no nodes, so there is no node CPU. There is no fixed memory pool, so there is no memory percentage. Any alarm still referencing them becomes permanently blind, which is exactly what happened to several alarms elsewhere in this account.
What you monitor instead is processing units consumed (ECPU), bytes stored, and throttled commands. Note also that the dimension name changed from CacheClusterId to clusterId — a single-character-case difference that will silently produce an alarm which never fires.
| Alarm Name | Metric | Namespace | Threshold | Period | Eval Periods | State |
|---|---|---|---|---|---|---|
Trinity-Beast-ElastiCache-ECPU-High |
ElastiCacheProcessingUnits | AWS/ElastiCache |
> 6,000 ECPU/s (60% of the 10,000 limit) | 300s |
2 | OK |
Trinity-Beast-ElastiCache-Storage-High |
BytesUsedForCache | AWS/ElastiCache |
> 24 GB (80% of the 30 GB limit) | 300s |
2 | OK |
Trinity-Beast-ElastiCache-Throttled |
ThrottledCmds | AWS/ElastiCache |
> 0 — any throttling at all is a signal | 300s |
2 | OK |
Configured limits: 30 GB data storage, 10,000 ECPU/second. Both are ceilings you set on the Serverless cache, not hardware facts — raise them and these thresholds should move with them. At the time of writing the cache holds roughly 0.20 GB and consumes around 1,000 ECPU/s, so all three alarms sit in comfortable quiet.
Why ThrottledCmds > 0 and not some larger number? Because unlike CPU or storage, throttling has no healthy amount. ECPU and bytes are resources you are meant to consume, so their alarms sensibly fire at a fraction of the ceiling. A throttled command, by contrast, is a request the cache refused to serve. One of them is information; a hundred of them is an incident you are late to. This is a good instinct to carry generally — ask whether the metric measures usage (alarm on a proportion) or failure (alarm on any occurrence).
These two alarms are different in kind from everything else in this section, and they are worth pausing on. They do not watch an AWS service metric — they watch metrics we publish ourselves into the TBI/Security namespace from CloudTrail activity. They answer a question no built-in metric answers: did somebody change something they should not have?
| Alarm Name | Metric | Namespace | Threshold | Period | Eval Periods | State |
|---|---|---|---|---|---|---|
TBI-Security-IAM-Changes |
IAMChangesCount | TBI/Security |
>= 1 | 300s |
1 | OK |
TBI-Security-EC2-RunInstances |
EC2RunInstancesCount | TBI/Security |
>= 1 | 300s |
1 | OK |
Both fire on the first occurrence, for the same reason the throttling alarm does — these are events, not volumes. TBI runs no EC2 instances at all, so a single RunInstances call is either you doing something deliberate or somebody else doing something you need to know about immediately. IAM changes are rare and consequential enough to deserve the same treatment.
When you attach target-tracking auto-scaling to an ECS service, AWS silently creates a pair of alarms per service to drive it. They carry generated names like TargetTracking-service/trinity-beast-fargate-cluster/trinity-beast-main-service-AlarmHigh-63fe11ef…. You did not write them and you should not edit them — Application Auto Scaling owns them and will recreate them if you interfere.
| Service | AlarmHigh (scale out) | AlarmLow (scale in) |
|---|---|---|
trinity-beast-main-service | CPU > 60% | CPU < 54% |
trinity-beast-mirror-service | CPU > 60% | CPU < 54% |
trinity-beast-lrs-service | CPU > 60% | CPU < 54% |
Expect the three AlarmLow alarms to sit permanently in ALARM, and do not let it alarm you. This is the single most common source of confusion when you first look at the alarm list. CPU across these services runs at 3–5%, which is comfortably below the 54% scale-in threshold, so "scale in if you can" is continuously true. The alarm is not reporting a fault — it is reporting an opportunity, and auto-scaling has already taken it by holding the services at their minimum task count.
This is why our tooling and our daily reports count named alarms separately from these six. When you audit alarm state, filter out anything beginning TargetTracking- or you will chase three permanent false positives forever.
| Alarm Name | Metric | Namespace | Threshold | Period | Eval Periods | State |
|---|---|---|---|---|---|---|
Trinity-Beast-S3-Size-Unusual-Growth |
BucketSizeBytes | AWS/S3 |
> 10 GB | 86400s |
1 | OK |
| Alarm Name | Metric | Namespace | Threshold | Period | Eval Periods | State |
|---|---|---|---|---|---|---|
TrinityBeast-WAF-HighBlockRate |
BlockedRequests | AWS/WAFV2 |
> 300 | 300s |
1 | OK |
TrinityBeast-API-5xx-Spike |
HTTPCode_Target_5XX_Count | AWS/ApplicationELB |
> 9 | 300s |
1 | OK |
TrinityBeast-API-ELB-5xx-Spike |
HTTPCode_ELB_5XX_Count | AWS/ApplicationELB |
> 9 | 300s |
1 | OK |
TrinityBeast-API-4xx-Spike |
HTTPCode_Target_4XX_Count | AWS/ApplicationELB |
> 300 | 300s |
1 | OK |
Trinity-Beast-CloudFront-5XX-High (us-east-1) |
5xxErrorRate | AWS/CloudFront |
> 9% | 300s |
3 | OK |
Target-5xx and ELB-5xx are two different failure signals, added deliberately as a pair on 2026-08-01. HTTPCode_Target_5XX_Count means the container itself returned an error — the request reached application code and something inside it failed. HTTPCode_ELB_5XX_Count means the ALB could not get a usable response from any target at all (502/503/504) — the request never reached working application code. A table-bloat incident that manifests as ALB 502s (target unreachable under load) would never trip the target-5xx alarm, which is exactly the class of incident that motivated adding the ELB-side alarm as its own signal rather than assuming the target-side alarm already covered it.
The WAF alarm needs three dimensions, and two of them are easy to forget. AWS/WAFV2 metrics are only addressable with WebACL, Region, and Rule together. Ours are WebACL=trinity-beast-api-waf, Region=us-east-2, Rule=ALL. Supply only Rule=ALL — which is a very natural thing to do, since it reads like "all rules" — and CloudWatch will happily accept the alarm and then never send it a single datapoint. This alarm sat in that state, silently, until July 30, 2026.
Where these thresholds come from. Background internet scanning produces roughly 2,000 WAF blocks per day against this API — around 400 an hour, or about 33 per five-minute period. A threshold of 300 per five minutes is therefore about 9× normal background noise: high enough that routine scanning never wakes anybody, low enough to catch a genuine flood. The 4xx/5xx thresholds (300 and 9 respectively) were aligned to the multiples-of-3 convention on 2026-08-01, with headroom preserved against the baseline traffic profile (~3 target-4xx/5min, ~0 target-5xx/5min at the time). Baseline your own noise before choosing a number like this; a threshold set without knowing the floor is a guess.
The CloudFront alarm is region-locked, and that's the point, not an inconsistency. AWS/CloudFront metrics are published only to us-east-1 regardless of where the distribution's origin lives, so this alarm cannot exist anywhere else. It publishes to the us-east-1 copy of tbi-ops-notifications, which has the us-east-2 tbi-ops-notify Lambda as a cross-region SNS subscriber — a Lambda subscription across regions is permitted, so there is no need for a separate forwarder or a second formatter. It replaced a deleted 4xx-based alarm that was structurally blind (missing the required Region=Global dimension) and, once fixed, turned out to be uninformative anyway — at 1–50 requests/5min a handful of bot 404s pins the 4xx rate near 100%. A 5xx-based signal is volume-robust by comparison: it reads 0% under normal operation and only trips on a genuine origin or distribution failure.
A former alarm, and a lesson in reading the fine print. This group used to include TrinityBeast-GuardDuty-Finding, watching metric finding in namespace AWS/GuardDuty with a threshold of > 0. It was deleted on July 30, 2026 for a simple and slightly humbling reason: GuardDuty does not publish that metric. It does not publish any CloudWatch metric of that shape. The alarm was syntactically valid, looked entirely reasonable in the console, and could never have fired under any circumstances.
Nothing was lost by removing it, because the real GuardDuty integration never depended on it. Findings of severity 7 or above reach us through the tbi-ops-guardduty-high-finding EventBridge rule, which routes them to the tbi-ops-bedrock-analyze Lambda for AI threat assessment. That path works and always did. The lesson: CloudWatch will let you build an alarm on a metric that does not exist. It will not warn you. The only symptom is a permanent INSUFFICIENT_DATA — which is exactly why Section 11 treats that state as something to investigate rather than something to tolerate.
Full category breakdown, verified 2026-08-12 against a live describe-alarms listing (not restated from memory or incremented by hand):
| Category | Count | Includes |
|---|---|---|
| Infrastructure | 25 | Load balancers, target groups (7), ECS CPU/service-count, Aurora, ElastiCache Serverless, S3, coverage alarms (3), application-log metric filters (2) |
| Security / API | 6 | WAF, target-5xx, ELB-5xx, 4xx, and the 2 TBI-Security-* CloudTrail metric alarms |
| Anomaly Detection | 2 | RequestRate, ErrorRate — actions currently disabled, models awaiting rebuild (see Section 9) |
| ECS Auto-Scaling (AWS-managed) | 6 | TargetTracking-* AlarmHigh/AlarmLow pairs — drive scaling policies, not monitored the same way |
| us-east-2 total | 39 | — |
| us-east-1 (CloudFront, region-locked) | 1 | Trinity-Beast-CloudFront-5XX-High |
| Account total | 40 | — |
All CloudWatch alarms route through a unified AutoOps pipeline. No raw text emails from AWS — every notification is formatted by the tbi-ops-notify Lambda before delivery via SES.
┌─────────────────────────────────────────────────────────────────────────┐
│ NOTIFICATION ROUTING │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ CloudWatch Alarm (any of 40 alarms — 39 us-east-2, 1 us-east-1) │
│ ├─→ SNS: tbi-ops-notifications │
│ │ └─→ tbi-ops-notify Lambda │
│ │ └─→ Formatted HTML email (SES) │
│ │ → CoryDeanKalani@CPMP-Site.org │
│ │ │
│ └─→ EventBridge: tbi-ops-alarm-trigger │
│ └─→ Step Function: tbi-ops-health-check-heal │
│ └─→ Self-heal → verify recovery → notify │
│ │
│ GuardDuty Finding (severity ≥ 7) │
│ └─→ EventBridge: tbi-ops-guardduty-high-finding │
│ └─→ tbi-ops-bedrock-analyze Lambda │
│ └─→ AI threat assessment → auto-action → notify │
│ │
│ Honeypot Hits (every 5 min) │
│ └─→ EventBridge: tbi-ops-honeypot-queue-processor │
│ └─→ tbi-ops-honeypot-processor Lambda │
│ └─→ WAF IP block → notify │
│ │
│ Bedrock Threat Analysis (every 5 min) │
│ └─→ EventBridge: tbi-ops-bedrock-analyze-schedule │
│ └─→ tbi-ops-bedrock-analyze Lambda │
│ └─→ Correlate signals → report → notify if HIGH/CRITICAL │
│ │
│ Support Ticket Submitted │
│ └─→ Application invokes tbi-rhema-support Lambda │
│ └─→ Categorize → draft response → notify │
│ │
│ Daily/Weekly Digest (cron) │
│ └─→ EventBridge: tbi-ops-daily-digest / tbi-ops-weekly-digest │
│ └─→ tbi-ops-digest Lambda │
│ └─→ Bedrock summary → formatted email │
│ │
└─────────────────────────────────────────────────────────────────────────┘
| Protocol | Endpoint | Purpose | Status |
|---|---|---|---|
| Lambda | tbi-ops-notify |
Formats alert → sends HTML email via SES to CoryDeanKalani@CPMP-Site.org |
Active |
How it works: When any alarm transitions to ALARM or OK, SNS invokes the tbi-ops-notify Lambda. The Lambda parses the alarm payload, formats a branded HTML email with severity badges and context, and sends it via Amazon SES. Subject lines include severity: [INFO], [WARNING], [CRITICAL], [SELF-HEALED].
Sender: The Trinity Beast <No-Reply@CPMP-Site.org>
Recipient: CoryDeanKalani@CPMP-Site.org
Format: HTML email with dark theme, severity color coding, alarm details, and recommended actions.
Status: This topic is retained for potential future SMS escalation but no alarms currently target it. All 21 alarms were migrated to tbi-ops-notifications on May 15, 2026. The SMS subscription remains active as a backup escalation channel.
| Protocol | Endpoint | Status |
|---|---|---|
CoryDeanKalani@CPMP-Site.org |
Inactive (no triggers) | |
SMS |
+16156128200 |
Inactive (no triggers) |
Design Decision (May 2026): All notifications route through a single Lambda (tbi-ops-notify) for consistent formatting, content review, and delivery control. This eliminates raw AWS text emails and ensures every alert arrives as a branded, readable HTML message with actionable context. AWS User Notifications service was disabled — it was sending unformatted alarm summaries that bypassed the AutoOps pipeline.
CloudWatch log groups capture application and infrastructure output across both regions the account uses (us-east-2 primary, plus tbi-support-inbound's log group in us-east-1, forced there by SES inbound-email regional availability). All groups have explicit 90-day retention (the Trinity Beast multiples-of-3 convention: 3 × 30). The exact current count fluctuates as functions and services are added and removed, so this doc doesn't restate a specific total — Unified Logs & Observability is the tracked source of truth for the live inventory, and includes the orphan-audit tooling that catches an unretained or dead-function log group automatically before it grows unbounded. For infrastructure-level logs specifically (ALB access, CloudFront, WAF requests, S3 access), see the same companion document — those flow to a dedicated S3 bucket for long-term archive and Athena query.
aws-waf-logs-trinity-beast handles high-volume infrastructure logs (WAF full request logs, ALB access logs, CloudFront standard logs, S3 server access logs) with a 365-day lifecycle. This two-tier split keeps CloudWatch costs low while giving maximum retention and Athena queryability on the bulk data. Full architecture documented in Unified Logs & Observability.
| Log Group | Retention | Source |
|---|---|---|
/aws/ecs/trinity-beast | 90 days | All 4 services (Main, Mirror, LRS, Webhook) — UME self-identifies via cluster_node |
/aws/ecs/trinity-beast-sync | 90 days | BeastReconciler (nightly sync job) |
/ecs/tbi-translate-worker | 90 days | BeastTranslate (persistent SQS poller + batch orchestrator) |
| Log Group | Retention | Function |
|---|---|---|
/aws/lambda/trinity-beast-receipt | 90 days | Stripe receipt processing |
/aws/lambda/trinity-beast-queued-writer | 90 days | SQS → Aurora batch inserts |
/aws/lambda/tbi-ops-notify | 90 days | Formatted SES notifications |
/aws/lambda/tbi-ops-self-heal | 90 days | ECS task restart automation |
/aws/lambda/tbi-ops-waf-action | 90 days | WAF rule management |
/aws/lambda/tbi-ops-honeypot-processor | 90 days | Honeypot queue → WAF blocks |
/aws/lambda/tbi-ops-bedrock-analyze | 90 days | AI threat correlation |
/aws/lambda/tbi-rhema-support | 90 days | AI support assistant |
/aws/lambda/tbi-ops-digest | 90 days | Daily/weekly digest |
/aws/lambda/trinity-beast-email-sender | 90 days | Outbound receipt/notification email delivery |
/aws/lambda/tbi-support-inbound | 90 days | Inbound customer email replies (us-east-1 — SES inbound receiving is unavailable in us-east-2) |
tbi-translate-init, tbi-translate-deploy, tbi-translate-finalize, tbi-translate-batch-prepare, tbi-translate-batch-submit — all removed 2026-06-19/2026-06-27 when translation moved to direct SQS fan-out) and omitted two live functions (trinity-beast-email-sender, added 2026-07-24, and tbi-support-inbound, which lives in us-east-1 rather than us-east-2). The orphaned log groups for the five deleted functions have since been deleted as well — an empty, unretained log group is otherwise indistinguishable from a live one at a glance.
| Log Group | Retention | Source |
|---|---|---|
/aws/rds/cluster/trinity-beast-aurora-cluster/postgresql | 90 days | Aurora slow queries (>1s), lock waits, errors |
/aws/elasticache/trinity-beast-cache/slow-log | 90 days | Valkey commands exceeding slowlog threshold |
/aws/elasticache/trinity-beast-cache/engine-log | 90 days | Valkey engine events (startup, failover, persistence) |
/aws/vpc/trinity-beast-flowlogs | 90 days | VPC Flow Logs (both VPCs) |
/aws/cloudtrail/trinity-beast | 30 days | CloudTrail API audit (S3 archive is indefinite) |
/aws/ecs/containerinsights/trinity-beast-fargate-cluster/performance | 90 days | Container Insights (CPU, memory, network per task) |
RDSOSMetrics | 90 days | Aurora Enhanced Monitoring (OS-level, every 30s) |
The following log types are delivered directly to S3 bucket aws-waf-logs-trinity-beast rather than CloudWatch. They are high-volume, low-urgency logs best queried via download + gunzip + jq, or via Athena (future). Full details in Unified Logs & Observability.
| S3 Prefix | Source | Retention |
|---|---|---|
AWSLogs/.../WAFLogs/ | WAF full request logs (every API request — IP, URI, headers, rule match) | 365 days (S3 lifecycle) |
alb/ | ALB access logs (per-request latency, status, target) | 365 days |
cloudfront/ | CloudFront standard logs (every website request — edge, cache status) | 365 days |
s3-access/ | S3 server access logs (object-level access audit) | 365 days |
The application publishes custom metrics to two CloudWatch namespaces, providing business-level observability beyond standard AWS metrics.
Metrics published by the Live Price Oracle service:
| Metric | Description |
|---|---|
Requests | Total LPO requests received |
CacheHits | Requests served from ElastiCache cache |
CacheMisses | Requests requiring upstream source fetch |
Errors | Failed requests (all error types) |
SourceFailovers | Times a primary source failed and secondary was used |
AvgLatency | Average response time in milliseconds |
Metrics published by the Live Report Service:
| Metric | Description |
|---|---|
Requests | Total LRS report requests |
AvgLatency | Average report generation time in milliseconds |
Errors | Failed report generations |
MonthlyLimitExceeded | Requests rejected due to monthly quota |
DailyLimitExceeded | Requests rejected due to daily quota |
AddOnRequests | Requests using add-on quota beyond base plan |
The 5-layer AutoOps system has its own monitoring footprint — 7 Lambda functions, 8 EventBridge rules, 2 anomaly detection alarms, and a dedicated SNS topic. All feed into the Security Dashboard.
These alarms use CloudWatch Anomaly Detection — a machine-learned band of "expected" values — rather than a fixed threshold. Instead of you choosing a number, CloudWatch observes the metric for roughly two weeks, learns its normal rhythm including daily and weekly cycles, and alarms when reality leaves the band.
| Alarm Name | Metric | Band | Direction | Catches |
|---|---|---|---|---|
TrinityBeast-Anomaly-RequestRate | ALB RequestCount (Sum) | 12 | Both (↑↓) | Traffic drops (outage) or unexpected spikes (attack) |
TrinityBeast-Anomaly-ErrorRate | ALB 5xx Count (Sum) | 10 | Above only (↑) | Error spikes beyond normal noise |
Configuration: 300s period, 3 evaluation periods, 2 datapoints to alarm, treat missing as notBreaching. Both currently have ActionsEnabled: false — see the caution below.
Anomaly detection is a wonderful tool applied to the wrong metric more often than the right one. This deserves a real explanation, because it is the least intuitive part of CloudWatch and the place where well-meaning monitoring quietly becomes noise.
An anomaly band works beautifully when a metric has a stable, repeating shape — a business whose traffic rises each morning and falls each night, week after week. The model learns that shape and catches genuine departures from it. It works poorly, and sometimes disastrously, in three situations we have now met all three of:
CacheHitRate is hits divided by hits-plus-misses. In a five-minute window containing one miss and no hits, that ratio is exactly 0.0 — not because the cache is broken, but because arithmetic. At around one cache operation every 28 seconds, most windows looked like that, the learned band settled near 0.07–0.13, and the series crossed it constantly. Meanwhile the true aggregate hit rate, computed from the raw counters over a full day, was a perfectly healthy 86.6%. If you must monitor a rate, build it from the raw numerator and denominator over a window wide enough to contain real traffic — CacheHits / (CacheHits + CacheMisses) over 900 seconds or more — never the pre-computed ratio at a short period.TargetResponseTime on this ALB runs a median of 5.4 ms with a p90 of 78 ms and a maximum of 161 ms — a 30× spread, with a standard deviation six times the median. That is two populations wearing one metric: fast cached price reads, and slower admin and report endpoints. A band trained on the average of a bimodal metric hugs whichever mode dominates, so any window weighted toward the slower one reads as an anomaly. No band width fixes this; the metric itself is the wrong shape for the technique.Two alarms were removed on July 30, 2026 for exactly these reasons — TrinityBeast-Anomaly-CacheHitRate (the ratio problem, 60 state changes in 24 hours) and TrinityBeast-Anomaly-Latency (the bimodal problem, 23 state changes). Latency is being replaced by a static p99 > 300 ms threshold at launch, because latency is one of the few metrics where "bad" has an absolute definition and no model is needed to recognise it.
Two practical notes if you ever remove one of these. First, an anomaly alarm and its underlying detector are separate resources; deleting the alarm leaves the trained detector behind. Second, delete the alarm, wait, and only then delete the detector — alarm deletion is asynchronous, and a still-evaluating alarm will request its band, which causes CloudWatch to implicitly recreate the detector you just removed. It will look haunted. It is a race.
Why "actions disabled" is not the same as "off." Both remaining anomaly alarms have ActionsEnabled: false, deferred until a real traffic baseline exists. That flag suppresses the alarm's own action — the SNS publish — and nothing else. The alarm still evaluates, still changes state, and CloudWatch still emits a state-change event. Since tbi-ops-alarm-trigger listens for that event rather than for the action, a muted alarm still drives the self-healing Step Function on every transition.
That distinction cost us something real. The two deleted alarms produced 83 ALARM transitions in a single day, which fired 82 Step Function executions and 82 tbi-ops-self-heal invocations. Every one checked the system, found it healthy, and stopped — no harm done — but nobody noticed for months precisely because the alarms were muted and no email ever arrived. When you mute an alarm, ask what else is listening to its state.
All 7 functions share the tbi-autonomous-ops-role IAM role. Metrics visible on the Security Dashboard.
| Function | Purpose | Log Group |
|---|---|---|
tbi-ops-notify | SNS notifications with severity levels | /aws/lambda/tbi-ops-notify |
tbi-ops-self-heal | ECS task restart, force-deploy | /aws/lambda/tbi-ops-self-heal |
tbi-ops-waf-action | WAF IP set block/unblock | /aws/lambda/tbi-ops-waf-action |
tbi-ops-honeypot-processor | Drain honeypot queue → WAF block | /aws/lambda/tbi-ops-honeypot-processor |
tbi-ops-bedrock-analyze | AI threat analysis via Bedrock | /aws/lambda/tbi-ops-bedrock-analyze |
tbi-rhema-support | AI ticket categorization + drafts | /aws/lambda/tbi-rhema-support |
tbi-ops-digest | Daily/weekly operational digests | /aws/lambda/tbi-ops-digest |
| Rule Name | Trigger | Target |
|---|---|---|
tbi-ops-alarm-trigger | CloudWatch alarm → ALARM (any alarm, any account) | Step Function: health-check-heal |
tbi-ops-honeypot-queue-processor | rate(5 minutes) | Lambda: honeypot-processor |
tbi-ops-bedrock-analyze-schedule | rate(30 minutes) | Lambda: bedrock-analyze |
tbi-ops-guardduty-high-finding | GuardDuty severity ≥ 7 | Lambda: bedrock-analyze |
tbi-ops-daily-digest | cron(0 11 * * ? *) — 6 AM EST | Lambda: digest |
tbi-ops-weekly-digest | cron(0 12 ? * MON *) — Mon 7 AM EST | Lambda: digest |
tbi-cpmp-monthly-newsletter | cron(0 15 ? * 1#1 *) — 10 AM EST, 1st Sunday | Lambda: digest |
trinity-beast-nightly-sync | cron(0 5 * * ? *) — 1 AM EST | ECS RunTask: BeastReconciler sync job |
Note the breadth of the first rule. tbi-ops-alarm-trigger matches every CloudWatch alarm state change to ALARM in this account — it does not filter by alarm name. That is a deliberate and good design: nothing escapes the self-healing net. But it means the blast radius of a noisy alarm is larger than the alarm itself. Add a flapping alarm anywhere in the account and you have added Step Function executions everywhere. Keep it in mind whenever you create or tune an alarm.
Topic: tbi-ops-notifications (arn:aws:sns:us-east-2:211998422884:tbi-ops-notifications)
Subscriber: tbi-ops-notify Lambda (formats + sends via SES to CoryDeanKalani@CPMP-Site.org)
Severity levels in subject: [INFO], [WARNING], [CRITICAL], [SELF-HEALED]
All 21 named alarms route here — no raw AWS emails, everything formatted by Lambda.
One quirk of the subject line worth knowing in advance. The tbi-ops-notify Lambda tags any transition into OK as [SELF-HEALED], because in the ordinary case that is exactly what it means — something broke, automation fixed it, the alarm recovered. But the tag is applied to the transition, not to any evidence that healing occurred. So a brand-new alarm moving from INSUFFICIENT_DATA to OK for the first time also arrives labelled [SELF-HEALED].
This surprises people. When five previously-blind alarms were repaired on July 30, 2026, four of them transitioned to OK within two minutes and produced four [SELF-HEALED] emails in a row — with no Step Function executions at all, because nothing had actually healed. If you are about to fix, create, or re-dimension an alarm whose actions are enabled, expect mail, and tell whoever is on the receiving end before you do it.
When an alarm fires, use the following runbooks to diagnose and resolve the issue. Each category includes the most common root causes and recommended actions.
What it means: One or more ECS tasks are failing health checks from the load balancer.
/aws/ecs/trinity-beast for startup errors or OOM killsWhat it means: An ECS service is consuming more than 80% CPU over a sustained period.
What it means: A container has crashed and no tasks are running for the service. These alarms use TreatMissing: breaching, so missing data also triggers the alarm.
What it means: The Aurora Serverless v2 cluster is consuming more than 80% CPU.
pg_stat_statementsWhat it means: More than 80 active database connections — approaching the connection limit.
What it means: The ElastiCache Serverless cache is approaching its configured limits or experiencing latency degradation. Unlike node-based caches, Serverless doesn't run out of "CPU" — it runs out of provisioned capacity (ECPU) or storage.
What it means: The S3 bucket has exceeded 10 GB, which may indicate unexpected data accumulation.
PutObject eventsWhat it means: The alarm is very likely blind — watching a metric that receives no data, usually because its dimensions name a resource that no longer exists.
bash scripts/kcc.sh alarm-audit — it validates every alarm's dimensions against the live resource inventory and names the offender directlyStateReason. If it cites a datapoint from months ago, that timestamp tells you when the alarm went blindaws elbv2 describe-load-balancers, aws rds describe-db-clusters, and so onput-metric-alarm, which upserts on alarm name — the same command that creates an alarm also repairs oneTreatMissingData deliberately (see Section 5). Prefer missing on health alarms so a future mistake stays visible[SELF-HEALED] email as the repaired alarm reaches OK for the first time — warn your team before you startEverything up to this point has described what exists. This section is about something harder and more valuable — how to keep believing it.
Monitoring has a peculiar failure mode that most systems do not share. When application code breaks, you find out: something returns a 500, a test goes red, a customer writes in. When monitoring breaks, the symptom is silence, and silence is indistinguishable from success. A dashboard full of green is what a healthy system looks like. It is also what a system with no working monitoring at all looks like. Learning to tell those two apart is a skill, and it is worth building deliberately rather than acquiring in the middle of an incident.
On July 30, 2026 we audited all 27 alarms in this account and found that six of them had never fired once, because all six were watching resources that did not exist. Four sat in INSUFFICIENT_DATA. Two displayed a confident green OK. Nothing had been neglected — every one broke during an ordinary, correctly-executed infrastructure change. That is the part worth absorbing: this failure mode is not caused by carelessness, so being careful is not a sufficient defence against it.
| Mode | What you see | What is actually happening |
|---|---|---|
| Blind | OK forever, or INSUFFICIENT_DATA forever |
Dimensions name a deleted or renamed resource. No data arrives, so the alarm reports whatever TreatMissingData tells it to. With notBreaching, that is a reassuring green. |
| Flapping | Alarm oscillates OK ↔ ALARM all day | The threshold or band does not match the metric's real behaviour. Every transition fires downstream automation. Costs little money; costs a great deal of trust. |
| Muted | ActionsEnabled: false — looks safely switched off |
Only the SNS publish is suppressed. The alarm still evaluates and still emits state-change events, so EventBridge automation keeps running with nobody watching. |
Notice what these three have in common: none of them is visible from an alarm's state value. A blind alarm and a healthy one both read OK. A flapping alarm reads OK roughly half the times you happen to look. A muted alarm reads whatever it likes and tells nobody. State is the thing the console shows you most prominently, and it is the one thing that cannot answer the question you actually care about.
| Alarm | Was watching | Root cause |
|---|---|---|
TrinityBeast-API-5xx-Spike | An ALB ID matching no load balancer in the account | Copied dimension, never verified |
TrinityBeast-API-4xx-Spike | The same nonexistent ALB ID | Same |
Trinity-Beast-Aurora-Connections-High | Cluster cpmp-backend-aurora | Cluster renamed; alarm not updated |
Trinity-Beast-ALB-UnhealthyTargets | trinity-beast-alb-v3, deleted in April | ALB replaced during an upgrade |
TrinityBeast-WAF-HighBlockRate | Rule=ALL with no WebACL or Region | WAFV2 requires three dimensions; two were omitted |
TrinityBeast-GuardDuty-Finding | A metric GuardDuty does not publish | Alarm built on a metric that never existed |
Four of those six broke during infrastructure changes we were right to make — replacing a load balancer, renaming a database cluster, migrating ElastiCache to Serverless. Each change was executed correctly. What was missing every time was the step after the change: going back to ask which alarms had been quietly orphaned by it.
The two that displayed OK deserve special attention, because they are the reason this section exists. Trinity-Beast-ALB-UnhealthyTargets had been green since April 19 — three and a half months — and we know the date precisely because its stale StateReason still cited a datapoint from 19/04/26 22:06. Every morning it appeared on the dashboard as evidence that the load balancer was healthy. It was evidence of nothing at all.
The habit worth building: after any migration, rename, or resource replacement, ask "which alarms referenced the thing I just changed?" before you close the task. Two minutes then saves three months of false confidence.
A discipline that depends on remembering is a discipline that will lapse, so we turned this one into a command. It checks all three failure modes in a single pass:
bash scripts/kcc.sh alarm-audit
bash scripts/kcc.sh alarm-audit --hours 72
| Check | How it works |
|---|---|
| Blind dimensions | Builds a live inventory of load balancers, target groups, RDS clusters and instances, ECS services, ElastiCache caches, WAF ACLs, Lambda functions, and S3 buckets — then flags any alarm dimension naming something absent from it. Calls out separately the ones rendering OK while blind, since those are the dangerous ones. |
| Flapping | Counts ALARM transitions per alarm over the window and flags any exceeding 6, with a reminder that each transition drives a Step Function execution. |
| Muted | Lists every ActionsEnabled: false alarm, noting that muting stops the email and not the automation. |
| Insufficient data | Reports persistent INSUFFICIENT_DATA — the visible symptom of a bad dimension, and the one state you should never train yourself to ignore. |
One detail the audit handles for you: it excludes the six TargetTracking- alarms that ECS auto-scaling manages, because three of them sit permanently in ALARM by design (see Section 5). Any audit that does not filter them reports three false positives forever — and an audit you have learned to ignore is worse than no audit at all.
OK is not evidence of health. It is evidence of one of two very different things, and you cannot tell which by looking at it. Verify dimensions, not states.INSUFFICIENT_DATA is a finding, not a mood. Acceptable for fifteen minutes after a config change. Past that it means the metric never reports, and the fault is in the alarm rather than in the world.TreatMissingData as a decision, never as a default. Ask: if this metric goes silent, do I want to be reassured, alerted, or told plainly that I do not know? All three answers are sometimes right. notBreaching is the most comfortable and the most likely to hide a problem.A closing thought. None of the six blind alarms caused an outage, and it would be easy to file the whole episode as a near miss and move on. We would rather you take the opposite lesson. Our threat checks and health reviews found nothing wrong that day because they query WAF, GuardDuty, and the services directly — not because the alarms were working. The safety net had a hole in it for three months, and the only reason it never mattered is that nobody happened to fall. Good operations means inspecting the net on a schedule, whether or not anyone has fallen recently. That is what alarm-audit exists for, and it is the part of this guide we would most want you to remember.