diff --git a/azure/main.bicep b/azure/main.bicep index 3af83fa..58b30f7 100644 --- a/azure/main.bicep +++ b/azure/main.bicep @@ -68,9 +68,11 @@ param budgetStartDate string = '' @description('Email addresses for budget and alert notifications. Sourced from the environment by the param files — never a literal, this repository is public.') param contactEmails array +var applicationTagKey = 'Application' + var defaultTags = { Project: 'EPIC Analytics' - Application: 'eagle-analytics' + '${applicationTagKey}': 'eagle-analytics' Environment: environmentName ManagedBy: 'Bicep' CostCenter: 'c4b0a8' @@ -186,6 +188,8 @@ module costBudget './modules/cost-budget.bicep' = { budgetAmount: budgetAmount contactEmails: contactEmails startDate: budgetStartDate + applicationTagKey: applicationTagKey + applicationTagValue: defaultTags[applicationTagKey] } } diff --git a/azure/modules/cost-budget.bicep b/azure/modules/cost-budget.bicep index 9a017c9..27ef6c6 100644 --- a/azure/modules/cost-budget.bicep +++ b/azure/modules/cost-budget.bicep @@ -20,6 +20,12 @@ param budgetAmount int = 60 @description('Email addresses to receive budget threshold alerts. Passed in, never defaulted: an address in a public repository is a spam target.') param contactEmails array +@description('Tag key and value every resource in this estate carries. main.bicep passes both from the same defaultTags entry, so the filter key and the tags cannot drift apart.') +@minLength(1) +param applicationTagKey string +@minLength(1) +param applicationTagValue string + // utcNow() is only legal as a parameter default in Bicep, which is the shape wanted here: evaluated // once at deployment, never drifting on a redeploy of an unchanged template. @description('First day of the budget period. Defaults to the first of the current month for a NEW budget; an existing budget REJECTS any startDate change, so param files pin the live value.') @@ -36,6 +42,18 @@ resource costBudget 'Microsoft.Consumption/budgets@2021-10-01' = { timePeriod: { startDate: '${effectiveStartDate}T00:00:00Z' } + // A budget with no filter measures the WHOLE resource group, and both groups are shared: test + // sits with eagle-demi (about 150 CAD/month on its own), prod with the eagle-public estate. That + // is what fired all three thresholds on 2026-09-06 without analytics spending anything near 60. + // Every billable resource here is stamped by main.bicep's defaultTags and its usage records + // carry the tag; `In` is the only operator a budget filter accepts. + filter: { + tags: { + name: applicationTagKey + operator: 'In' + values: [applicationTagValue] + } + } notifications: { Actual_80_Percent: { enabled: true diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 266137b..d1eab7d 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -32,6 +32,10 @@ step wrote. Point the client at both: eagle-public and eagle-admin keep sending to penguin and also send to `/analytics/events`. Nothing is switched off yet. +Since eao-nginx v2.7.32 (2026-09-06), the browser-facing prefix is `/api/usage/`; rproxy rewrites it +to `/analytics/` for the gateway, because ad blockers refuse paths containing "analytics". Config +`EAGLE_ANALYTICS_URL` is `/api/usage` on test. + ## 3. Compare the counts In penguin: