UNPKG

swagger-stats

Version:

API Telemetry and APM. Trace API calls and Monitor API performance, health and usage statistics in Node.js Microservices, based on express routes and Swagger (Open API) specification

464 lines (359 loc) 14.9 kB
/** * Created by sv2 on 2/18/17. * swagger-stats Processor. Processes requests / responses and maintains metrics */ 'use strict'; const os = require('os'); const util = require('util'); const debug = require('debug')('sws:processor'); const debugrrr = require('debug')('sws:rrr'); const swsSettings = require('./swssettings'); const swsUtil = require('./swsUtil'); const pathToRegexp = require('path-to-regexp'); const moment = require('moment'); const swsReqResStats = require('./swsReqResStats'); const SwsSysStats = require('./swssysstats'); const SwsCoreStats = require('./swsCoreStats'); const swsErrors = require('./swsErrors'); const swsTimeline = require('./swsTimeline'); const swsAPIStats = require('./swsAPIStats'); const swsLastErrors = require('./swsLastErrors'); const swsLongestRequests = require('./swsLongestReq'); const swsElasticsearchEmitter = require('./swsElasticEmitter'); // swagger-stats Processor. Processes requests / responses and maintains metrics class SwsProcessor { constructor() { // Timestamp when collecting statistics started this.startts = Date.now(); // Name: Should be name of the service provided by this component this.name = 'sws'; // Options //this.options = null; // Version of this component this.version = ''; // This node hostname this.nodehostname = ''; // Node name: there could be multiple nodes in this service this.nodename = ''; // Node address: there could be multiple nodes in this service this.nodeaddress = ''; // onResponseFinish callback, if specified in options this.onResponseFinish = null; // If set to true via options, track only API defined in swagger spec this.swaggerOnly = false; // System statistics this.sysStats = new SwsSysStats(); // Core statistics this.coreStats = new SwsCoreStats(); // Core Egress statistics this.coreEgressStats = new SwsCoreStats(); // Timeline this.timeline = new swsTimeline(); // API Stats this.apiStats = new swsAPIStats(); // Errors this.errorsStats = new swsErrors(); // Last Errors this.lastErrors = new swsLastErrors(); // Longest Requests this.longestRequests = new swsLongestRequests(); // ElasticSearch Emitter this.elasticsearchEmitter = new swsElasticsearchEmitter(); } init() { this.processOptions(); this.sysStats.initialize(); this.coreStats.initialize(); this.coreEgressStats.initialize('egress_'); this.timeline.initialize(swsSettings); this.apiStats.initialize(swsSettings); this.elasticsearchEmitter.initialize(swsSettings); // Start tick this.timer = setInterval(this.tick, 200, this); } // Stop stop() { clearInterval(this.timer); } processOptions() { this.name = swsSettings.name; this.hostname = swsSettings.hostname; this.version = swsSettings.version; this.ip = swsSettings.ip; this.onResponseFinish = swsSettings.onResponseFinish; this.swaggerOnly = swsSettings.swaggerOnly; }; // Tick - called with specified interval to refresh timelines tick(that) { let ts = Date.now(); let totalElapsedSec = (ts - that.startts)/1000; that.sysStats.tick(ts,totalElapsedSec); that.coreStats.tick(ts,totalElapsedSec); that.timeline.tick(ts,totalElapsedSec); that.apiStats.tick(ts,totalElapsedSec); that.elasticsearchEmitter.tick(ts,totalElapsedSec); }; // Collect all data for request/response pair // TODO Support option to add arbitrary extra properties to sws request/response record collectRequestResponseData(res) { var req = res._swsReq; var codeclass = swsUtil.getStatusCodeClass(res.statusCode); var rrr = { 'path': req.sws.originalUrl, 'method': req.method, 'query' : req.method + ' ' + req.sws.originalUrl, 'startts': 0, 'endts': 0, 'responsetime': 0, "node": { "name": this.name, "version": this.version, "hostname": this.hostname, "ip": this.ip }, "http": { "request": { "url" : req.url }, "response": { 'code': res.statusCode, 'class': codeclass, 'phrase': res.statusMessage } } }; // Request Headers if ("headers" in req) { rrr.http.request.headers = {}; for(var hdr in req.headers){ rrr.http.request.headers[hdr] = req.headers[hdr]; } // TODO Split Cookies } // Response Headers var responseHeaders = res.getHeaders(); if (responseHeaders){ rrr.http.response.headers = responseHeaders; } // Additional details from collected info per request / response pair if ("sws" in req) { rrr.ip = req.sws.ip; rrr.real_ip = req.sws.real_ip; rrr.port = req.sws.port; rrr["@timestamp"] = moment(req.sws.startts).toISOString(); //rrr.end = moment(req.sws.endts).toISOString(); rrr.startts = req.sws.startts; rrr.endts = req.sws.endts; rrr.responsetime = req.sws.duration; rrr.http.request.clength = req.sws.req_clength; rrr.http.response.clength = req.sws.res_clength; rrr.http.request.route_path = req.sws.route_path; // Add detailed swagger API info rrr.api = {}; rrr.api.path = req.sws.api_path; rrr.api.query = req.method + ' ' + req.sws.api_path; if( 'swagger' in req.sws ) rrr.api.swagger = req.sws.swagger; if( 'deprecated' in req.sws ) rrr.api.deprecated = req.sws.deprecated; if( 'operationId' in req.sws ) rrr.api.operationId = req.sws.operationId; if( 'tags' in req.sws ) rrr.api.tags = req.sws.tags; // Get API parameter values per definition in swagger spec var apiParams = this.apiStats.getApiOpParameterValues(req.sws.api_path,req.method,req,res); if(apiParams!==null){ rrr.api.params = apiParams; } // TODO Support Arbitrary extra properties added to request under sws // So app can add any custom data to request, and it will be emitted in record } // Express/Koa parameters: req.params (router) and req.body (body parser) if (req.hasOwnProperty("params")) { rrr.http.request.params = {}; swsUtil.swsStringRecursive(rrr.http.request.params, req.params); } if (req.sws && req.sws.hasOwnProperty("query")) { rrr.http.request.query = {}; swsUtil.swsStringRecursive(rrr.http.request.query, req.sws.query); } if (req.hasOwnProperty("body")) { rrr.http.request.body = Object.assign({}, req.body); //swsUtil.swsStringRecursive(rrr.http.request.body, req.body); } return rrr; }; getRemoteIP(req ) { let ip = ''; try { ip = req.connection.remoteAddress; }catch(e){} return ip; }; getPort(req ) { let p = 0; try{ p = req.connection.localPort; }catch(e){} return p; }; getRemoteRealIP(req ) { var remoteaddress = null; var xfwd = req.headers['x-forwarded-for']; if (xfwd) { var fwdaddrs = xfwd.split(','); // Could be "client IP, proxy 1 IP, proxy 2 IP" remoteaddress = fwdaddrs[0]; } if (!remoteaddress) { remoteaddress = this.getRemoteIP(req); } return remoteaddress; }; getResponseContentLength(req, res){ if ("contentLength" in res && res['_contentLength'] !== null ){ return res['_contentLength']; } // Try to get header let hcl = res.getHeader('content-length'); if( (hcl !== undefined) && hcl && !isNaN(hcl)) { return parseInt(hcl); } // If this does not work, calculate using bytesWritten // taking into account res._header let initial = req.sws.initialBytesWritten || 0; let written = req.socket.bytesWritten - initial; if('_header' in res){ const hbuf = Buffer.from(res['_header']); let hslen = hbuf.length; written -= hslen; } return written; } processRequest(req, res) { // Placeholder for sws-specific attributes req.sws = req.sws || {}; // Setup sws props and pass to stats processors var ts = Date.now(); var reqContentLength = 0; if('content-length' in req.headers) { reqContentLength = parseInt(req.headers['content-length']); } req.sws.originalUrl = req.originalUrl || req.url; req.sws.track = true; req.sws.startts = ts; req.sws.timelineid = Math.floor( ts/ this.timeline.settings.bucket_duration ); req.sws.req_clength = reqContentLength; req.sws.ip = this.getRemoteIP(req); req.sws.real_ip = this.getRemoteRealIP(req); req.sws.port = this.getPort(req); req.sws.initialBytesWritten = req.socket.bytesWritten; // Try to match to API right away this.apiStats.matchRequest(req); // if no match, and tracking of non-swagger requests is disabled, return if( !req.sws.match && this.swaggerOnly){ req.sws.track = false; return; } // Core stats this.coreStats.countRequest(req, res); // Timeline this.timeline.countRequest(req, res); // TODO Check if needed this.apiStats.countRequest(req, res); }; processResponse(res) { let req = res._swsReq; req.sws = req.sws || {}; let startts = req.sws.startts || 0; req.sws.endts = Date.now(); req.sws.duration = req.sws.endts - startts; //let timelineid = req.sws.timelineid || 0; if("inflightTimer" in req.sws) { clearTimeout(req.sws.inflightTimer); } req.sws.res_clength = this.getResponseContentLength(req,res); var route_path = ''; if( 'route_path' in req.sws ){ // Route path could be pre-set in sws by previous handlers/hooks ( Fastify ) route_path = req.sws.route_path; } if (("route" in req) && ("path" in req.route)) { // Capture route path for the request, if set by router (Express) if (("baseUrl" in req) && (req.baseUrl != undefined)) route_path = req.baseUrl; route_path += req.route.path; req.sws.route_path = route_path; } // If request was not matched to Swagger API, set API path: // Use route_path, if exist; if not, use sws.originalUrl if(!('api_path' in req.sws)){ req.sws.api_path = (route_path!=''?route_path:req.sws.originalUrl); } // Pass through Core Statistics this.coreStats.countResponse(res); // Pass through Timeline this.timeline.countResponse(res); // Pass through API Statistics this.apiStats.countResponse(res); // Pass through Errors this.errorsStats.countResponse(res); // Collect request / response record var rrr = this.collectRequestResponseData(res); // Pass through last errors this.lastErrors.processReqResData(rrr); // Pass through longest request this.longestRequests.processReqResData(rrr); // Pass to app if callback is specified if(this.onResponseFinish !== null ){ this.onResponseFinish(req,res,rrr); } // Push Request/Response Data to Emitter(s) this.elasticsearchEmitter.processRecord(rrr); //debugrrr('%s', JSON.stringify(rrr)); }; // Get stats according to fields and params specified in query getStats( query ) { query = typeof query !== 'undefined' ? query: {}; query = query !== null ? query: {}; var statfields = []; // Default // Check if we have query parameter "fields" if ('fields' in query) { if (query.fields instanceof Array) { statfields = query.fields; } else { var fieldsstr = query.fields; statfields = fieldsstr.split(','); } } // sys, ingress and egress core statistics are returned always let result = { startts: this.startts }; result.all = this.coreStats.getStats(); result.egress = this.coreEgressStats.getStats(); result.sys = this.sysStats.getStats(); // add standard properties, returned always result.name = this.name; result.version = this.version; result.hostname = this.hostname; result.ip = this.ip; result.apdexThreshold = swsSettings.apdexThreshold; var fieldMask = 0; for(var i=0;i<statfields.length;i++){ var fld = statfields[i]; if( fld in swsUtil.swsStatFields ) fieldMask |= swsUtil.swsStatFields[fld]; } //console.log('Field mask:' + fieldMask.toString(2) ); // Populate per mask if( fieldMask & swsUtil.swsStatFields.method ) result.method = this.coreStats.getMethodStats(); if( fieldMask & swsUtil.swsStatFields.timeline ) result.timeline = this.timeline.getStats(); if( fieldMask & swsUtil.swsStatFields.lasterrors ) result.lasterrors = this.lastErrors.getStats(); if( fieldMask & swsUtil.swsStatFields.longestreq ) result.longestreq = this.longestRequests.getStats(); if( fieldMask & swsUtil.swsStatFields.apidefs ) result.apidefs = this.apiStats.getAPIDefs(); if( fieldMask & swsUtil.swsStatFields.apistats ) result.apistats = this.apiStats.getAPIStats(); if( fieldMask & swsUtil.swsStatFields.errors ) result.errors = this.errorsStats.getStats(); if( fieldMask & swsUtil.swsStatFields.apiop ) { if(("path" in query) && ("method" in query)) { result.apiop = this.apiStats.getAPIOperationStats(query.path, query.method); } } return result; }; } let swsProcessor = new SwsProcessor(); module.exports = swsProcessor;