Cloud
banshee/cloud-collection/cloud/azure/https/
metrics
The newest value of one Azure Monitor metric of one Azure resource, and its age, by Azure Resource Manager.
by banshee1 installation2 versions
v2Latest
Published on October 28, 2026. One naming convention across collections; documentation completed.
// One metric of one Azure resource, by Azure Monitor, logged in as a service principal: its newest value over
// the last thirty minutes, in the aggregation chosen, and how old that value is. Any resource, any metric
// Azure Monitor keeps for it: the resource and the metric are set on the probe.
// Reads POST https://login.microsoftonline.com/<tenant>/oauth2/v2.0/token, then GET
// <resource>/providers/Microsoft.Insights/metrics on management.azure.com.
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.',
));
$resource = variable('azure:resource', 'text', array(
'label' => 'Resource ID',
'help' => 'The full ID of the resource, as its page "JSON View" gives it: /subscriptions/…/resourceGroups/…/providers/….',
));
$metric = variable('azure:metric', 'text', array(
'label' => 'Metric',
'help' => 'The name of the metric as Azure Monitor knows it, for instance Percentage CPU for a virtual machine.',
));
$aggregation = variable('azure:aggregation', 'choice', array(
'label' => 'Aggregation',
'help' => 'How the values of each minute are combined: the one the metric supports, its default aggregation at best.',
'choices' => array('average', 'minimum', 'maximum', 'total', 'count'),
'default' => 'average',
));
$namespace = variable('azure:metric-namespace', 'text', array(
'label' => 'Metric namespace',
'help' => 'The namespace of the metric, only when it is not the resource\'s own, for instance a guest metric.',
'required' => false,
));
output('value', 'number', array('label' => 'Value'));
output('age', 'number', array('label' => 'Age of the value', 'unit' => 'min'));
watch.above('high', 'Value too high', 'value', array(
'help' => 'Raises when the value goes above the thresholds set on the probe, in the unit of the metric.',
));
watch.below('low', 'Value too low', 'value', array(
'help' => 'Raises when the value goes below the thresholds set on the probe, in the unit of the metric.',
'enabled' => false,
));
watch.above('stale', 'Value not fresh', 'age', array('warning' => 15));
// 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 resource ID goes into the path as it is written: it must be one.
if (list.count(text.match($resource, '^/subscriptions/[^?#]+$')) == 0) {
fail('misconfigured', 'The resource ID does not start with /subscriptions/', 'Copy it from the page "JSON View" of the resource.');
}
// The last thirty minutes, a value a minute, or the finest grain the metric has (AutoAdjustTimegrain). The
// timespan is written to the second, 2026-10-27T07:35:00Z/2026-10-27T08:05:00Z.
$now = number.round(time.now());
$query = array(
'api-version' => '2023-10-01',
'metricnames' => $metric,
'aggregation' => $aggregation,
'timespan' => time.format($now - 1800, 'rfc3339') . '/' . time.format($now, 'rfc3339'),
'interval' => 'PT1M',
'AutoAdjustTimegrain' => 'true',
);
if ($namespace != '') {
$query['metricnamespace'] = $namespace;
}
$answer = arm_get('https://management.azure.com' . $resource . '/providers/Microsoft.Insights/metrics?' . url.query($query),
'the metric ' . $metric, $tenant, $client, $secret);
// {"value": [{"name": {"value": "Percentage CPU"}, "unit": "Percent", "errorCode": "Success", "timeseries":
// [{"data": [{"timeStamp": "2026-10-27T08:04:00Z", "average": 12.5}, {"timeStamp": "2026-10-27T08:05:00Z"}]}]}]}.
// A minute not yet computed comes without the aggregation, or with null: the newest value is that of the last
// minute that carries it.
$found = list.get($answer, 'value', array());
if (list.count($found) == 0) {
fail('misconfigured', 'Azure Monitor returns no metric ' . $metric);
}
$result = $found[0];
if (list.get($result, 'errorCode', 'Success') != 'Success') {
fail('misconfigured', 'Azure Monitor cannot read the metric ' . $metric . ': ' . $result['errorCode'],
list.get($result, 'errorMessage', ''));
}
$last = '';
$at = '';
foreach (list.get($result, 'timeseries', array()) as $series) {
foreach (list.get($series, 'data', array()) as $point) {
if (list.get($point, $aggregation, '') != '') {
$last = $point[$aggregation];
$at = $point['timeStamp'];
}
}
break;
}
if ($last == '') {
fail('unreachable', 'Azure Monitor has no value of ' . $metric . ' in the last thirty minutes');
}
$epoch = time.parse($at, 'iso8601');
value('value', $last);
value('age', number.round(($now - $epoch) / 60, 1));