Skip to content

Commit

Permalink
Added Apdex Score stats (all, methods, timeline, apistats). UI: Apdex…
Browse files Browse the repository at this point in the history
… Score added to Requests by Method table, API Operation table - #10
  • Loading branch information
sv2 committed Oct 31, 2017
1 parent 6725d24 commit 041bb98
Show file tree
Hide file tree
Showing 12 changed files with 98 additions and 55 deletions.
12 changes: 1 addition & 11 deletions TODO.txt
Original file line number Diff line number Diff line change
@@ -1,22 +1,12 @@

TODO Current rates calculation seem to be off. Artillery : 200 req / sec -> 400 ?

TODO If no entries in API defs, API op fails because chosen can't select first entry !!!
TODO If no entries in API defs, API op fails because chosen can't select first entry !

TODO Statistics: Clients stats field / page - analyze clients by remote address

TODO Statistics: TOP "Error" Resources (???)

TODO Reconsider Payload page charts: too different values on the same chart; total send in minute could be way bigger then max/avg

TODO Check API Matching for definitions like this: /Accounts/{AccountSid}/Recordings{mediaTypeExtension}

TODO Consider ReqRes Stat for entire last hour ( sliding window ). Update it when time bucket added/removed ( add / substract from bucket's stats )

TODO Count Distribution of requestts / responces across scale by content size
TODO i.e. 0: 10, 1kb: 1, 2k: 2 .... 10k: 8, 20k:8
TODO Show distribution chart

TODO Biggest request / response tables

TODO Bubble Chart: X: response size, Y: number of requests, Z: Average processing time (number of errors ???)
2 changes: 1 addition & 1 deletion dist/js/sws.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/maps/sws.min.js.map

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion examples/spectest/spectest.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ parser.validate(specLocation,function(err, api) {
uriPath: '/swagger-stats',
durationBuckets: [10, 25, 50, 100, 200],
requestSizeBuckets: [10, 25, 50, 100, 200],
responseSizeBuckets: [10, 25, 50, 100, 200]
responseSizeBuckets: [10, 25, 50, 100, 200],
apdexThreshold: 100
}));

// Implement mock API
Expand Down
11 changes: 8 additions & 3 deletions lib/swsAPIStats.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ var swsBucketStats = require('./swsBucketStats');
// Stores Detailed Stats for each API request
function swsAPIStats() {

// Options
this.options = null;

// API Base path per swagger spec
this.basePath = '/';

Expand Down Expand Up @@ -134,10 +137,12 @@ swsAPIStats.prototype.getFullPath = function (path) {

swsAPIStats.prototype.initialize = function(swsOptions) {

if(typeof swsOptions === 'undefined') return;
if(typeof swsOptions === 'undefined') return; // TODO ??? remove
if(!swsOptions) return;

if('durationBuckets' in swsOptions) this.durationBuckets = swsOptions.durationBuckets ;
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;

Expand Down Expand Up @@ -284,7 +289,7 @@ swsAPIStats.prototype.getApiOpDetails = function(path,method) {
// Get or create API Operation Stats
swsAPIStats.prototype.getAPIOpStats = function( path, method ) {
if( !(path in this.apistats)) this.apistats[path] = {};
if( !(method in this.apistats[path])) this.apistats[path][method] = new swsReqResStats();
if( !(method in this.apistats[path])) this.apistats[path][method] = new swsReqResStats(this.options.apdexThreshold);
return this.apistats[path][method];
};

Expand Down
72 changes: 47 additions & 25 deletions lib/swsCoreStats.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,43 +15,28 @@ var swsReqResStats = require('./swsReqResStats');
// Constructor
function swsCoreStats() {

// Options
this.options = null;

// Timestamp when collecting statistics started
this.startts = Date.now();

// Statistics for all requests
this.all = new swsReqResStats();
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 = {
'GET': new swsReqResStats(),
'POST': new swsReqResStats(),
'PUT': new swsReqResStats(),
'DELETE': new swsReqResStats()
};
this.method = null;

// System statistics
this.sys = {
rss: 0,
heapTotal: 0,
heapUsed: 0,
external: 0,
cpu: 0
// TODO event loop delays
};
this.sys = null;

// CPU
this.startTime = process.hrtime();
this.startUsage = process.cpuUsage();
this.startTime = null;
this.startUsage = null;

// 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() }
];
this.startTimeAndUsage = null;

// Prometheus metrics
this.promClientMetrics = {};
Expand Down Expand Up @@ -96,7 +81,44 @@ function swsCoreStats() {

// Initialize
swsCoreStats.prototype.initialize = function (swsOptions) {
// NOOP //

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)
};

// System statistics
this.sys = {
rss: 0,
heapTotal: 0,
heapUsed: 0,
external: 0,
cpu: 0
// TODO event loop delays
};

// CPU
this.startTime = process.hrtime();
this.startUsage = process.cpuUsage();

// 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() }
];

};

swsCoreStats.prototype.getStats = function () {
Expand Down
1 change: 1 addition & 0 deletions lib/swsInterface.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ var swsOptions = {
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: 50,
onResponseFinish: null
};

Expand Down
24 changes: 17 additions & 7 deletions lib/swsReqResStats.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
var swsUtil = require('./swsUtil');

// Request / Response statistics
function swsReqResStats() {
// apdex_threshold: Thresold for apdex calculation, in milliseconds 50 (ms) by default
function swsReqResStats(apdex_threshold) {
this.requests=0; // Total number of requests received
this.responses=0; // Total number of responses sent
this.errors=0; // Total number of error responses
Expand All @@ -27,12 +28,11 @@ function swsReqResStats() {
this.avg_res_clength=0; // Average Content Length in sent responses
this.req_rate=0; // Request Rate
this.err_rate=0; // Error Rate

// TODO Counts by exact response code
// TODO Histogram of durations ( processing time ) + configurable buckets
// TODO Histogram of request size ( content length ) + configurable buckets
// TODO Histogram of response size ( content length ) + configurable buckets

this.apdex_threshold = typeof apdex_threshold !== 'undefined' ? apdex_threshold : 50; // Apdex threshold
this.apdex_satisfied = 0; // Total number of "satisfied" responses for Apdex: time <= apdex_threshold
this.apdex_tolerated = 0; // Total number of "tolerated" responses for Apdex: time <= (apdex_threshold*4)
this.apdex_score = 0; // Apdex score: (apdex_satisfied + (apdex_tolerated/2))/responses
// TODO Consider: counts by exact response code
}

swsReqResStats.prototype.countRequest = function(clength) {
Expand All @@ -52,6 +52,16 @@ swsReqResStats.prototype.countResponse = function(code,codeclass,duration,clengt
this.total_res_clength += clength;
if (this.max_res_clength < clength) this.max_res_clength = clength;
this.avg_res_clength = Math.floor(this.total_res_clength / this.responses);

// Apdex: https://en.wikipedia.org/wiki/Apdex
if( codeclass=="success" ) {
if (duration <= this.apdex_threshold) {
this.apdex_satisfied++;
} else if (duration <= (this.apdex_threshold * 4)) {
this.apdex_tolerated++;
}
}
this.apdex_score = (this.apdex_satisfied + (this.apdex_tolerated/2)) / this.responses;
};

swsReqResStats.prototype.updateRates = function(elapsed) {
Expand Down
11 changes: 6 additions & 5 deletions lib/swsTimeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ var swsReqResStats = require('./swsReqResStats');

function swsTimeline() {

// TODO Remove
//this.timeline_bucket_duration = 60000;
//this.timeline_bucket_current = 0;
//this.timeline_length = 60;
// Options
this.options = null;

// Timeline Settings
this.settings = {
Expand All @@ -42,6 +40,9 @@ swsTimeline.prototype.getStats = function(reqresdata) {

// Create empty timeline going back 60 minutes
swsTimeline.prototype.initialize = function (swsOptions) {

this.options = swsOptions;

var curr = Date.now();
if( swsUtil.supportedOptions.timelineBucketDuration in swsOptions ) {
this.settings.bucket_duration = swsOptions[swsUtil.supportedOptions.timelineBucketDuration];
Expand Down Expand Up @@ -92,7 +93,7 @@ swsTimeline.prototype.getTimelineBucket = function (timelineid) {
swsTimeline.prototype.openTimelineBucket = function(timelineid) {

// Open new bucket
this.data[timelineid] = { stats: new swsReqResStats(), sys: { rss:0, heapTotal:0, heapUsed:0, external:0, cpu: 0} };
this.data[timelineid] = { stats: new swsReqResStats(this.options.apdexThreshold), sys: { rss:0, heapTotal:0, heapUsed:0, external:0, cpu: 0} };

};

Expand Down
4 changes: 4 additions & 0 deletions lib/swsUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ module.exports.supportedOptions = {
// Most likely needs to be defined per app to account for application specifics.
responseSizeBuckets : "responseSizeBuckets",

// Apdex threshold, in milliseconds
// 50 ms by default
apdexThreshold : "apdexThreshold",

// Callback to invoke when response is finished - https://github.com/slanatech/swagger-stats/issues/5
// Application may implement it to trace Request Response Record (RRR), which is passed as parameter
// the following parameters are passed to this callback:
Expand Down
3 changes: 2 additions & 1 deletion ui/sws.js
Original file line number Diff line number Diff line change
Expand Up @@ -814,7 +814,7 @@
var reqStats = this.apistats.method[method];
var row = [ method, reqStats.requests, reqStats.responses, (reqStats.requests-reqStats.responses),
reqStats.errors, reqStats.req_rate.toFixed(4), reqStats.err_rate.toFixed(4),
reqStats.success, reqStats.redirect, reqStats.client_error, reqStats.server_error,
reqStats.success, reqStats.redirect, reqStats.client_error, reqStats.server_error, reqStats.apdex_score.toFixed(2),
reqStats.total_time, reqStats.max_time, reqStats.avg_time.toFixed(2),
reqStats.total_req_clength,reqStats.max_req_clength,reqStats.avg_req_clength,
reqStats.total_res_clength,reqStats.max_res_clength,reqStats.avg_res_clength ];
Expand Down Expand Up @@ -1052,6 +1052,7 @@
apiOpStats.redirect,
apiOpStats.client_error,
apiOpStats.server_error,
apiOpStats.apdex_score.toFixed(2),
apiOpStats.max_time,
apiOpStats.avg_time.toFixed(2),
apiOpStats.avg_req_clength,
Expand Down
8 changes: 8 additions & 0 deletions ui/swsLayout.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,10 @@ var SWSLayout = function(){
if(data>0) return '<span class="badge badge-table badge-danger">'+data+'</span>';
return data;
}},
{title:'Apdex Score', render:function( data, type, full, meta ) {
if(data<0.5) return '<span class="badge badge-table badge-warning">'+data+'</span>';
return data;
}},
{title:'Total Time(ms)',visible:false},
{title:'Max Handle Time(ms)'},
{title:'Avg Handle Time(ms)'},
Expand Down Expand Up @@ -785,6 +789,10 @@ var SWSLayout = function(){
if(data>0) return '<span class="badge badge-table badge-danger">'+data+'</span>';
return data;
}},
{title:'Apdex Score', render:function( data, type, full, meta ) {
if(data<0.5) return '<span class="badge badge-table badge-warning">'+data+'</span>';
return data;
}},
{title:'Max Time(ms)'},
{title:'Avg Time(ms)'},
{title:'Avg Req Payload'},
Expand Down

0 comments on commit 041bb98

Please sign in to comment.