Skip to content

Instantly share code, notes, and snippets.

@DazWilkin
Created January 5, 2018 00:09
Show Gist options
  • Save DazWilkin/9249dc9988c158284715ef1a6d7aef5b to your computer and use it in GitHub Desktop.
Save DazWilkin/9249dc9988c158284715ef1a6d7aef5b to your computer and use it in GitHub Desktop.
Cloud Functions Prometheus Exporter
/* jshint strict: true */
/* jshint esversion: 6 */
/* globals exports,require */
const client = require("prom-client");
const Registry = client.Registry;
const register = new Registry();
const Particle = require("particle-api-js");
const particle = new Particle();
const runtimeConfig = require("cloud-functions-runtime-config");
const getVariables = (runtimeConfig, config, variables) =>
Promise
.all(variables
// Converts a variable into a Promise that resolves to the value from Runtime Config
.map(variable => runtimeConfig.getVariable(config, variable))
// Lazily filter out rejects in order to use Promise.all
.map(p => p.catch(() => undefined))
);
const config = {
name: "particle",
variables: [
"username",
"password"
]
};
// Using Runtime Configuration get a Promise for the array of variables
const tokenPromise = getVariables(
runtimeConfig,
config.name,
// variables[0==username, 1==password]
config.variables
)
// Get a Promise on a token with login to the Particle cloud
.then(values => particle
.login({
username: values[0],
password: values[1]
})
);
const
counter = new client.Counter({
name: "particle_counter",
help: "Particle variable measurements counter",
labelNames: ["device", "variable"],
registers: [register],
}),
gauge = new client.Gauge({
name: "particle_gauge",
help: "Particle variable measurements gauge",
labelNames:["device", "variable"],
registers: [register],
}),
histogram = new client.Histogram({
name: "particle_histogram",
help: "Particle variable measurements histogram",
labelNames: ["device", "variable"],
buckets: [0,10,20,30,40,50,60,70,80,90,100],
registers: [register],
aggregator: 'average',
});
const
createMetrics = (v) => {
console.log(`[${v.id}] Variable: ${v.name}=${v.value}`);
counter.labels(v.id, v.name).inc();
gauge.labels(v.id, v.name).set(v.value, Date.now());
// Histograms do not accept timestamps
histogram.labels(v.id, v.name).observe(v.value);
return(v.value);
},
getDevicesVariablesValuesPromise = (access_token, devices) => {
// Only devices that are (a) 'connected' (b) have variables should be included
// The 'devices' async forks into one per device (getDevice)
// And then subsequently into one per variable (getVariable)
// Need to synchronize these async calls of async calls
// Using Promise.all but simplistically by ignoring rejects
// Returns a Promise that resolves to an array of arrays of variable values
// i.e. P.then == [[v1, v2],...]
return Promise.all(devices
// Only check devices that are connected
.filter(device => device.connected)
.map(device => particle
.getDevice({
deviceId: device.id,
auth: access_token
})
)
// Avoids Promise.all failing on a single reject
.map(p => p.catch(() => undefined))
.map(devicePromise => devicePromise
.then(deviceResponse => getDeviceVariableValuesPromise(
// NB closes over access_token
access_token,
deviceResponse.body.id,
deviceResponse.body.variables
))
)
);
},
getDeviceVariableValuesPromise = (access_token, id, variables) => {
console.log(`[${id}] Accessed`);
let variablePromises = [];
// Variables are objects, the property|key is the variable name and the value is its type
if (Object.keys(variables).length>0) {
console.log(`[${id}] Connected and ${Object.keys(variables).length} variables`);
for (let variable in variables) {
if (variables.hasOwnProperty(variable)) {
console.log(`[${id}] Variable: ${variable}`);
variablePromises.push(particle.getVariable({
deviceId: id,
name: variable,
auth: access_token
}));
}
}
} else {
console.log(`[${id}] Connected has no variables`);
}
// One promise when all variables resolve
// Resolve to an array of variableResponses
return Promise
.all(variablePromises
// Avoids Promse.all failing on a single reject
.map(p => p.catch(() => undefined))
);
};
// cloud Functions entry-point 'metrics'
exports.metrics = (req, res) => tokenPromise
// Resolve the tokenPromise to an access token
.then(loginResponse => loginResponse.body.access_token)
// Use the access token to enumerate (all) devices
.then(access_token => particle.listDevices({
auth: access_token
})
.then(resp => resp.body)
.then(devices =>
getDevicesVariablesValuesPromise(access_token, devices)
// Flatten the array of device variable response arrays into an array of variable responses
// [[v1, v2, ...], [v1, v2...]... ] --> [v11, v12, v21, v22, ...]
// https://developer.mozilla.org
.then(arr => arr
.reduce(
(a, b) => a.concat(b),
[]
)
)
.then(variableResponses => variableResponses
// Slightly overkill but more clearly shows the mappings
// Convert variableResponse --> {id, name, value}
.map(variableResponse => ({
id: variableResponse.body.coreInfo.deviceID,
name: variableResponse.body.name,
value: variableResponse.body.result
}))
// Convert {id, name, value} --> value
// Side-effect is that it creates Prometheus counter, gauge, histogram
// Returns the value purely to propagate the Promise
.map(variable => createMetrics(variable))
)
// Done
.then(values => res.status(200).send(register.metrics()))
// Unless we're not
.catch(err => res.status(500).send(err))
)
// listDevices
.catch(err => {
console.log(err);
res.status(500).send(err);
})
)
// tokenPromise
.catch(err => {
console.log(err);
res.status(500).send(err);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment