diff --git a/appmetrics-zipkin.js b/appmetrics-zipkin.js index 54efd22..4a29db3 100755 --- a/appmetrics-zipkin.js +++ b/appmetrics-zipkin.js @@ -21,7 +21,11 @@ var aspect = require('./lib/aspect.js'); var fs = require('fs'); var PropertyReader = require('properties-reader'); var properties = PropertyReader(__dirname + '/appmetrics-zipkin.properties'); -var tcpp = require('tcp-ping'); +var {Endpoint} = require('zipkin/lib/model'); +Endpoint.prototype.setServiceName = function setServiceName(serviceName) { + // In zipkin, names are lowercase. This eagerly converts to alert users early. + this.serviceName = serviceName || undefined; +}; const { BatchRecorder @@ -29,6 +33,7 @@ const { const { HttpLogger } = require('zipkin-transport-http'); +const HttpsLogger = require('./lib/zipkin-transport-https'); // Load module probes into probes array by searching the probes directory. var probes = []; @@ -54,10 +59,22 @@ module.exports = function(options) { function start(options) { // Set up the zipkin var host, port, serviceName, sampleRate; + var zipkin_endpoint, pfx, passphase; + + global.KNJ_TT_MAX_LENGTH = global.KNJ_TT_MAX_LENGTH || 128; if (options) { host = options['host']; port = options['port']; + if (options.zipkinEndpoint){ + zipkin_endpoint = options.zipkinEndpoint; + } + if (options.pfx){ + pfx = options.pfx; + } + if (options.passphase){ + passphase = options.passphase; + } serviceName = options['serviceName']; sampleRate = options['sampleRate']; } @@ -92,20 +109,28 @@ function start(options) { } // Test if the host & port are valid - tcpp.probe(host, port, function(err, available) { - if (err) { - console.log('Unable to contact Zipkin at ' + host + ':' + port); - return; - } - if (!available) { - console.log('Unable to contact Zipkin at ' + host + ':' + port); - } - }); + // if (host && port) { + // tcpp.probe(host, port, function(err, available) { + // if (err) { + // console.log('Unable to contact Zipkin at ' + host + ':' + port); + // return; + // } + // if (!available) { + // console.log('Unable to contact Zipkin at ' + host + ':' + port); + // } + // }); + // } - const zipkinUrl = `http://${host}:${port}`; + const zipkinUrl = zipkin_endpoint || `http://${host}:${port}/api/v1/spans`; const recorder = new BatchRecorder({ - logger: new HttpLogger({ - endpoint: `${zipkinUrl}/api/v1/spans` + logger: zipkinUrl.startsWith('https:') ? + new HttpsLogger({ + endpoint: zipkinUrl, + pfx: pfx, + passphase: passphase + }) : + new HttpLogger({ + endpoint: zipkinUrl }) }); @@ -119,6 +144,60 @@ function start(options) { }); } +module.exports.update = function(options) { + start(options); + // for (var i = 0; i < probes.length; i++) { + // probes[i].updateServiceName(probes[i].serviceName); + // } + probes.forEach(function(probe) { + probe.updateProbes(); + // probe.enableRequests(); + }); +}; + +module.exports.updateServiceName = function(serviceName){ + probes.forEach(function(probe) { + probe.setServiceName(serviceName); + probe.updateProbes(); + }); +}; + +module.exports.updatePathFilter = function(paths){ + probes.forEach(function(probe) { + probe.setPathFilter(paths); + probe.updateProbes(); + }); +}; + + +module.exports.updateHeaderFilter = function(headers){ + probes.forEach(function(probe) { + probe.setHeaderFilter(headers); + probe.updateProbes(); + }); +}; + + +module.exports.updateIbmapmContext = function(context) { + probes.forEach(function(probe) { + probe.setIbmapmContext(context); + probe.updateProbes(); + }); +}; + +module.exports.stop = function(){ + probes.forEach(function(probe) { + probe.stop(); + // probe.enableRequests(); + }); +}; + +module.exports.disable = function(){ + probes.forEach(function(probe) { + probe.disable(); + // probe.enableRequests(); + }); +}; /* * Patch the module require function to run the probe attach function * for any matching module. This loads the monitoring probes into the modules diff --git a/lib/aspect.js b/lib/aspect.js index 3ea5a0a..edcb8bc 100755 --- a/lib/aspect.js +++ b/lib/aspect.js @@ -17,7 +17,7 @@ exports.aroundCallback = function(args, context, hookBefore, hookAfter) { var position = this.findCallbackArg(args); - if (position == undefined) return; + if (position === undefined) return; var orig = args[position]; diff --git a/lib/probe.js b/lib/probe.js index 9ee3f1c..13daf33 100755 --- a/lib/probe.js +++ b/lib/probe.js @@ -27,6 +27,9 @@ function Probe(name) { this.config = {}; this.recorder = {}; this.serviceName = ''; + this.ibmapmContext = {}; + this.pathFilters = []; + this.headerFilters = {}; } /* @@ -54,6 +57,16 @@ Probe.prototype.setRecorder = function(recorder) { Probe.prototype.setServiceName = function(name) { this.serviceName = name; }; +Probe.prototype.setPathFilter = function(paths) { + this.pathFilters = paths || []; +}; +Probe.prototype.setHeaderFilter = function(headers) { + this.headerFilters = headers || {}; +}; + +Probe.prototype.setIbmapmContext = function(ibmapmContext) { + this.ibmapmContext = ibmapmContext; +}; /* * Lightweight metrics probes diff --git a/lib/timer.js b/lib/timer.js index ed685cd..701540b 100755 --- a/lib/timer.js +++ b/lib/timer.js @@ -24,7 +24,7 @@ function Timer() { Timer.prototype.stop = function() { // Prevent the timer being stopped twice. - if (this.timeDelta == -1) { + if (this.timeDelta === -1) { var dur = process.hrtime(this.startTime); this.timeDelta = (dur[0] * 1000) + (dur[1] / 1000000); } diff --git a/lib/tools.js b/lib/tools.js new file mode 100644 index 0000000..f57989f --- /dev/null +++ b/lib/tools.js @@ -0,0 +1,43 @@ + +'use strict'; + +module.exports.recordIbmapmContext = function(tracer, ibmapmContext){ + if (ibmapmContext && ibmapmContext.podName) { + tracer.recordBinary('pod.name', ibmapmContext.podName); + tracer.recordBinary('container.id', ibmapmContext.containerId); + tracer.recordBinary('namespace', ibmapmContext.nameSpace); + tracer.recordBinary('cluster.id', ibmapmContext.clusterID || 'unamedcluster'); + tracer.recordBinary('node.name', ibmapmContext.nodeName); + tracer.recordBinary('service.name', ibmapmContext.serviceName); + + } + if (ibmapmContext && ibmapmContext.applicationName) { + tracer.recordBinary('application.name', ibmapmContext.applicationName); + } + if (ibmapmContext && ibmapmContext['resource.id']) { + tracer.recordBinary('resource.id', ibmapmContext['resource.id']); + } + if (ibmapmContext && ibmapmContext.tenantId) { + tracer.recordBinary('tenant.id', ibmapmContext.tenantId); + } + if (ibmapmContext && ibmapmContext.ip) { + tracer.recordBinary('ip', ibmapmContext.ip); + } +}; + +module.exports.isIcamInternalRequest = function(options, headerFilters, pathFilters) { + if (options.headers) { + for (var key in headerFilters){ + if (Object.keys(options.headers).indexOf(key) >= 0 + || options.headers[key] === headerFilters[key]){ + return true; + } + } + } + for (var i = 0; i < pathFilters.length; i++) { + if (options.path.indexOf(pathFilters[i]) >= 0){ + return true; + } + } + return false; +}; diff --git a/lib/zipkin-transport-https.js b/lib/zipkin-transport-https.js new file mode 100644 index 0000000..c9c0308 --- /dev/null +++ b/lib/zipkin-transport-https.js @@ -0,0 +1,103 @@ +'use strict'; +var uuid = require('uuid'); +var https = require('https'); +var url = require('url'); +var commonTools = require('../../lib/tool/common.js'); + +var log4js = require('log4js'); +var logger = log4js.getLogger('knj_log'); +var _createClass = function() { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function(Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +var _require = require('zipkin'); +var JSON_V1 = _require.jsonEncoder.JSON_V1; + +var HttpsLogger = function() { + function HttpsLogger(_ref) { + var _this = this; + + var endpoint = _ref.endpoint; + var _ref$httpInterval = _ref.httpInterval; + var httpInterval = _ref$httpInterval === undefined ? 1000 : _ref$httpInterval; + var _ref$jsonEncoder = _ref.jsonEncoder; + var jsonEncoder = _ref$jsonEncoder === undefined ? JSON_V1 : _ref$jsonEncoder; + + _classCallCheck(this, HttpsLogger); + + this.endpoint = endpoint; + this.pfx = _ref.pfx; + this.passphase = _ref.passphase; + this.queue = []; + this.jsonEncoder = jsonEncoder; + + var timer = setInterval(function() { + _this.processQueue(); + }, httpInterval); + if (timer.unref) { + // unref might not be available in browsers + timer.unref(); // Allows Node to terminate instead of blocking on timer + } + } + + _createClass(HttpsLogger, [{ + key: 'logSpan', + value: function logSpan(span) { + this.queue.push(this.jsonEncoder.encode(span)); + } + }, { + key: 'processQueue', + value: function processQueue() { + if (this.queue.length > 0) { + var postBody = '[' + this.queue.join(',') + ']'; + var options = url.parse(this.endpoint); + var header = { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-TransactionID': uuid.v1(), + 'User-Agent': 'NodeDC' + }; + if (process.env.APM_TENANT_ID) { + header['X-TenantId'] = process.env.APM_TENANT_ID; + } + var finalOptions = { + hostname: options.hostname, + host: options.host, + port: options.port, + path: options.path, + protocol: options.protocol, + pfx: this.pfx, + passphrase: this.passphase, + ca: this.pfx, + requestCert: true, + rejectUnauthorized: false, + method: 'POST', + headers: header + }; + commonTools.tlsFix8(finalOptions); + try { + var req = https.request(finalOptions, function(res){ + if (res.statusCode === 202) + logger.debug('Send to Jaeger server successfully: ', postBody); + else + logger.warn('Failed to sent to Jaeger server. statusCode=', res.statusCode, 'options=', finalOptions); + }); + req.on('error', function(err){ + logger.error('Failed to sent to Jaeger server'); + logger.error(err); + }); + req.write(postBody); + req.end(); + } catch (e) { + logger.error('Failed to sent to Jaeger server'); + logger.error(e); + } + this.queue.length = 0; + } + } + }]); + + return HttpsLogger; +}(); + +module.exports = HttpsLogger; diff --git a/probes/http-outbound-probe-zipkin.js b/probes/http-outbound-probe-zipkin.js index ec089a0..7af5950 100644 --- a/probes/http-outbound-probe-zipkin.js +++ b/probes/http-outbound-probe-zipkin.js @@ -16,12 +16,19 @@ 'use strict'; var Probe = require('../lib/probe.js'); var aspect = require('../lib/aspect.js'); +var tool = require('../lib/tools.js'); var util = require('util'); var url = require('url'); var semver = require('semver'); const zipkin = require('zipkin'); +var log4js = require('log4js'); +var logger = log4js.getLogger('knj_log'); var serviceName; +var ibmapmContext; +var headerFilters; +var pathFilters; +var tracer; const { Request, @@ -46,11 +53,27 @@ function HttpOutboundProbeZipkin() { } util.inherits(HttpOutboundProbeZipkin, Probe); +HttpOutboundProbeZipkin.prototype.updateProbes = function() { + serviceName = this.serviceName; + ibmapmContext = this.ibmapmContext; + headerFilters = this.headerFilters; + pathFilters = this.pathFilters; + tracer = new zipkin.Tracer({ + ctxImpl, + recorder: this.recorder, + sampler: new zipkin.sampler.CountingSampler(this.config.sampleRate), + // sample rate 0.01 will sample 1 % of all incoming requests + traceId128Bit: true // to generate 128-bit trace IDs. + }); +}; + + HttpOutboundProbeZipkin.prototype.attach = function(name, target) { - const tracer = new zipkin.Tracer({ + tracer = new zipkin.Tracer({ ctxImpl, recorder: this.recorder, - sampler: new zipkin.sampler.CountingSampler(this.config.sampleRate), // sample rate 0.01 will sample 1 % of all incoming requests + sampler: new zipkin.sampler.CountingSampler(this.config.sampleRate), + // sample rate 0.01 will sample 1 % of all incoming requests traceId128Bit: true // to generate 128-bit trace IDs. }); serviceName = this.serviceName; @@ -63,10 +86,16 @@ HttpOutboundProbeZipkin.prototype.attach = function(name, target) { // Before 'http.request' function function(obj, methodName, methodArgs, probeData) { // Get HTTP request method from options + if (process.env.JAEGER_ENDPOINT_NOTREADY === 'true'){ + return; + } var options = methodArgs[0]; var requestMethod = 'GET'; var urlRequested = ''; if (typeof options === 'object') { + if (tool.isIcamInternalRequest(options, headerFilters, pathFilters)){ + return; + } urlRequested = formatURL(options); if (options.method) { requestMethod = options.method; @@ -84,20 +113,40 @@ HttpOutboundProbeZipkin.prototype.attach = function(name, target) { } if (!methodArgs[0].headers) methodArgs[0].headers = {}; - let { headers } = Request.addZipkinHeaders(methodArgs[0], tracer.createChildId()); + var childId = tracer.createChildId(); + let { headers } = Request.addZipkinHeaders(methodArgs[0], childId); Object.assign(methodArgs[0].headers, { headers }); + tracer.setId(childId); + if (urlRequested.length > global.KNJ_TT_MAX_LENGTH) { + urlRequested = urlRequested.substr(0, global.KNJ_TT_MAX_LENGTH); + } tracer.recordServiceName(serviceName); - tracer.recordRpc(requestMethod); + tracer.recordRpc(urlRequested); tracer.recordBinary('http.url', urlRequested); + tracer.recordBinary('http.method', requestMethod.toUpperCase()); + if (process.env.APM_TENANT_ID){ + tracer.recordBinary('tenant.id', process.env.APM_TENANT_ID); + } + tracer.recordBinary('edge.request', 'false'); + tracer.recordBinary('request.type', 'http'); + tool.recordIbmapmContext(tracer, ibmapmContext); tracer.recordAnnotation(new Annotation.ClientSend()); + logger.debug('send http-outbound-tracer(before): ', tracer.id); // End metrics aspect.aroundCallback( methodArgs, probeData, function(target, args, probeData) { - tracer.recordBinary('http.status_code', target.res.statusCode.toString()); + tracer.setId(childId); + logger.debug('confirm:', urlRequested); + var status_code = target.res.statusCode.toString(); + tracer.recordBinary('http.status_code', status_code); + if (status_code >= 400) { + tracer.recordBinary('error', 'true'); + } tracer.recordAnnotation(new Annotation.ClientRecv()); + logger.debug('send http-outbound-tracer(aroundCallback): ', tracer.id); }, function(target, args, probeData, ret) { return ret; @@ -131,11 +180,14 @@ function formatURL(httpOptions) { url += httpOptions.host; } else if (httpOptions.hostname) { url += httpOptions.hostname; + if (httpOptions.port) { + url += ':' + httpOptions.port; + } } else { url += 'localhost'; - } - if (httpOptions.port) { - url += ':' + httpOptions.port; + if (httpOptions.port) { + url += ':' + httpOptions.port; + } } if (httpOptions.path) { url += httpOptions.path; @@ -144,5 +196,4 @@ function formatURL(httpOptions) { } return url; } - module.exports = HttpOutboundProbeZipkin; diff --git a/probes/http-probe-zipkin.js b/probes/http-probe-zipkin.js index 576eec4..464e9d2 100644 --- a/probes/http-probe-zipkin.js +++ b/probes/http-probe-zipkin.js @@ -17,10 +17,15 @@ var Probe = require('../lib/probe.js'); var aspect = require('../lib/aspect.js'); +var tool = require('../lib/tools.js'); var util = require('util'); const zipkin = require('zipkin'); +var log4js = require('log4js'); +var logger = log4js.getLogger('knj_log'); var serviceName; +var ibmapmContext; +var tracer; const { Request, @@ -41,11 +46,10 @@ function hasZipkinHeader(httpReq) { return headers[(Header.TraceId).toLowerCase()] !== undefined && headers[(Header.SpanId).toLowerCase()] !== undefined; } - function HttpProbeZipkin() { Probe.call(this, 'http'); this.config = { - filters: [], + filters: [] }; } util.inherits(HttpProbeZipkin, Probe); @@ -57,23 +61,37 @@ function stringToBoolean(str) { function stringToIntOption(str) { try { + // eslint-disable-next-line radix return new Some(parseInt(str, 10)); } catch (err) { return None; } } +HttpProbeZipkin.prototype.updateProbes = function() { + serviceName = this.serviceName; + ibmapmContext = this.ibmapmContext; + tracer = new zipkin.Tracer({ + ctxImpl, + recorder: this.recorder, + sampler: new zipkin.sampler.CountingSampler(this.config.sampleRate), + // sample rate 0.01 will sample 1 % of all incoming requests + traceId128Bit: true // to generate 128-bit trace IDs. + }); +}; + HttpProbeZipkin.prototype.attach = function(name, target) { serviceName = this.serviceName; - const tracer = new zipkin.Tracer({ + tracer = new zipkin.Tracer({ ctxImpl, recorder: this.recorder, - sampler: new zipkin.sampler.CountingSampler(this.config.sampleRate), // sample rate 0.01 will sample 1 % of all incoming requests + sampler: new zipkin.sampler.CountingSampler(this.config.sampleRate), + // sample rate 0.01 will sample 1 % of all incoming requests traceId128Bit: true // to generate 128-bit trace IDs. }); - if (name == 'http') { + if (name === 'http') { if (target.__zipkinProbeAttached__) return target; target.__zipkinProbeAttached__ = true; var methods = ['on', 'addListener']; @@ -84,13 +102,20 @@ HttpProbeZipkin.prototype.attach = function(name, target) { if (obj.__zipkinhttpProbe__) return; obj.__zipkinhttpProbe__ = true; aspect.aroundCallback(args, probeData, function(obj, args, probeData) { + if (process.env.JAEGER_ENDPOINT_NOTREADY === 'true'){ + return; + } var httpReq = args[0]; var res = args[1]; + var childId; // Filter out urls where filter.to is '' var traceUrl = parse(httpReq.url); - // console.log(util.inspect(httpReq)); if (traceUrl !== '') { - const method = httpReq.method; + var reqMethod = httpReq.method; + var edgeRequest = false; + if (reqMethod.toUpperCase() === 'OPTIONS' && httpReq.headers['access-control-request-method']) { + reqMethod = httpReq.headers['access-control-request-method']; + } if (hasZipkinHeader(httpReq)) { const headers = httpReq.headers; var spanId = headers[(Header.SpanId).toLowerCase()]; @@ -107,9 +132,12 @@ HttpProbeZipkin.prototype.attach = function(name, target) { flags }); tracer.setId(id); + childId = tracer.createChildId(); + tracer.setId(childId); probeData.traceId = tracer.id; }; } else { + edgeRequest = true; tracer.setId(tracer.createRootId()); probeData.traceId = tracer.id; // Must assign new options back to args[0] @@ -117,15 +145,40 @@ HttpProbeZipkin.prototype.attach = function(name, target) { Object.assign(args[0].headers, headers); } - tracer.recordServiceName(serviceName); - tracer.recordRpc(method.toUpperCase()); - tracer.recordBinary('http.url', httpReq.headers.host + traceUrl); - tracer.recordAnnotation(new Annotation.ServerRecv()); - tracer.recordAnnotation(new Annotation.LocalAddr(0)); + var urlPrefix = 'http://' + httpReq.headers.host; + var maxUrlLength = global.KNJ_TT_MAX_LENGTH; + if (urlPrefix.length < global.KNJ_TT_MAX_LENGTH) { + maxUrlLength = global.KNJ_TT_MAX_LENGTH - urlPrefix.length; + } else { + maxUrlLength = 1; + } + if (traceUrl.length > maxUrlLength) { + traceUrl = traceUrl.substr(0, maxUrlLength); + } + tracer.recordBinary('http.url', urlPrefix + traceUrl); + tracer.recordAnnotation(new Annotation.ServerRecv()); + logger.debug('http-tracer(before): ', tracer.id); aspect.after(res, 'end', probeData, function(obj, methodName, args, probeData, ret) { - tracer.recordBinary('http.status_code', res.statusCode.toString()); + tracer.setId(probeData.traceId); + tracer.recordServiceName(serviceName); + tracer.recordBinary('service.name', serviceName); + tracer.recordRpc(traceUrl); + tracer.recordAnnotation(new Annotation.LocalAddr(0)); + var status_code = res.statusCode.toString(); + tracer.recordBinary('http.status_code', status_code); + if (status_code >= 400) { + tracer.recordBinary('error', 'true'); + } + tracer.recordBinary('http.method', reqMethod.toUpperCase()); + if (process.env.APM_TENANT_ID){ + tracer.recordBinary('tenant.id', process.env.APM_TENANT_ID); + } + tracer.recordBinary('edge.request', '' + edgeRequest); + tracer.recordBinary('request.type', 'http'); + tool.recordIbmapmContext(tracer, ibmapmContext); tracer.recordAnnotation(new Annotation.ServerSend()); + logger.debug('http-tracer(after): ', tracer.id); }); } }); @@ -144,5 +197,4 @@ function parse(url) { return url; }; - module.exports = HttpProbeZipkin; diff --git a/probes/https-outbound-probe-zipkin.js b/probes/https-outbound-probe-zipkin.js index 09c9352..290f8c3 100644 --- a/probes/https-outbound-probe-zipkin.js +++ b/probes/https-outbound-probe-zipkin.js @@ -16,12 +16,19 @@ 'use strict'; var Probe = require('../lib/probe.js'); var aspect = require('../lib/aspect.js'); +var tool = require('../lib/tools.js'); var util = require('util'); var url = require('url'); var semver = require('semver'); const zipkin = require('zipkin'); +var log4js = require('log4js'); +var logger = log4js.getLogger('knj_log'); var serviceName; +var ibmapmContext; +var headerFilters; +var pathFilters; +var tracer; const { Request, @@ -46,11 +53,27 @@ function HttpsOutboundProbeZipkin() { } util.inherits(HttpsOutboundProbeZipkin, Probe); + +HttpsOutboundProbeZipkin.prototype.updateProbes = function() { + serviceName = this.serviceName; + ibmapmContext = this.ibmapmContext; + headerFilters = this.headerFilters; + pathFilters = this.pathFilters; + tracer = new zipkin.Tracer({ + ctxImpl, + recorder: this.recorder, + sampler: new zipkin.sampler.CountingSampler(this.config.sampleRate), + // sample rate 0.01 will sample 1 % of all incoming requests + traceId128Bit: true // to generate 128-bit trace IDs. + }); +}; + HttpsOutboundProbeZipkin.prototype.attach = function(name, target) { - const tracer = new zipkin.Tracer({ + tracer = new zipkin.Tracer({ ctxImpl, recorder: this.recorder, - sampler: new zipkin.sampler.CountingSampler(this.config.sampleRate), // sample rate 0.01 will sample 1 % of all incoming requests + sampler: new zipkin.sampler.CountingSampler(this.config.sampleRate), + // sample rate 0.01 will sample 1 % of all incoming requests traceId128Bit: true // to generate 128-bit trace IDs. }); serviceName = this.serviceName; @@ -63,10 +86,16 @@ HttpsOutboundProbeZipkin.prototype.attach = function(name, target) { // Before 'http.request' function function(obj, methodName, methodArgs, probeData) { // Get HTTP request method from options + if (process.env.JAEGER_ENDPOINT_NOTREADY === 'true'){ + return; + } var options = methodArgs[0]; var requestMethod = 'GET'; var urlRequested = ''; if (typeof options === 'object') { + if (tool.isIcamInternalRequest(options, headerFilters, pathFilters)){ + return; + } urlRequested = formatURL(options); if (options.method) { requestMethod = options.method; @@ -77,20 +106,47 @@ HttpsOutboundProbeZipkin.prototype.attach = function(name, target) { if (parsedOptions.method) { requestMethod = parsedOptions.method; } + + // This converts the outgoing request's options to an object + // so that we can add headers onto it + methodArgs[0] = Object.assign({}, parsedOptions); + } + + if (!methodArgs[0].headers) methodArgs[0].headers = {}; + var childId = tracer.createChildId(); + let { headers } = Request.addZipkinHeaders(methodArgs[0], childId); + Object.assign(methodArgs[0].headers, { headers }); + tracer.setId(childId); + + if (urlRequested.length > global.KNJ_TT_MAX_LENGTH) { + urlRequested = urlRequested.substr(0, global.KNJ_TT_MAX_LENGTH); } - // Must assign new options back to methodArgs[0] - methodArgs[0] = Request.addZipkinHeaders(methodArgs[0], tracer.createChildId()); tracer.recordServiceName(serviceName); - tracer.recordRpc(requestMethod); + tracer.recordRpc(urlRequested); tracer.recordBinary('http.url', urlRequested); + tracer.recordBinary('http.method', requestMethod.toUpperCase()); + if (process.env.APM_TENANT_ID){ + tracer.recordBinary('tenant.id', process.env.APM_TENANT_ID); + } + tracer.recordBinary('edge.request', 'false'); + tracer.recordBinary('request.type', 'https'); + tool.recordIbmapmContext(tracer, ibmapmContext); tracer.recordAnnotation(new Annotation.ClientSend()); + logger.debug('send https-outbound-tracer(before): ', tracer.id); // End metrics aspect.aroundCallback( methodArgs, probeData, function(target, args, probeData) { - tracer.recordBinary('http.status_code', target.res.statusCode.toString()); + tracer.setId(childId); + logger.debug('confirm:', urlRequested); + var status_code = target.res.statusCode.toString(); + tracer.recordBinary('http.status_code', status_code); + if (status_code >= 400) { + tracer.recordBinary('error', 'true'); + } tracer.recordAnnotation(new Annotation.ClientRecv()); + logger.debug('send https-outbound-tracer(aroundCallback): ', tracer.id); }, function(target, args, probeData, ret) { return ret; @@ -124,11 +180,14 @@ function formatURL(httpsOptions) { url += httpsOptions.host; } else if (httpsOptions.hostname) { url += httpsOptions.hostname; + if (httpsOptions.port) { + url += ':' + httpsOptions.port; + } } else { url += 'localhost'; - } - if (httpsOptions.port) { - url += ':' + httpsOptions.port; + if (httpsOptions.port) { + url += ':' + httpsOptions.port; + } } if (httpsOptions.path) { url += httpsOptions.path; diff --git a/probes/https-probe-zipkin.js b/probes/https-probe-zipkin.js index 2f6d57b..06f5679 100644 --- a/probes/https-probe-zipkin.js +++ b/probes/https-probe-zipkin.js @@ -17,10 +17,15 @@ var Probe = require('../lib/probe.js'); var aspect = require('../lib/aspect.js'); +var tool = require('../lib/tools.js'); var util = require('util'); const zipkin = require('zipkin'); +var log4js = require('log4js'); +var logger = log4js.getLogger('knj_log'); var serviceName; +var ibmapmContext; +var tracer; const { Request, @@ -38,14 +43,14 @@ const ctxImpl = new CLSContext(); function hasZipkinHeader(httpsReq) { const headers = httpsReq.headers || {}; - return headers[(Header.TraceId).toLowerCase()] !== undefined && headers[(Header.SpanId).toLowerCase()] !== undefined; + return headers[(Header.TraceId).toLowerCase()] !== undefined + && headers[(Header.SpanId).toLowerCase()] !== undefined; } - function HttpsProbeZipkin() { Probe.call(this, 'https'); this.config = { - filters: [], + filters: [] }; } util.inherits(HttpsProbeZipkin, Probe); @@ -57,23 +62,38 @@ function stringToBoolean(str) { function stringToIntOption(str) { try { + // eslint-disable-next-line radix return new Some(parseInt(str, 10)); } catch (err) { return None; } } +HttpsProbeZipkin.prototype.updateProbes = function() { + serviceName = this.serviceName; + ibmapmContext = this.ibmapmContext; + tracer = new zipkin.Tracer({ + ctxImpl, + recorder: this.recorder, + sampler: new zipkin.sampler.CountingSampler(this.config.sampleRate), + // sample rate 0.01 will sample 1 % of all incoming requests + traceId128Bit: true // to generate 128-bit trace IDs. + }); +}; + + HttpsProbeZipkin.prototype.attach = function(name, target) { serviceName = this.serviceName; - const tracer = new zipkin.Tracer({ + tracer = new zipkin.Tracer({ ctxImpl, recorder: this.recorder, - sampler: new zipkin.sampler.CountingSampler(this.config.sampleRate), // sample rate 0.01 will sample 1 % of all incoming requests + sampler: new zipkin.sampler.CountingSampler(this.config.sampleRate), + // sample rate 0.01 will sample 1 % of all incoming requests traceId128Bit: true // to generate 128-bit trace IDs. }); - if (name == 'https') { + if (name === 'https') { if (target.__zipkinProbeAttached__) return target; target.__zipkinProbeAttached__ = true; var methods = ['on', 'addListener']; @@ -84,13 +104,21 @@ HttpsProbeZipkin.prototype.attach = function(name, target) { if (obj.__zipkinhttpsProbe__) return; obj.__zipkinhttpsProbe__ = true; aspect.aroundCallback(args, probeData, function(obj, args, probeData) { + if (process.env.JAEGER_ENDPOINT_NOTREADY === 'true'){ + return; + } var httpsReq = args[0]; var res = args[1]; + var childId; // Filter out urls where filter.to is '' var traceUrl = parse(httpsReq.url); if (traceUrl !== '') { - const method = httpsReq.method; - + var reqMethod = httpsReq.method; + var edgeRequest = false; + if (reqMethod.toUpperCase() === 'OPTIONS' + && httpsReq.headers['access-control-request-method']) { + reqMethod = httpsReq.headers['access-control-request-method']; + } if (hasZipkinHeader(httpsReq)) { const headers = httpsReq.headers; var spanId = headers[(Header.SpanId).toLowerCase()]; @@ -98,7 +126,8 @@ HttpsProbeZipkin.prototype.attach = function(name, target) { const traceId = new Some(headers[(Header.TraceId).toLowerCase()]); const parentSpanId = new Some(headers[(Header.ParentSpanId).toLowerCase()]); const sampled = new Some(headers[(Header.Sampled).toLowerCase()]); - const flags = (new Some(headers[(Header.Flags).toLowerCase()])).flatMap(stringToIntOption).getOrElse(0); + const flags = (new Some(headers[(Header.Flags).toLowerCase()])) + .flatMap(stringToIntOption).getOrElse(0); var id = new TraceId({ traceId: traceId, parentId: parentSpanId, @@ -107,25 +136,54 @@ HttpsProbeZipkin.prototype.attach = function(name, target) { flags }); tracer.setId(id); + childId = tracer.createChildId(); + tracer.setId(childId); probeData.traceId = tracer.id; }; } else { + edgeRequest = true; tracer.setId(tracer.createRootId()); probeData.traceId = tracer.id; // Must assign new options back to args[0] - args[0] = Request.addZipkinHeaders(args[0], tracer.id); + const { headers } = Request.addZipkinHeaders(args[0], tracer.id); + Object.assign(args[0].headers, headers); } - tracer.recordServiceName(serviceName); - tracer.recordRpc(method.toUpperCase()); - tracer.recordBinary('http.url', httpsReq.headers.host + traceUrl); - tracer.recordAnnotation(new Annotation.ServerRecv()); - tracer.recordAnnotation(new Annotation.LocalAddr(0)); + var urlPrefix = 'https://' + httpsReq.headers.host; + var maxUrlLength = global.KNJ_TT_MAX_LENGTH; + if (urlPrefix.length < global.KNJ_TT_MAX_LENGTH) { + maxUrlLength = global.KNJ_TT_MAX_LENGTH - urlPrefix.length; + } else { + maxUrlLength = 1; + } + if (traceUrl.length > maxUrlLength) { + traceUrl = traceUrl.substr(0, maxUrlLength); + } + tracer.recordBinary('http.url', urlPrefix + traceUrl); + tracer.recordAnnotation(new Annotation.ServerRecv()); + logger.debug('https-tracer(before): ', tracer.id); aspect.after(res, 'end', probeData, function(obj, methodName, args, probeData, ret) { - tracer.recordBinary('http.status_code', res.statusCode.toString()); + tracer.setId(probeData.traceId); + tracer.recordServiceName(serviceName); + tracer.recordBinary('service.name', serviceName); + tracer.recordRpc(traceUrl); + tracer.recordAnnotation(new Annotation.LocalAddr(0)); + var status_code = res.statusCode.toString(); + tracer.recordBinary('http.status_code', status_code); + if (status_code >= 400) { + tracer.recordBinary('error', 'true'); + } + tracer.recordBinary('http.method', reqMethod.toUpperCase()); + if (process.env.APM_TENANT_ID){ + tracer.recordBinary('tenant.id', process.env.APM_TENANT_ID); + } + tracer.recordBinary('edge.request', '' + edgeRequest); + tracer.recordBinary('request.type', 'https'); + tool.recordIbmapmContext(tracer, ibmapmContext); tracer.recordAnnotation(new Annotation.ServerSend()); + logger.debug('https-tracer(after): ', tracer.id); }); } }); @@ -137,7 +195,7 @@ HttpsProbeZipkin.prototype.attach = function(name, target) { /* * Custom req.url parser that strips out any trailing query */ -var parse = function(url) { +function parse(url) { ['?', '#'].forEach(function(separator) { var index = url.indexOf(separator); if (index !== -1) url = url.substring(0, index); @@ -145,5 +203,4 @@ var parse = function(url) { return url; }; - module.exports = HttpsProbeZipkin; diff --git a/test/unit/aspect.test.js b/test/unit/aspect.test.js index e75ebc4..d8896a6 100644 --- a/test/unit/aspect.test.js +++ b/test/unit/aspect.test.js @@ -40,7 +40,7 @@ describe('aspect', () => { }); it('should not find a callback among parameters with no callback', () => { const args = ['a', {}, 123]; - expect(findCallbackArg(args) == undefined).to.be.ok; + expect(findCallbackArg(args) === undefined).to.be.ok; }); }); describe('aroundCallBack', () => {