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

293 lines (224 loc) 8.02 kB
/** * ElasticSearch Emitter. Store Request/Response records in Elasticsearch */ 'use strict'; const os = require('os'); const util = require('util'); const http = require('http'); const url = require('url'); const https = require('https'); //const axios = require('axios'); let axios = null; let axiosPromise = new Promise(async (resolve) => { axios = (await import('axios')).default; resolve(axios); }); /* (async () => { axios = (await import('axios')).default; })(); */ const debug = require('debug')('sws:elastic'); const swsUtil = require('./swsUtil'); const moment = require('moment'); const indexTemplate = require('../schema/elasticsearch/api_index_template.json'); const indexTemplate7X = require('../schema/elasticsearch/api_index_template_7x.json'); const ES_MAX_BUFF = 50; // ElasticSearch Emitter. Store Request/Response records in Elasticsearch function swsElasticEmitter() { // Options this.options = null; this.es7 = false; this.es8 = true; this.indexBuffer = ''; this.bufferCount = 0; this.lastFlush = 0; this.elasticURL = null; this.elasticURLBulk = null; this.elasticProto = null; this.elasticHostname = null; this.elasticPort = null; this.elasticUsername = null; this.elasticPassword = null; this.elasticsearchCert = null; this.elasticsearchKey = null this.indexPrefix = "api-"; this.enabled = false; } // Initialize swsElasticEmitter.prototype.initialize = function (swsOptions) { if(typeof swsOptions === 'undefined') return; if(!swsOptions) return; this.options = swsOptions; // Set or detect hostname if(!(swsUtil.supportedOptions.elasticsearch in swsOptions)) { debug('Elasticsearch is disabled'); return; } this.elasticURL = swsOptions[swsUtil.supportedOptions.elasticsearch]; if (!this.elasticURL) { debug('Elasticsearch url is invalid'); return; } this.elasticURLBulk = this.elasticURL +'/_bulk'; if(swsUtil.supportedOptions.elasticsearchIndexPrefix in swsOptions) { this.indexPrefix = swsOptions[swsUtil.supportedOptions.elasticsearchIndexPrefix]; } if(swsUtil.supportedOptions.elasticsearchUsername in swsOptions) { this.elasticUsername = swsOptions[swsUtil.supportedOptions.elasticsearchUsername]; } if(swsUtil.supportedOptions.elasticsearchPassword in swsOptions) { this.elasticPassword = swsOptions[swsUtil.supportedOptions.elasticsearchPassword]; } if(swsUtil.supportedOptions.elasticsearchCert in swsOptions) { this.elasticsearchCert = swsOptions[swsUtil.supportedOptions.elasticsearchCert] } if( swsUtil.supportedOptions.elasticsearchKey in swsOptions) { this.elasticsearchKey = swsOptions[swsUtil.supportedOptions.elasticsearchKey] } if(this.elasticsearchKey && this.elasticsearchCert){ axios.defaults.httpsAgent = new https.Agent({ rejectUnauthorized: false, // (NOTE: this will disable client verification) cert: this.elasticsearchCert, key: this.elasticsearchKey, }); } // Check / Initialize schema Promise.resolve(axiosPromise).then( () => { this.initTemplate(); }); }; // initialize index template swsElasticEmitter.prototype.initTemplate = function(rrr) { var that = this; var requiredTemplateVersion = indexTemplate7X.version; // Check if there is a template var templateURL = this.elasticURL+'/_template/template_api'; var getOptionsVersion = {method:'get',url:this.elasticURL}; var getOptions = {method:'get',url:templateURL}; let putOptions = {method:'put',url:templateURL, data:indexTemplate7X}; if (this.elasticUsername && this.elasticPassword) { var auth = { username: this.elasticUsername, password: this.elasticPassword, } getOptionsVersion.auth = auth; getOptions.auth = auth; putOptions.auth = auth; } axios(getOptionsVersion).then( (response) => { const body = response.data || {}; if(body && ('version' in body) && ('number' in body.version)){ that.es7 = body.version.number.startsWith('7'); that.es8 = body.version.number.startsWith('8'); } if( !that.es7 && !that.es8){ putOptions.json = indexTemplate; } let initializeNeeded = false; axios(getOptions).then( (response) => { const body = response.data || {}; if( 'template_api' in body ) { if (!('version' in body.template_api) || (body.template_api.version < requiredTemplateVersion)) { initializeNeeded = true; } } }).catch((error)=>{ if(error.response && error.response.status === 404) { initializeNeeded = true; } else { debug(`Error querying template: ${error.message}`); that.enabled = false; } }).finally(()=> { if(initializeNeeded){ axios(putOptions).then((response) =>{ debug("Elasticsearch template updated"); that.enabled = true; }).catch((error)=>{ debug(`Failed to update template: ${error.message}`); that.enabled = false; }); }else{ that.enabled = true; } }); }).catch((error)=>{ debug(`Error getting version: ${error.message}`); that.enabled = false; }); }; // Update timeline and stats per tick swsElasticEmitter.prototype.tick = function (ts,totalElapsedSec) { // Flush if buffer is not empty and not flushed in more than 1 second if( (this.bufferCount > 0) && ((ts-this.lastFlush) >= 1000) ){ this.flush(); } }; // Pre-process RRR swsElasticEmitter.prototype.preProcessRecord = function(rrr){ // handle custom attributes if('attrs' in rrr){ var attrs = rrr.attrs; for(var attrname in attrs){ attrs[attrname] = swsUtil.swsStringValue(attrs[attrname]); } } if('attrsint' in rrr){ var intattrs = rrr.attrsint; for(var intattrname in intattrs){ intattrs[intattrname] = swsUtil.swsNumValue(intattrs[intattrname]); } } }; // Index Request Response Record swsElasticEmitter.prototype.processRecord = function(rrr){ if(!this.enabled){ return; } this.preProcessRecord(rrr); // Create metadata var indexName = this.indexPrefix+moment(rrr['@timestamp']).utc().format('YYYY.MM.DD'); let meta = {index:{_index:indexName,_id:rrr.id}}; if(!this.es7 && !this.es8){ meta = {index:{_index:indexName,_type:'api',_id:rrr.id}}; } // Add to buffer this.indexBuffer += JSON.stringify(meta) + '\n'; this.indexBuffer += JSON.stringify(rrr) + '\n'; this.bufferCount++; if( this.bufferCount >= ES_MAX_BUFF ) { this.flush(); } }; swsElasticEmitter.prototype.flush = function(){ if(!this.enabled){ return; } this.lastFlush = Date.now(); let options = { method: 'post', url: this.elasticURLBulk, headers: { 'Content-Type': 'application/x-ndjson' }, data: this.indexBuffer, }; if (this.elasticUsername && this.elasticPassword) { options.auth = { username: this.elasticUsername, password: this.elasticPassword, } } axios(options).then((response)=> { if (response && ('status' in response) && (response.status !== 200)) { debug('Indexing Error: %d %s',response.status, response.message); } }).catch((error)=>{ debug(`Indexing Error: ${error.message}`); that.enabled = false; }); this.indexBuffer = ''; this.bufferCount = 0; }; module.exports = swsElasticEmitter;