[{"data":1,"prerenderedAt":4474},["ShallowReactive",2],{"post:\u002F2026\u002F02\u002F18\u002Fnode-js-azure-a-slack-bot-for-resource-group-budget-alerts\u002F":3},{"post":4,"newer":4432,"older":4443,"related":4454,"series":4472},{"id":5,"title":6,"body":7,"canonical":4413,"categories":4414,"date":4417,"description":4418,"extension":4419,"featured":4420,"hero":4421,"image":4421,"meta":4422,"navigation":418,"path":4423,"readingTime":349,"seo":4424,"series":4421,"seriesOrder":4421,"sites":4425,"source":4421,"stem":4426,"tags":4427,"updated":4421,"url":4430,"__hash__":4431},"blog\u002Fblog\u002F2026\u002F02\u002F18\u002Fnode-js-azure-a-slack-bot-for-resource-group-budget-alerts.md","Node.js: Azure – A Slack Bot for Resource Group Budget Alerts",{"type":8,"value":9,"toc":4403},"minimark",[10,25,30,83,87,193,197,207,353,356,442,450,453,504,508,4076,4080,4128,4133,4149,4317,4331,4335,4399],[11,12,13,14,19,20,24],"p",{},"Azure's built-in budget alerts are fine, but out of the box they email a distribution list, which means they get filtered into a folder nobody checks (wiring them to Slack takes more plumbing, covered below). What actually gets attention on my teams is a Slack message in the channel where the engineers who provisioned the resources already live. This script queries Azure Cost Management for month-to-date spend on a list of resource groups, projects where each one will land at month end, compares both against a configured budget, and posts a single Slack message covering everything over the line. I run it as a scheduled task, but it's plain enough to drop into an Azure Function on a timer trigger. It is the budget half of ",[15,16,18],"a",{"href":17},"\u002F2026\u002F08\u002F12\u002Fazure-cost-governance-without-killing-developer-velocity\u002F","Azure: Cost Governance Without Killing Developer Velocity","; the anomaly half is ",[15,21,23],{"href":22},"\u002F2025\u002F12\u002F24\u002Fpython-azure-automate-cost-anomaly-alerts-with-the-cost-management-api\u002F","Python: Azure – Cost Anomaly Alerts from the Cost Management API",".",[26,27,29],"h2",{"id":28},"requirements","Requirements",[31,32,33,41,70,77,80],"ul",{},[34,35,36,37,24],"li",{},"A Node.js LTS release (20 or later) for the built-in ",[38,39,40],"code",{},"fetch",[34,42,43,46,47,50,51,54,55,58,59,58,62,65,66,69],{},[38,44,45],{},"@azure\u002Fidentity"," (",[38,48,49],{},"npm install @azure\u002Fidentity","). The script uses ",[38,52,53],{},"DefaultAzureCredential",", so the same code works with a managed identity in Azure, a service principal from ",[38,56,57],{},"AZURE_TENANT_ID"," \u002F ",[38,60,61],{},"AZURE_CLIENT_ID",[38,63,64],{},"AZURE_CLIENT_SECRET",", or an ",[38,67,68],{},"az login"," session while testing.",[34,71,72,76],{},[73,74,75],"strong",{},"Cost Management Reader"," on each resource group being queried (or once at the subscription or management group above them). Resource group scope is a supported Cost Management scope, so the identity doesn't need anything wider.",[34,78,79],{},"A Slack incoming webhook URL for the channel that should receive alerts.",[34,81,82],{},"A small JSON config file listing the resource groups to watch and their monthly budgets.",[26,84,86],{"id":85},"parameters","Parameters",[88,89,90,109],"table",{},[91,92,93],"thead",{},[94,95,96,100,103,106],"tr",{},[97,98,99],"th",{},"Name",[97,101,102],{},"Type",[97,104,105],{},"Required",[97,107,108],{},"Description",[110,111,112,144,163,178],"tbody",{},[94,113,114,120,123,126],{},[115,116,117],"td",{},[38,118,119],{},"--config",[115,121,122],{},"String",[115,124,125],{},"Yes",[115,127,128,129,132,133,132,136,139,140,143],{},"Path to the JSON config (array of ",[38,130,131],{},"subscriptionId",", ",[38,134,135],{},"resourceGroup",[38,137,138],{},"monthlyBudget",", optional ",[38,141,142],{},"owner",").",[94,145,146,151,154,157],{},[115,147,148],{},[38,149,150],{},"--threshold-percent",[115,152,153],{},"Number",[115,155,156],{},"No",[115,158,159,160,24],{},"Alert when actual or projected spend reaches this percentage of budget. Default ",[38,161,162],{},"80",[94,164,165,170,173,175],{},[115,166,167],{},[38,168,169],{},"--dry-run",[115,171,172],{},"Switch",[115,174,156],{},[115,176,177],{},"Print the Slack payload instead of posting it.",[94,179,180,185,188,190],{},[115,181,182],{},[38,183,184],{},"SLACK_WEBHOOK_URL",[115,186,187],{},"Env var",[115,189,125],{},[115,191,192],{},"Incoming webhook URL. Treat it as a secret.",[26,194,196],{"id":195},"usage","Usage",[11,198,199,200,203,204,206],{},"Create a ",[38,201,202],{},"budgets.json"," config next to the script. ",[38,205,142],{}," is optional text that ends up in the message, such as a Slack handle or team name:",[208,209,214],"pre",{"className":210,"code":211,"language":212,"meta":213,"style":213},"language-json shiki shiki-themes github-dark","[\n  { \"subscriptionId\": \"\u003Csubscription-id>\", \"resourceGroup\": \"rg-prod-app\", \"monthlyBudget\": 4000, \"owner\": \"@app-team\" },\n  { \"subscriptionId\": \"\u003Csubscription-id>\", \"resourceGroup\": \"rg-prod-data\", \"monthlyBudget\": 2500, \"owner\": \"@data-team\" },\n  { \"subscriptionId\": \"\u003Csubscription-id>\", \"resourceGroup\": \"rg-shared-network\", \"monthlyBudget\": 800 }\n]\n","json","",[38,215,216,225,275,315,347],{"__ignoreMap":213},[217,218,221],"span",{"class":219,"line":220},"line",1,[217,222,224],{"class":223},"s95oV","[\n",[217,226,228,231,235,238,242,244,247,249,252,254,257,259,262,264,267,269,272],{"class":219,"line":227},2,[217,229,230],{"class":223},"  { ",[217,232,234],{"class":233},"sDLfK","\"subscriptionId\"",[217,236,237],{"class":223},": ",[217,239,241],{"class":240},"sU2Wk","\"\u003Csubscription-id>\"",[217,243,132],{"class":223},[217,245,246],{"class":233},"\"resourceGroup\"",[217,248,237],{"class":223},[217,250,251],{"class":240},"\"rg-prod-app\"",[217,253,132],{"class":223},[217,255,256],{"class":233},"\"monthlyBudget\"",[217,258,237],{"class":223},[217,260,261],{"class":233},"4000",[217,263,132],{"class":223},[217,265,266],{"class":233},"\"owner\"",[217,268,237],{"class":223},[217,270,271],{"class":240},"\"@app-team\"",[217,273,274],{"class":223}," },\n",[217,276,278,280,282,284,286,288,290,292,295,297,299,301,304,306,308,310,313],{"class":219,"line":277},3,[217,279,230],{"class":223},[217,281,234],{"class":233},[217,283,237],{"class":223},[217,285,241],{"class":240},[217,287,132],{"class":223},[217,289,246],{"class":233},[217,291,237],{"class":223},[217,293,294],{"class":240},"\"rg-prod-data\"",[217,296,132],{"class":223},[217,298,256],{"class":233},[217,300,237],{"class":223},[217,302,303],{"class":233},"2500",[217,305,132],{"class":223},[217,307,266],{"class":233},[217,309,237],{"class":223},[217,311,312],{"class":240},"\"@data-team\"",[217,314,274],{"class":223},[217,316,318,320,322,324,326,328,330,332,335,337,339,341,344],{"class":219,"line":317},4,[217,319,230],{"class":223},[217,321,234],{"class":233},[217,323,237],{"class":223},[217,325,241],{"class":240},[217,327,132],{"class":223},[217,329,246],{"class":233},[217,331,237],{"class":223},[217,333,334],{"class":240},"\"rg-shared-network\"",[217,336,132],{"class":223},[217,338,256],{"class":233},[217,340,237],{"class":223},[217,342,343],{"class":233},"800",[217,345,346],{"class":223}," }\n",[217,348,350],{"class":219,"line":349},5,[217,351,352],{"class":223},"]\n",[11,354,355],{},"Set the environment and run it. With a service principal:",[208,357,361],{"className":358,"code":359,"language":360,"meta":213,"style":213},"language-bash shiki shiki-themes github-dark","export AZURE_TENANT_ID=\"\u003Ctenant-id>\"\nexport AZURE_CLIENT_ID=\"\u003Cclient-id>\"\nexport AZURE_CLIENT_SECRET=\"\u003Cclient-secret>\"\nexport SLACK_WEBHOOK_URL=\"\u003Cslack-webhook-url>\"\n\nnode budget-alert-bot.js --config budgets.json --threshold-percent 80\n","bash",[38,362,363,378,390,402,414,420],{"__ignoreMap":213},[217,364,365,369,372,375],{"class":219,"line":220},[217,366,368],{"class":367},"snl16","export",[217,370,371],{"class":223}," AZURE_TENANT_ID",[217,373,374],{"class":367},"=",[217,376,377],{"class":240},"\"\u003Ctenant-id>\"\n",[217,379,380,382,385,387],{"class":219,"line":227},[217,381,368],{"class":367},[217,383,384],{"class":223}," AZURE_CLIENT_ID",[217,386,374],{"class":367},[217,388,389],{"class":240},"\"\u003Cclient-id>\"\n",[217,391,392,394,397,399],{"class":219,"line":277},[217,393,368],{"class":367},[217,395,396],{"class":223}," AZURE_CLIENT_SECRET",[217,398,374],{"class":367},[217,400,401],{"class":240},"\"\u003Cclient-secret>\"\n",[217,403,404,406,409,411],{"class":219,"line":317},[217,405,368],{"class":367},[217,407,408],{"class":223}," SLACK_WEBHOOK_URL",[217,410,374],{"class":367},[217,412,413],{"class":240},"\"\u003Cslack-webhook-url>\"\n",[217,415,416],{"class":219,"line":349},[217,417,419],{"emptyLinePlaceholder":418},true,"\n",[217,421,423,427,430,433,436,439],{"class":219,"line":422},6,[217,424,426],{"class":425},"svObZ","node",[217,428,429],{"class":240}," budget-alert-bot.js",[217,431,432],{"class":233}," --config",[217,434,435],{"class":240}," budgets.json",[217,437,438],{"class":233}," --threshold-percent",[217,440,441],{"class":233}," 80\n",[208,443,448],{"className":444,"code":446,"language":447,"meta":213},[445],"language-text","rg-prod-app: 2,961.40 of 4,000.00 USD (74.0%), projected 4,877.60 (121.9%) ALERT\nrg-prod-data: 1,104.77 of 2,500.00 USD (44.2%), projected 1,819.62 (72.8%)\nrg-shared-network: 702.15 of 800.00 USD (87.8%), projected 1,156.48 (144.6%) ALERT\n2 of 3 resource group(s) at or over 80%. Posted 1 Slack message(s).\n","text",[38,449,446],{"__ignoreMap":213},[11,451,452],{},"Schedule it daily. Budgets and cost data only refresh a few times a day, and Microsoft recommends calling the cost APIs no more than once per day:",[208,454,456],{"className":358,"code":455,"language":360,"meta":213,"style":213},"0 8 * * * cd \u002Fopt\u002Fbudget-bot && node budget-alert-bot.js --config budgets.json --threshold-percent 80 >> \u002Fvar\u002Flog\u002Fbudget-bot.log 2>&1\n",[38,457,458],{"__ignoreMap":213},[217,459,460,463,466,469,471,473,476,479,482,484,486,488,490,492,495,498,501],{"class":219,"line":220},[217,461,462],{"class":425},"0",[217,464,465],{"class":233}," 8",[217,467,468],{"class":233}," *",[217,470,468],{"class":233},[217,472,468],{"class":233},[217,474,475],{"class":240}," cd",[217,477,478],{"class":240}," \u002Fopt\u002Fbudget-bot",[217,480,481],{"class":223}," && ",[217,483,426],{"class":425},[217,485,429],{"class":240},[217,487,432],{"class":233},[217,489,435],{"class":240},[217,491,438],{"class":233},[217,493,494],{"class":233}," 80",[217,496,497],{"class":367}," >>",[217,499,500],{"class":240}," \u002Fvar\u002Flog\u002Fbudget-bot.log",[217,502,503],{"class":367}," 2>&1\n",[26,505,507],{"id":506},"script","Script",[208,509,513],{"className":510,"code":511,"language":512,"meta":213,"style":213},"language-javascript shiki shiki-themes github-dark","#!\u002Fusr\u002Fbin\u002Fenv node\n\u002F**\n * budget-alert-bot.js\n *\n * Queries Azure Cost Management for month-to-date actual cost on a list of\n * resource groups, projects month-end spend with a straight-line run rate,\n * and posts Slack Block Kit messages (20 groups each) listing every resource group whose\n * actual or projected spend reached the threshold percentage of its budget.\n *\n * Reads:  a JSON config file (--config) of { subscriptionId, resourceGroup,\n *         monthlyBudget, owner? } entries, and the Cost Management Query API.\n * Writes: nothing locally; posts to the Slack incoming webhook in\n *         SLACK_WEBHOOK_URL (or prints the payload with --dry-run).\n *\n * Auth:   DefaultAzureCredential (managed identity, AZURE_* environment\n *         variables for a service principal, or an Azure CLI login).\n *\u002F\n\nconst fs = require('fs');\nconst { DefaultAzureCredential } = require('@azure\u002Fidentity');\n\nconst ARM = 'https:\u002F\u002Fmanagement.azure.com';\nconst API_VERSION = '2025-03-01';\nconst TOKEN_SCOPE = 'https:\u002F\u002Fmanagement.azure.com\u002F.default';\nconst QPU_RETRY_HEADER = 'x-ms-ratelimit-microsoft.costmanagement-qpu-retry-after';\nconst MAX_ATTEMPTS = 4;\nconst SECTIONS_PER_MESSAGE = 20;\n\nconst sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\n\nfunction parseArgs(argv) {\n    const args = { configPath: null, thresholdPercent: 80, dryRun: false };\n\n    for (let i = 0; i \u003C argv.length; i++) {\n        if (argv[i] === '--config') {\n            args.configPath = argv[++i];\n        } else if (argv[i] === '--threshold-percent') {\n            args.thresholdPercent = Number(argv[++i]);\n        } else if (argv[i] === '--dry-run') {\n            args.dryRun = true;\n        }\n    }\n\n    if (!args.configPath || !Number.isFinite(args.thresholdPercent)) {\n        throw new Error('Usage: node budget-alert-bot.js --config \u003Cpath> [--threshold-percent \u003Cn>] [--dry-run]');\n    }\n    return args;\n}\n\nfunction loadConfig(path) {\n    const entries = JSON.parse(fs.readFileSync(path, 'utf8'));\n    if (!Array.isArray(entries) || entries.length === 0) {\n        throw new Error(`${path} must be a non-empty JSON array.`);\n    }\n    for (const entry of entries) {\n        if (!entry.subscriptionId || !entry.resourceGroup || !(Number(entry.monthlyBudget) > 0)) {\n            throw new Error(`Invalid config entry: ${JSON.stringify(entry)}`);\n        }\n    }\n    return entries;\n}\n\nfunction monthProgress(now = new Date()) {\n    const daysInMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0)).getUTCDate();\n    \u002F\u002F Cost data lags, so count only completed UTC days (minimum 1) toward the run rate.\n    const daysElapsed = Math.max(now.getUTCDate() - 1, 1);\n    return { daysElapsed, daysInMonth };\n}\n\nasync function queryMonthToDate(token, subscriptionId, resourceGroup) {\n    const scope = `\u002Fsubscriptions\u002F${subscriptionId}\u002FresourceGroups\u002F${resourceGroup}`;\n    const url = `${ARM}${scope}\u002Fproviders\u002FMicrosoft.CostManagement\u002Fquery?api-version=${API_VERSION}`;\n    const body = JSON.stringify({\n        type: 'ActualCost',\n        timeframe: 'MonthToDate',\n        dataset: {\n            granularity: 'None',\n            aggregation: { totalCost: { name: 'PreTaxCost', function: 'Sum' } },\n        },\n    });\n\n    let response;\n    for (let attempt = 1; attempt \u003C= MAX_ATTEMPTS; attempt++) {\n        response = await fetch(url, {\n            method: 'POST',\n            headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application\u002Fjson' },\n            body,\n        });\n        if (![429, 503].includes(response.status) || attempt === MAX_ATTEMPTS) {\n            break;\n        }\n        const wait = Number(response.headers.get(QPU_RETRY_HEADER) || response.headers.get('retry-after') || 30);\n        console.error(`${resourceGroup}: throttled (${response.status}), retrying in ${wait}s`);\n        await sleep(wait * 1000);\n    }\n\n    if (!response.ok) {\n        throw new Error(`${resourceGroup}: Cost Management query failed ${response.status} ${await response.text()}`);\n    }\n    if (response.status === 204) {\n        \u002F\u002F 204 No Content is a documented success: nothing to report yet.\n        return { spend: 0, currency: '' };\n    }\n\n    const { properties } = await response.json();\n    const costIndex = properties.columns.findIndex((col) => col.type === 'Number');\n    const currencyIndex = properties.columns.findIndex((col) => col.name === 'Currency');\n    const row = properties.rows[0];\n\n    \u002F\u002F No rows means no cost yet this month.\n    return {\n        spend: row ? Number(row[costIndex]) : 0,\n        currency: row && currencyIndex >= 0 ? row[currencyIndex] : '',\n    };\n}\n\nfunction formatAmount(value) {\n    return value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });\n}\n\nfunction buildSlackMessages(alerts, thresholdPercent) {\n    const messages = [];\n    for (let i = 0; i \u003C alerts.length; i += SECTIONS_PER_MESSAGE) {\n        const chunk = alerts.slice(i, i + SECTIONS_PER_MESSAGE);\n        const blocks = [\n            {\n                type: 'header',\n                text: { type: 'plain_text', text: `Azure budget alert: ${alerts.length} resource group(s) at or over ${thresholdPercent}%` },\n            },\n        ];\n        for (const a of chunk) {\n            blocks.push({\n                type: 'section',\n                text: { type: 'mrkdwn', text: `*${a.resourceGroup}*${a.owner ? `  ${a.owner}` : ''}` },\n                fields: [\n                    { type: 'mrkdwn', text: `*Month to date*\\n${formatAmount(a.spend)} ${a.currency} (${a.percentUsed.toFixed(1)}%)` },\n                    { type: 'mrkdwn', text: `*Budget*\\n${formatAmount(a.monthlyBudget)} ${a.currency}` },\n                    { type: 'mrkdwn', text: `*Projected month end*\\n${formatAmount(a.projected)} ${a.currency} (${a.projectedPercent.toFixed(1)}%)` },\n                    { type: 'mrkdwn', text: `*Subscription*\\n\\`${a.subscriptionId}\\`` },\n                ],\n            });\n        }\n        blocks.push({ type: 'context', elements: [{ type: 'mrkdwn', text: 'Actual cost from Azure Cost Management; data can lag by a day or more.' }] });\n\n        messages.push({\n            \u002F\u002F Top-level text is the notification and fallback when blocks can't be shown.\n            text: `Azure budget alert: ${chunk.map((a) => a.resourceGroup).join(', ')}`,\n            blocks,\n        });\n    }\n    return messages;\n}\n\nasync function postToSlack(webhookUrl, message) {\n    let response;\n    for (let attempt = 1; attempt \u003C= MAX_ATTEMPTS; attempt++) {\n        response = await fetch(webhookUrl, {\n            method: 'POST',\n            headers: { 'Content-Type': 'application\u002Fjson' },\n            body: JSON.stringify(message),\n        });\n        if (response.status !== 429 || attempt === MAX_ATTEMPTS) {\n            break;\n        }\n        const wait = Number(response.headers.get('retry-after') || 1);\n        await sleep(wait * 1000);\n    }\n    if (!response.ok) {\n        \u002F\u002F Slack returns a short error string such as invalid_payload, no_text or channel_is_archived.\n        throw new Error(`Slack webhook post failed: ${response.status} ${await response.text()}`);\n    }\n}\n\nasync function main() {\n    const args = parseArgs(process.argv.slice(2));\n    const webhookUrl = process.env.SLACK_WEBHOOK_URL;\n    if (!webhookUrl && !args.dryRun) {\n        throw new Error('SLACK_WEBHOOK_URL is not set.');\n    }\n\n    const config = loadConfig(args.configPath);\n    const { token } = await new DefaultAzureCredential().getToken(TOKEN_SCOPE);\n    const { daysElapsed, daysInMonth } = monthProgress();\n\n    const alerts = [];\n    let failures = 0;\n\n    \u002F\u002F Serial on purpose: Query API quotas are per tenant.\n    for (const entry of config) {\n        try {\n            const { spend, currency } = await queryMonthToDate(token, entry.subscriptionId, entry.resourceGroup);\n            const monthlyBudget = Number(entry.monthlyBudget);\n            const projected = (spend \u002F daysElapsed) * daysInMonth;\n            const result = {\n                ...entry,\n                monthlyBudget,\n                spend,\n                currency,\n                projected,\n                percentUsed: (spend \u002F monthlyBudget) * 100,\n                projectedPercent: (projected \u002F monthlyBudget) * 100,\n            };\n            const isAlert = result.percentUsed >= args.thresholdPercent || result.projectedPercent >= args.thresholdPercent;\n\n            console.log(\n                `${entry.resourceGroup}: ${formatAmount(spend)} of ${formatAmount(monthlyBudget)} ${currency} ` +\n                `(${result.percentUsed.toFixed(1)}%), projected ${formatAmount(projected)} ` +\n                `(${result.projectedPercent.toFixed(1)}%)${isAlert ? ' ALERT' : ''}`\n            );\n            if (isAlert) {\n                alerts.push(result);\n            }\n        } catch (error) {\n            failures++;\n            console.error(error.message);\n        }\n    }\n\n    const messages = buildSlackMessages(alerts, args.thresholdPercent);\n    for (const [index, message] of messages.entries()) {\n        if (args.dryRun) {\n            console.log(JSON.stringify(message, null, 2));\n        } else {\n            if (index > 0) {\n                \u002F\u002F Incoming webhooks allow about one message per second.\n                await sleep(1100);\n            }\n            await postToSlack(webhookUrl, message);\n        }\n    }\n\n    const posted = args.dryRun ? 0 : messages.length;\n    console.log(`${alerts.length} of ${config.length} resource group(s) at or over ${args.thresholdPercent}%. Posted ${posted} Slack message(s).`);\n    if (failures > 0) {\n        process.exitCode = 1;\n    }\n}\n\nmain().catch((error) => {\n    console.error(error);\n    process.exitCode = 1;\n});\n","javascript",[38,514,515,521,526,531,536,541,546,552,558,563,569,575,581,587,592,598,604,610,615,639,663,668,684,699,714,729,744,759,764,808,813,830,861,866,905,922,938,959,978,996,1009,1015,1021,1026,1055,1073,1078,1087,1093,1098,1113,1146,1178,1199,1204,1222,1260,1296,1301,1306,1314,1319,1324,1347,1401,1407,1441,1449,1454,1459,1486,1510,1542,1561,1573,1584,1590,1601,1619,1625,1631,1636,1645,1676,1693,1704,1729,1735,1741,1780,1788,1793,1840,1877,1896,1901,1906,1918,1962,1967,1982,1988,2007,2012,2017,2040,2075,2107,2125,2130,2136,2144,2165,2195,2201,2206,2211,2226,2256,2261,2266,2286,2299,2332,2356,2369,2375,2386,2417,2423,2429,2447,2458,2468,2517,2523,2591,2631,2691,2725,2731,2737,2742,2769,2774,2784,2790,2836,2842,2847,2852,2860,2865,2870,2892,2899,2926,2940,2949,2963,2978,2983,3007,3014,3019,3046,3061,3066,3077,3083,3119,3124,3129,3134,3147,3169,3186,3205,3221,3226,3231,3246,3279,3302,3307,3319,3333,3338,3344,3360,3368,3393,3408,3432,3444,3453,3459,3465,3471,3477,3495,3511,3517,3545,3550,3562,3606,3644,3681,3687,3696,3707,3713,3724,3734,3744,3749,3754,3759,3773,3806,3814,3840,3849,3863,3869,3884,3889,3900,3905,3910,3915,3940,3987,4001,4013,4018,4023,4028,4048,4058,4070],{"__ignoreMap":213},[217,516,517],{"class":219,"line":220},[217,518,520],{"class":519},"sAwPA","#!\u002Fusr\u002Fbin\u002Fenv node\n",[217,522,523],{"class":219,"line":227},[217,524,525],{"class":519},"\u002F**\n",[217,527,528],{"class":219,"line":277},[217,529,530],{"class":519}," * budget-alert-bot.js\n",[217,532,533],{"class":219,"line":317},[217,534,535],{"class":519}," *\n",[217,537,538],{"class":219,"line":349},[217,539,540],{"class":519}," * Queries Azure Cost Management for month-to-date actual cost on a list of\n",[217,542,543],{"class":219,"line":422},[217,544,545],{"class":519}," * resource groups, projects month-end spend with a straight-line run rate,\n",[217,547,549],{"class":219,"line":548},7,[217,550,551],{"class":519}," * and posts Slack Block Kit messages (20 groups each) listing every resource group whose\n",[217,553,555],{"class":219,"line":554},8,[217,556,557],{"class":519}," * actual or projected spend reached the threshold percentage of its budget.\n",[217,559,561],{"class":219,"line":560},9,[217,562,535],{"class":519},[217,564,566],{"class":219,"line":565},10,[217,567,568],{"class":519}," * Reads:  a JSON config file (--config) of { subscriptionId, resourceGroup,\n",[217,570,572],{"class":219,"line":571},11,[217,573,574],{"class":519}," *         monthlyBudget, owner? } entries, and the Cost Management Query API.\n",[217,576,578],{"class":219,"line":577},12,[217,579,580],{"class":519}," * Writes: nothing locally; posts to the Slack incoming webhook in\n",[217,582,584],{"class":219,"line":583},13,[217,585,586],{"class":519}," *         SLACK_WEBHOOK_URL (or prints the payload with --dry-run).\n",[217,588,590],{"class":219,"line":589},14,[217,591,535],{"class":519},[217,593,595],{"class":219,"line":594},15,[217,596,597],{"class":519}," * Auth:   DefaultAzureCredential (managed identity, AZURE_* environment\n",[217,599,601],{"class":219,"line":600},16,[217,602,603],{"class":519}," *         variables for a service principal, or an Azure CLI login).\n",[217,605,607],{"class":219,"line":606},17,[217,608,609],{"class":519}," *\u002F\n",[217,611,613],{"class":219,"line":612},18,[217,614,419],{"emptyLinePlaceholder":418},[217,616,618,621,624,627,630,633,636],{"class":219,"line":617},19,[217,619,620],{"class":367},"const",[217,622,623],{"class":233}," fs",[217,625,626],{"class":367}," =",[217,628,629],{"class":425}," require",[217,631,632],{"class":223},"(",[217,634,635],{"class":240},"'fs'",[217,637,638],{"class":223},");\n",[217,640,642,644,647,649,652,654,656,658,661],{"class":219,"line":641},20,[217,643,620],{"class":367},[217,645,646],{"class":223}," { ",[217,648,53],{"class":233},[217,650,651],{"class":223}," } ",[217,653,374],{"class":367},[217,655,629],{"class":425},[217,657,632],{"class":223},[217,659,660],{"class":240},"'@azure\u002Fidentity'",[217,662,638],{"class":223},[217,664,666],{"class":219,"line":665},21,[217,667,419],{"emptyLinePlaceholder":418},[217,669,671,673,676,678,681],{"class":219,"line":670},22,[217,672,620],{"class":367},[217,674,675],{"class":233}," ARM",[217,677,626],{"class":367},[217,679,680],{"class":240}," 'https:\u002F\u002Fmanagement.azure.com'",[217,682,683],{"class":223},";\n",[217,685,687,689,692,694,697],{"class":219,"line":686},23,[217,688,620],{"class":367},[217,690,691],{"class":233}," API_VERSION",[217,693,626],{"class":367},[217,695,696],{"class":240}," '2025-03-01'",[217,698,683],{"class":223},[217,700,702,704,707,709,712],{"class":219,"line":701},24,[217,703,620],{"class":367},[217,705,706],{"class":233}," TOKEN_SCOPE",[217,708,626],{"class":367},[217,710,711],{"class":240}," 'https:\u002F\u002Fmanagement.azure.com\u002F.default'",[217,713,683],{"class":223},[217,715,717,719,722,724,727],{"class":219,"line":716},25,[217,718,620],{"class":367},[217,720,721],{"class":233}," QPU_RETRY_HEADER",[217,723,626],{"class":367},[217,725,726],{"class":240}," 'x-ms-ratelimit-microsoft.costmanagement-qpu-retry-after'",[217,728,683],{"class":223},[217,730,732,734,737,739,742],{"class":219,"line":731},26,[217,733,620],{"class":367},[217,735,736],{"class":233}," MAX_ATTEMPTS",[217,738,626],{"class":367},[217,740,741],{"class":233}," 4",[217,743,683],{"class":223},[217,745,747,749,752,754,757],{"class":219,"line":746},27,[217,748,620],{"class":367},[217,750,751],{"class":233}," SECTIONS_PER_MESSAGE",[217,753,626],{"class":367},[217,755,756],{"class":233}," 20",[217,758,683],{"class":223},[217,760,762],{"class":219,"line":761},28,[217,763,419],{"emptyLinePlaceholder":418},[217,765,767,769,772,774,776,780,783,786,789,792,795,798,800,802,805],{"class":219,"line":766},29,[217,768,620],{"class":367},[217,770,771],{"class":425}," sleep",[217,773,626],{"class":367},[217,775,46],{"class":223},[217,777,779],{"class":778},"s9osk","ms",[217,781,782],{"class":223},") ",[217,784,785],{"class":367},"=>",[217,787,788],{"class":367}," new",[217,790,791],{"class":233}," Promise",[217,793,794],{"class":223},"((",[217,796,797],{"class":778},"resolve",[217,799,782],{"class":223},[217,801,785],{"class":367},[217,803,804],{"class":425}," setTimeout",[217,806,807],{"class":223},"(resolve, ms));\n",[217,809,811],{"class":219,"line":810},30,[217,812,419],{"emptyLinePlaceholder":418},[217,814,816,819,822,824,827],{"class":219,"line":815},31,[217,817,818],{"class":367},"function",[217,820,821],{"class":425}," parseArgs",[217,823,632],{"class":223},[217,825,826],{"class":778},"argv",[217,828,829],{"class":223},") {\n",[217,831,833,836,839,841,844,847,850,852,855,858],{"class":219,"line":832},32,[217,834,835],{"class":367},"    const",[217,837,838],{"class":233}," args",[217,840,626],{"class":367},[217,842,843],{"class":223}," { configPath: ",[217,845,846],{"class":233},"null",[217,848,849],{"class":223},", thresholdPercent: ",[217,851,162],{"class":233},[217,853,854],{"class":223},", dryRun: ",[217,856,857],{"class":233},"false",[217,859,860],{"class":223}," };\n",[217,862,864],{"class":219,"line":863},33,[217,865,419],{"emptyLinePlaceholder":418},[217,867,869,872,874,877,880,882,885,888,891,894,897,900,903],{"class":219,"line":868},34,[217,870,871],{"class":367},"    for",[217,873,46],{"class":223},[217,875,876],{"class":367},"let",[217,878,879],{"class":223}," i ",[217,881,374],{"class":367},[217,883,884],{"class":233}," 0",[217,886,887],{"class":223},"; i ",[217,889,890],{"class":367},"\u003C",[217,892,893],{"class":223}," argv.",[217,895,896],{"class":233},"length",[217,898,899],{"class":223},"; i",[217,901,902],{"class":367},"++",[217,904,829],{"class":223},[217,906,908,911,914,917,920],{"class":219,"line":907},35,[217,909,910],{"class":367},"        if",[217,912,913],{"class":223}," (argv[i] ",[217,915,916],{"class":367},"===",[217,918,919],{"class":240}," '--config'",[217,921,829],{"class":223},[217,923,925,928,930,933,935],{"class":219,"line":924},36,[217,926,927],{"class":223},"            args.configPath ",[217,929,374],{"class":367},[217,931,932],{"class":223}," argv[",[217,934,902],{"class":367},[217,936,937],{"class":223},"i];\n",[217,939,941,944,947,950,952,954,957],{"class":219,"line":940},37,[217,942,943],{"class":223},"        } ",[217,945,946],{"class":367},"else",[217,948,949],{"class":367}," if",[217,951,913],{"class":223},[217,953,916],{"class":367},[217,955,956],{"class":240}," '--threshold-percent'",[217,958,829],{"class":223},[217,960,962,965,967,970,973,975],{"class":219,"line":961},38,[217,963,964],{"class":223},"            args.thresholdPercent ",[217,966,374],{"class":367},[217,968,969],{"class":425}," Number",[217,971,972],{"class":223},"(argv[",[217,974,902],{"class":367},[217,976,977],{"class":223},"i]);\n",[217,979,981,983,985,987,989,991,994],{"class":219,"line":980},39,[217,982,943],{"class":223},[217,984,946],{"class":367},[217,986,949],{"class":367},[217,988,913],{"class":223},[217,990,916],{"class":367},[217,992,993],{"class":240}," '--dry-run'",[217,995,829],{"class":223},[217,997,999,1002,1004,1007],{"class":219,"line":998},40,[217,1000,1001],{"class":223},"            args.dryRun ",[217,1003,374],{"class":367},[217,1005,1006],{"class":233}," true",[217,1008,683],{"class":223},[217,1010,1012],{"class":219,"line":1011},41,[217,1013,1014],{"class":223},"        }\n",[217,1016,1018],{"class":219,"line":1017},42,[217,1019,1020],{"class":223},"    }\n",[217,1022,1024],{"class":219,"line":1023},43,[217,1025,419],{"emptyLinePlaceholder":418},[217,1027,1029,1032,1034,1037,1040,1043,1046,1049,1052],{"class":219,"line":1028},44,[217,1030,1031],{"class":367},"    if",[217,1033,46],{"class":223},[217,1035,1036],{"class":367},"!",[217,1038,1039],{"class":223},"args.configPath ",[217,1041,1042],{"class":367},"||",[217,1044,1045],{"class":367}," !",[217,1047,1048],{"class":223},"Number.",[217,1050,1051],{"class":425},"isFinite",[217,1053,1054],{"class":223},"(args.thresholdPercent)) {\n",[217,1056,1058,1061,1063,1066,1068,1071],{"class":219,"line":1057},45,[217,1059,1060],{"class":367},"        throw",[217,1062,788],{"class":367},[217,1064,1065],{"class":425}," Error",[217,1067,632],{"class":223},[217,1069,1070],{"class":240},"'Usage: node budget-alert-bot.js --config \u003Cpath> [--threshold-percent \u003Cn>] [--dry-run]'",[217,1072,638],{"class":223},[217,1074,1076],{"class":219,"line":1075},46,[217,1077,1020],{"class":223},[217,1079,1081,1084],{"class":219,"line":1080},47,[217,1082,1083],{"class":367},"    return",[217,1085,1086],{"class":223}," args;\n",[217,1088,1090],{"class":219,"line":1089},48,[217,1091,1092],{"class":223},"}\n",[217,1094,1096],{"class":219,"line":1095},49,[217,1097,419],{"emptyLinePlaceholder":418},[217,1099,1101,1103,1106,1108,1111],{"class":219,"line":1100},50,[217,1102,818],{"class":367},[217,1104,1105],{"class":425}," loadConfig",[217,1107,632],{"class":223},[217,1109,1110],{"class":778},"path",[217,1112,829],{"class":223},[217,1114,1116,1118,1121,1123,1126,1128,1131,1134,1137,1140,1143],{"class":219,"line":1115},51,[217,1117,835],{"class":367},[217,1119,1120],{"class":233}," entries",[217,1122,626],{"class":367},[217,1124,1125],{"class":233}," JSON",[217,1127,24],{"class":223},[217,1129,1130],{"class":425},"parse",[217,1132,1133],{"class":223},"(fs.",[217,1135,1136],{"class":425},"readFileSync",[217,1138,1139],{"class":223},"(path, ",[217,1141,1142],{"class":240},"'utf8'",[217,1144,1145],{"class":223},"));\n",[217,1147,1149,1151,1153,1155,1158,1161,1164,1166,1169,1171,1174,1176],{"class":219,"line":1148},52,[217,1150,1031],{"class":367},[217,1152,46],{"class":223},[217,1154,1036],{"class":367},[217,1156,1157],{"class":223},"Array.",[217,1159,1160],{"class":425},"isArray",[217,1162,1163],{"class":223},"(entries) ",[217,1165,1042],{"class":367},[217,1167,1168],{"class":223}," entries.",[217,1170,896],{"class":233},[217,1172,1173],{"class":367}," ===",[217,1175,884],{"class":233},[217,1177,829],{"class":223},[217,1179,1181,1183,1185,1187,1189,1192,1194,1197],{"class":219,"line":1180},53,[217,1182,1060],{"class":367},[217,1184,788],{"class":367},[217,1186,1065],{"class":425},[217,1188,632],{"class":223},[217,1190,1191],{"class":240},"`${",[217,1193,1110],{"class":223},[217,1195,1196],{"class":240},"} must be a non-empty JSON array.`",[217,1198,638],{"class":223},[217,1200,1202],{"class":219,"line":1201},54,[217,1203,1020],{"class":223},[217,1205,1207,1209,1211,1213,1216,1219],{"class":219,"line":1206},55,[217,1208,871],{"class":367},[217,1210,46],{"class":223},[217,1212,620],{"class":367},[217,1214,1215],{"class":233}," entry",[217,1217,1218],{"class":367}," of",[217,1220,1221],{"class":223}," entries) {\n",[217,1223,1225,1227,1229,1231,1234,1236,1238,1241,1243,1245,1247,1249,1252,1255,1257],{"class":219,"line":1224},56,[217,1226,910],{"class":367},[217,1228,46],{"class":223},[217,1230,1036],{"class":367},[217,1232,1233],{"class":223},"entry.subscriptionId ",[217,1235,1042],{"class":367},[217,1237,1045],{"class":367},[217,1239,1240],{"class":223},"entry.resourceGroup ",[217,1242,1042],{"class":367},[217,1244,1045],{"class":367},[217,1246,632],{"class":223},[217,1248,153],{"class":425},[217,1250,1251],{"class":223},"(entry.monthlyBudget) ",[217,1253,1254],{"class":367},">",[217,1256,884],{"class":233},[217,1258,1259],{"class":223},")) {\n",[217,1261,1263,1266,1268,1270,1272,1275,1278,1280,1283,1285,1288,1291,1294],{"class":219,"line":1262},57,[217,1264,1265],{"class":367},"            throw",[217,1267,788],{"class":367},[217,1269,1065],{"class":425},[217,1271,632],{"class":223},[217,1273,1274],{"class":240},"`Invalid config entry: ${",[217,1276,1277],{"class":233},"JSON",[217,1279,24],{"class":240},[217,1281,1282],{"class":425},"stringify",[217,1284,632],{"class":240},[217,1286,1287],{"class":223},"entry",[217,1289,1290],{"class":240},")",[217,1292,1293],{"class":240},"}`",[217,1295,638],{"class":223},[217,1297,1299],{"class":219,"line":1298},58,[217,1300,1014],{"class":223},[217,1302,1304],{"class":219,"line":1303},59,[217,1305,1020],{"class":223},[217,1307,1309,1311],{"class":219,"line":1308},60,[217,1310,1083],{"class":367},[217,1312,1313],{"class":223}," entries;\n",[217,1315,1317],{"class":219,"line":1316},61,[217,1318,1092],{"class":223},[217,1320,1322],{"class":219,"line":1321},62,[217,1323,419],{"emptyLinePlaceholder":418},[217,1325,1327,1329,1332,1334,1337,1339,1341,1344],{"class":219,"line":1326},63,[217,1328,818],{"class":367},[217,1330,1331],{"class":425}," monthProgress",[217,1333,632],{"class":223},[217,1335,1336],{"class":778},"now",[217,1338,626],{"class":367},[217,1340,788],{"class":367},[217,1342,1343],{"class":425}," Date",[217,1345,1346],{"class":223},"()) {\n",[217,1348,1350,1352,1355,1357,1359,1361,1364,1367,1370,1373,1376,1379,1382,1385,1388,1390,1392,1395,1398],{"class":219,"line":1349},64,[217,1351,835],{"class":367},[217,1353,1354],{"class":233}," daysInMonth",[217,1356,626],{"class":367},[217,1358,788],{"class":367},[217,1360,1343],{"class":425},[217,1362,1363],{"class":223},"(Date.",[217,1365,1366],{"class":425},"UTC",[217,1368,1369],{"class":223},"(now.",[217,1371,1372],{"class":425},"getUTCFullYear",[217,1374,1375],{"class":223},"(), now.",[217,1377,1378],{"class":425},"getUTCMonth",[217,1380,1381],{"class":223},"() ",[217,1383,1384],{"class":367},"+",[217,1386,1387],{"class":233}," 1",[217,1389,132],{"class":223},[217,1391,462],{"class":233},[217,1393,1394],{"class":223},")).",[217,1396,1397],{"class":425},"getUTCDate",[217,1399,1400],{"class":223},"();\n",[217,1402,1404],{"class":219,"line":1403},65,[217,1405,1406],{"class":519},"    \u002F\u002F Cost data lags, so count only completed UTC days (minimum 1) toward the run rate.\n",[217,1408,1410,1412,1415,1417,1420,1423,1425,1427,1429,1432,1434,1436,1439],{"class":219,"line":1409},66,[217,1411,835],{"class":367},[217,1413,1414],{"class":233}," daysElapsed",[217,1416,626],{"class":367},[217,1418,1419],{"class":223}," Math.",[217,1421,1422],{"class":425},"max",[217,1424,1369],{"class":223},[217,1426,1397],{"class":425},[217,1428,1381],{"class":223},[217,1430,1431],{"class":367},"-",[217,1433,1387],{"class":233},[217,1435,132],{"class":223},[217,1437,1438],{"class":233},"1",[217,1440,638],{"class":223},[217,1442,1444,1446],{"class":219,"line":1443},67,[217,1445,1083],{"class":367},[217,1447,1448],{"class":223}," { daysElapsed, daysInMonth };\n",[217,1450,1452],{"class":219,"line":1451},68,[217,1453,1092],{"class":223},[217,1455,1457],{"class":219,"line":1456},69,[217,1458,419],{"emptyLinePlaceholder":418},[217,1460,1462,1465,1468,1471,1473,1476,1478,1480,1482,1484],{"class":219,"line":1461},70,[217,1463,1464],{"class":367},"async",[217,1466,1467],{"class":367}," function",[217,1469,1470],{"class":425}," queryMonthToDate",[217,1472,632],{"class":223},[217,1474,1475],{"class":778},"token",[217,1477,132],{"class":223},[217,1479,131],{"class":778},[217,1481,132],{"class":223},[217,1483,135],{"class":778},[217,1485,829],{"class":223},[217,1487,1489,1491,1494,1496,1499,1501,1504,1506,1508],{"class":219,"line":1488},71,[217,1490,835],{"class":367},[217,1492,1493],{"class":233}," scope",[217,1495,626],{"class":367},[217,1497,1498],{"class":240}," `\u002Fsubscriptions\u002F${",[217,1500,131],{"class":223},[217,1502,1503],{"class":240},"}\u002FresourceGroups\u002F${",[217,1505,135],{"class":223},[217,1507,1293],{"class":240},[217,1509,683],{"class":223},[217,1511,1513,1515,1518,1520,1523,1526,1529,1532,1535,1538,1540],{"class":219,"line":1512},72,[217,1514,835],{"class":367},[217,1516,1517],{"class":233}," url",[217,1519,626],{"class":367},[217,1521,1522],{"class":240}," `${",[217,1524,1525],{"class":233},"ARM",[217,1527,1528],{"class":240},"}${",[217,1530,1531],{"class":223},"scope",[217,1533,1534],{"class":240},"}\u002Fproviders\u002FMicrosoft.CostManagement\u002Fquery?api-version=${",[217,1536,1537],{"class":233},"API_VERSION",[217,1539,1293],{"class":240},[217,1541,683],{"class":223},[217,1543,1545,1547,1550,1552,1554,1556,1558],{"class":219,"line":1544},73,[217,1546,835],{"class":367},[217,1548,1549],{"class":233}," body",[217,1551,626],{"class":367},[217,1553,1125],{"class":233},[217,1555,24],{"class":223},[217,1557,1282],{"class":425},[217,1559,1560],{"class":223},"({\n",[217,1562,1564,1567,1570],{"class":219,"line":1563},74,[217,1565,1566],{"class":223},"        type: ",[217,1568,1569],{"class":240},"'ActualCost'",[217,1571,1572],{"class":223},",\n",[217,1574,1576,1579,1582],{"class":219,"line":1575},75,[217,1577,1578],{"class":223},"        timeframe: ",[217,1580,1581],{"class":240},"'MonthToDate'",[217,1583,1572],{"class":223},[217,1585,1587],{"class":219,"line":1586},76,[217,1588,1589],{"class":223},"        dataset: {\n",[217,1591,1593,1596,1599],{"class":219,"line":1592},77,[217,1594,1595],{"class":223},"            granularity: ",[217,1597,1598],{"class":240},"'None'",[217,1600,1572],{"class":223},[217,1602,1604,1607,1610,1613,1616],{"class":219,"line":1603},78,[217,1605,1606],{"class":223},"            aggregation: { totalCost: { name: ",[217,1608,1609],{"class":240},"'PreTaxCost'",[217,1611,1612],{"class":223},", function: ",[217,1614,1615],{"class":240},"'Sum'",[217,1617,1618],{"class":223}," } },\n",[217,1620,1622],{"class":219,"line":1621},79,[217,1623,1624],{"class":223},"        },\n",[217,1626,1628],{"class":219,"line":1627},80,[217,1629,1630],{"class":223},"    });\n",[217,1632,1634],{"class":219,"line":1633},81,[217,1635,419],{"emptyLinePlaceholder":418},[217,1637,1639,1642],{"class":219,"line":1638},82,[217,1640,1641],{"class":367},"    let",[217,1643,1644],{"class":223}," response;\n",[217,1646,1648,1650,1652,1654,1657,1659,1661,1664,1667,1669,1672,1674],{"class":219,"line":1647},83,[217,1649,871],{"class":367},[217,1651,46],{"class":223},[217,1653,876],{"class":367},[217,1655,1656],{"class":223}," attempt ",[217,1658,374],{"class":367},[217,1660,1387],{"class":233},[217,1662,1663],{"class":223},"; attempt ",[217,1665,1666],{"class":367},"\u003C=",[217,1668,736],{"class":233},[217,1670,1671],{"class":223},"; attempt",[217,1673,902],{"class":367},[217,1675,829],{"class":223},[217,1677,1679,1682,1684,1687,1690],{"class":219,"line":1678},84,[217,1680,1681],{"class":223},"        response ",[217,1683,374],{"class":367},[217,1685,1686],{"class":367}," await",[217,1688,1689],{"class":425}," fetch",[217,1691,1692],{"class":223},"(url, {\n",[217,1694,1696,1699,1702],{"class":219,"line":1695},85,[217,1697,1698],{"class":223},"            method: ",[217,1700,1701],{"class":240},"'POST'",[217,1703,1572],{"class":223},[217,1705,1707,1710,1713,1715,1717,1719,1722,1724,1727],{"class":219,"line":1706},86,[217,1708,1709],{"class":223},"            headers: { Authorization: ",[217,1711,1712],{"class":240},"`Bearer ${",[217,1714,1475],{"class":223},[217,1716,1293],{"class":240},[217,1718,132],{"class":223},[217,1720,1721],{"class":240},"'Content-Type'",[217,1723,237],{"class":223},[217,1725,1726],{"class":240},"'application\u002Fjson'",[217,1728,274],{"class":223},[217,1730,1732],{"class":219,"line":1731},87,[217,1733,1734],{"class":223},"            body,\n",[217,1736,1738],{"class":219,"line":1737},88,[217,1739,1740],{"class":223},"        });\n",[217,1742,1744,1746,1748,1750,1753,1756,1758,1761,1764,1767,1770,1772,1774,1776,1778],{"class":219,"line":1743},89,[217,1745,910],{"class":367},[217,1747,46],{"class":223},[217,1749,1036],{"class":367},[217,1751,1752],{"class":223},"[",[217,1754,1755],{"class":233},"429",[217,1757,132],{"class":223},[217,1759,1760],{"class":233},"503",[217,1762,1763],{"class":223},"].",[217,1765,1766],{"class":425},"includes",[217,1768,1769],{"class":223},"(response.status) ",[217,1771,1042],{"class":367},[217,1773,1656],{"class":223},[217,1775,916],{"class":367},[217,1777,736],{"class":233},[217,1779,829],{"class":223},[217,1781,1783,1786],{"class":219,"line":1782},90,[217,1784,1785],{"class":367},"            break",[217,1787,683],{"class":223},[217,1789,1791],{"class":219,"line":1790},91,[217,1792,1014],{"class":223},[217,1794,1796,1799,1802,1804,1806,1809,1812,1814,1817,1819,1821,1824,1826,1828,1831,1833,1835,1838],{"class":219,"line":1795},92,[217,1797,1798],{"class":367},"        const",[217,1800,1801],{"class":233}," wait",[217,1803,626],{"class":367},[217,1805,969],{"class":425},[217,1807,1808],{"class":223},"(response.headers.",[217,1810,1811],{"class":425},"get",[217,1813,632],{"class":223},[217,1815,1816],{"class":233},"QPU_RETRY_HEADER",[217,1818,782],{"class":223},[217,1820,1042],{"class":367},[217,1822,1823],{"class":223}," response.headers.",[217,1825,1811],{"class":425},[217,1827,632],{"class":223},[217,1829,1830],{"class":240},"'retry-after'",[217,1832,782],{"class":223},[217,1834,1042],{"class":367},[217,1836,1837],{"class":233}," 30",[217,1839,638],{"class":223},[217,1841,1843,1846,1849,1851,1853,1855,1858,1861,1863,1866,1869,1872,1875],{"class":219,"line":1842},93,[217,1844,1845],{"class":223},"        console.",[217,1847,1848],{"class":425},"error",[217,1850,632],{"class":223},[217,1852,1191],{"class":240},[217,1854,135],{"class":223},[217,1856,1857],{"class":240},"}: throttled (${",[217,1859,1860],{"class":223},"response",[217,1862,24],{"class":240},[217,1864,1865],{"class":223},"status",[217,1867,1868],{"class":240},"}), retrying in ${",[217,1870,1871],{"class":223},"wait",[217,1873,1874],{"class":240},"}s`",[217,1876,638],{"class":223},[217,1878,1880,1883,1885,1888,1891,1894],{"class":219,"line":1879},94,[217,1881,1882],{"class":367},"        await",[217,1884,771],{"class":425},[217,1886,1887],{"class":223},"(wait ",[217,1889,1890],{"class":367},"*",[217,1892,1893],{"class":233}," 1000",[217,1895,638],{"class":223},[217,1897,1899],{"class":219,"line":1898},95,[217,1900,1020],{"class":223},[217,1902,1904],{"class":219,"line":1903},96,[217,1905,419],{"emptyLinePlaceholder":418},[217,1907,1909,1911,1913,1915],{"class":219,"line":1908},97,[217,1910,1031],{"class":367},[217,1912,46],{"class":223},[217,1914,1036],{"class":367},[217,1916,1917],{"class":223},"response.ok) {\n",[217,1919,1921,1923,1925,1927,1929,1931,1933,1936,1938,1940,1942,1945,1948,1951,1953,1955,1958,1960],{"class":219,"line":1920},98,[217,1922,1060],{"class":367},[217,1924,788],{"class":367},[217,1926,1065],{"class":425},[217,1928,632],{"class":223},[217,1930,1191],{"class":240},[217,1932,135],{"class":223},[217,1934,1935],{"class":240},"}: Cost Management query failed ${",[217,1937,1860],{"class":223},[217,1939,24],{"class":240},[217,1941,1865],{"class":223},[217,1943,1944],{"class":240},"} ${",[217,1946,1947],{"class":367},"await",[217,1949,1950],{"class":223}," response",[217,1952,24],{"class":240},[217,1954,447],{"class":425},[217,1956,1957],{"class":240},"()",[217,1959,1293],{"class":240},[217,1961,638],{"class":223},[217,1963,1965],{"class":219,"line":1964},99,[217,1966,1020],{"class":223},[217,1968,1970,1972,1975,1977,1980],{"class":219,"line":1969},100,[217,1971,1031],{"class":367},[217,1973,1974],{"class":223}," (response.status ",[217,1976,916],{"class":367},[217,1978,1979],{"class":233}," 204",[217,1981,829],{"class":223},[217,1983,1985],{"class":219,"line":1984},101,[217,1986,1987],{"class":519},"        \u002F\u002F 204 No Content is a documented success: nothing to report yet.\n",[217,1989,1991,1994,1997,1999,2002,2005],{"class":219,"line":1990},102,[217,1992,1993],{"class":367},"        return",[217,1995,1996],{"class":223}," { spend: ",[217,1998,462],{"class":233},[217,2000,2001],{"class":223},", currency: ",[217,2003,2004],{"class":240},"''",[217,2006,860],{"class":223},[217,2008,2010],{"class":219,"line":2009},103,[217,2011,1020],{"class":223},[217,2013,2015],{"class":219,"line":2014},104,[217,2016,419],{"emptyLinePlaceholder":418},[217,2018,2020,2022,2024,2027,2029,2031,2033,2036,2038],{"class":219,"line":2019},105,[217,2021,835],{"class":367},[217,2023,646],{"class":223},[217,2025,2026],{"class":233},"properties",[217,2028,651],{"class":223},[217,2030,374],{"class":367},[217,2032,1686],{"class":367},[217,2034,2035],{"class":223}," response.",[217,2037,212],{"class":425},[217,2039,1400],{"class":223},[217,2041,2043,2045,2048,2050,2053,2056,2058,2061,2063,2065,2068,2070,2073],{"class":219,"line":2042},106,[217,2044,835],{"class":367},[217,2046,2047],{"class":233}," costIndex",[217,2049,626],{"class":367},[217,2051,2052],{"class":223}," properties.columns.",[217,2054,2055],{"class":425},"findIndex",[217,2057,794],{"class":223},[217,2059,2060],{"class":778},"col",[217,2062,782],{"class":223},[217,2064,785],{"class":367},[217,2066,2067],{"class":223}," col.type ",[217,2069,916],{"class":367},[217,2071,2072],{"class":240}," 'Number'",[217,2074,638],{"class":223},[217,2076,2078,2080,2083,2085,2087,2089,2091,2093,2095,2097,2100,2102,2105],{"class":219,"line":2077},107,[217,2079,835],{"class":367},[217,2081,2082],{"class":233}," currencyIndex",[217,2084,626],{"class":367},[217,2086,2052],{"class":223},[217,2088,2055],{"class":425},[217,2090,794],{"class":223},[217,2092,2060],{"class":778},[217,2094,782],{"class":223},[217,2096,785],{"class":367},[217,2098,2099],{"class":223}," col.name ",[217,2101,916],{"class":367},[217,2103,2104],{"class":240}," 'Currency'",[217,2106,638],{"class":223},[217,2108,2110,2112,2115,2117,2120,2122],{"class":219,"line":2109},108,[217,2111,835],{"class":367},[217,2113,2114],{"class":233}," row",[217,2116,626],{"class":367},[217,2118,2119],{"class":223}," properties.rows[",[217,2121,462],{"class":233},[217,2123,2124],{"class":223},"];\n",[217,2126,2128],{"class":219,"line":2127},109,[217,2129,419],{"emptyLinePlaceholder":418},[217,2131,2133],{"class":219,"line":2132},110,[217,2134,2135],{"class":519},"    \u002F\u002F No rows means no cost yet this month.\n",[217,2137,2139,2141],{"class":219,"line":2138},111,[217,2140,1083],{"class":367},[217,2142,2143],{"class":223}," {\n",[217,2145,2147,2150,2153,2155,2158,2161,2163],{"class":219,"line":2146},112,[217,2148,2149],{"class":223},"        spend: row ",[217,2151,2152],{"class":367},"?",[217,2154,969],{"class":425},[217,2156,2157],{"class":223},"(row[costIndex]) ",[217,2159,2160],{"class":367},":",[217,2162,884],{"class":233},[217,2164,1572],{"class":223},[217,2166,2168,2171,2174,2177,2180,2182,2185,2188,2190,2193],{"class":219,"line":2167},113,[217,2169,2170],{"class":223},"        currency: row ",[217,2172,2173],{"class":367},"&&",[217,2175,2176],{"class":223}," currencyIndex ",[217,2178,2179],{"class":367},">=",[217,2181,884],{"class":233},[217,2183,2184],{"class":367}," ?",[217,2186,2187],{"class":223}," row[currencyIndex] ",[217,2189,2160],{"class":367},[217,2191,2192],{"class":240}," ''",[217,2194,1572],{"class":223},[217,2196,2198],{"class":219,"line":2197},114,[217,2199,2200],{"class":223},"    };\n",[217,2202,2204],{"class":219,"line":2203},115,[217,2205,1092],{"class":223},[217,2207,2209],{"class":219,"line":2208},116,[217,2210,419],{"emptyLinePlaceholder":418},[217,2212,2214,2216,2219,2221,2224],{"class":219,"line":2213},117,[217,2215,818],{"class":367},[217,2217,2218],{"class":425}," formatAmount",[217,2220,632],{"class":223},[217,2222,2223],{"class":778},"value",[217,2225,829],{"class":223},[217,2227,2229,2231,2234,2237,2239,2242,2245,2248,2251,2253],{"class":219,"line":2228},118,[217,2230,1083],{"class":367},[217,2232,2233],{"class":223}," value.",[217,2235,2236],{"class":425},"toLocaleString",[217,2238,632],{"class":223},[217,2240,2241],{"class":240},"'en-US'",[217,2243,2244],{"class":223},", { minimumFractionDigits: ",[217,2246,2247],{"class":233},"2",[217,2249,2250],{"class":223},", maximumFractionDigits: ",[217,2252,2247],{"class":233},[217,2254,2255],{"class":223}," });\n",[217,2257,2259],{"class":219,"line":2258},119,[217,2260,1092],{"class":223},[217,2262,2264],{"class":219,"line":2263},120,[217,2265,419],{"emptyLinePlaceholder":418},[217,2267,2269,2271,2274,2276,2279,2281,2284],{"class":219,"line":2268},121,[217,2270,818],{"class":367},[217,2272,2273],{"class":425}," buildSlackMessages",[217,2275,632],{"class":223},[217,2277,2278],{"class":778},"alerts",[217,2280,132],{"class":223},[217,2282,2283],{"class":778},"thresholdPercent",[217,2285,829],{"class":223},[217,2287,2289,2291,2294,2296],{"class":219,"line":2288},122,[217,2290,835],{"class":367},[217,2292,2293],{"class":233}," messages",[217,2295,626],{"class":367},[217,2297,2298],{"class":223}," [];\n",[217,2300,2302,2304,2306,2308,2310,2312,2314,2316,2318,2321,2323,2325,2328,2330],{"class":219,"line":2301},123,[217,2303,871],{"class":367},[217,2305,46],{"class":223},[217,2307,876],{"class":367},[217,2309,879],{"class":223},[217,2311,374],{"class":367},[217,2313,884],{"class":233},[217,2315,887],{"class":223},[217,2317,890],{"class":367},[217,2319,2320],{"class":223}," alerts.",[217,2322,896],{"class":233},[217,2324,887],{"class":223},[217,2326,2327],{"class":367},"+=",[217,2329,751],{"class":233},[217,2331,829],{"class":223},[217,2333,2335,2337,2340,2342,2344,2347,2350,2352,2354],{"class":219,"line":2334},124,[217,2336,1798],{"class":367},[217,2338,2339],{"class":233}," chunk",[217,2341,626],{"class":367},[217,2343,2320],{"class":223},[217,2345,2346],{"class":425},"slice",[217,2348,2349],{"class":223},"(i, i ",[217,2351,1384],{"class":367},[217,2353,751],{"class":233},[217,2355,638],{"class":223},[217,2357,2359,2361,2364,2366],{"class":219,"line":2358},125,[217,2360,1798],{"class":367},[217,2362,2363],{"class":233}," blocks",[217,2365,626],{"class":367},[217,2367,2368],{"class":223}," [\n",[217,2370,2372],{"class":219,"line":2371},126,[217,2373,2374],{"class":223},"            {\n",[217,2376,2378,2381,2384],{"class":219,"line":2377},127,[217,2379,2380],{"class":223},"                type: ",[217,2382,2383],{"class":240},"'header'",[217,2385,1572],{"class":223},[217,2387,2389,2392,2395,2398,2401,2403,2405,2407,2410,2412,2415],{"class":219,"line":2388},128,[217,2390,2391],{"class":223},"                text: { type: ",[217,2393,2394],{"class":240},"'plain_text'",[217,2396,2397],{"class":223},", text: ",[217,2399,2400],{"class":240},"`Azure budget alert: ${",[217,2402,2278],{"class":223},[217,2404,24],{"class":240},[217,2406,896],{"class":233},[217,2408,2409],{"class":240},"} resource group(s) at or over ${",[217,2411,2283],{"class":223},[217,2413,2414],{"class":240},"}%`",[217,2416,274],{"class":223},[217,2418,2420],{"class":219,"line":2419},129,[217,2421,2422],{"class":223},"            },\n",[217,2424,2426],{"class":219,"line":2425},130,[217,2427,2428],{"class":223},"        ];\n",[217,2430,2432,2435,2437,2439,2442,2444],{"class":219,"line":2431},131,[217,2433,2434],{"class":367},"        for",[217,2436,46],{"class":223},[217,2438,620],{"class":367},[217,2440,2441],{"class":233}," a",[217,2443,1218],{"class":367},[217,2445,2446],{"class":223}," chunk) {\n",[217,2448,2450,2453,2456],{"class":219,"line":2449},132,[217,2451,2452],{"class":223},"            blocks.",[217,2454,2455],{"class":425},"push",[217,2457,1560],{"class":223},[217,2459,2461,2463,2466],{"class":219,"line":2460},133,[217,2462,2380],{"class":223},[217,2464,2465],{"class":240},"'section'",[217,2467,1572],{"class":223},[217,2469,2471,2473,2476,2478,2481,2483,2485,2487,2490,2492,2494,2496,2498,2501,2503,2505,2507,2509,2512,2515],{"class":219,"line":2470},134,[217,2472,2391],{"class":223},[217,2474,2475],{"class":240},"'mrkdwn'",[217,2477,2397],{"class":223},[217,2479,2480],{"class":240},"`*${",[217,2482,15],{"class":223},[217,2484,24],{"class":240},[217,2486,135],{"class":223},[217,2488,2489],{"class":240},"}*${",[217,2491,15],{"class":223},[217,2493,24],{"class":240},[217,2495,142],{"class":223},[217,2497,2184],{"class":367},[217,2499,2500],{"class":240}," `  ${",[217,2502,15],{"class":223},[217,2504,24],{"class":240},[217,2506,142],{"class":223},[217,2508,1293],{"class":240},[217,2510,2511],{"class":367}," :",[217,2513,2514],{"class":240}," ''}`",[217,2516,274],{"class":223},[217,2518,2520],{"class":219,"line":2519},135,[217,2521,2522],{"class":223},"                fields: [\n",[217,2524,2526,2529,2531,2533,2536,2539,2542,2545,2547,2549,2551,2554,2556,2558,2560,2562,2565,2568,2570,2572,2575,2577,2580,2582,2584,2586,2589],{"class":219,"line":2525},136,[217,2527,2528],{"class":223},"                    { type: ",[217,2530,2475],{"class":240},[217,2532,2397],{"class":223},[217,2534,2535],{"class":240},"`*Month to date*",[217,2537,2538],{"class":233},"\\n",[217,2540,2541],{"class":240},"${",[217,2543,2544],{"class":425},"formatAmount",[217,2546,632],{"class":240},[217,2548,15],{"class":223},[217,2550,24],{"class":240},[217,2552,2553],{"class":223},"spend",[217,2555,1290],{"class":240},[217,2557,1944],{"class":240},[217,2559,15],{"class":223},[217,2561,24],{"class":240},[217,2563,2564],{"class":223},"currency",[217,2566,2567],{"class":240},"} (${",[217,2569,15],{"class":223},[217,2571,24],{"class":240},[217,2573,2574],{"class":223},"percentUsed",[217,2576,24],{"class":240},[217,2578,2579],{"class":425},"toFixed",[217,2581,632],{"class":240},[217,2583,1438],{"class":233},[217,2585,1290],{"class":240},[217,2587,2588],{"class":240},"}%)`",[217,2590,274],{"class":223},[217,2592,2594,2596,2598,2600,2603,2605,2607,2609,2611,2613,2615,2617,2619,2621,2623,2625,2627,2629],{"class":219,"line":2593},137,[217,2595,2528],{"class":223},[217,2597,2475],{"class":240},[217,2599,2397],{"class":223},[217,2601,2602],{"class":240},"`*Budget*",[217,2604,2538],{"class":233},[217,2606,2541],{"class":240},[217,2608,2544],{"class":425},[217,2610,632],{"class":240},[217,2612,15],{"class":223},[217,2614,24],{"class":240},[217,2616,138],{"class":223},[217,2618,1290],{"class":240},[217,2620,1944],{"class":240},[217,2622,15],{"class":223},[217,2624,24],{"class":240},[217,2626,2564],{"class":223},[217,2628,1293],{"class":240},[217,2630,274],{"class":223},[217,2632,2634,2636,2638,2640,2643,2645,2647,2649,2651,2653,2655,2658,2660,2662,2664,2666,2668,2670,2672,2674,2677,2679,2681,2683,2685,2687,2689],{"class":219,"line":2633},138,[217,2635,2528],{"class":223},[217,2637,2475],{"class":240},[217,2639,2397],{"class":223},[217,2641,2642],{"class":240},"`*Projected month end*",[217,2644,2538],{"class":233},[217,2646,2541],{"class":240},[217,2648,2544],{"class":425},[217,2650,632],{"class":240},[217,2652,15],{"class":223},[217,2654,24],{"class":240},[217,2656,2657],{"class":223},"projected",[217,2659,1290],{"class":240},[217,2661,1944],{"class":240},[217,2663,15],{"class":223},[217,2665,24],{"class":240},[217,2667,2564],{"class":223},[217,2669,2567],{"class":240},[217,2671,15],{"class":223},[217,2673,24],{"class":240},[217,2675,2676],{"class":223},"projectedPercent",[217,2678,24],{"class":240},[217,2680,2579],{"class":425},[217,2682,632],{"class":240},[217,2684,1438],{"class":233},[217,2686,1290],{"class":240},[217,2688,2588],{"class":240},[217,2690,274],{"class":223},[217,2692,2694,2696,2698,2700,2703,2706,2708,2710,2712,2714,2717,2720,2723],{"class":219,"line":2693},139,[217,2695,2528],{"class":223},[217,2697,2475],{"class":240},[217,2699,2397],{"class":223},[217,2701,2702],{"class":240},"`*Subscription*",[217,2704,2705],{"class":233},"\\n\\`",[217,2707,2541],{"class":240},[217,2709,15],{"class":223},[217,2711,24],{"class":240},[217,2713,131],{"class":223},[217,2715,2716],{"class":240},"}",[217,2718,2719],{"class":233},"\\`",[217,2721,2722],{"class":240},"`",[217,2724,274],{"class":223},[217,2726,2728],{"class":219,"line":2727},140,[217,2729,2730],{"class":223},"                ],\n",[217,2732,2734],{"class":219,"line":2733},141,[217,2735,2736],{"class":223},"            });\n",[217,2738,2740],{"class":219,"line":2739},142,[217,2741,1014],{"class":223},[217,2743,2745,2748,2750,2753,2756,2759,2761,2763,2766],{"class":219,"line":2744},143,[217,2746,2747],{"class":223},"        blocks.",[217,2749,2455],{"class":425},[217,2751,2752],{"class":223},"({ type: ",[217,2754,2755],{"class":240},"'context'",[217,2757,2758],{"class":223},", elements: [{ type: ",[217,2760,2475],{"class":240},[217,2762,2397],{"class":223},[217,2764,2765],{"class":240},"'Actual cost from Azure Cost Management; data can lag by a day or more.'",[217,2767,2768],{"class":223}," }] });\n",[217,2770,2772],{"class":219,"line":2771},144,[217,2773,419],{"emptyLinePlaceholder":418},[217,2775,2777,2780,2782],{"class":219,"line":2776},145,[217,2778,2779],{"class":223},"        messages.",[217,2781,2455],{"class":425},[217,2783,1560],{"class":223},[217,2785,2787],{"class":219,"line":2786},146,[217,2788,2789],{"class":519},"            \u002F\u002F Top-level text is the notification and fallback when blocks can't be shown.\n",[217,2791,2793,2796,2798,2801,2803,2806,2808,2810,2812,2814,2816,2818,2820,2822,2825,2827,2830,2832,2834],{"class":219,"line":2792},147,[217,2794,2795],{"class":223},"            text: ",[217,2797,2400],{"class":240},[217,2799,2800],{"class":223},"chunk",[217,2802,24],{"class":240},[217,2804,2805],{"class":425},"map",[217,2807,794],{"class":240},[217,2809,15],{"class":233},[217,2811,782],{"class":240},[217,2813,785],{"class":367},[217,2815,2441],{"class":223},[217,2817,24],{"class":240},[217,2819,135],{"class":223},[217,2821,143],{"class":240},[217,2823,2824],{"class":425},"join",[217,2826,632],{"class":240},[217,2828,2829],{"class":240},"', '",[217,2831,1290],{"class":240},[217,2833,1293],{"class":240},[217,2835,1572],{"class":223},[217,2837,2839],{"class":219,"line":2838},148,[217,2840,2841],{"class":223},"            blocks,\n",[217,2843,2845],{"class":219,"line":2844},149,[217,2846,1740],{"class":223},[217,2848,2850],{"class":219,"line":2849},150,[217,2851,1020],{"class":223},[217,2853,2855,2857],{"class":219,"line":2854},151,[217,2856,1083],{"class":367},[217,2858,2859],{"class":223}," messages;\n",[217,2861,2863],{"class":219,"line":2862},152,[217,2864,1092],{"class":223},[217,2866,2868],{"class":219,"line":2867},153,[217,2869,419],{"emptyLinePlaceholder":418},[217,2871,2873,2875,2877,2880,2882,2885,2887,2890],{"class":219,"line":2872},154,[217,2874,1464],{"class":367},[217,2876,1467],{"class":367},[217,2878,2879],{"class":425}," postToSlack",[217,2881,632],{"class":223},[217,2883,2884],{"class":778},"webhookUrl",[217,2886,132],{"class":223},[217,2888,2889],{"class":778},"message",[217,2891,829],{"class":223},[217,2893,2895,2897],{"class":219,"line":2894},155,[217,2896,1641],{"class":367},[217,2898,1644],{"class":223},[217,2900,2902,2904,2906,2908,2910,2912,2914,2916,2918,2920,2922,2924],{"class":219,"line":2901},156,[217,2903,871],{"class":367},[217,2905,46],{"class":223},[217,2907,876],{"class":367},[217,2909,1656],{"class":223},[217,2911,374],{"class":367},[217,2913,1387],{"class":233},[217,2915,1663],{"class":223},[217,2917,1666],{"class":367},[217,2919,736],{"class":233},[217,2921,1671],{"class":223},[217,2923,902],{"class":367},[217,2925,829],{"class":223},[217,2927,2929,2931,2933,2935,2937],{"class":219,"line":2928},157,[217,2930,1681],{"class":223},[217,2932,374],{"class":367},[217,2934,1686],{"class":367},[217,2936,1689],{"class":425},[217,2938,2939],{"class":223},"(webhookUrl, {\n",[217,2941,2943,2945,2947],{"class":219,"line":2942},158,[217,2944,1698],{"class":223},[217,2946,1701],{"class":240},[217,2948,1572],{"class":223},[217,2950,2952,2955,2957,2959,2961],{"class":219,"line":2951},159,[217,2953,2954],{"class":223},"            headers: { ",[217,2956,1721],{"class":240},[217,2958,237],{"class":223},[217,2960,1726],{"class":240},[217,2962,274],{"class":223},[217,2964,2966,2969,2971,2973,2975],{"class":219,"line":2965},160,[217,2967,2968],{"class":223},"            body: ",[217,2970,1277],{"class":233},[217,2972,24],{"class":223},[217,2974,1282],{"class":425},[217,2976,2977],{"class":223},"(message),\n",[217,2979,2981],{"class":219,"line":2980},161,[217,2982,1740],{"class":223},[217,2984,2986,2988,2990,2993,2996,2999,3001,3003,3005],{"class":219,"line":2985},162,[217,2987,910],{"class":367},[217,2989,1974],{"class":223},[217,2991,2992],{"class":367},"!==",[217,2994,2995],{"class":233}," 429",[217,2997,2998],{"class":367}," ||",[217,3000,1656],{"class":223},[217,3002,916],{"class":367},[217,3004,736],{"class":233},[217,3006,829],{"class":223},[217,3008,3010,3012],{"class":219,"line":3009},163,[217,3011,1785],{"class":367},[217,3013,683],{"class":223},[217,3015,3017],{"class":219,"line":3016},164,[217,3018,1014],{"class":223},[217,3020,3022,3024,3026,3028,3030,3032,3034,3036,3038,3040,3042,3044],{"class":219,"line":3021},165,[217,3023,1798],{"class":367},[217,3025,1801],{"class":233},[217,3027,626],{"class":367},[217,3029,969],{"class":425},[217,3031,1808],{"class":223},[217,3033,1811],{"class":425},[217,3035,632],{"class":223},[217,3037,1830],{"class":240},[217,3039,782],{"class":223},[217,3041,1042],{"class":367},[217,3043,1387],{"class":233},[217,3045,638],{"class":223},[217,3047,3049,3051,3053,3055,3057,3059],{"class":219,"line":3048},166,[217,3050,1882],{"class":367},[217,3052,771],{"class":425},[217,3054,1887],{"class":223},[217,3056,1890],{"class":367},[217,3058,1893],{"class":233},[217,3060,638],{"class":223},[217,3062,3064],{"class":219,"line":3063},167,[217,3065,1020],{"class":223},[217,3067,3069,3071,3073,3075],{"class":219,"line":3068},168,[217,3070,1031],{"class":367},[217,3072,46],{"class":223},[217,3074,1036],{"class":367},[217,3076,1917],{"class":223},[217,3078,3080],{"class":219,"line":3079},169,[217,3081,3082],{"class":519},"        \u002F\u002F Slack returns a short error string such as invalid_payload, no_text or channel_is_archived.\n",[217,3084,3086,3088,3090,3092,3094,3097,3099,3101,3103,3105,3107,3109,3111,3113,3115,3117],{"class":219,"line":3085},170,[217,3087,1060],{"class":367},[217,3089,788],{"class":367},[217,3091,1065],{"class":425},[217,3093,632],{"class":223},[217,3095,3096],{"class":240},"`Slack webhook post failed: ${",[217,3098,1860],{"class":223},[217,3100,24],{"class":240},[217,3102,1865],{"class":223},[217,3104,1944],{"class":240},[217,3106,1947],{"class":367},[217,3108,1950],{"class":223},[217,3110,24],{"class":240},[217,3112,447],{"class":425},[217,3114,1957],{"class":240},[217,3116,1293],{"class":240},[217,3118,638],{"class":223},[217,3120,3122],{"class":219,"line":3121},171,[217,3123,1020],{"class":223},[217,3125,3127],{"class":219,"line":3126},172,[217,3128,1092],{"class":223},[217,3130,3132],{"class":219,"line":3131},173,[217,3133,419],{"emptyLinePlaceholder":418},[217,3135,3137,3139,3141,3144],{"class":219,"line":3136},174,[217,3138,1464],{"class":367},[217,3140,1467],{"class":367},[217,3142,3143],{"class":425}," main",[217,3145,3146],{"class":223},"() {\n",[217,3148,3150,3152,3154,3156,3158,3161,3163,3165,3167],{"class":219,"line":3149},175,[217,3151,835],{"class":367},[217,3153,838],{"class":233},[217,3155,626],{"class":367},[217,3157,821],{"class":425},[217,3159,3160],{"class":223},"(process.argv.",[217,3162,2346],{"class":425},[217,3164,632],{"class":223},[217,3166,2247],{"class":233},[217,3168,1145],{"class":223},[217,3170,3172,3174,3177,3179,3182,3184],{"class":219,"line":3171},176,[217,3173,835],{"class":367},[217,3175,3176],{"class":233}," webhookUrl",[217,3178,626],{"class":367},[217,3180,3181],{"class":223}," process.env.",[217,3183,184],{"class":233},[217,3185,683],{"class":223},[217,3187,3189,3191,3193,3195,3198,3200,3202],{"class":219,"line":3188},177,[217,3190,1031],{"class":367},[217,3192,46],{"class":223},[217,3194,1036],{"class":367},[217,3196,3197],{"class":223},"webhookUrl ",[217,3199,2173],{"class":367},[217,3201,1045],{"class":367},[217,3203,3204],{"class":223},"args.dryRun) {\n",[217,3206,3208,3210,3212,3214,3216,3219],{"class":219,"line":3207},178,[217,3209,1060],{"class":367},[217,3211,788],{"class":367},[217,3213,1065],{"class":425},[217,3215,632],{"class":223},[217,3217,3218],{"class":240},"'SLACK_WEBHOOK_URL is not set.'",[217,3220,638],{"class":223},[217,3222,3224],{"class":219,"line":3223},179,[217,3225,1020],{"class":223},[217,3227,3229],{"class":219,"line":3228},180,[217,3230,419],{"emptyLinePlaceholder":418},[217,3232,3234,3236,3239,3241,3243],{"class":219,"line":3233},181,[217,3235,835],{"class":367},[217,3237,3238],{"class":233}," config",[217,3240,626],{"class":367},[217,3242,1105],{"class":425},[217,3244,3245],{"class":223},"(args.configPath);\n",[217,3247,3249,3251,3253,3255,3257,3259,3261,3263,3266,3269,3272,3274,3277],{"class":219,"line":3248},182,[217,3250,835],{"class":367},[217,3252,646],{"class":223},[217,3254,1475],{"class":233},[217,3256,651],{"class":223},[217,3258,374],{"class":367},[217,3260,1686],{"class":367},[217,3262,788],{"class":367},[217,3264,3265],{"class":425}," DefaultAzureCredential",[217,3267,3268],{"class":223},"().",[217,3270,3271],{"class":425},"getToken",[217,3273,632],{"class":223},[217,3275,3276],{"class":233},"TOKEN_SCOPE",[217,3278,638],{"class":223},[217,3280,3282,3284,3286,3289,3291,3294,3296,3298,3300],{"class":219,"line":3281},183,[217,3283,835],{"class":367},[217,3285,646],{"class":223},[217,3287,3288],{"class":233},"daysElapsed",[217,3290,132],{"class":223},[217,3292,3293],{"class":233},"daysInMonth",[217,3295,651],{"class":223},[217,3297,374],{"class":367},[217,3299,1331],{"class":425},[217,3301,1400],{"class":223},[217,3303,3305],{"class":219,"line":3304},184,[217,3306,419],{"emptyLinePlaceholder":418},[217,3308,3310,3312,3315,3317],{"class":219,"line":3309},185,[217,3311,835],{"class":367},[217,3313,3314],{"class":233}," alerts",[217,3316,626],{"class":367},[217,3318,2298],{"class":223},[217,3320,3322,3324,3327,3329,3331],{"class":219,"line":3321},186,[217,3323,1641],{"class":367},[217,3325,3326],{"class":223}," failures ",[217,3328,374],{"class":367},[217,3330,884],{"class":233},[217,3332,683],{"class":223},[217,3334,3336],{"class":219,"line":3335},187,[217,3337,419],{"emptyLinePlaceholder":418},[217,3339,3341],{"class":219,"line":3340},188,[217,3342,3343],{"class":519},"    \u002F\u002F Serial on purpose: Query API quotas are per tenant.\n",[217,3345,3347,3349,3351,3353,3355,3357],{"class":219,"line":3346},189,[217,3348,871],{"class":367},[217,3350,46],{"class":223},[217,3352,620],{"class":367},[217,3354,1215],{"class":233},[217,3356,1218],{"class":367},[217,3358,3359],{"class":223}," config) {\n",[217,3361,3363,3366],{"class":219,"line":3362},190,[217,3364,3365],{"class":367},"        try",[217,3367,2143],{"class":223},[217,3369,3371,3374,3376,3378,3380,3382,3384,3386,3388,3390],{"class":219,"line":3370},191,[217,3372,3373],{"class":367},"            const",[217,3375,646],{"class":223},[217,3377,2553],{"class":233},[217,3379,132],{"class":223},[217,3381,2564],{"class":233},[217,3383,651],{"class":223},[217,3385,374],{"class":367},[217,3387,1686],{"class":367},[217,3389,1470],{"class":425},[217,3391,3392],{"class":223},"(token, entry.subscriptionId, entry.resourceGroup);\n",[217,3394,3396,3398,3401,3403,3405],{"class":219,"line":3395},192,[217,3397,3373],{"class":367},[217,3399,3400],{"class":233}," monthlyBudget",[217,3402,626],{"class":367},[217,3404,969],{"class":425},[217,3406,3407],{"class":223},"(entry.monthlyBudget);\n",[217,3409,3411,3413,3416,3418,3421,3424,3427,3429],{"class":219,"line":3410},193,[217,3412,3373],{"class":367},[217,3414,3415],{"class":233}," projected",[217,3417,626],{"class":367},[217,3419,3420],{"class":223}," (spend ",[217,3422,3423],{"class":367},"\u002F",[217,3425,3426],{"class":223}," daysElapsed) ",[217,3428,1890],{"class":367},[217,3430,3431],{"class":223}," daysInMonth;\n",[217,3433,3435,3437,3440,3442],{"class":219,"line":3434},194,[217,3436,3373],{"class":367},[217,3438,3439],{"class":233}," result",[217,3441,626],{"class":367},[217,3443,2143],{"class":223},[217,3445,3447,3450],{"class":219,"line":3446},195,[217,3448,3449],{"class":367},"                ...",[217,3451,3452],{"class":223},"entry,\n",[217,3454,3456],{"class":219,"line":3455},196,[217,3457,3458],{"class":223},"                monthlyBudget,\n",[217,3460,3462],{"class":219,"line":3461},197,[217,3463,3464],{"class":223},"                spend,\n",[217,3466,3468],{"class":219,"line":3467},198,[217,3469,3470],{"class":223},"                currency,\n",[217,3472,3474],{"class":219,"line":3473},199,[217,3475,3476],{"class":223},"                projected,\n",[217,3478,3480,3483,3485,3488,3490,3493],{"class":219,"line":3479},200,[217,3481,3482],{"class":223},"                percentUsed: (spend ",[217,3484,3423],{"class":367},[217,3486,3487],{"class":223}," monthlyBudget) ",[217,3489,1890],{"class":367},[217,3491,3492],{"class":233}," 100",[217,3494,1572],{"class":223},[217,3496,3498,3501,3503,3505,3507,3509],{"class":219,"line":3497},201,[217,3499,3500],{"class":223},"                projectedPercent: (projected ",[217,3502,3423],{"class":367},[217,3504,3487],{"class":223},[217,3506,1890],{"class":367},[217,3508,3492],{"class":233},[217,3510,1572],{"class":223},[217,3512,3514],{"class":219,"line":3513},202,[217,3515,3516],{"class":223},"            };\n",[217,3518,3520,3522,3525,3527,3530,3532,3535,3537,3540,3542],{"class":219,"line":3519},203,[217,3521,3373],{"class":367},[217,3523,3524],{"class":233}," isAlert",[217,3526,626],{"class":367},[217,3528,3529],{"class":223}," result.percentUsed ",[217,3531,2179],{"class":367},[217,3533,3534],{"class":223}," args.thresholdPercent ",[217,3536,1042],{"class":367},[217,3538,3539],{"class":223}," result.projectedPercent ",[217,3541,2179],{"class":367},[217,3543,3544],{"class":223}," args.thresholdPercent;\n",[217,3546,3548],{"class":219,"line":3547},204,[217,3549,419],{"emptyLinePlaceholder":418},[217,3551,3553,3556,3559],{"class":219,"line":3552},205,[217,3554,3555],{"class":223},"            console.",[217,3557,3558],{"class":425},"log",[217,3560,3561],{"class":223},"(\n",[217,3563,3565,3568,3570,3572,3574,3577,3579,3581,3583,3585,3588,3590,3592,3594,3596,3598,3600,3603],{"class":219,"line":3564},206,[217,3566,3567],{"class":240},"                `${",[217,3569,1287],{"class":223},[217,3571,24],{"class":240},[217,3573,135],{"class":223},[217,3575,3576],{"class":240},"}: ${",[217,3578,2544],{"class":425},[217,3580,632],{"class":240},[217,3582,2553],{"class":223},[217,3584,1290],{"class":240},[217,3586,3587],{"class":240},"} of ${",[217,3589,2544],{"class":425},[217,3591,632],{"class":240},[217,3593,138],{"class":223},[217,3595,1290],{"class":240},[217,3597,1944],{"class":240},[217,3599,2564],{"class":223},[217,3601,3602],{"class":240},"} `",[217,3604,3605],{"class":367}," +\n",[217,3607,3609,3612,3615,3617,3619,3621,3623,3625,3627,3629,3632,3634,3636,3638,3640,3642],{"class":219,"line":3608},207,[217,3610,3611],{"class":240},"                `(${",[217,3613,3614],{"class":223},"result",[217,3616,24],{"class":240},[217,3618,2574],{"class":223},[217,3620,24],{"class":240},[217,3622,2579],{"class":425},[217,3624,632],{"class":240},[217,3626,1438],{"class":233},[217,3628,1290],{"class":240},[217,3630,3631],{"class":240},"}%), projected ${",[217,3633,2544],{"class":425},[217,3635,632],{"class":240},[217,3637,2657],{"class":223},[217,3639,1290],{"class":240},[217,3641,3602],{"class":240},[217,3643,3605],{"class":367},[217,3645,3647,3649,3651,3653,3655,3657,3659,3661,3663,3665,3668,3671,3673,3676,3678],{"class":219,"line":3646},208,[217,3648,3611],{"class":240},[217,3650,3614],{"class":223},[217,3652,24],{"class":240},[217,3654,2676],{"class":223},[217,3656,24],{"class":240},[217,3658,2579],{"class":425},[217,3660,632],{"class":240},[217,3662,1438],{"class":233},[217,3664,1290],{"class":240},[217,3666,3667],{"class":240},"}%)${",[217,3669,3670],{"class":223},"isAlert",[217,3672,2184],{"class":367},[217,3674,3675],{"class":240}," ' ALERT'",[217,3677,2511],{"class":367},[217,3679,3680],{"class":240}," ''}`\n",[217,3682,3684],{"class":219,"line":3683},209,[217,3685,3686],{"class":223},"            );\n",[217,3688,3690,3693],{"class":219,"line":3689},210,[217,3691,3692],{"class":367},"            if",[217,3694,3695],{"class":223}," (isAlert) {\n",[217,3697,3699,3702,3704],{"class":219,"line":3698},211,[217,3700,3701],{"class":223},"                alerts.",[217,3703,2455],{"class":425},[217,3705,3706],{"class":223},"(result);\n",[217,3708,3710],{"class":219,"line":3709},212,[217,3711,3712],{"class":223},"            }\n",[217,3714,3716,3718,3721],{"class":219,"line":3715},213,[217,3717,943],{"class":223},[217,3719,3720],{"class":367},"catch",[217,3722,3723],{"class":223}," (error) {\n",[217,3725,3727,3730,3732],{"class":219,"line":3726},214,[217,3728,3729],{"class":223},"            failures",[217,3731,902],{"class":367},[217,3733,683],{"class":223},[217,3735,3737,3739,3741],{"class":219,"line":3736},215,[217,3738,3555],{"class":223},[217,3740,1848],{"class":425},[217,3742,3743],{"class":223},"(error.message);\n",[217,3745,3747],{"class":219,"line":3746},216,[217,3748,1014],{"class":223},[217,3750,3752],{"class":219,"line":3751},217,[217,3753,1020],{"class":223},[217,3755,3757],{"class":219,"line":3756},218,[217,3758,419],{"emptyLinePlaceholder":418},[217,3760,3762,3764,3766,3768,3770],{"class":219,"line":3761},219,[217,3763,835],{"class":367},[217,3765,2293],{"class":233},[217,3767,626],{"class":367},[217,3769,2273],{"class":425},[217,3771,3772],{"class":223},"(alerts, args.thresholdPercent);\n",[217,3774,3776,3778,3780,3782,3785,3788,3790,3792,3795,3798,3801,3804],{"class":219,"line":3775},220,[217,3777,871],{"class":367},[217,3779,46],{"class":223},[217,3781,620],{"class":367},[217,3783,3784],{"class":223}," [",[217,3786,3787],{"class":233},"index",[217,3789,132],{"class":223},[217,3791,2889],{"class":233},[217,3793,3794],{"class":223},"] ",[217,3796,3797],{"class":367},"of",[217,3799,3800],{"class":223}," messages.",[217,3802,3803],{"class":425},"entries",[217,3805,1346],{"class":223},[217,3807,3809,3811],{"class":219,"line":3808},221,[217,3810,910],{"class":367},[217,3812,3813],{"class":223}," (args.dryRun) {\n",[217,3815,3817,3819,3821,3823,3825,3827,3829,3832,3834,3836,3838],{"class":219,"line":3816},222,[217,3818,3555],{"class":223},[217,3820,3558],{"class":425},[217,3822,632],{"class":223},[217,3824,1277],{"class":233},[217,3826,24],{"class":223},[217,3828,1282],{"class":425},[217,3830,3831],{"class":223},"(message, ",[217,3833,846],{"class":233},[217,3835,132],{"class":223},[217,3837,2247],{"class":233},[217,3839,1145],{"class":223},[217,3841,3843,3845,3847],{"class":219,"line":3842},223,[217,3844,943],{"class":223},[217,3846,946],{"class":367},[217,3848,2143],{"class":223},[217,3850,3852,3854,3857,3859,3861],{"class":219,"line":3851},224,[217,3853,3692],{"class":367},[217,3855,3856],{"class":223}," (index ",[217,3858,1254],{"class":367},[217,3860,884],{"class":233},[217,3862,829],{"class":223},[217,3864,3866],{"class":219,"line":3865},225,[217,3867,3868],{"class":519},"                \u002F\u002F Incoming webhooks allow about one message per second.\n",[217,3870,3872,3875,3877,3879,3882],{"class":219,"line":3871},226,[217,3873,3874],{"class":367},"                await",[217,3876,771],{"class":425},[217,3878,632],{"class":223},[217,3880,3881],{"class":233},"1100",[217,3883,638],{"class":223},[217,3885,3887],{"class":219,"line":3886},227,[217,3888,3712],{"class":223},[217,3890,3892,3895,3897],{"class":219,"line":3891},228,[217,3893,3894],{"class":367},"            await",[217,3896,2879],{"class":425},[217,3898,3899],{"class":223},"(webhookUrl, message);\n",[217,3901,3903],{"class":219,"line":3902},229,[217,3904,1014],{"class":223},[217,3906,3908],{"class":219,"line":3907},230,[217,3909,1020],{"class":223},[217,3911,3913],{"class":219,"line":3912},231,[217,3914,419],{"emptyLinePlaceholder":418},[217,3916,3918,3920,3923,3925,3928,3930,3932,3934,3936,3938],{"class":219,"line":3917},232,[217,3919,835],{"class":367},[217,3921,3922],{"class":233}," posted",[217,3924,626],{"class":367},[217,3926,3927],{"class":223}," args.dryRun ",[217,3929,2152],{"class":367},[217,3931,884],{"class":233},[217,3933,2511],{"class":367},[217,3935,3800],{"class":223},[217,3937,896],{"class":233},[217,3939,683],{"class":223},[217,3941,3943,3946,3948,3950,3952,3954,3956,3958,3960,3963,3965,3967,3969,3972,3974,3976,3979,3982,3985],{"class":219,"line":3942},233,[217,3944,3945],{"class":223},"    console.",[217,3947,3558],{"class":425},[217,3949,632],{"class":223},[217,3951,1191],{"class":240},[217,3953,2278],{"class":223},[217,3955,24],{"class":240},[217,3957,896],{"class":233},[217,3959,3587],{"class":240},[217,3961,3962],{"class":223},"config",[217,3964,24],{"class":240},[217,3966,896],{"class":233},[217,3968,2409],{"class":240},[217,3970,3971],{"class":223},"args",[217,3973,24],{"class":240},[217,3975,2283],{"class":223},[217,3977,3978],{"class":240},"}%. Posted ${",[217,3980,3981],{"class":223},"posted",[217,3983,3984],{"class":240},"} Slack message(s).`",[217,3986,638],{"class":223},[217,3988,3990,3992,3995,3997,3999],{"class":219,"line":3989},234,[217,3991,1031],{"class":367},[217,3993,3994],{"class":223}," (failures ",[217,3996,1254],{"class":367},[217,3998,884],{"class":233},[217,4000,829],{"class":223},[217,4002,4004,4007,4009,4011],{"class":219,"line":4003},235,[217,4005,4006],{"class":223},"        process.exitCode ",[217,4008,374],{"class":367},[217,4010,1387],{"class":233},[217,4012,683],{"class":223},[217,4014,4016],{"class":219,"line":4015},236,[217,4017,1020],{"class":223},[217,4019,4021],{"class":219,"line":4020},237,[217,4022,1092],{"class":223},[217,4024,4026],{"class":219,"line":4025},238,[217,4027,419],{"emptyLinePlaceholder":418},[217,4029,4031,4034,4036,4038,4040,4042,4044,4046],{"class":219,"line":4030},239,[217,4032,4033],{"class":425},"main",[217,4035,3268],{"class":223},[217,4037,3720],{"class":425},[217,4039,794],{"class":223},[217,4041,1848],{"class":778},[217,4043,782],{"class":223},[217,4045,785],{"class":367},[217,4047,2143],{"class":223},[217,4049,4051,4053,4055],{"class":219,"line":4050},240,[217,4052,3945],{"class":223},[217,4054,1848],{"class":425},[217,4056,4057],{"class":223},"(error);\n",[217,4059,4061,4064,4066,4068],{"class":219,"line":4060},241,[217,4062,4063],{"class":223},"    process.exitCode ",[217,4065,374],{"class":367},[217,4067,1387],{"class":233},[217,4069,683],{"class":223},[217,4071,4073],{"class":219,"line":4072},242,[217,4074,4075],{"class":223},"});\n",[26,4077,4079],{"id":4078},"notes","Notes",[31,4081,4082,4088,4091,4108,4115,4122,4125],{},[34,4083,4084,4087],{},[73,4085,4086],{},"The projection is deliberately crude."," Month-to-date spend divided by completed days, times days in the month. It warns early about a resource group trending over budget, but it overshoots after a one-off purchase early in the month and undershoots for workloads that ramp at month end. Azure budgets have a proper forecast-based alert (a threshold on forecasted cost) if you want Microsoft's model instead.",[34,4089,4090],{},"Cost data is not real time. For EA and MCA subscriptions it typically arrives within 8 to 24 hours (up to 72 hours for pay-as-you-go), and figures are estimates until the invoice is generated. Treat this as a daily trend alert, not a meter that catches a runaway resource in the hour it happens.",[34,4092,4093,4096,4097,4100,4101,4104,4105,4107],{},[38,4094,4095],{},"MonthToDate"," follows the calendar month, and ",[38,4098,4099],{},"monthProgress"," counts days in UTC. Pay-as-you-go, MSDN and Visual Studio subscriptions can have invoice periods that don't align with calendar months; the Query API also accepts ",[38,4102,4103],{},"BillingMonthToDate"," for those, and ",[38,4106,4099],{}," would need the same change.",[34,4109,4110,4111,4114],{},"Resource groups are queried one at a time because the Query API limits are per tenant (12 query processing units per 10 seconds, 60 per minute, 600 per hour at the time of writing, one unit per month of data). A 429 response carries the back-off in ",[38,4112,4113],{},"x-ms-ratelimit-microsoft.costmanagement-qpu-retry-after",", which the script waits out.",[34,4116,4117,4118,4121],{},"Slack allows roughly one incoming-webhook message per second, with short bursts, and answers a flood with HTTP 429 and a ",[38,4119,4120],{},"Retry-After"," header. Batching every alert into one message (split at 20 resource groups) keeps a bad day from turning into a message storm. A section block's text is capped at 3,000 characters and its fields render as two columns.",[34,4123,4124],{},"The webhook URL is a credential. Slack actively looks for leaked webhook URLs and revokes them. Keep it in Key Vault or your scheduler's secret store, not in the config file or the repo.",[34,4126,4127],{},"A resource group that is renamed or deleted makes its query fail. The script logs it, carries on with the rest, and exits non-zero so the scheduler flags the run.",[4129,4130,4132],"h3",{"id":4131},"native-budgets-and-action-groups","Native budgets and action groups",[11,4134,4135,4136,3423,4138,4141,4142,4144,4145,4148],{},"If you would rather let Azure do the threshold math, create a budget on each resource group and attach an action group: budgets can call action groups at subscription and resource group scope, and they are evaluated against costs every 24 hours. The catch for Slack is the payload. The action group webhook sends Azure's budget notification JSON, not the ",[38,4137,447],{},[38,4139,4140],{},"blocks"," body a Slack webhook expects (Slack rejects a body without ",[38,4143,447],{}," as ",[38,4146,4147],{},"no_text","), and the action group can't add custom headers. So you still need a small Logic App or Function in between to reshape it. The payload Microsoft documents looks like this:",[208,4150,4152],{"className":210,"code":4151,"language":212,"meta":213,"style":213},"{\n  \"schemaId\": \"AIP Budget Notification\",\n  \"data\": {\n    \"SubscriptionName\": \"\u003Csubscription-name>\",\n    \"SubscriptionId\": \"\u003Csubscription-id>\",\n    \"SpendingAmount\": \"100\",\n    \"BudgetStartDate\": \"6\u002F1\u002F2018\",\n    \"Budget\": \"50\",\n    \"Unit\": \"USD\",\n    \"BudgetCreator\": \"user@example.com\",\n    \"BudgetName\": \"BudgetName\",\n    \"BudgetType\": \"Cost\",\n    \"ResourceGroup\": \"\",\n    \"NotificationThresholdAmount\": \"0.8\"\n  }\n}\n",[38,4153,4154,4159,4171,4179,4191,4202,4214,4226,4238,4250,4262,4274,4286,4298,4308,4313],{"__ignoreMap":213},[217,4155,4156],{"class":219,"line":220},[217,4157,4158],{"class":223},"{\n",[217,4160,4161,4164,4166,4169],{"class":219,"line":227},[217,4162,4163],{"class":233},"  \"schemaId\"",[217,4165,237],{"class":223},[217,4167,4168],{"class":240},"\"AIP Budget Notification\"",[217,4170,1572],{"class":223},[217,4172,4173,4176],{"class":219,"line":277},[217,4174,4175],{"class":233},"  \"data\"",[217,4177,4178],{"class":223},": {\n",[217,4180,4181,4184,4186,4189],{"class":219,"line":317},[217,4182,4183],{"class":233},"    \"SubscriptionName\"",[217,4185,237],{"class":223},[217,4187,4188],{"class":240},"\"\u003Csubscription-name>\"",[217,4190,1572],{"class":223},[217,4192,4193,4196,4198,4200],{"class":219,"line":349},[217,4194,4195],{"class":233},"    \"SubscriptionId\"",[217,4197,237],{"class":223},[217,4199,241],{"class":240},[217,4201,1572],{"class":223},[217,4203,4204,4207,4209,4212],{"class":219,"line":422},[217,4205,4206],{"class":233},"    \"SpendingAmount\"",[217,4208,237],{"class":223},[217,4210,4211],{"class":240},"\"100\"",[217,4213,1572],{"class":223},[217,4215,4216,4219,4221,4224],{"class":219,"line":548},[217,4217,4218],{"class":233},"    \"BudgetStartDate\"",[217,4220,237],{"class":223},[217,4222,4223],{"class":240},"\"6\u002F1\u002F2018\"",[217,4225,1572],{"class":223},[217,4227,4228,4231,4233,4236],{"class":219,"line":554},[217,4229,4230],{"class":233},"    \"Budget\"",[217,4232,237],{"class":223},[217,4234,4235],{"class":240},"\"50\"",[217,4237,1572],{"class":223},[217,4239,4240,4243,4245,4248],{"class":219,"line":560},[217,4241,4242],{"class":233},"    \"Unit\"",[217,4244,237],{"class":223},[217,4246,4247],{"class":240},"\"USD\"",[217,4249,1572],{"class":223},[217,4251,4252,4255,4257,4260],{"class":219,"line":565},[217,4253,4254],{"class":233},"    \"BudgetCreator\"",[217,4256,237],{"class":223},[217,4258,4259],{"class":240},"\"user@example.com\"",[217,4261,1572],{"class":223},[217,4263,4264,4267,4269,4272],{"class":219,"line":571},[217,4265,4266],{"class":233},"    \"BudgetName\"",[217,4268,237],{"class":223},[217,4270,4271],{"class":240},"\"BudgetName\"",[217,4273,1572],{"class":223},[217,4275,4276,4279,4281,4284],{"class":219,"line":577},[217,4277,4278],{"class":233},"    \"BudgetType\"",[217,4280,237],{"class":223},[217,4282,4283],{"class":240},"\"Cost\"",[217,4285,1572],{"class":223},[217,4287,4288,4291,4293,4296],{"class":219,"line":583},[217,4289,4290],{"class":233},"    \"ResourceGroup\"",[217,4292,237],{"class":223},[217,4294,4295],{"class":240},"\"\"",[217,4297,1572],{"class":223},[217,4299,4300,4303,4305],{"class":219,"line":589},[217,4301,4302],{"class":233},"    \"NotificationThresholdAmount\"",[217,4304,237],{"class":223},[217,4306,4307],{"class":240},"\"0.8\"\n",[217,4309,4310],{"class":219,"line":594},[217,4311,4312],{"class":223},"  }\n",[217,4314,4315],{"class":219,"line":600},[217,4316,1092],{"class":223},[11,4318,4319,4320,132,4323,4326,4327,4330],{},"Every value arrives as a string, so parse ",[38,4321,4322],{},"SpendingAmount",[38,4324,4325],{},"Budget"," and ",[38,4328,4329],{},"NotificationThresholdAmount"," before doing math on them. Budgets work with action groups whether or not the common alert schema is enabled on the group. I still prefer the script for the channel-per-team case, because one config file is easier to review than forty budgets, but the native route has no server to run.",[26,4332,4334],{"id":4333},"references","References",[31,4336,4337,4345,4352,4359,4366,4373,4380,4387],{},[34,4338,4339],{},[15,4340,4344],{"href":4341,"rel":4342},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Frest\u002Fapi\u002Fcost-management\u002Fquery\u002Fusage",[4343],"nofollow","Query - Usage (Cost Management REST API)",[34,4346,4347],{},[15,4348,4351],{"href":4349,"rel":4350},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fazure\u002Fcost-management-billing\u002Fcosts\u002Fmanage-automation",[4343],"Manage Azure costs with automation (data latency, QPU rate limits, Budgets API)",[34,4353,4354],{},[15,4355,4358],{"href":4356,"rel":4357},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fazure\u002Fcost-management-billing\u002Fcosts\u002Ftutorial-acm-create-budgets",[4343],"Tutorial: Create and manage budgets (action groups, evaluation frequency)",[34,4360,4361],{},[15,4362,4365],{"href":4363,"rel":4364},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fazure\u002Fcost-management-billing\u002Fmanage\u002Fcost-management-budget-scenario",[4343],"Manage costs with budgets (budget alert payload for action groups)",[34,4367,4368],{},[15,4369,4372],{"href":4370,"rel":4371},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fazure\u002Fcost-management-billing\u002Fcosts\u002Fassign-access-acm-data",[4343],"Assign access to Cost Management data",[34,4374,4375],{},[15,4376,4379],{"href":4377,"rel":4378},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fjavascript\u002Fapi\u002Foverview\u002Fazure\u002Fidentity-readme",[4343],"Azure Identity client library for JavaScript",[34,4381,4382],{},[15,4383,4386],{"href":4384,"rel":4385},"https:\u002F\u002Fdocs.slack.dev\u002Fmessaging\u002Fsending-messages-using-incoming-webhooks\u002F",[4343],"Slack: Sending messages using incoming webhooks",[34,4388,4389,4326,4394],{},[15,4390,4393],{"href":4391,"rel":4392},"https:\u002F\u002Fdocs.slack.dev\u002Fapis\u002Fweb-api\u002Frate-limits\u002F",[4343],"Slack: Rate limits",[15,4395,4398],{"href":4396,"rel":4397},"https:\u002F\u002Fdocs.slack.dev\u002Freference\u002Fblock-kit\u002Fblocks\u002Fsection-block\u002F",[4343],"Section block reference",[4400,4401,4402],"style",{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":213,"searchDepth":227,"depth":227,"links":4404},[4405,4406,4407,4408,4409,4412],{"id":28,"depth":227,"text":29},{"id":85,"depth":227,"text":86},{"id":195,"depth":227,"text":196},{"id":506,"depth":227,"text":507},{"id":4078,"depth":227,"text":4079,"children":4410},[4411],{"id":4131,"depth":277,"text":4132},{"id":4333,"depth":227,"text":4334},"techcolumnist",[4415,4416],"scripts","engineering","2026-02-18T14:00:00Z","A Node.js script that queries month-to-date Azure cost per resource group, projects month-end spend, and posts Block Kit messages to Slack for groups over budget.","md",false,null,{},"\u002Fblog\u002F2026\u002F02\u002F18\u002Fnode-js-azure-a-slack-bot-for-resource-group-budget-alerts",{"title":6,"description":4418},[4413],"blog\u002F2026\u002F02\u002F18\u002Fnode-js-azure-a-slack-bot-for-resource-group-budget-alerts",[4428,4429],"nodejs","azure","\u002F2026\u002F02\u002F18\u002Fnode-js-azure-a-slack-bot-for-resource-group-budget-alerts\u002F","Mf1R-dgVba7T6v_C3BPhegYbO0OHA_2RhAlqofO24Ug",{"title":4433,"description":4434,"date":4435,"url":4436,"categories":4437,"tags":4438,"image":4421,"readingTime":349,"canonical":4413,"sites":4442,"series":4421,"seriesOrder":4421},"Node.js: Data Warehouse – Streaming ETL Rows Without Buffering Them","A Node.js script that streams Postgres rows through a cursor, a transform and COPY FROM STDIN with backpressure, so memory tracks batch size, not table size.","2026-02-25T14:00:00Z","\u002F2026\u002F02\u002F25\u002Fnode-js-data-warehouse-streaming-etl-rows-without-loading-them-all-in-memory\u002F",[4415,4416],[4428,4439,4440,4441],"data-warehouse","etl","sql",[4413],{"title":4444,"description":4445,"date":4446,"url":4447,"categories":4448,"tags":4449,"image":4421,"readingTime":277,"canonical":4413,"sites":4453,"series":4421,"seriesOrder":4421},"Node.js: Discovery – Scanning a Subnet for Open RDP and SSH","A dependency-free Node.js TCP connect scanner that sweeps a subnet for RDP and SSH, separates closed from filtered ports, grabs SSH banners and writes CSV.","2026-02-11T14:00:00Z","\u002F2026\u002F02\u002F11\u002Fnode-js-discovery-scanning-a-subnet-for-open-rdp-and-ssh\u002F",[4415,4416],[4428,4450,4451,4452],"discovery","network","ssh",[4413],[4455,4463,4432],{"title":4456,"description":4457,"date":4458,"url":4459,"categories":4460,"tags":4461,"image":4421,"readingTime":277,"canonical":4413,"sites":4462,"series":4421,"seriesOrder":4421},"Node.js: Discovery – Diffing Two Network Scans to Flag New Hosts","A dependency-free Node.js script that diffs two nmap XML scans and flags new, missing and moved hosts plus port changes, with an exit code for cron or CI.","2026-03-11T14:00:00Z","\u002F2026\u002F03\u002F11\u002Fnode-js-discovery-diffing-two-network-scans-to-flag-new-hosts\u002F",[4415,4416],[4428,4450,4451],[4413],{"title":4464,"description":4465,"date":4466,"url":4467,"categories":4468,"tags":4469,"image":4421,"readingTime":317,"canonical":4413,"sites":4471,"series":4421,"seriesOrder":4421},"Node.js: gcloud – Rotating Service Account Keys Before They Expire","A Node.js script that finds aging Google Cloud service account keys, mints one replacement, and disables the old keys once it is deployed.","2026-03-04T14:00:00Z","\u002F2026\u002F03\u002F04\u002Fnode-js-gcloud-rotating-service-account-keys-before-they-expire\u002F",[4415,4416],[4428,4470],"gcloud",[4413],{"doc":4421,"posts":4473},[],1790052513643]