diff --git a/README.md b/README.md index cc2c153..5f0a9c4 100644 --- a/README.md +++ b/README.md @@ -6,47 +6,14 @@ A utility providing a means of easily capturing trace messages for offline analy ## Installation -The only prerequisites not handled during the installation are a functional Node environment, the availability of npm, and sufficient priviledges to run commands as adminstrator. The steps below are applicable to a Mac OS X environment, similar steps work under Linux or Windows. - -Clone this project to your local machine: - -`$ git clone https://github.com/apigeecs/apigee-cli-trace.git` - -Alternatively you can download the zip file via the GitHub home page and unzip the archive. - -Navigate to the package directory: - -`$ cd path/to/apigee-cli-trace/package/` - -Install globally: - -`$ sudo npm install . -g` +npm install apigee-coverage ## Usage ``` - var trace = require("./package/apigee-cli-trace"); + apigee-coverage -o askanapigeek -e test -a No-Target -r 4 + Set Apigee_User and Apigee_Secret to utilize this feature as your environment variables - trace.capture({ - debug: true, - org: "davidwallen2014", - env: "prod", - api: "24Solver", - rev: "19", - auth: "Basic encodeduserandsecret", - saveTo: "./capturedTraceFiles" - }); ``` - -Execute the following: - -`$ node ./capture.js` - -Where `capture.js` is a script as outlined above. Note the script runs until cancelled. - -Output includes a information summarizing captured trace messages: - -Note that the utility captures a subset of traffic - it is not capable of nor intended to capture all traffic in a given run. Consider it as sampling as much as 90% or as low as 60% of traffic depending on the speed of your local machine, local network, and rate of traffic in the target proxy. - ## Tests none yet diff --git a/lib/apigee-cli-trace.js b/lib/apigee-cli-trace.js new file mode 100644 index 0000000..f59d8e2 --- /dev/null +++ b/lib/apigee-cli-trace.js @@ -0,0 +1,301 @@ +// packages +var path = require("path"), + fs = require("fs"), + https = require("https"), + traceResponse = { + "traceFiles": [], + "curTraceFile": {} + }, + traceMessages = {}, + config, + count = 0; + +function print(msg) { + try { + if (msg && (typeof msg === "object")) { + console.log(JSON.stringify(msg)); + } else { + console.log(msg); + } + } catch (error) { + console.log(error); + } +} + +function debugPrint(msg) { + if (config.debug) { + print(msg); + } +} + +function getStackTrace(e) { + return e.stack.replace(/^[^\(]+?[\n$]/gm, "") + .replace(/^\s+at\s+/gm, "") + .replace(/^Object.\s*\(/gm, "{anonymous}()@") + .split("\n"); +} + +function mkdirSync(path) { + try { + fs.existsSync(path) || fs.mkdirSync(path); + } catch (e) { + if (e.code !== "EEXIST") { + throw e; + } + } +} + +function mkdirpSync(dirpath) { + var parts = dirpath.split(path.sep); + for (var i = 1; i <= parts.length; i++) { + mkdirSync(path.join.apply(null, parts.slice(0, i))); + } +} + +function writeTraceFile(id, data) { + //file exists? If not create + //append to it + if (config.saveTo) { + fs.existsSync(config.saveTo) || mkdirpSync(config.saveTo); + fs.writeFile(config.saveTo + "/" + id + ".xml", data, function(err) { + if (err) { console.error(err); } + }); + if ((++count % 10) === 0) print(count + " messages saved..."); + } +} + +function processTraceTransaction(trans) { + var data = "", + id = trans.id; + + var options = { + host: "api.enterprise.apigee.com", + port: 443, + path: "/v1/organizations/" + config.org + "/environments/" + config.env + "/apis/" + config.api + "/revisions/" + config.rev + "/debugsessions/" + config.debugSessionId + "/data/" + id, + method: "GET", + headers: { + Accept: "application/xml", + Authorization: config.auth + } + }; + + var req = https.request(options, function(res) { + res.on("data", function(d) { + data += d; + }); + res.on("end", function() { + if (data.indexOf("true") > -1) { + writeTraceFile(id, data); + } + }); + }); + + req.on("error", function(e) { + print("error in the https call"); + console.error(e); + trans.processed = false; + }); + req.end(); + +} + +function uuid() { + var d = new Date().getTime(); + var theUuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { + var r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c === "x" ? r : (r & 0x7 | 0x8)).toString(16); + }); + return theUuid; +} + +function isJson(blob) { + return (/^[\],:{}\s]*$/.test(blob.replace(/\\["\\\/bfnrtu]/g, "@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, "]").replace(/(?:^|:|,)(?:\s*\[)+/g, ""))); +} + +function processDebugSession() { + var options = { + host: "api.enterprise.apigee.com", + port: 443, + path: "/v1/organizations/" + config.org + "/environments/" + config.env + "/apis/" + config.api + "/revisions/" + config.rev + "/debugsessions?session=" + config.debugSessionId, + method: "POST", + headers: { + //Accept: "application/json", + Authorization: config.auth + // "Content-Type": "application/x-www-url-form-encoded" + } + }; + + var req = https.request(options, function(res) { + res.setEncoding("utf8"); + if (res.statusCode >= 300) { + console.error(res.statusCode + ": " + res.statusMessage + " with " + JSON.stringify(options)); + } + res.on("data", function(d) { + d = JSON.parse(d); + config.debugSessionId = d.name; + config.debugStart = new Date(); + //now we want to call the retrieval loop + processTraceTransactions(); + }); + }); + + req.on("error", function(e) { + console.error(e); + console.error("error in creating a debug session with " + JSON.stringify(options)); + }); + req.end(); +} + +function processTraceMessages() { + for (var id in traceMessages) { + if ({}.hasOwnProperty.call(traceMessages, id)) { + if (!traceMessages[id].processed && !traceMessages[id].inProcess) { + traceMessages[id].inProcess = true; + processTraceTransaction(traceMessages[id]); + + } + } + } +} + +function processTransactionPayload(str) { + var d = JSON.parse(str); + /*"{ + "code" : "distribution.DebugSessionNotFound", + "message" : "DebugSession bdbfa0a5-3dc3-4971-edfb-25a277f5d7bd not found", + "contexts" : [ ] + }"*/ + + if (d.code === "distribution.DebugSessionNotFound" || d.length >= 20 || ((new Date() - config.debugStart) > 10 * 60 * 1000)) { + config.debugSessionId = uuid(); + processDebugSession(); + } else { + for (var i = d.length; i-- > 0;) { + traceMessages[d[i]] = traceMessages[d[i]] || { + id: d[i], + processed: false, + inProcess: false + }; + } + processTraceMessages(); + processTraceTransactions(); + } +} + +function processTraceTransactions() { + var options = { + host: "api.enterprise.apigee.com", + port: 443, + path: "/v1/organizations/" + config.org + "/environments/" + config.env + "/apis/" + config.api + "/revisions/" + config.rev + "/debugsessions/" + config.debugSessionId + "/data", + method: "GET", + headers: { + Accept: "application/json", + Authorization: config.auth + } + }, + data = ""; + + var req = https.request(options, function(res) { + res.setEncoding("utf8"); + res.on("data", function(d) { + data += d; + }); + res.on("end", function() { + if (isJson(data)) { + processTransactionPayload(data); + } else { + print("error in the the response - JSON not found"); + print(data); + } + }); + }); + + req.setTimeout(30000, function() { + //when timeout, this callback will be called + }); + + req.on("error", function(e) { + print("error in the https call"); + console.error(e); + }); + req.end(); +} + +function buildAuth() { + var user = process.env.Apigee_User, + secret = process.env.Apigee_Secret; + if (!user || !secret) { + var errMsg = "no authorization provided and no env variable(s) for Apigee_User and/or Apigee_Secret"; + print(errMsg); + print(process.env); + throw new Error(errMsg); + } + return ("Basic " + (new Buffer(user + ":" + secret)).toString("base64")); +} + +function processStopTraceSession() { + + var options = { + host: "api.enterprise.apigee.com", + port: 443, + path: "/v1/organizations/" + config.org + "/environments/" + config.env + "/apis/" + config.api + "/revisions/" + config.rev + "/debugsessions/" + config.debugSessionId, + method: "DELETE", + headers: { + //Accept: "application/json", + Authorization: config.auth, + "Content-length":0, + "Content-Type": "application/x-www-url-form-encoded" + } + }; + var req = https.request(options, function(res) { + res.setEncoding("utf8"); + if (res.statusCode >= 400) { + console.error(res.statusCode + ": " + res.statusMessage + " with " + JSON.stringify(options)); + } + res.on("data", function(d) { + d = JSON.parse(d); + process.exit(130); + }); + + res.on("error", function(e) { + console.log("error="+e); + } + + ); + }); + req.on("error", function(e) { + console.error(e); + console.error("error in creating a debug session with " + JSON.stringify(options)); + }); + req.end(); +} + + +var capture = function(aConfig) { + config = aConfig; + + process.on('SIGINT', function() { + print("Caught interrupt signal"); + print(count + " messages saved..."); + print("________cleaningup-wait____________"); + processStopTraceSession(); + }); + + + try { + console.log("loading live trace data"); + config.debugSessionId = config.debugSessionId || uuid(); + config.auth = config.auth || buildAuth(); + processDebugSession(); + } catch (e) { + var stack = getStackTrace(e); + print("error:"); + print(e); + print(stack); + } +}; + +module.exports = { + capture +}; diff --git a/main.js b/main.js new file mode 100644 index 0000000..353f020 --- /dev/null +++ b/main.js @@ -0,0 +1,35 @@ +#!/usr/bin/env node +var trace = require("./lib/apigee-cli-trace"), + program = require('commander'); + +program + .usage('') + .version('0.1.0') + .option('-o --org ','organization name') + .option('-e --env ','environment name') + .option('-a --api ','Apiproxy name') + .option('-r --revision ','Proxy revision number') + + program.on('--help', function(){ + console.log("example"); + console.log(''); + console.log('apigee-coverage -o askanapigeek -e test -a No-Target -r 4'); + console.log(''); + }); + +program.parse(process.argv); + +var config = {}; +config.org = program.org; +config.env = program.env; +config.api = program.api; +config.rev = program.revision; +config.saveTo = "./capturedTraceFiles"; + + +if (!process.argv.slice(2).length) { + program.outputHelp(); +} + else { + trace.capture(config); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..041af84 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "apigee-coverage", + "version": "0.1.2", + "description": "Node module to facilitate capturing and saving trace files for offline analysis via CLI.", + "main": "main.js", + "bin": { + "apigee-coverage": "./main.js" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/apigeecs/apigee-cli-trace" + }, + "keywords": [ + "trace", + "Apigee", + "Edge" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/apigeecs/apigee-cli-trace/issues" + }, + "private": false, + "dependencies": { + "commander": "^2.10.0", + "https": "^1.0.0", + "path": "^0.12.7" + } +}