swagger-stats
Advanced tools
Comparing version 0.95.9 to 0.95.10
@@ -67,3 +67,3 @@ 'use strict'; | ||
name: 'swagger-stats-authtest', | ||
version: '0.95.9', | ||
version: '0.95.10', | ||
hostname: "hostname", | ||
@@ -70,0 +70,0 @@ ip: "127.0.0.1", |
'use strict'; | ||
const http = require('http'); | ||
const Hapi = require('@hapi/hapi'); | ||
@@ -8,2 +9,3 @@ const swStats = require('../../lib'); // require('swagger-stats'); | ||
const swaggerSpec = require('./petstore.json'); | ||
const githubAPISpec = require('./github.json'); | ||
@@ -59,5 +61,14 @@ let server = null; | ||
server.route({ | ||
method: 'GET', | ||
path: '/request', | ||
handler: (request, h) => { | ||
testEgressRequest(); | ||
return 'OK'; | ||
} | ||
}); | ||
let swsOptions = { | ||
name: 'swagger-stats-hapitest', | ||
version: '0.95.9', | ||
version: '0.95.10', | ||
hostname: "hostname", | ||
@@ -68,2 +79,4 @@ ip: "127.0.0.1", | ||
swaggerSpec:swaggerSpec, | ||
durationBuckets: [10,100,1000], | ||
metricsPrefix: 'hapitest_', | ||
elasticsearch: 'http://127.0.0.1:9200', | ||
@@ -202,3 +215,30 @@ elasticsearchIndexPrefix: 'swaggerstats-' | ||
function testEgressRequest(){ | ||
const options = { | ||
hostname: 'www.google.com', | ||
port: 80, | ||
path: '/', | ||
method: 'GET', | ||
}; | ||
const req = http.request(options, (res) => { | ||
console.log(`STATUS: ${res.statusCode}`); | ||
console.log(`HEADERS: ${JSON.stringify(res.headers)}`); | ||
res.setEncoding('utf8'); | ||
res.on('data', (chunk) => { | ||
console.log(`BODY: ${chunk}`); | ||
}); | ||
res.on('end', () => { | ||
console.log('No more data in response.'); | ||
}); | ||
}); | ||
req.on('error', (e) => { | ||
console.error(`problem with request: ${e.message}`); | ||
}); | ||
req.end(); | ||
} | ||
// TODO https://api.github.com/orgs/slanatech/repos | ||
init(); |
@@ -79,3 +79,3 @@ 'use strict'; | ||
name: 'swagger-stats-spectest', | ||
version: '0.95.9', | ||
version: '0.95.10', | ||
hostname: "hostname", | ||
@@ -82,0 +82,0 @@ ip: "127.0.0.1", |
@@ -112,3 +112,3 @@ 'use strict'; | ||
name: 'swagger-stats-testapp', | ||
version: '0.95.9', | ||
version: '0.95.10', | ||
timelineBucketDuration: tlBucket, | ||
@@ -115,0 +115,0 @@ uriPath: '/swagger-stats', |
@@ -8,13 +8,13 @@ /** | ||
var util = require('util'); | ||
var pathToRegexp = require('path-to-regexp'); | ||
var debug = require('debug')('sws:apistats'); | ||
var promClient = require("prom-client"); | ||
const util = require('util'); | ||
const pathToRegexp = require('path-to-regexp'); | ||
const debug = require('debug')('sws:apistats'); | ||
const promClient = require("prom-client"); | ||
const swsSettings = require('./swssettings'); | ||
const swsMetrics = require('./swsmetrics'); | ||
const swsUtil = require('./swsUtil'); | ||
const swsReqResStats = require('./swsReqResStats'); | ||
const swsBucketStats = require('./swsBucketStats'); | ||
var swsUtil = require('./swsUtil'); | ||
var swsReqResStats = require('./swsReqResStats'); | ||
var swsBucketStats = require('./swsBucketStats'); | ||
// API Statistics | ||
@@ -55,25 +55,2 @@ // Stores Definition of API based on Swagger Spec | ||
this.promClientMetrics = {}; | ||
this.promClientMetrics.api_request_total = new promClient.Counter({ | ||
name: swsUtil.swsMetrics.api_request_total.name, | ||
help: swsUtil.swsMetrics.api_request_total.help, | ||
labelNames: ['method','path','code'] }); | ||
this.promClientMetrics.api_request_duration_milliseconds = new promClient.Histogram({ | ||
name: swsUtil.swsMetrics.api_request_duration_milliseconds.name, | ||
help: swsUtil.swsMetrics.api_request_duration_milliseconds.help, | ||
labelNames: ['method','path','code'], | ||
buckets: this.durationBuckets }); | ||
this.promClientMetrics.api_request_size_bytes = new promClient.Histogram({ | ||
name: swsUtil.swsMetrics.api_request_size_bytes.name, | ||
help: swsUtil.swsMetrics.api_request_size_bytes.help, | ||
labelNames: ['method','path','code'], | ||
buckets: this.requestSizeBuckets }); | ||
this.promClientMetrics.api_response_size_bytes = new promClient.Histogram({ | ||
name: swsUtil.swsMetrics.api_response_size_bytes.name, | ||
help: swsUtil.swsMetrics.api_response_size_bytes.help, | ||
labelNames: ['method','path','code'], | ||
buckets: this.responseSizeBuckets }); | ||
} | ||
@@ -142,11 +119,18 @@ | ||
if(typeof swsOptions === 'undefined') return; // TODO ??? remove | ||
// TODO remove | ||
if(!swsOptions) return; | ||
this.options = swsOptions; | ||
if('durationBuckets' in swsOptions) this.durationBuckets = swsOptions.durationBuckets; | ||
if('requestSizeBuckets' in swsOptions) this.requestSizeBuckets = swsOptions.requestSizeBuckets; | ||
if('responseSizeBuckets' in swsOptions) this.responseSizeBuckets = swsOptions.responseSizeBuckets; | ||
this.durationBuckets = swsSettings.durationBuckets; | ||
this.requestSizeBuckets = swsSettings.requestSizeBuckets; | ||
this.responseSizeBuckets = swsSettings.responseSizeBuckets; | ||
// Update buckets to reflect passed options | ||
swsMetrics.apiMetricsDefs.api_request_duration_milliseconds.buckets = this.durationBuckets; | ||
swsMetrics.apiMetricsDefs.api_request_size_bytes.buckets = this.requestSizeBuckets; | ||
swsMetrics.apiMetricsDefs.api_response_size_bytes.buckets = this.responseSizeBuckets; | ||
swsMetrics.clearPrometheusMetrics(this.promClientMetrics); | ||
this.promClientMetrics = swsMetrics.getPrometheusMetrics(swsSettings.metricsPrefix,swsMetrics.apiMetricsDefs); | ||
if(!('swaggerSpec' in swsOptions)) return; | ||
@@ -153,0 +137,0 @@ if(swsOptions.swaggerSpec === null) return; |
@@ -8,264 +8,162 @@ /** | ||
var util = require('util'); | ||
var debug = require('debug')('sws:corestats'); | ||
var promClient = require("prom-client"); | ||
const util = require('util'); | ||
const debug = require('debug')('sws:corestats'); | ||
const promClient = require("prom-client"); | ||
const swsSettings = require('./swssettings'); | ||
const swsMetrics = require('./swsmetrics'); | ||
const swsUtil = require('./swsUtil'); | ||
const SwsReqResStats = require('./swsReqResStats'); | ||
var swsUtil = require('./swsUtil'); | ||
var swsReqResStats = require('./swsReqResStats'); | ||
// Constructor | ||
function swsCoreStats() { | ||
/* swagger=stats Prometheus metrics */ | ||
class SwsCoreStats { | ||
// Options | ||
this.options = null; | ||
constructor(){ | ||
// Timestamp when collecting statistics started | ||
this.startts = Date.now(); | ||
// Statistics for all requests | ||
this.all = null; | ||
// Statistics for all requests | ||
this.all = null; | ||
// Statistics for requests by method | ||
// Initialized with most frequent ones, other methods will be added on demand if actually used | ||
this.method = null; | ||
// Statistics for requests by method | ||
// Initialized with most frequent ones, other methods will be added on demand if actually used | ||
this.method = null; | ||
// Additional prefix for prometheus metrics. Used if this coreStats instance | ||
// plays special role, i.e. count stats for egress | ||
this.metricsRolePrefix = ''; | ||
// System statistics | ||
this.sys = null; | ||
// Prometheus metrics | ||
this.promClientMetrics = {}; | ||
} | ||
// CPU | ||
this.startTime = null; | ||
this.startUsage = null; | ||
// Initialize | ||
initialize(metricsRolePrefix) { | ||
// Array with last 5 hrtime / cpuusage, to calculate CPU usage during the last second sliding window ( 5 ticks ) | ||
this.startTimeAndUsage = null; | ||
this.metricsRolePrefix = metricsRolePrefix || ''; | ||
// Prometheus metrics | ||
this.promClientMetrics = {}; | ||
// Statistics for all requests | ||
this.all = new SwsReqResStats(swsSettings.apdexThreshold); | ||
this.promClientMetrics.api_all_request_total = new promClient.Counter({ | ||
name: swsUtil.swsMetrics.api_all_request_total.name, | ||
help: swsUtil.swsMetrics.api_all_request_total.help }); | ||
// Statistics for requests by method | ||
// Initialized with most frequent ones, other methods will be added on demand if actually used | ||
this.method = { | ||
'GET': new SwsReqResStats(swsSettings.apdexThreshold), | ||
'POST': new SwsReqResStats(swsSettings.apdexThreshold), | ||
'PUT': new SwsReqResStats(swsSettings.apdexThreshold), | ||
'DELETE': new SwsReqResStats(swsSettings.apdexThreshold) | ||
}; | ||
this.promClientMetrics.api_all_success_total = new promClient.Counter({ | ||
name: swsUtil.swsMetrics.api_all_success_total.name, | ||
help: swsUtil.swsMetrics.api_all_success_total.help }); | ||
this.promClientMetrics.api_all_errors_total = new promClient.Counter({ | ||
name: swsUtil.swsMetrics.api_all_errors_total.name, | ||
help: swsUtil.swsMetrics.api_all_errors_total.help }); | ||
this.promClientMetrics.api_all_client_error_total = new promClient.Counter({ | ||
name: swsUtil.swsMetrics.api_all_client_error_total.name, | ||
help: swsUtil.swsMetrics.api_all_client_error_total.help }); | ||
this.promClientMetrics.api_all_server_error_total = new promClient.Counter({ | ||
name: swsUtil.swsMetrics.api_all_server_error_total.name, | ||
help: swsUtil.swsMetrics.api_all_server_error_total.help }); | ||
this.promClientMetrics.api_all_request_in_processing_total = new promClient.Gauge({ | ||
name: swsUtil.swsMetrics.api_all_request_in_processing_total.name, | ||
help: swsUtil.swsMetrics.api_all_request_in_processing_total.help }); | ||
// metrics | ||
swsMetrics.clearPrometheusMetrics(this.promClientMetrics); | ||
this.promClientMetrics.nodejs_process_memory_rss_bytes = new promClient.Gauge({ | ||
name: swsUtil.swsMetrics.nodejs_process_memory_rss_bytes.name, | ||
help: swsUtil.swsMetrics.nodejs_process_memory_rss_bytes.help }); | ||
this.promClientMetrics.nodejs_process_memory_heap_total_bytes = new promClient.Gauge({ | ||
name: swsUtil.swsMetrics.nodejs_process_memory_heap_total_bytes.name, | ||
help: swsUtil.swsMetrics.nodejs_process_memory_heap_total_bytes.help }); | ||
this.promClientMetrics.nodejs_process_memory_heap_used_bytes = new promClient.Gauge({ | ||
name: swsUtil.swsMetrics.nodejs_process_memory_heap_used_bytes.name, | ||
help: swsUtil.swsMetrics.nodejs_process_memory_heap_used_bytes.help }); | ||
this.promClientMetrics.nodejs_process_memory_external_bytes = new promClient.Gauge({ | ||
name: swsUtil.swsMetrics.nodejs_process_memory_external_bytes.name, | ||
help: swsUtil.swsMetrics.nodejs_process_memory_external_bytes.help }); | ||
this.promClientMetrics.nodejs_process_cpu_usage_percentage = new promClient.Gauge({ | ||
name: swsUtil.swsMetrics.nodejs_process_cpu_usage_percentage.name, | ||
help: swsUtil.swsMetrics.nodejs_process_cpu_usage_percentage.help }); | ||
let prefix = swsSettings.metricsPrefix + this.metricsRolePrefix; | ||
this.promClientMetrics = swsMetrics.getPrometheusMetrics(prefix,swsMetrics.coreMetricsDefs); | ||
}; | ||
} | ||
getStats() { | ||
return this.all; | ||
}; | ||
// Initialize | ||
swsCoreStats.prototype.initialize = function (swsOptions) { | ||
this.options = swsOptions; | ||
// Statistics for all requests | ||
this.all = new swsReqResStats(this.options.apdexThreshold); | ||
// Statistics for requests by method | ||
// Initialized with most frequent ones, other methods will be added on demand if actually used | ||
this.method = { | ||
'GET': new swsReqResStats(this.options.apdexThreshold), | ||
'POST': new swsReqResStats(this.options.apdexThreshold), | ||
'PUT': new swsReqResStats(this.options.apdexThreshold), | ||
'DELETE': new swsReqResStats(this.options.apdexThreshold) | ||
getMethodStats() { | ||
return this.method; | ||
}; | ||
// System statistics | ||
this.sys = { | ||
rss: 0, | ||
heapTotal: 0, | ||
heapUsed: 0, | ||
external: 0, | ||
cpu: 0 | ||
// TODO event loop delays | ||
// Update timeline and stats per tick | ||
tick(ts,totalElapsedSec) { | ||
// Rates | ||
this.all.updateRates(totalElapsedSec); | ||
for( let mname of Object.keys(this.method)) { | ||
this.method[mname].updateRates(totalElapsedSec); | ||
} | ||
}; | ||
// CPU | ||
this.startTime = process.hrtime(); | ||
this.startUsage = process.cpuUsage(); | ||
// Count request | ||
countRequest(req) { | ||
// Array with last 5 hrtime / cpuusage, to calculate CPU usage during the last second sliding window ( 5 ticks ) | ||
this.startTimeAndUsage = [ | ||
{ hrtime: process.hrtime(), cpuUsage: process.cpuUsage() }, | ||
{ hrtime: process.hrtime(), cpuUsage: process.cpuUsage() }, | ||
{ hrtime: process.hrtime(), cpuUsage: process.cpuUsage() }, | ||
{ hrtime: process.hrtime(), cpuUsage: process.cpuUsage() }, | ||
{ hrtime: process.hrtime(), cpuUsage: process.cpuUsage() } | ||
]; | ||
// Count in all | ||
this.all.countRequest(req.sws.req_clength); | ||
}; | ||
// Count by method | ||
var method = req.method; | ||
if (!(method in this.method)) { | ||
this.method[method] = new SwsReqResStats(); | ||
} | ||
this.method[method].countRequest(req.sws.req_clength); | ||
swsCoreStats.prototype.getStats = function () { | ||
return { startts: this.startts, all: this.all, sys: this.sys }; | ||
}; | ||
// Update prom-client metrics | ||
this.promClientMetrics.api_all_request_total.inc(); | ||
this.promClientMetrics.api_all_request_in_processing_total.inc(); | ||
req.sws.inflightTimer = setTimeout(function() { | ||
this.promClientMetrics.api_all_request_in_processing_total.dec(); | ||
}.bind(this), 250000); | ||
}; | ||
swsCoreStats.prototype.getMethodStats = function () { | ||
return this.method; | ||
}; | ||
countResponse (res) { | ||
var req = res._swsReq; | ||
// Update timeline and stats per tick | ||
swsCoreStats.prototype.tick = function (ts,totalElapsedSec) { | ||
// Defaults | ||
var startts = 0; | ||
var duration = 0; | ||
var resContentLength = 0; | ||
var timelineid = 0; | ||
var path = req.sws.originalUrl; | ||
// Rates | ||
this.all.updateRates(totalElapsedSec); | ||
for( var method in this.method) { | ||
this.method[method].updateRates(totalElapsedSec); | ||
} | ||
// TODO move all this to Processor, so it'll be in single place | ||
// System stats | ||
this.calculateSystemStats(ts,totalElapsedSec); | ||
}; | ||
if ("_contentLength" in res && res['_contentLength'] !== null ){ | ||
resContentLength = res['_contentLength']; | ||
}else{ | ||
// Try header | ||
let hcl = res.getHeader('content-length'); | ||
if( (hcl !== undefined) && hcl && !isNaN(hcl)) { | ||
resContentLength = parseInt(res.getHeader('content-length')); | ||
} | ||
} | ||
// Calculate and store system statistics | ||
swsCoreStats.prototype.calculateSystemStats = function(ts,totalElapsedSec) { | ||
if("sws" in req) { | ||
startts = req.sws.startts; | ||
timelineid = req.sws.timelineid; | ||
var endts = Date.now(); | ||
req['sws'].endts = endts; | ||
duration = endts - startts; | ||
req['sws'].duration = duration; | ||
req['sws'].res_clength = resContentLength; | ||
path = req['sws'].api_path; | ||
clearTimeout(req.sws.inflightTimer); | ||
} | ||
// Memory | ||
var memUsage = process.memoryUsage(); | ||
// Determine status code type | ||
var codeclass = swsUtil.getStatusCodeClass(res.statusCode); | ||
// See https://stackoverflow.com/questions/12023359/what-do-the-return-values-of-node-js-process-memoryusage-stand-for | ||
// #22 Handle properly if any property is missing | ||
this.sys.rss = 'rss' in memUsage ? memUsage.rss : 0; | ||
this.sys.heapTotal = 'heapTotal' in memUsage ? memUsage.heapTotal : 0; | ||
this.sys.heapUsed = 'heapUsed' in memUsage ? memUsage.heapUsed : 0; | ||
this.sys.external = 'external' in memUsage ? memUsage.external : 0; | ||
// update counts for all requests | ||
this.all.countResponse(res.statusCode,codeclass,duration,resContentLength); | ||
var startTU = this.startTimeAndUsage.shift(); | ||
// Update method-specific stats | ||
var method = req.method; | ||
if (method in this.method) { | ||
var mstat = this.method[method]; | ||
mstat.countResponse(res.statusCode,codeclass,duration,resContentLength); | ||
} | ||
var cpuPercent = swsUtil.swsCPUUsagePct(startTU.hrtime, startTU.cpuUsage); | ||
this.startTimeAndUsage.push( { hrtime: process.hrtime(), cpuUsage: process.cpuUsage() } ); | ||
//this.startTime = process.hrtime(); | ||
//this.startUsage = process.cpuUsage(); | ||
this.sys.cpu = cpuPercent; | ||
// Update prom-client metrics | ||
this.promClientMetrics.nodejs_process_memory_rss_bytes.set(this.sys.rss); | ||
this.promClientMetrics.nodejs_process_memory_heap_total_bytes.set(this.sys.heapTotal); | ||
this.promClientMetrics.nodejs_process_memory_heap_used_bytes.set(this.sys.heapUsed); | ||
this.promClientMetrics.nodejs_process_memory_external_bytes.set(this.sys.external); | ||
this.promClientMetrics.nodejs_process_cpu_usage_percentage.set(this.sys.cpu); | ||
}; | ||
// Count request | ||
swsCoreStats.prototype.countRequest = function (req, res) { | ||
// Count in all | ||
this.all.countRequest(req.sws.req_clength); | ||
// Count by method | ||
var method = req.method; | ||
if (!(method in this.method)) { | ||
this.method[method] = new swsReqResStats(); | ||
} | ||
this.method[method].countRequest(req.sws.req_clength); | ||
// Update prom-client metrics | ||
this.promClientMetrics.api_all_request_total.inc(); | ||
this.promClientMetrics.api_all_request_in_processing_total.inc(); | ||
req.sws.inflightTimer = setTimeout(function() { | ||
// Update Prometheus metrics | ||
switch(codeclass){ | ||
case "success": | ||
this.promClientMetrics.api_all_success_total.inc(); | ||
break; | ||
case "redirect": | ||
// NOOP // | ||
break; | ||
case "client_error": | ||
this.promClientMetrics.api_all_errors_total.inc(); | ||
this.promClientMetrics.api_all_client_error_total.inc(); | ||
break; | ||
case "server_error": | ||
this.promClientMetrics.api_all_errors_total.inc(); | ||
this.promClientMetrics.api_all_server_error_total.inc(); | ||
break; | ||
} | ||
this.promClientMetrics.api_all_request_in_processing_total.dec(); | ||
}.bind(this), 250000); | ||
}; | ||
}; | ||
} | ||
// Count finished response | ||
swsCoreStats.prototype.countResponse = function (res) { | ||
var req = res._swsReq; | ||
// Defaults | ||
var startts = 0; | ||
var duration = 0; | ||
var resContentLength = 0; | ||
var timelineid = 0; | ||
var path = req.sws.originalUrl; | ||
// TODO move all this to Processor, so it'll be in single place | ||
if ("_contentLength" in res && res['_contentLength'] !== null ){ | ||
resContentLength = res['_contentLength']; | ||
}else{ | ||
// Try header | ||
if(res.getHeader('content-length') !==null ) { | ||
resContentLength = parseInt(res.getHeader('content-length')); | ||
} | ||
} | ||
if("sws" in req) { | ||
startts = req.sws.startts; | ||
timelineid = req.sws.timelineid; | ||
var endts = Date.now(); | ||
req['sws'].endts = endts; | ||
duration = endts - startts; | ||
req['sws'].duration = duration; | ||
req['sws'].res_clength = resContentLength; | ||
path = req['sws'].api_path; | ||
clearTimeout(req.sws.inflightTimer); | ||
} | ||
// Determine status code type | ||
var codeclass = swsUtil.getStatusCodeClass(res.statusCode); | ||
// update counts for all requests | ||
this.all.countResponse(res.statusCode,codeclass,duration,resContentLength); | ||
// Update method-specific stats | ||
var method = req.method; | ||
if (method in this.method) { | ||
var mstat = this.method[method]; | ||
mstat.countResponse(res.statusCode,codeclass,duration,resContentLength); | ||
} | ||
// Update Prometheus metrics | ||
switch(codeclass){ | ||
case "success": | ||
this.promClientMetrics.api_all_success_total.inc(); | ||
break; | ||
case "redirect": | ||
// NOOP // | ||
break; | ||
case "client_error": | ||
this.promClientMetrics.api_all_errors_total.inc(); | ||
this.promClientMetrics.api_all_client_error_total.inc(); | ||
break; | ||
case "server_error": | ||
this.promClientMetrics.api_all_errors_total.inc(); | ||
this.promClientMetrics.api_all_server_error_total.inc(); | ||
break; | ||
} | ||
this.promClientMetrics.api_all_request_in_processing_total.dec(); | ||
}; | ||
module.exports = swsCoreStats; | ||
module.exports = SwsCoreStats; |
/* swagger-stats Hapi plugin */ | ||
const path = require('path'); | ||
const promClient = require("prom-client"); | ||
const SwsProcessor = require('./swsProcessor'); | ||
const swsSettings = require('./swssettings'); | ||
const swsProcessor = require('./swsProcessor'); | ||
const swsUtil = require('./swsUtil'); | ||
const debug = require('debug')('sws:hapi'); | ||
//const Inert = require('@hapi/inert'); | ||
const url = require('url'); | ||
@@ -16,12 +16,4 @@ const qs = require('qs'); | ||
constructor() { | ||
this.name = 'swagger-stats'; | ||
this.version = '0.97.5'; | ||
this.effectiveOptions = {}; | ||
this.processor = null; | ||
this.pathBase = '/'; | ||
this.pathUI = '/ui'; | ||
this.pathDist = '/dist'; | ||
this.pathStats = '/stats'; | ||
this.pathMetrics = '/metrics'; | ||
this.pathLogout = '/logout'; | ||
this.processor = swsProcessor; | ||
} | ||
@@ -31,8 +23,3 @@ | ||
async register(server, options) { | ||
this.processOptions(options); | ||
this.processor = new SwsProcessor(); | ||
this.processor.init(this.effectiveOptions); | ||
let processor = this.processor; | ||
let swsURIPath = this.effectiveOptions.uriPath; | ||
let swsPathUI = this.pathUI; | ||
server.events.on('response', function(request){ | ||
@@ -48,3 +35,3 @@ let nodeReq = request.raw.req; | ||
}catch(e){ | ||
debug("processRequest:ERROR: " + e); | ||
debug("processResponse:ERROR: " + e); | ||
} | ||
@@ -59,3 +46,3 @@ }); | ||
let reqUrl = nodeReq.url; | ||
if(reqUrl.startsWith(swsURIPath)){ | ||
if(reqUrl.startsWith(swsSettings.uriPath)){ | ||
// Don't track sws requests | ||
@@ -75,3 +62,3 @@ nodeReq.sws.track = false; | ||
method: 'GET', | ||
path: this.pathStats, | ||
path: swsSettings.pathStats, | ||
handler: function (request, h) { | ||
@@ -84,3 +71,3 @@ return processor.getStats(request.raw.req.sws.query); | ||
method: 'GET', | ||
path: this.pathMetrics, | ||
path: swsSettings.pathMetrics, | ||
handler: function (request, h) { | ||
@@ -96,5 +83,5 @@ const response = h.response(promClient.register.metrics()); | ||
method: 'GET', | ||
path: this.pathBase, | ||
path: swsSettings.uriPath, | ||
handler: function (request, h) { | ||
return h.redirect(swsPathUI); | ||
return h.redirect(swsSettings.pathUI); | ||
} | ||
@@ -105,3 +92,3 @@ }); | ||
method: 'GET', | ||
path: this.pathUI, | ||
path: swsSettings.pathUI, | ||
handler: function (request, h) { | ||
@@ -114,3 +101,3 @@ return swsUtil.swsEmbeddedUIMarkup; | ||
method: 'GET', | ||
path: this.pathDist+'/{file*}', | ||
path: swsSettings.pathDist+'/{file*}', | ||
handler: function (request, h) { | ||
@@ -129,51 +116,5 @@ let fileName = request.params.file; | ||
} | ||
setPaths(){ | ||
this.pathBase = this.effectiveOptions.uriPath; | ||
this.pathUI = this.effectiveOptions.uriPath+'/ui'; | ||
this.pathDist = this.effectiveOptions.uriPath+'/dist'; | ||
this.pathStats = this.effectiveOptions.uriPath+'/stats'; | ||
this.pathMetrics = this.effectiveOptions.uriPath+'/metrics'; | ||
this.pathLogout = this.effectiveOptions.uriPath+'/logout'; | ||
} | ||
setDefaultOptions(options){ | ||
this.effectiveOptions = options; | ||
this.setPaths(); | ||
} | ||
// Override defaults if options are provided | ||
processOptions(options){ | ||
if(!options) return; | ||
for(let op in swsUtil.supportedOptions){ | ||
if(op in options){ | ||
this.effectiveOptions[op] = options[op]; | ||
} | ||
} | ||
// update standard path | ||
this.setPaths(); | ||
/* no auth for now | ||
if( swsOptions.authentication ){ | ||
setInterval(expireSessionIDs,500); | ||
} | ||
*/ | ||
} | ||
} | ||
let swsHapi = new SwsHapi(); | ||
let swsHapiPlugin = { | ||
name: 'swagger-stats', | ||
version: '0.97.5', | ||
register: async function (server, options) { | ||
return swsHapi.register(server, options); | ||
} | ||
}; | ||
module.exports = { | ||
swsHapi, | ||
swsHapiPlugin | ||
}; | ||
module.exports = swsHapi; |
@@ -16,38 +16,16 @@ /** | ||
const swsSettings = require('./swssettings'); | ||
const swsUtil = require('./swsUtil'); | ||
const swsProcessor = require('./swsProcessor'); | ||
const swsEgress = require('./swsegress'); | ||
const send = require('send'); | ||
const qs = require('qs'); | ||
const {swsHapi,swsHapiPlugin} = require('./swsHapi'); | ||
const swsHapi = require('./swsHapi'); | ||
// API data processor | ||
var processor = null; | ||
//var processor = null; | ||
// swagger-stats default options | ||
let swsOptions = { | ||
version:'', | ||
swaggerSpec: null, | ||
uriPath: '/swagger-stats', | ||
durationBuckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000], | ||
requestSizeBuckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000], | ||
responseSizeBuckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000], | ||
apdexThreshold: 25, | ||
onResponseFinish: null, | ||
authentication: false, | ||
sessionMaxAge: 900, | ||
onAuthenticate: null | ||
}; | ||
swsHapi.setDefaultOptions(swsOptions); | ||
var uiMarkup = swsUtil.swsEmbeddedUIMarkup; | ||
var pathUI = swsOptions.uriPath+'/ui'; | ||
var pathDist = swsOptions.uriPath+'/dist'; | ||
var pathStats = swsOptions.uriPath+'/stats'; | ||
var pathMetrics = swsOptions.uriPath+'/metrics'; | ||
var pathLogout = swsOptions.uriPath+'/logout'; | ||
// Session IDs storage | ||
@@ -58,3 +36,3 @@ var sessionIDs = {}; | ||
function storeSessionID(sid){ | ||
var tssec = Date.now() + swsOptions.sessionMaxAge*1000; | ||
var tssec = Date.now() + swsSettings.sessionMaxAge*1000; | ||
sessionIDs[sid] = tssec; | ||
@@ -87,3 +65,3 @@ //debug('Session ID updated: %s=%d', sid,tssec); | ||
try { | ||
processor.processRequest(req,res); | ||
swsProcessor.processRequest(req,res); | ||
}catch(e){ | ||
@@ -108,3 +86,3 @@ debug("SWS:processRequest:ERROR: " + e); | ||
try { | ||
processor.processResponse(res); | ||
swsProcessor.processResponse(res); | ||
}catch(e){ | ||
@@ -115,29 +93,6 @@ debug("SWS:processResponse:ERROR: " + e); | ||
// Override defaults if options are provided | ||
function processOptions(options){ | ||
if(!options) return; | ||
for(let op in swsUtil.supportedOptions){ | ||
if(op in options){ | ||
swsOptions[op] = options[op]; | ||
} | ||
} | ||
// update standard path | ||
pathUI = swsOptions.uriPath+'/ui'; | ||
pathDist = swsOptions.uriPath+'/dist'; | ||
pathStats = swsOptions.uriPath+'/stats'; | ||
pathMetrics = swsOptions.uriPath+'/metrics'; | ||
pathLogout = swsOptions.uriPath+'/logout'; | ||
if( swsOptions.authentication ){ | ||
setInterval(expireSessionIDs,500); | ||
} | ||
} | ||
function processAuth(req,res,useWWWAuth) { | ||
return new Promise( function (resolve, reject) { | ||
if( !swsOptions.authentication ){ | ||
if( !swsSettings.authentication ){ | ||
return resolve(true); | ||
@@ -156,3 +111,3 @@ } | ||
storeSessionID(sessionIdCookie); | ||
cookies.set('sws-session-id',sessionIdCookie,{path:swsOptions.uriPath,maxAge:swsOptions.sessionMaxAge*1000}); | ||
cookies.set('sws-session-id',sessionIdCookie,{path:swsSettings.uriPath,maxAge:swsSettings.sessionMaxAge*1000}); | ||
// Ok | ||
@@ -170,5 +125,5 @@ req['sws-auth'] = true; | ||
if( (authInfo !== undefined) && (authInfo!==null) && ('name' in authInfo) && ('pass' in authInfo)){ | ||
if(typeof swsOptions.onAuthenticate === 'function'){ | ||
if(typeof swsSettings.onAuthenticate === 'function'){ | ||
Promise.resolve(swsOptions.onAuthenticate(req, authInfo.name, authInfo.pass)).then(function(onAuthResult) { | ||
Promise.resolve(swsSettings.onAuthenticate(req, authInfo.name, authInfo.pass)).then(function(onAuthResult) { | ||
if( onAuthResult ){ | ||
@@ -179,3 +134,3 @@ | ||
// Session is only for stats requests | ||
if(req.url.startsWith(pathStats)){ | ||
if(req.url.startsWith(swsSettings.pathStats)){ | ||
// Generate session id | ||
@@ -185,3 +140,3 @@ var sessid = uuidv1(); | ||
// Set session cookie with expiration in 15 min | ||
cookies.set('sws-session-id',sessid,{path:swsOptions.uriPath,maxAge:swsOptions.sessionMaxAge*1000}); | ||
cookies.set('sws-session-id',sessid,{path:swsSettings.uriPath,maxAge:swsSettings.sessionMaxAge*1000}); | ||
} | ||
@@ -247,3 +202,3 @@ | ||
res.setHeader('Content-Type', 'application/json'); | ||
res.end(JSON.stringify(processor.getStats(req.sws.query))); | ||
res.end(JSON.stringify(swsProcessor.getStats(req.sws.query))); | ||
}); | ||
@@ -270,4 +225,19 @@ } | ||
// Returns Hapi plugin | ||
getHapiPlugin: swsHapiPlugin, | ||
getHapiPlugin: { | ||
name: 'swagger-stats', | ||
version: '0.97.9', | ||
register: async function (server, options) { | ||
// Init settings | ||
swsSettings.init(options); | ||
// Init probes TODO Reconsider | ||
swsEgress.init(); | ||
swsProcessor.init(); | ||
return swsHapi.register(server, options); | ||
} | ||
}, | ||
// Initialize swagger-stats and return | ||
@@ -277,7 +247,14 @@ // middleware to perform API Data collection | ||
processOptions(options); | ||
// Init settings | ||
swsSettings.init(options); | ||
processor = new swsProcessor(); | ||
processor.init(swsOptions); | ||
// Init probes | ||
swsEgress.init(); | ||
if( swsSettings.authentication ){ | ||
setInterval(expireSessionIDs,500); | ||
} | ||
swsProcessor.init(); | ||
return function trackingMiddleware(req, res, next) { | ||
@@ -291,10 +268,10 @@ | ||
// swagger-stats requests will not be counted in statistics | ||
if(req.url.startsWith(pathStats)) { | ||
if(req.url.startsWith(swsSettings.pathStats)) { | ||
return processGetStats(req, res); | ||
}else if(req.url.startsWith(pathMetrics)){ | ||
}else if(req.url.startsWith(swsSettings.pathMetrics)){ | ||
return processGetMetrics(req,res); | ||
}else if(req.url.startsWith(pathLogout)){ | ||
}else if(req.url.startsWith(swsSettings.pathLogout)){ | ||
processLogout(req,res); | ||
return; | ||
}else if(req.url.startsWith(pathUI) ){ | ||
}else if(req.url.startsWith(swsSettings.pathUI) ){ | ||
res.statusCode = 200; | ||
@@ -304,4 +281,4 @@ res.setHeader('Content-Type', 'text/html'); | ||
return; | ||
}else if(req.url.startsWith(pathDist)) { | ||
var fileName = req.url.replace(pathDist+'/',''); | ||
}else if(req.url.startsWith(swsSettings.pathDist)) { | ||
var fileName = req.url.replace(swsSettings.pathDist+'/',''); | ||
var qidx = fileName.indexOf('?'); | ||
@@ -329,7 +306,3 @@ if(qidx!=-1) fileName = fileName.substring(0,qidx); | ||
getCoreStats: function() { | ||
if(processor) { | ||
return processor.getStats(); | ||
}else if(swsHapi.processor){ | ||
return swsHapi.processor.getStats(); | ||
} | ||
return swsProcessor.getStats(); | ||
}, | ||
@@ -349,8 +322,4 @@ | ||
stop: function () { | ||
if(processor) { | ||
return processor.stop(); | ||
}else if(swsHapi.processor){ | ||
return swsHapi.processor.stop(); | ||
} | ||
return swsProcessor.stop(); | ||
} | ||
}; |
/** | ||
* Created by sv2 on 2/18/17. | ||
* API usage statistics data | ||
* swagger-stats Processor. Processes requests / responses and maintains metrics | ||
*/ | ||
@@ -8,448 +8,415 @@ | ||
var os = require('os'); | ||
var util = require('util'); | ||
const os = require('os'); | ||
const util = require('util'); | ||
var debug = require('debug')('sws:processor'); | ||
var debugrrr = require('debug')('sws:rrr'); | ||
const debug = require('debug')('sws:processor'); | ||
const debugrrr = require('debug')('sws:rrr'); | ||
var swsUtil = require('./swsUtil'); | ||
var pathToRegexp = require('path-to-regexp'); | ||
var moment = require('moment'); | ||
const swsSettings = require('./swssettings'); | ||
const swsUtil = require('./swsUtil'); | ||
const pathToRegexp = require('path-to-regexp'); | ||
const moment = require('moment'); | ||
var swsReqResStats = require('./swsReqResStats'); | ||
var swsCoreStats = require('./swsCoreStats'); | ||
var swsErrors = require('./swsErrors'); | ||
var swsTimeline = require('./swsTimeline'); | ||
var swsAPIStats = require('./swsAPIStats'); | ||
var swsLastErrors = require('./swsLastErrors'); | ||
var swsLongestRequests = require('./swsLongestReq'); | ||
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'); | ||
var swsElasticsearchEmitter = require('./swsElasticEmitter'); | ||
// swagger-stats Processor. Processes requests / responses and maintains metrics | ||
class SwsProcessor { | ||
// Constructor | ||
function swsProcessor() { | ||
constructor() { | ||
// Name: Should be name of the service provided by this component | ||
this.name = 'sws'; | ||
// Timestamp when collecting statistics started | ||
this.startts = Date.now(); | ||
// Options | ||
this.options = null; | ||
// Name: Should be name of the service provided by this component | ||
this.name = 'sws'; | ||
// Version of this component | ||
this.version = ''; | ||
// Options | ||
//this.options = null; | ||
// This node hostname | ||
this.nodehostname = ''; | ||
// Version of this component | ||
this.version = ''; | ||
// Node name: there could be multiple nodes in this service | ||
this.nodename = ''; | ||
// This node hostname | ||
this.nodehostname = ''; | ||
// Node address: there could be multiple nodes in this service | ||
this.nodeaddress = ''; | ||
// Node name: there could be multiple nodes in this service | ||
this.nodename = ''; | ||
// onResponseFinish callback, if specified in options | ||
this.onResponseFinish = null; | ||
// Node address: there could be multiple nodes in this service | ||
this.nodeaddress = ''; | ||
// If set to true via options, track only API defined in swagger spec | ||
this.swaggerOnly = false; | ||
// onResponseFinish callback, if specified in options | ||
this.onResponseFinish = null; | ||
// Core statistics | ||
this.coreStats = new swsCoreStats(); | ||
// If set to true via options, track only API defined in swagger spec | ||
this.swaggerOnly = false; | ||
// Timeline | ||
this.timeline = new swsTimeline(); | ||
// System statistics | ||
this.sysStats = new SwsSysStats(); | ||
// API Stats | ||
this.apiStats = new swsAPIStats(); | ||
// Core statistics | ||
this.coreStats = new SwsCoreStats(); | ||
// Errors | ||
this.errorsStats = new swsErrors(); | ||
// Core Egress statistics | ||
this.coreEgressStats = new SwsCoreStats(); | ||
// Last Errors | ||
this.lastErrors = new swsLastErrors(); | ||
// Timeline | ||
this.timeline = new swsTimeline(); | ||
// Longest Requests | ||
this.longestRequests = new swsLongestRequests(); | ||
// API Stats | ||
this.apiStats = new swsAPIStats(); | ||
// ElasticSearch Emitter | ||
this.elasticsearchEmitter = new swsElasticsearchEmitter(); | ||
} | ||
// Errors | ||
this.errorsStats = new swsErrors(); | ||
// Initialize | ||
swsProcessor.prototype.init = function (swsOptions) { | ||
// Last Errors | ||
this.lastErrors = new swsLastErrors(); | ||
this.processOptions(swsOptions); | ||
// Longest Requests | ||
this.longestRequests = new swsLongestRequests(); | ||
this.coreStats.initialize(swsOptions); | ||
// ElasticSearch Emitter | ||
this.elasticsearchEmitter = new swsElasticsearchEmitter(); | ||
} | ||
this.timeline.initialize(swsOptions); | ||
init() { | ||
this.processOptions(); | ||
this.apiStats.initialize(swsOptions); | ||
this.sysStats.initialize(); | ||
this.elasticsearchEmitter.initialize(swsOptions); | ||
this.coreStats.initialize(); | ||
// Start tick | ||
this.timer = setInterval(this.tick, 200, this); | ||
}; | ||
this.coreEgressStats.initialize('egress_'); | ||
// Stop | ||
swsProcessor.prototype.stop = function () { | ||
this.timeline.initialize(swsSettings); | ||
clearInterval(this.timer); | ||
this.apiStats.initialize(swsSettings); | ||
}; | ||
this.elasticsearchEmitter.initialize(swsSettings); | ||
swsProcessor.prototype.processOptions = function (swsOptions) { | ||
if(typeof swsOptions === 'undefined') return; | ||
if(!swsOptions) return; | ||
this.options = swsOptions; | ||
// Set or detect hostname | ||
if(swsUtil.supportedOptions.hostname in swsOptions) { | ||
this.hostname = swsOptions[swsUtil.supportedOptions.hostname]; | ||
}else{ | ||
this.hostname = os.hostname(); | ||
// Start tick | ||
this.timer = setInterval(this.tick, 200, this); | ||
} | ||
// Set name and version | ||
if(swsUtil.supportedOptions.name in swsOptions) { | ||
this.name = swsOptions[swsUtil.supportedOptions.name]; | ||
}else{ | ||
this.name = this.hostname; | ||
// Stop | ||
stop() { | ||
clearInterval(this.timer); | ||
} | ||
if(swsUtil.supportedOptions.version in swsOptions) { | ||
this.version = swsOptions[swsUtil.supportedOptions.version]; | ||
} | ||
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; | ||
}; | ||
// Set or detect node address | ||
if(swsUtil.supportedOptions.ip in swsOptions) { | ||
this.ip = swsOptions[swsUtil.supportedOptions.ip]; | ||
}else{ | ||
// Attempt to detect network address | ||
// Use first found interface name which starts from "e" ( en0, em0 ... ) | ||
var address = null; | ||
var ifaces = os.networkInterfaces(); | ||
for( var ifacename in ifaces ){ | ||
var iface = ifaces[ifacename]; | ||
if( !address && !iface.internal && (ifacename.charAt(0)=='e') ){ | ||
if((iface instanceof Array) && (iface.length>0) ) { | ||
address = iface[0].address; | ||
} | ||
} | ||
} | ||
this.ip = address ? address : '127.0.0.1'; | ||
} | ||
// 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); | ||
}; | ||
if( swsOptions.onResponseFinish && (typeof swsOptions.onResponseFinish === 'function') ){ | ||
this.onResponseFinish = swsOptions.onResponseFinish; | ||
} | ||
// Collect all data for request/response pair | ||
// TODO Support option to add arbitrary extra properties to sws request/response record | ||
collectRequestResponseData(res) { | ||
if(swsUtil.supportedOptions.swaggerOnly in swsOptions) { | ||
this.swaggerOnly = swsUtil.supportedOptions.swaggerOnly; | ||
} | ||
var req = res._swsReq; | ||
}; | ||
var codeclass = swsUtil.getStatusCodeClass(res.statusCode); | ||
// Tick - called with specified interval to refresh timelines | ||
swsProcessor.prototype.tick = function (that) { | ||
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 | ||
} | ||
} | ||
}; | ||
var ts = Date.now(); | ||
var totalElapsedSec = (ts - that.coreStats.startts)/1000; | ||
// 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 | ||
} | ||
that.coreStats.tick(ts,totalElapsedSec); | ||
// Response Headers | ||
if ("_headers" in res){ | ||
rrr.http.response.headers = {}; | ||
for(var hdr in res['_headers']){ | ||
rrr.http.response.headers[hdr] = res['_headers'][hdr]; | ||
} | ||
} | ||
that.timeline.tick(ts,totalElapsedSec); | ||
// Additional details from collected info per request / response pair | ||
that.apiStats.tick(ts,totalElapsedSec); | ||
if ("sws" in req) { | ||
that.elasticsearchEmitter.tick(ts,totalElapsedSec); | ||
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; | ||
// Collect all data for request/response pair | ||
// TODO Support option to add arbitrary extra properties to sws request/response record | ||
swsProcessor.prototype.collectRequestResponseData = function (res) { | ||
// 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; | ||
var req = res._swsReq; | ||
// 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; | ||
} | ||
var codeclass = swsUtil.getStatusCodeClass(res.statusCode); | ||
// 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 | ||
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]; | ||
// 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); | ||
} | ||
// TODO Split Cookies | ||
} | ||
// Response Headers | ||
if ("_headers" in res){ | ||
rrr.http.response.headers = {}; | ||
for(var hdr in res['_headers']){ | ||
rrr.http.response.headers[hdr] = res['_headers'][hdr]; | ||
if (req.sws && req.sws.hasOwnProperty("query")) { | ||
rrr.http.request.query = {}; | ||
swsUtil.swsStringRecursive(rrr.http.request.query, req.sws.query); | ||
} | ||
} | ||
// Additional details from collected info per request / response pair | ||
if (req.hasOwnProperty("body")) { | ||
rrr.http.request.body = Object.assign({}, req.body); | ||
swsUtil.swsStringRecursive(rrr.http.request.body, req.body); | ||
} | ||
if ("sws" in req) { | ||
return rrr; | ||
}; | ||
rrr.ip = req.sws.ip; | ||
rrr.real_ip = req.sws.real_ip; | ||
rrr.port = req.sws.port; | ||
getRemoteIP(req ) { | ||
let ip = ''; | ||
try { | ||
ip = req.connection.remoteAddress; | ||
}catch(e){} | ||
return ip; | ||
}; | ||
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; | ||
getPort(req ) { | ||
let p = 0; | ||
try{ | ||
p = req.connection.localPort; | ||
}catch(e){} | ||
return p; | ||
}; | ||
// 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; | ||
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; | ||
}; | ||
// 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 | ||
processRequest(req, res) { | ||
} | ||
// Placeholder for sws-specific attributes | ||
req.sws = req.sws || {}; | ||
// 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); | ||
} | ||
// Setup sws props and pass to stats processors | ||
var ts = Date.now(); | ||
if (req.sws && req.sws.hasOwnProperty("query")) { | ||
rrr.http.request.query = {}; | ||
swsUtil.swsStringRecursive(rrr.http.request.query, req.sws.query); | ||
} | ||
var reqContentLength = 0; | ||
if('content-length' in req.headers) { | ||
reqContentLength = parseInt(req.headers['content-length']); | ||
} | ||
if (req.hasOwnProperty("body")) { | ||
rrr.http.request.body = Object.assign({}, req.body); | ||
swsUtil.swsStringRecursive(rrr.http.request.body, req.body); | ||
} | ||
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); | ||
return rrr; | ||
}; | ||
// 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; | ||
} | ||
swsProcessor.prototype.getRemoteIP = function (req ) { | ||
var ip = ''; | ||
try { | ||
ip = req.connection.remoteAddress; | ||
}catch(e){} | ||
return ip; | ||
}; | ||
// Core stats | ||
this.coreStats.countRequest(req, res); | ||
swsProcessor.prototype.getPort = function (req ) { | ||
var p = 0; | ||
try{ | ||
p = req.connection.localPort; | ||
}catch(e){} | ||
return p; | ||
}; | ||
// Timeline | ||
this.timeline.countRequest(req, res); | ||
// TODO Check if needed | ||
this.apiStats.countRequest(req, res); | ||
}; | ||
swsProcessor.prototype.getRemoteRealIP = function (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; | ||
}; | ||
processResponse(res) { | ||
swsProcessor.prototype.processRequest = function (req, res) { | ||
var req = res._swsReq; | ||
// Placeholder for sws-specific attributes | ||
req.sws = req.sws || {}; | ||
// Capture route path for the request, if set by router | ||
var route_path = ''; | ||
if (("route" in req) && ("path" in req.route)) { | ||
if (("baseUrl" in req) && (req.baseUrl != undefined)) route_path = req.baseUrl; | ||
route_path += req.route.path; | ||
req.sws.route_path = route_path; | ||
} | ||
// Setup sws props and pass to stats processors | ||
var ts = Date.now(); | ||
// 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); | ||
} | ||
var reqContentLength = 0; | ||
if('content-length' in req.headers) { | ||
reqContentLength = parseInt(req.headers['content-length']); | ||
} | ||
// Pass through Core Statistics | ||
this.coreStats.countResponse(res); | ||
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); | ||
// Pass through Timeline | ||
this.timeline.countResponse(res); | ||
// Try to match to API right away | ||
this.apiStats.matchRequest(req); | ||
// Pass through API Statistics | ||
this.apiStats.countResponse(res); | ||
// if no match, and tracking of non-swagger requests is disabled, return | ||
if( !req.sws.match && this.swaggerOnly){ | ||
req.sws.track = false; | ||
return; | ||
} | ||
// Pass through Errors | ||
this.errorsStats.countResponse(res); | ||
// Core stats | ||
this.coreStats.countRequest(req, res); | ||
// Collect request / response record | ||
var rrr = this.collectRequestResponseData(res); | ||
// Timeline | ||
this.timeline.countRequest(req, res); | ||
// Pass through last errors | ||
this.lastErrors.processReqResData(rrr); | ||
// TODO Check if needed | ||
this.apiStats.countRequest(req, res); | ||
}; | ||
// Pass through longest request | ||
this.longestRequests.processReqResData(rrr); | ||
swsProcessor.prototype.processResponse = function (res) { | ||
// Pass to app if callback is specified | ||
if(this.onResponseFinish !== null ){ | ||
this.onResponseFinish(req,res,rrr); | ||
} | ||
var req = res._swsReq; | ||
// Push Request/Response Data to Emitter(s) | ||
this.elasticsearchEmitter.processRecord(rrr); | ||
// Capture route path for the request, if set by router | ||
var route_path = ''; | ||
if (("route" in req) && ("path" in req.route)) { | ||
if (("baseUrl" in req) && (req.baseUrl != undefined)) route_path = req.baseUrl; | ||
route_path += req.route.path; | ||
req.sws.route_path = route_path; | ||
} | ||
//debugrrr('%s', JSON.stringify(rrr)); | ||
}; | ||
// 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); | ||
} | ||
// Get stats according to fields and params specified in query | ||
getStats( query ) { | ||
// Pass through Core Statistics | ||
this.coreStats.countResponse(res); | ||
query = typeof query !== 'undefined' ? query: {}; | ||
query = query !== null ? query: {}; | ||
// Pass through Timeline | ||
this.timeline.countResponse(res); | ||
var statfields = []; // Default | ||
// Pass through API Statistics | ||
this.apiStats.countResponse(res); | ||
// 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(','); | ||
} | ||
} | ||
// Pass through Errors | ||
this.errorsStats.countResponse(res); | ||
// 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(); | ||
// Collect request / response record | ||
var rrr = this.collectRequestResponseData(res); | ||
// 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; | ||
// Pass through last errors | ||
this.lastErrors.processReqResData(rrr); | ||
var fieldMask = 0; | ||
for(var i=0;i<statfields.length;i++){ | ||
var fld = statfields[i]; | ||
if( fld in swsUtil.swsStatFields ) fieldMask |= swsUtil.swsStatFields[fld]; | ||
} | ||
// Pass through longest request | ||
this.longestRequests.processReqResData(rrr); | ||
//console.log('Field mask:' + fieldMask.toString(2) ); | ||
// Pass to app if callback is specified | ||
if(this.onResponseFinish !== null ){ | ||
this.onResponseFinish(req,res,rrr); | ||
} | ||
// 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(); | ||
// 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 | ||
swsProcessor.prototype.getStats = function ( 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(','); | ||
if( fieldMask & swsUtil.swsStatFields.apiop ) { | ||
if(("path" in query) && ("method" in query)) { | ||
result.apiop = this.apiStats.getAPIOperationStats(query.path, query.method); | ||
} | ||
} | ||
} | ||
// core statistics are returned always | ||
var result = this.coreStats.getStats(); | ||
return result; | ||
}; | ||
// add standard properties, returned always | ||
result.name = this.name; | ||
result.version = this.version; | ||
result.hostname = this.hostname; | ||
result.ip = this.ip; | ||
result.apdexThreshold = this.options.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; |
@@ -11,3 +11,3 @@ /** | ||
// apdex_threshold: Thresold for apdex calculation, in milliseconds 50 (ms) by default | ||
function swsReqResStats(apdex_threshold) { | ||
function SwsReqResStats(apdex_threshold) { | ||
this.requests=0; // Total number of requests received | ||
@@ -39,3 +39,3 @@ this.responses=0; // Total number of responses sent | ||
swsReqResStats.prototype.countRequest = function(clength) { | ||
SwsReqResStats.prototype.countRequest = function(clength) { | ||
this.requests++; | ||
@@ -47,3 +47,3 @@ this.total_req_clength += clength; | ||
swsReqResStats.prototype.countResponse = function(code,codeclass,duration,clength) { | ||
SwsReqResStats.prototype.countResponse = function(code,codeclass,duration,clength) { | ||
this.responses++; | ||
@@ -70,3 +70,3 @@ this[codeclass]++; | ||
swsReqResStats.prototype.updateRates = function(elapsed) { | ||
SwsReqResStats.prototype.updateRates = function(elapsed) { | ||
//this.req_rate = Math.round( (this.requests / elapsed) * 1e2 ) / 1e2; //; | ||
@@ -77,2 +77,2 @@ this.req_rate = this.requests / elapsed; | ||
module.exports = swsReqResStats; | ||
module.exports = SwsReqResStats; |
@@ -163,40 +163,3 @@ /* | ||
// METRICS ////////////////////////////////////////////////////////////// // | ||
module.exports.swsMetrics = { | ||
// TOP level counters for all requests / responses, no labels | ||
api_all_request_total: { name: 'api_all_request_total', type: 'counter', help: 'The total number of all API requests received'}, | ||
api_all_success_total: { name: 'api_all_success_total', type: 'counter', help: 'The total number of all API requests with success response'}, | ||
api_all_errors_total: { name: 'api_all_errors_total', type: 'counter', help: 'The total number of all API requests with error response'}, | ||
api_all_client_error_total: { name: 'api_all_client_error_total', type: 'counter', help: 'The total number of all API requests with client error response'}, | ||
api_all_server_error_total: { name: 'api_all_server_error_total', type: 'counter', help: 'The total number of all API requests with server error response'}, | ||
api_all_request_in_processing_total: { name: 'api_all_request_in_processing_total', type: 'gauge', help: 'The total number of all API requests currently in processing (no response yet)'}, | ||
// System metrics for node process | ||
nodejs_process_memory_rss_bytes: { name: 'nodejs_process_memory_rss_bytes', type: 'gauge', help: 'Node.js process resident memory (RSS) bytes '}, | ||
nodejs_process_memory_heap_total_bytes: { name: 'nodejs_process_memory_heap_total_bytes', type: 'gauge', help: 'Node.js process memory heapTotal bytes'}, | ||
nodejs_process_memory_heap_used_bytes: { name: 'nodejs_process_memory_heap_used_bytes', type: 'gauge', help: 'Node.js process memory heapUsed bytes'}, | ||
nodejs_process_memory_external_bytes: { name: 'nodejs_process_memory_external_bytes', type: 'gauge', help: 'Node.js process memory external bytes'}, | ||
nodejs_process_cpu_usage_percentage: { name: 'nodejs_process_cpu_usage_percentage', type: 'gauge', help: 'Node.js process CPU usage percentage'}, | ||
// API Operation counters, labeled with method, path and code | ||
api_request_total: { name: 'api_request_total', type: 'counter', help: 'The total number of all API requests'}, | ||
// DISABLED API Operation counters, labeled with method, path and codeclass | ||
//api_request_codeclass_total: { name: 'api_request_codeclass_total', type: 'counter', help: 'The total number of all API requests by response code class'}, | ||
// API request duration histogram, labeled with method, path and code | ||
api_request_duration_milliseconds: { name: 'api_request_duration_milliseconds', type: 'histogram', help: 'API requests duration'}, | ||
// API request size histogram, labeled with method, path and code | ||
api_request_size_bytes: { name: 'api_request_size_bytes', type: 'histogram', help: 'API requests size'}, | ||
// API response size histogram, labeled with method, path and code | ||
api_response_size_bytes: { name: 'api_response_size_bytes', type: 'histogram', help: 'API requests size'} | ||
}; | ||
// ////////////////////////////////////////////////////////////////////// // | ||
// returns string value of argument, depending on typeof | ||
@@ -203,0 +166,0 @@ module.exports.swsStringValue = function (val) { |
{ | ||
"name": "swagger-stats", | ||
"version": "0.95.9", | ||
"version": "0.95.10", | ||
"description": "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", | ||
@@ -10,3 +10,4 @@ "main": "lib/index.js", | ||
"hapitestapp": "nyc --reporter=lcov --reporter=html --reporter=json --reporter=text --report-dir=coverage/hapitestapp node examples/hapijstest/hapijstest.js", | ||
"hapitestappstop": "mocha --delay --exit test/stoptestapp.js", | ||
"testappstop": "mocha --delay --exit test/stoptestapp.js", | ||
"delay1s": "mocha --delay --exit test/delay.js", | ||
"test-old": "npm run coverage && npm run karma-ci", | ||
@@ -24,3 +25,3 @@ "coverage": "nyc --reporter=lcov --reporter=html --reporter=text --report-dir=coverage/mocha mocha -S --delay", | ||
"testHapi": "concurrently -k --success first \"npm run hapitestapp\" \"npm run covHapi\"", | ||
"covHapi": "npm run cov000h && npm run cov100h && npm run cov200h && npm run cov300h && npm run cov500h && npm run hapitestappstop", | ||
"covHapi": "npm run delay1s && npm run cov000h && npm run cov100h && npm run cov200h && npm run cov300h && npm run cov500h && npm run testappstop", | ||
"cov000h": "nyc --reporter=lcov --reporter=html --reporter=json --reporter=text --report-dir=coverage/000h mocha --delay --exit test/000_baseline.js", | ||
@@ -42,2 +43,4 @@ "cov100h": "nyc --reporter=lcov --reporter=html --reporter=json --reporter=text --report-dir=coverage/100h mocha --delay --exit test/100_method.js", | ||
"express", | ||
"koa", | ||
"hapi", | ||
"api", | ||
@@ -44,0 +47,0 @@ "restful", |
138
README.md
<p align="center"> | ||
<img src="https://github.com/slanatech/swagger-stats/blob/master/screenshots/logo-c-ssm.png?raw=true" alt="swagger-stats"/> | ||
<img src="https://github.com/slanatech/swagger-stats/blob/master/screenshots/logo.png?raw=true" alt="swagger-stats"/> | ||
</p> | ||
@@ -8,3 +8,3 @@ | ||
#### [http://swaggerstats.io](http://swaggerstats.io) | [Documentation](http://swaggerstats.io/docs.html) | [API DOC](http://swaggerstats.io/apidoc.html) | [API SPEC](http://swaggerstats.io/sws-api-swagger.yaml) | ||
#### [https://swaggerstats.io](https://swaggerstats.io) | [Guide](https://swaggerstats.io/guide/) | ||
@@ -16,2 +16,3 @@ [![Build Status](https://travis-ci.org/slanatech/swagger-stats.svg?branch=master)](https://travis-ci.org/slanatech/swagger-stats) | ||
[![npm version](https://badge.fury.io/js/swagger-stats.svg)](https://badge.fury.io/js/swagger-stats) | ||
[![Gitter](https://badges.gitter.im/swagger-stats/community.svg)](https://gitter.im/swagger-stats/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) | ||
@@ -213,137 +214,6 @@ | ||
### Embedded Monitoring User Interface | ||
Swagger-stats comes with built-in User Interface. Navigate to `/swagger-stats/ui` in your app to start monitoring right away | ||
``` | ||
http://<your app host:port>/swagger-stats/ui | ||
``` | ||
##### Key metrics | ||
![swagger-stats bundled User Interface](screenshots/metrics.png?raw=true) | ||
##### Timeline | ||
![swagger-stats bundled User Interface](screenshots/timeline.png?raw=true) | ||
##### Request and error rates | ||
![swagger-stats bundled User Interface](screenshots/rates.png?raw=true) | ||
##### API Operations | ||
![swagger-stats bundled User Interface](screenshots/apitable.png?raw=true) | ||
##### Stats By Method | ||
![swagger-stats bundled User Interface](screenshots/methods.png?raw=true) | ||
## Updates | ||
#### v0.95.9 | ||
See [Changelog](https://github.com/slanatech/swagger-stats/blob/master/CHANGELOG.md) | ||
* [bug] Removed dependency on Inert when using with Hapi [#79](https://github.com/slanatech/swagger-stats/issues/79) | ||
#### v0.95.8 | ||
* [feature] Hapijs support [#75](https://github.com/slanatech/swagger-stats/issues/75) - [Example how to use](https://github.com/slanatech/swagger-stats/blob/master/examples/hapijstest/hapijstest.js) | ||
* [feature] Koa support [#70](https://github.com/slanatech/swagger-stats/pull/70), [#67](https://github.com/slanatech/swagger-stats/issues/67) - thank you @gombosg! | ||
#### v0.95.7 | ||
* [bug] Fixes error in body stringification [#59](https://github.com/slanatech/swagger-stats/issues/59), [#60](https://github.com/slanatech/swagger-stats/pull/60) | ||
* [bug] Cannot upload to elk and Built-In API Telemetry [#46](https://github.com/slanatech/swagger-stats/issues/46) | ||
* [feature] Option `elasticsearchIndexPrefix` [#45](https://github.com/slanatech/swagger-stats/issues/45),[#47](https://github.com/slanatech/swagger-stats/issues/47) | ||
#### v0.95.6 | ||
* [bug] Last Errors and Errors tab no populated using FeatherJS [#42](https://github.com/slanatech/swagger-stats/issues/42) | ||
* [bug] Request Content Length null or undefined [#40](https://github.com/slanatech/swagger-stats/issues/40) | ||
#### v0.95.5 | ||
* [feature] Allow onAuthenticate to be asynchronous [#31](https://github.com/slanatech/swagger-stats/issues/31) | ||
* [feature] Prevent tracking of specific routes [#36](https://github.com/slanatech/swagger-stats/issues/36) | ||
* [feature] Support for extracting request body [#38](https://github.com/slanatech/swagger-stats/issues/38) | ||
Thanks to [DavisJaunzems](https://github.com/DavisJaunzems)! | ||
#### v0.95.0 | ||
* [feature] Elasticsearch support [#12](https://github.com/slanatech/swagger-stats/issues/12) | ||
*swagger-stats* now supports storing details about each API Request/Response in [Elasticsearch](https://www.elastic.co/), so you may use [Kibana](https://www.elastic.co/products/kibana) to perform analysis of API usage over time, build visualizations and dashboards. | ||
Example Kibana dashboards provided in `dashboards/elastic6` | ||
#### v0.94.0 | ||
* [feature] Apdex score [#10](https://github.com/slanatech/swagger-stats/issues/10) | ||
* [feature] Support Authentication for /stats and /metrics [#14](https://github.com/slanatech/swagger-stats/issues/14) | ||
* [feature] Add label "code" to Prometheus histogram metrics [#21](https://github.com/slanatech/swagger-stats/issues/21) | ||
See updated dashboard at [Grafana Dashboards](https://grafana.com/dashboards/3091) | ||
#### v0.93.1 | ||
* [bug] Can't start on node v7.10.1, Mac Os 10.12.6 [#22](https://github.com/slanatech/swagger-stats/issues/22) | ||
#### v0.93.0 | ||
* [feature] Support providing Prometheus metrics via [prom-client](https://www.npmjs.com/package/prom-client) library [#20](https://github.com/slanatech/swagger-stats/issues/20) | ||
#### v0.92.0 | ||
* [feature] OnResponseFinish hook: pass request/response record to callback so app can post proceses it add it to the log [#5](https://github.com/slanatech/swagger-stats/issues/5) | ||
#### v0.91.0 | ||
* [feature] Option to specify alternative URI path for ui,stats and metrics [#17](https://github.com/slanatech/swagger-stats/issues/17) | ||
```javascript | ||
app.use(swStats.getMiddleware({ | ||
uriPath: '/myservice', | ||
swaggerSpec:swaggerSpec | ||
})); | ||
``` | ||
``` | ||
$ curl http://<your app host:port>/myservice/stats | ||
``` | ||
#### v0.90.3 | ||
* [feature] Added new chart to API Operation Page [#16](https://github.com/slanatech/swagger-stats/issues/16) | ||
- handle time histogram | ||
- request size histogram | ||
- response size histogram | ||
- response codes counts | ||
#### v0.90.2 | ||
* [feature] Added [Prometheus](https://prometheus.io/) metrics and [Grafana](https://grafana.com/) dashboards [#9](https://github.com/slanatech/swagger-stats/issues/9) | ||
#### v0.90.1 | ||
* [feature] Added CPU and Memory Usage Stats and monitoring in UI [#8](https://github.com/slanatech/swagger-stats/issues/8) | ||
## Enhancements and Bug Reports | ||
@@ -350,0 +220,0 @@ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
6107927
73
36246
223
7