Cloud
banshee/cloud-collection/cloud/azure/https/
service-health
The active Azure service issues, planned maintenances and advisories that touch an Azure subscription, by Azure Resource Health.
by banshee1 installation2 versions
v2Latest
Published on October 28, 2026. One naming convention across collections; documentation completed.
// The Azure service health events that touch a subscription, by Azure Resource Health, logged in as a service
// principal: how many Azure service issues are active, how many planned maintenances and advisories, and the
// titles of the issues.
// Reads POST https://login.microsoftonline.com/<tenant>/oauth2/v2.0/token, then GET
// /subscriptions/<id>/providers/Microsoft.ResourceHealth/events on management.azure.com, page after page.
need('protocol:http');
$client = need('protocol:http:user');
$secret = need('protocol:http:password');
$tenant = variable('azure:tenant', 'text', array(
'label' => 'Tenant',
'help' => 'The directory (tenant) ID of the service principal, or one of the tenant\'s domains.',
));
$subscription = variable('azure:subscription', 'text', array(
'label' => 'Subscription',
'help' => 'The ID of the subscription whose service health events are read.',
));
output('issues', 'number', array('label' => 'Active service issues'));
output('maintenances', 'number', array('label' => 'Active planned maintenances'));
output('advisories', 'number', array('label' => 'Active advisories'));
output('issue-titles', 'text', array('label' => 'Titles of the active service issues'));
watch.above('issue', 'Azure service issue', 'issues', array('warning' => 0));
watch.above('maintenance', 'Azure planned maintenance', 'maintenances', array('warning' => 0, 'enabled' => false));
watch.above('advisory', 'Azure advisory', 'advisories', array('warning' => 0, 'enabled' => false));
// Log in as the service principal, by the client credentials grant. The secret goes in the form, which makes
// the answer secret, and its access token with it. The token is kept in the cache, encrypted on the poller,
// thirty minutes, under the hour or so Microsoft gives it.
function login($tenant, $client, $secret, $scope) {
$body = url.query(array('grant_type' => 'client_credentials', 'client_id' => $client,
'client_secret' => $secret, 'scope' => $scope));
$answer = http.post('https://login.microsoftonline.com/' . url.encode($tenant) . '/oauth2/v2.0/token', $body,
headers=array('Content-Type' => 'application/x-www-form-urlencoded'));
if ($answer['status'] == 400 || $answer['status'] == 401) {
fail('misconfigured', 'Microsoft Entra ID refuses the service principal, code ' . $answer['status']);
}
if ($answer['status'] != 200) {
fail('unreachable', 'Microsoft Entra ID answers the login with code ' . $answer['status']);
}
$session = json.read($answer['body']);
cache.set('token', $session['access_token'], 1800);
return $session['access_token'];
}
// One GET on Azure Resource Manager with the token kept in the cache; a token Azure no longer takes answers
// 401, and the collector logs in again, once.
function arm_get($url, $what, $tenant, $client, $secret) {
if (cache.has('token')) {
$token = cache.get('token');
} else {
$token = login($tenant, $client, $secret, 'https://management.azure.com/.default');
}
$answer = http.get($url, headers=array('Authorization' => 'Bearer ' . $token));
if ($answer['status'] == 401) {
$token = login($tenant, $client, $secret, 'https://management.azure.com/.default');
$answer = http.get($url, headers=array('Authorization' => 'Bearer ' . $token));
}
if ($answer['status'] == 403) {
fail('misconfigured', 'The service principal may not read ' . $what . ', code 403');
}
if ($answer['status'] == 400 || $answer['status'] == 404) {
fail('misconfigured', 'Azure refuses to read ' . $what . ', code ' . $answer['status'],
'Check the IDs and names set on the probe.');
}
if ($answer['status'] != 200) {
fail('unreachable', 'Azure answers the reading of ' . $what . ' with code ' . $answer['status']);
}
return json.read($answer['body']);
}
// The items of a list, page after page: Azure gives the address of the next page under nextLink, until the
// last. A next page is only followed on Azure Resource Manager itself, where the token may go.
function arm_list($url, $what, $tenant, $client, $secret) {
$items = array();
while ($url != '') {
if (!text.like($url, 'https://management.azure.com/*')) {
fail('unreachable', 'Azure gives the next page of ' . $what . ' outside management.azure.com');
}
$page = arm_get($url, $what, $tenant, $client, $secret);
foreach (list.get($page, 'value', array()) as $item) {
$items = list.add($items, $item);
}
$url = list.get($page, 'nextLink', '');
}
return $items;
}
// Azure lists, by default, the events updated in the last three days only: an issue still active but not
// updated since would drop out of the list. The events updated in the last thirty days are asked for, the date
// written 10/7/2026, month/day/year in UTC, the form of queryStartTime.
$written = text.split(time.format(number.round(time.now()) - 30 * 86400, 'rfc3339'), 'T');
$day = text.split($written[0], '-');
$since = ($day[1] + 0) . '/' . ($day[2] + 0) . '/' . $day[0];
$url = 'https://management.azure.com/subscriptions/' . url.encode($subscription)
. '/providers/Microsoft.ResourceHealth/events?' . url.query(array('api-version' => '2025-05-01', 'queryStartTime' => $since));
// {"value": [{"name": "BC_1-FXZ", "properties": {"eventType": "ServiceIssue", "status": "Active", "title":
// "ACTIVE: Virtual machines in West US", "impact": [{"impactedService": "Virtual Machines", …}], …}}],
// "nextLink": "…"}. eventType is ServiceIssue, PlannedMaintenance, HealthAdvisory, SecurityAdvisory, RCA,
// EmergingIssues or Billing; status Active or Resolved.
$events = arm_list($url, 'the service health events', $tenant, $client, $secret);
$issues = 0;
$maintenances = 0;
$advisories = 0;
$titles = array();
foreach ($events as $event) {
$properties = list.get($event, 'properties', array());
if (list.get($properties, 'status', '') != 'Active') {
continue;
}
$type = list.get($properties, 'eventType', '');
if ($type == 'ServiceIssue') {
$issues = $issues + 1;
$titles = list.add($titles, list.get($properties, 'title', $event['name']));
} elseif ($type == 'PlannedMaintenance') {
$maintenances = $maintenances + 1;
} elseif ($type == 'HealthAdvisory' || $type == 'SecurityAdvisory') {
$advisories = $advisories + 1;
}
}
value('issues', $issues);
value('maintenances', $maintenances);
value('advisories', $advisories);
value('issue-titles', text.join($titles, ' | '));