From 5862cad681ece4638cbb7e48f563965c541f30d5 Mon Sep 17 00:00:00 2001 From: Daniel Truong Date: Sun, 6 Sep 2026 18:51:13 +0000 Subject: [PATCH 1/2] fix(azure): scope the budget to eagle-analytics resources The budget had no filter, so it measured the whole shared resource group and fired all three alerts on 2026-09-06. Filter on the Application tag that every estate resource already carries. --- azure/main.bicep | 1 + azure/modules/cost-budget.bicep | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/azure/main.bicep b/azure/main.bicep index 3af83fa..30b5950 100644 --- a/azure/main.bicep +++ b/azure/main.bicep @@ -186,6 +186,7 @@ module costBudget './modules/cost-budget.bicep' = { budgetAmount: budgetAmount contactEmails: contactEmails startDate: budgetStartDate + applicationTag: defaultTags.Application } } diff --git a/azure/modules/cost-budget.bicep b/azure/modules/cost-budget.bicep index 9a017c9..c1e3188 100644 --- a/azure/modules/cost-budget.bicep +++ b/azure/modules/cost-budget.bicep @@ -20,6 +20,9 @@ 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('Value of the `Application` tag every resource in this estate carries. main.bicep passes its own defaultTags.Application, so the filter and the tags cannot drift apart.') +param applicationTag 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 +39,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: 'Application' + operator: 'In' + values: [applicationTag] + } + } notifications: { Actual_80_Percent: { enabled: true From c15695cc7965e6ed342016694457ebf1ca976772 Mon Sep 17 00:00:00 2001 From: Daniel Truong Date: Sun, 6 Sep 2026 19:20:10 +0000 Subject: [PATCH 2/2] fix(azure): derive budget tag key and value from one source Review finding: the tag key was a literal, so a rename in defaultTags would leave a budget that matches nothing. Both key and value now come from the same object, and empty values fail the build. Also note the /api/usage browser prefix in MIGRATION.md. --- azure/main.bicep | 7 +++++-- azure/modules/cost-budget.bicep | 11 +++++++---- docs/MIGRATION.md | 4 ++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/azure/main.bicep b/azure/main.bicep index 30b5950..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,7 +188,8 @@ module costBudget './modules/cost-budget.bicep' = { budgetAmount: budgetAmount contactEmails: contactEmails startDate: budgetStartDate - applicationTag: defaultTags.Application + applicationTagKey: applicationTagKey + applicationTagValue: defaultTags[applicationTagKey] } } diff --git a/azure/modules/cost-budget.bicep b/azure/modules/cost-budget.bicep index c1e3188..27ef6c6 100644 --- a/azure/modules/cost-budget.bicep +++ b/azure/modules/cost-budget.bicep @@ -20,8 +20,11 @@ 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('Value of the `Application` tag every resource in this estate carries. main.bicep passes its own defaultTags.Application, so the filter and the tags cannot drift apart.') -param applicationTag string +@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. @@ -46,9 +49,9 @@ resource costBudget 'Microsoft.Consumption/budgets@2021-10-01' = { // carry the tag; `In` is the only operator a budget filter accepts. filter: { tags: { - name: 'Application' + name: applicationTagKey operator: 'In' - values: [applicationTag] + values: [applicationTagValue] } } notifications: { 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: