Created
January 5, 2018 00:09
-
-
Save DazWilkin/9249dc9988c158284715ef1a6d7aef5b to your computer and use it in GitHub Desktop.
Cloud Functions Prometheus Exporter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* 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