UNPKG

@artilleryio/platform-fargate

Version:
1 lines 8.72 kB
const EventEmitter=require("events"),https=require("https"),stats=require("./stats2"),AWS=require("aws-sdk"),Consumer=require("sqs-consumer")["Consumer"],HdrHistogram=require("hdr-histogram-js"),driftless=require("driftless"),debug=require("debug")("sqs-reporter"),debugV=require("debug")("sqs-reporter:v"),fs=require("fs"),_=require("lodash");class SqsReporter extends EventEmitter{constructor(opts){super(),this.sqsQueueUrl=opts.sqsQueueUrl,this.region=opts.region,this.testId=opts.testId,this.count=opts.count,this.periodsReportedFor=[],this.ee=new EventEmitter,this.intermediates=[],this.aggregate=[],this.aggregateHistogram=HdrHistogram.build({bitBucketSize:64,autoResize:!0,lowestDiscernibleValue:2,highestTrackableValue:1e12,numberOfSignificantValueDigits:1}),this.workerState={},this.lastIntermediateReportAt=0,this.taskWatcher=null,this.metricsByPeriod={},this.mergedPeriodMetrics=[],this.messagesProcessed={},this.metricsMessagesFromWorkers={}}_allWorkersDone(){return Object.keys(this.workerState).length===this.count}startEcsTaskWatcher(){const self=this;this.taskWatcher=driftless.setDriftlessInterval(()=>{var ecs=new AWS.ECS({region:self.region}),params={tasks:self.taskArns,cluster:self.clusterName};ecs.describeTasks(params,(ecsErr,ecsData)=>{ecsErr&&debug(ecsErr),ecsData&&ecsData.tasks?("STOPPED"===(ecsErr=ecsData.tasks.map(t=>({container:{exitCode:t.containers[0].exitCode,lastStatus:t.containers[0].lastStatus,reason:t.containers[0].reason},lastStatus:t.lastStatus,desiredStatus:t.desiredStatus,stoppedReason:t.stoppedReason,stopCode:t.stopCode})))[0].lastStatus&&(debug("🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨"),debug(ecsErr[0].stopCode,ecsErr[0].stoppedReason)),debug(ecsErr)):debug("taskTracker: returning",ecsData)})},1e4)}stop(){debug("stopping");for(const sqsConsumer of this.sqsConsumers)sqsConsumer.stop()}start(){debug("starting");const self=this;self.sqsDebugInterval=driftless.setDriftlessInterval(()=>{debug(self.messagesProcessed);let total=0;for(var[k,v]of Object.entries(self.messagesProcessed))total+=v;debug("total:",total)},1e4),self.intermediateReporterInterval=driftless.setDriftlessInterval(()=>{if(0!==Object.keys(self.metricsByPeriod).length){var earliestPeriodAvailable=Object.keys(self.metricsByPeriod).filter(x=>-1===self.periodsReportedFor.indexOf(x)).sort()[0],earliest=Object.keys(self.metricsByPeriod).sort()[0],earliest=(-1<self.periodsReportedFor.indexOf(earliest)&&(global.artillery.log("Warning: multiple batches of metrics for period",earliest,new Date(Number(earliest))),delete self.metricsByPeriod[earliest]),1e3*(2*Math.ceil(self.count/10)+20));if(void 0!==earliestPeriodAvailable&&(self.metricsByPeriod[earliestPeriodAvailable].length===self.count||Date.now()-Number(earliestPeriodAvailable)>earliest)){debug("have metrics from all workers for period or MAX_WAIT_FOR_PERIOD reached",earliestPeriodAvailable),debug("Report @",new Date(Number(earliestPeriodAvailable)),"made up of items:",self.metricsByPeriod[String(earliestPeriodAvailable)].length);var name,value,stats=global.artillery.__SSMS.mergeBuckets(self.metricsByPeriod[String(earliestPeriodAvailable)])[String(earliestPeriodAvailable)];self.mergedPeriodMetrics.push(stats),stats.summaries={};for([name,value]of Object.entries(stats.histograms||{})){var summary=global.artillery.__SSMS.summarizeHistogram(value);stats.summaries[name]=summary,delete self.metricsByPeriod[String(earliestPeriodAvailable)]}self.periodsReportedFor.push(earliestPeriodAvailable),debug("Emitting stats event"),self.emit("stats",stats)}else debug("Waiting for more workerStats before emitting stats event")}},5e3),self.workersDoneWatcher=driftless.setDriftlessInterval(()=>{if(self._allWorkersDone())if(0<Object.keys(self.metricsByPeriod).length)debug("All workers done but still waiting on some intermediate reports");else{if(global.artillery.version.startsWith("1.")){const report=updateReport(stats.combine(self.aggregate).report(),self.aggregateHistogram);self.emit("done",{report:()=>report})}else{debug("ready to emit done event"),debug("mergedPeriodMetrics"),debug(self.mergedPeriodMetrics);const stats=global.artillery.__SSMS.pack(self.mergedPeriodMetrics);stats.summaries={};for(var[name,value]of Object.entries(stats.histograms||{})){value=global.artillery.__SSMS.summarizeHistogram(value);stats.summaries[name]=value}if("sqs-reporter:v"===process.env.DEBUG)for(var[workerId,metrics]of Object.entries(self.metricsMessagesFromWorkers))debugV("worker",workerId,"->",metrics.length,"items");self.emit("done",stats)}driftless.clearDriftless(self.intermediateReporterInterval),driftless.clearDriftless(self.workersDoneWatcher),driftless.clearDriftless(self.sqsDebugInterval);for(const sqsConsumer of self.sqsConsumers)sqsConsumer.stop();self.emit("workersDone",self.workerState)}},5e3),this.ee.on("message",function(body,attrs){var period,workerId=attrs.workerId?.StringValue;if(workerId){if("workerDone"===body.event||"workerError"===body.event)self.workerState[workerId]=body.event,self.emit(body.event,body,attrs),debug(workerId,body.event);else if("done"!==body.event)if(body.msg)self.emit("workerMessage",body,attrs);else if(!body.event&&body.serializedHdr){self.intermediates.push(body),self.aggregate.push(body);var attrs=HdrHistogram.decodeFromCompressedBase64(body.serializedHdr);if(self.aggregateHistogram.add(attrs),self.intermediates.length>=self.count){var attrs=stats.combine(self.intermediates);const intermediateReport=attrs.report(),histo=HdrHistogram.build({bitBucketSize:64,autoResize:!0,lowestDiscernibleValue:2,highestTrackableValue:1e12,numberOfSignificantValueDigits:1});self.intermediates.forEach(b=>{histo.add(HdrHistogram.decodeFromCompressedBase64(b.serializedHdr))}),updateReport(intermediateReport,histo),0===intermediateReport.scenariosCreated&&0===intermediateReport.scenariosCompleted&&0===intermediateReport.requestsCompleted||(attrs=self.calculateSpread(attrs),debug("spread:",attrs),self.emit("stats",{report:()=>intermediateReport}),self.intermediates=[])}}else"workerStats"===body.event&&(period=(attrs=global.artillery.__SSMS.deserializeMetrics(body.stats)).period,debug("processing workerStats event, worker:",workerId,"period",period),debugV(attrs),void 0===self.metricsByPeriod[period]&&(self.metricsByPeriod[period]=[]),self.metricsByPeriod[period].push(attrs),"sqs-reporter:v"===process.env.DEBUG&&(void 0===self.metricsMessagesFromWorkers[workerId]&&(self.metricsMessagesFromWorkers[workerId]=[]),self.metricsMessagesFromWorkers[workerId].push(attrs)),debugV("metricsByPeriod:"),debugV(self.metricsByPeriod),debug("number of periods processed"),debug(Object.keys(self.metricsByPeriod)),debug("number of metrics collections for period:",period,":"),debug(self.metricsByPeriod[period].length,"expecting:",self.count))}else debug("Got message with no workerId"),debug(body)}),this.ee.on("messageReceiveTimeout",()=>{});this.sqsConsumers=[];for(let i=0;i<30;i++){const sqsConsumer=function(i){return Consumer.create({queueUrl:process.env.SQS_QUEUE_URL||self.sqsQueueUrl,region:self.region,waitTimeSeconds:10,messageAttributeNames:["testId","workerId"],visibilityTimeout:60,batchSize:10,sqs:new AWS.SQS({httpOptions:{agent:new https.Agent({keepAlive:!0})},region:self.region}),handleMessage:async message=>{let body=null;try{body=JSON.parse(message.Body)}catch(err){console.error(err),console.log(message.Body)}if(process.env.LOG_SQS_MESSAGES&&console.log(message),!body)throw new Error;const attrs=message.MessageAttributes;if(!attrs||!attrs.testId)throw new Error;if(self.testId!==attrs.testId.StringValue)throw new Error;self.messagesProcessed[i]||(self.messagesProcessed[i]=0),self.messagesProcessed[i]+=1,process.nextTick(function(){self.ee.emit("message",body,attrs)})}})}(i);sqsConsumer.on("error",err=>{err.message&&err.message.match(/ReceiptHandle.+expired/i)?debug(err.name,err.message):(artillery.log(err),sqsConsumer.stop(),self.emit("error",err))});let empty=0;sqsConsumer.on("empty",()=>{10<++empty&&self.ee.emit("messageReceiveTimeout")}),sqsConsumer.start(),self.sqsConsumers.push(sqsConsumer)}}calculateSpread(stats){stats=_.reduce(stats._requestTimestamps,(acc,ts)=>(acc.min=Math.min(acc.min,ts),acc.max=Math.max(acc.max,ts),acc),{min:1/0,max:0});return round((stats.max-stats.min)/1e3,1)}}function updateReport(report,histo){return report.latency.min=round(histo.minNonZeroValue,1),report.latency.max=round(histo.maxValue,1),report.latency.median=round(histo.getValueAtPercentile(50),1),report.latency.p95=round(histo.getValueAtPercentile(95),1),report.latency.p99=round(histo.getValueAtPercentile(99),1),report}function round(number,decimals){decimals=Math.pow(10,decimals);return Math.round(number*decimals)/decimals}module.exports={SqsReporter:SqsReporter};