Cost Monitoring¶
Cloud spend (AWS + GCP) and prepaid API balances (DeepSeek, Serper) as Prometheus gauges, a Grafana Costs dashboard, and Discord alerts on the same pipe as every other alert. Terraform budgets on each cloud are a second, independent backstop.
What it monitors¶
| Signal | Source | Metric |
|---|---|---|
| AWS month-to-date cost by service | Cost Explorer GetCostAndUsage (global, us-east-1) |
aws_cost_mtd_usd{service} |
| GCP month-to-date cost by service | BigQuery billing export | gcp_cost_mtd_usd{service} |
| DeepSeek prepaid balance | GET api.deepseek.com/user/balance |
deepseek_balance_usd{currency} |
| Serper credits remaining (est.) | backend serper_requests_total counter (separate change) |
serper_credits_remaining (recording rule) |
The exporter¶
A small Go program (k8s/envs/platform/cost-exporter/) exposing /metrics (promhttp) and /healthz, refreshing on an hourly ticker (REFRESH_INTERVAL, configurable). It runs in the monitoring namespace; Prometheus scrapes its ServiceMonitor (labelled release: monitoring) cluster-wide.
Each provider fails independently: a provider erroring bumps cost_exporter_scrape_errors_total{provider} and leaves its last-good gauges in place, never blanking the others. A provider whose secret is absent stays disabled (the exporter runs with any subset wired).
| Env var | Default | Purpose |
|---|---|---|
REFRESH_INTERVAL |
1h |
Scrape cadence |
SCRAPE_TIMEOUT |
30s |
Per-provider timeout |
AWS_REGION |
us-east-1 |
Cost Explorer is global; pinned here |
GCP_BILLING_TABLE |
development-485000.billing_export.gcp_billing_export_v1_01CBC0_F49F57_C4BF88 |
project.dataset.table; presence enables GCP provider |
GCP_PROJECT |
development-485000 |
BigQuery client project |
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY |
(from ESO) | Presence enables the AWS provider |
DEEPSEEK_API_KEY |
(from ESO) | Presence enables the DeepSeek provider |
GCP auth is Workload Identity (KSA monitoring/cost-exporter → GCP SA cost-exporter, BigQuery Data Viewer + Job User). AWS + DeepSeek creds are projected by ESO from GCP Secret Manager. See Secrets Management and IAM.
Build + push the image:
The image is built and pushed automatically by Cloud Build trigger (cost-exporter-push-trigger) on merges to main touching k8s/envs/platform/cost-exporter/**. Manual build/push is also available:
task cost-exporter:push # gcloud builds submit --config=cloudbuild-cost-exporter.yaml
Dashboard¶
Costs folder in Grafana (k8s/.../manifests/dashboards/tomoda-costs.yaml, sidecar ConfigMap):
- AWS + GCP MTD stacked by service, with totals and a straight-line month-end projection (
MTD / day_of_month × days_in_month). - DeepSeek balance gauge + trend.
- Serper credits-remaining gauge + burn rate (requests/day).
- Prepaid runway: days-to-empty and top-up-within countdowns (balance ÷ 7-day burn).
Alerts¶
PrometheusRule cost-alerts (k8s/.../manifests/cost-alerts.yaml). Thresholds live in a cost.thresholds recording group as constant series, so retuning is a one-line edit, not an expression rewrite.
| Alert | Warning | Critical |
|---|---|---|
AWSCostMTDHigh |
MTD > $200 | MTD > $400 |
GCPCostMTDHigh |
MTD > $300 | MTD > $600 |
DeepSeekBalanceLow |
balance < $20 | balance < $5 |
SerperCreditsLow |
credits < 10000 | credits < 3000 |
Thresholds are placeholders
The values above are starting points — tune them in the cost.thresholds group. SerperCreditsLow stays dormant until the backend serper_requests_total counter ships (a separate tomoda-repo change); the recording rule references it and evaluates to the full pack size until then.
Alerts carry severity + summary/description annotations, so the existing Discord receiver renders and delivers them with no routing change.
Top-up workflow¶
- Discord posts
DeepSeekBalanceLow/SerperCreditsLowto#monitoring. - Check the Costs dashboard runway panels for days-to-empty.
- Top up the provider account.
- For Serper: reset the pack size (
cost:serper_pack_credits) and the counter window incost-alerts.yamlat each top-up.
Terraform budgets (second backstop)¶
Independent of the exporter, cloud-native budgets alert on spend directly:
- AWS (
infrastructure/aws/cost_budget.tf):aws_budgets_budget→ SNS topic → email to the owner. Discord-via-Lambda is a future option; the SNS topic is the seam. Gated oncost_budget_owner_email. - GCP (
infrastructure/gcp/cost_budget.tf):google_billing_budgetwith threshold rules → a Pub/Sub topic (cost-budget-alerts) + owner email. Consuming the topic for Discord is future. Gated onbilling_account.
Operator prerequisites¶
The exporter runs immediately but reads real data only once these are done:
- GCP billing → BigQuery export (Manual Console step). In the Cloud Console, enable the standard usage cost billing export into the
billing_exportdataset (created byinfrastructure/gcp/cost_exporter_iam.tf). Then setGCP_BILLING_TABLEon the Deployment to the export table (development-485000.billing_export.gcp_billing_export_v1_<BILLING_ACCOUNT>). GCP does not provide an API for Terraform to toggle billing export. - DeepSeek key (IaC + Secret Value). Terraform provisions
tomoda-deepseek-api-keyin GCP Secret Manager (infrastructure/gcp/cost_exporter_iam.tf). Populate its secret value. ESO projects it intomonitoring. - AWS Cost Explorer creds (Fully Automated via IaC).
terraform applyoninfrastructure/aws/cost_explorer_iam.tfcreates the scoped IAM User and automatically provisionstomoda-aws-cost-explorer-credsin GCP Secret Manager, so ESO projects it directly intomonitoring.
Cost Explorer request cost
GetCostAndUsage bills ~$0.01/request. Hourly refresh is a few cents a month.