Cloud
banshee/cloud-collection/cloud/google/https/
monitoring-metric
The newest value of one Cloud Monitoring time series of a Google Cloud project, and its age, by the Cloud Monitoring API.
by banshee0 installations1 version
Published on October 27, 2026. First version.
// One Cloud Monitoring metric of a Google Cloud project, by the Cloud Monitoring API with a service account: the
// newest value of the one time series a filter picks, over the last thirty minutes, aligned as chosen, and how
// old that value is. Any metric, any resource: the filter is set on the probe.
// Reads POST https://oauth2.googleapis.com/token by google.token, then GET
// https://monitoring.googleapis.com/v3/projects/<project>/timeSeries.
need('protocol:http');
need('protocol:google');
$project = variable('google:project', 'text', array(
'label' => 'Project',
'help' => 'The ID of the Google Cloud project, as the console writes it under Project ID: my-project-123456.',
));
$filter = variable('google:filter', 'text', array(
'label' => 'Filter',
'help' => 'The monitoring filter that picks one time series, for instance metric.type = "compute.googleapis.com/instance/cpu/utilization" AND metric.labels.instance_name = "web-1".',
));
$aligner = variable('google:aligner', 'choice', array(
'label' => 'Aligner',
'help' => 'How the points of each period are combined: ALIGN_MEAN for a gauge, ALIGN_RATE for a counter.',
'choices' => array('ALIGN_MEAN', 'ALIGN_MIN', 'ALIGN_MAX', 'ALIGN_SUM', 'ALIGN_RATE', 'ALIGN_DELTA', 'ALIGN_COUNT'),
'default' => 'ALIGN_MEAN',
));
$period = variable('google:alignment-period', 'integer', array(
'label' => 'Alignment period',
'help' => 'The length of each period, in seconds, 60 at least.',
'default' => 60,
));
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));
// The access token of the service account for this scope, asked by google.token and kept in the cache,
// encrypted on the poller, fifty minutes, under the hour Google gives it.
function access_token($scope) {
if (cache.has('token')) {
return cache.get('token');
}
$token = google.token($scope);
cache.set('token', $token, 3000);
return $token;
}
// One GET on a Google Cloud API with the token kept in the cache; a token Google no longer takes answers 401,
// and a new one is asked for, once.
function google_get($url, $scope, $what) {
$answer = http.get($url, headers=array('Authorization' => 'Bearer ' . access_token($scope)));
if ($answer['status'] == 401) {
cache.delete('token');
$answer = http.get($url, headers=array('Authorization' => 'Bearer ' . access_token($scope)));
}
if ($answer['status'] == 403) {
fail('misconfigured', 'The service account may not read ' . $what . ', code 403',
'Check its role on the project, and that the API is enabled on the project.');
}
if ($answer['status'] == 400 || $answer['status'] == 404) {
fail('misconfigured', 'Google Cloud refuses to read ' . $what . ', code ' . $answer['status'],
'Check the project and the names set on the probe.');
}
if ($answer['status'] != 200) {
fail('unreachable', 'Google Cloud answers the reading of ' . $what . ' with code ' . $answer['status']);
}
return json.read($answer['body']);
}
if ($period < 60) {
fail('misconfigured', 'The alignment period is under 60 seconds');
}
// The last thirty minutes, the times written in UTC.
$now = number.round(time.now());
$query = array(
'filter' => $filter,
'interval.startTime' => time.format($now - 1800, 'rfc3339'),
'interval.endTime' => time.format($now, 'rfc3339'),
'aggregation.alignmentPeriod' => $period . 's',
'aggregation.perSeriesAligner' => $aligner,
'view' => 'FULL',
);
$answer = google_get('https://monitoring.googleapis.com/v3/projects/' . url.encode($project) . '/timeSeries?' . url.query($query),
'https://www.googleapis.com/auth/monitoring.read', 'the time series');
// {"timeSeries": [{"metric": {"type": "compute.googleapis.com/instance/cpu/utilization", …}, "resource": {…},
// "points": [{"interval": {"startTime": "…", "endTime": "2026-10-28T08:05:00Z"}, "value": {"doubleValue": 0.125}},
// …]}], "unit": "…"}: the points of a series come newest first. An int64Value is written as a JSON string.
$series = list.get($answer, 'timeSeries', array());
if (list.count($series) == 0) {
fail('unreachable', 'Cloud Monitoring has no point for the filter in the last thirty minutes',
'A metric or a label that Cloud Monitoring does not know gives no series either: check the filter.');
}
if (list.count($series) > 1 || list.get($answer, 'nextPageToken', '') != '') {
fail('misconfigured', 'The filter picks more than one time series', 'Add the labels that pick one, for instance metric.labels.instance_name.');
}
$points = list.get($series[0], 'points', array());
if (list.count($points) == 0) {
fail('unreachable', 'Cloud Monitoring has no point for the filter in the last thirty minutes');
}
$point = $points[0];
$last = list.get($point['value'], 'doubleValue', list.get($point['value'], 'int64Value', ''));
if ($last == '' || !text.is_number('' . $last)) {
fail('misconfigured', 'The metric is not a number', 'A boolean, a text or a distribution is not read by this collector.');
}
value('value', $last);
value('age', number.round(($now - time.parse($point['interval']['endTime'], 'rfc3339')) / 60, 1));