banshee is under active development: collectors will keep changing, and a version you publish will likely need adjustments. For stable use, wait for version 1.0.0.

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.

  • cloud
  • azure
  • service-health
  • resource-health
  • incident
  • outage
  • maintenance
  • https
  • api

by banshee1 installation2 versions

v2Latest

Published on October 28, 2026. One naming convention across collections; documentation completed.

Keen 1.0.0 · 125 lines
// 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, ' | '));