@risingstack/trace
Version:
RisingStack Trace Node.js collector
335 lines (285 loc) • 10.7 kB
JavaScript
var https = require('https')
var url = require('url')
var util = require('util')
var requestSync = require('sync-request')
var isNumber = require('lodash.isnumber')
var debug = require('debug')('risingstack/trace')
var assign = require('lodash.assign')
var HttpsProxyAgent = require('https-proxy-agent')
var bl = require('bl')
var libPackage = require('../../../package')
function CollectorApi (options) {
this.COLLECTOR_API_SAMPLE = url.resolve(options.collectorApiUrl, options.collectorApiSampleEndpoint)
this.COLLECTOR_API_SERVICE = url.resolve(options.collectorApiUrl, options.collectorApiServiceEndpoint)
this.COLLECTOR_API_METRICS = url.resolve(options.collectorApiUrl, options.collectorApiApmMetricsEndpoint)
this.COLLECTOR_API_RPM_METRICS = url.resolve(options.collectorApiUrl, options.collectorApiRpmMetricsEndpoint)
this.COLLECTOR_API_INCOMING_EDGE_METRICS = url.resolve(options.collectorApiUrl, options.collectorApiIncomingEdgeMetricsEndpoint)
this.COLLECTOR_API_EXTERNAL_EDGE_METRICS = url.resolve(options.collectorApiUrl, options.collectorApiExternalEdgeMetricsEndpoint)
this.COLLECTOR_API_HEALTHCHECK = url.resolve(options.collectorApiUrl, options.collectorApiHealthcheckEndpoint)
this.COLLECTOR_API_PROFILER_MEMORY_HEAPDUMP = url.resolve(options.collectorApiUrl, options.collectorApiProfilerMemoryHeapdumpEndpoint)
this.COLLECTOR_API_PROFILER_CPU_PROFILE = url.resolve(options.collectorApiUrl, options.collectorApiProfilerCpuProfileEndpoint)
this.COLLECTOR_API_CONTROL = url.resolve(options.collectorApiUrl, options.collectorApiControlEndpoint)
this.COLLECTOR_API_CUSTOM_METRICS = url.resolve(options.collectorApiUrl, options.collectorApiCustomMetrics)
this.COLLECTOR_API_SECURITY_DEPENDENCIES = url.resolve(options.collectorApiUrl, options.collectorApiSecurityDependenciesEndpoint)
this.collectorLanguage = options.collectorLanguage
this.apiKey = options.apiKey
this.system = options.system
this.serviceName = options.serviceName
this.baseRetryInterval = 1000 * 60 * 30 // 30 minutes
this.serviceKey = null
if (options.proxy) {
this.proxyAgent = new HttpsProxyAgent(options.proxy)
}
}
// USE THIS WITH CAUTION, IT WILL BE BLOCKING
CollectorApi.prototype._sendSync = function (destinationUrl, data) {
debug('sending data to trace servers sync: ', JSON.stringify(data))
try {
requestSync('POST', destinationUrl, {
json: data,
headers: {
'Authorization': 'Bearer ' + this.apiKey,
'Content-Type': 'application/json',
'X-Reporter-Version': libPackage.version,
'X-Reporter-Language': this.collectorLanguage
},
timeout: 1000
})
} catch (ex) {
debug('error sending data to trace servers sync: ', ex)
}
}
CollectorApi.prototype._withInstanceInfo = function (data) {
return assign({
hostname: this.system.hostname,
pid: this.system.processId
}, data)
}
CollectorApi.prototype._send = function (destinationUrl, data, callback) {
var opts = url.parse(destinationUrl)
var payload = JSON.stringify(data)
callback = callback || function () {}
var requestOptions = {
hostname: opts.hostname,
port: opts.port,
path: opts.path,
method: 'POST',
// if the proxy is not set, it will fallback to the default agent
agent: this.proxyAgent,
headers: {
'Authorization': 'Bearer ' + this.apiKey,
'Content-Type': 'application/json',
'X-Reporter-Version': libPackage.version,
'X-Reporter-Language': this.collectorLanguage,
'Content-Length': payload.length
}
}
var req = https.request(requestOptions, function (res) {
res.setEncoding('utf8')
res.pipe(bl(function (err, result) {
if (err) {
debug('There was an error when connecting to the Trace API', err)
return
}
callback(null, result)
debug('HTTP Traces sent successfully')
}))
})
debug('sending data to trace servers: ', destinationUrl, payload)
req.on('error', function (error) {
console.error('error: [trace]', 'There was an error connecting to the Trace servers. Make sure your servers can reach', opts.hostname)
debug('error connecting to the Trace servers', error)
callback(error)
})
req.write(payload)
req.end()
}
CollectorApi.prototype.sendRpmMetrics = function (data) {
if (!isNumber(this.serviceKey)) {
debug('Service id not present, cannot send rpm metrics')
return
}
var url = util.format(this.COLLECTOR_API_RPM_METRICS, this.serviceKey)
this._send(url, this._withInstanceInfo(data))
}
CollectorApi.prototype.ping = function () {
if (!isNumber(this.serviceKey)) {
debug('Service id not present, cannot do healthcheck')
return
}
var url = util.format(this.COLLECTOR_API_HEALTHCHECK, this.serviceKey)
this._send(url, this._withInstanceInfo({}))
}
CollectorApi.prototype.sendApmMetrics = function (data) {
if (!isNumber(this.serviceKey)) {
debug('Service id not present, cannot send metrics')
return
}
var url = util.format(this.COLLECTOR_API_METRICS, this.serviceKey)
this._send(url, this._withInstanceInfo(data))
}
CollectorApi.prototype.sendMemorySnapshot = function (data, callback) {
if (!isNumber(this.serviceKey)) {
debug('Service id not present, cannot send heapdump')
return callback(new Error('serviceKey is missing'))
}
var url = util.format(this.COLLECTOR_API_PROFILER_MEMORY_HEAPDUMP, this.serviceKey)
this._send(url, this._withInstanceInfo(data), callback)
}
CollectorApi.prototype.sendCpuProfile = function (data, callback) {
if (!isNumber(this.serviceKey)) {
debug('Service id not present, cannot send cpu profile')
return callback(new Error('serviceKey is missing'))
}
var url = util.format(this.COLLECTOR_API_PROFILER_CPU_PROFILE, this.serviceKey)
this._send(url, this._withInstanceInfo(data), callback)
}
CollectorApi.prototype.sendExternalEdgeMetrics = function (data) {
if (!isNumber(this.serviceKey)) {
debug('Service id not present, cannot send metrics')
return
}
var url = util.format(this.COLLECTOR_API_EXTERNAL_EDGE_METRICS, this.serviceKey)
this._send(url, this._withInstanceInfo(data))
}
CollectorApi.prototype.sendIncomingEdgeMetrics = function (data) {
if (!isNumber(this.serviceKey)) {
debug('Service id not present, cannot send metrics')
return
}
var url = util.format(this.COLLECTOR_API_INCOMING_EDGE_METRICS, this.serviceKey)
this._send(url, this._withInstanceInfo({
timestamp: (new Date()).toISOString(),
data: data
}))
}
CollectorApi.prototype.getUpdates = function (data, callback) {
if (!isNumber(this.serviceKey)) {
debug('Service id not present, cannot get updates')
return
}
var url = util.format(this.COLLECTOR_API_CONTROL, this.serviceKey)
this._send(url, this._withInstanceInfo({
latestCommandId: data.latestCommandId
}), function (err, response) {
if (err) {
return callback(err)
}
try {
callback(null, JSON.parse(response.toString('utf8')))
} catch (ex) {
return callback(ex)
}
})
}
CollectorApi.prototype.sendCustomMetrics = function (data) {
if (!isNumber(this.serviceKey)) {
debug('Service id not present, cannot send metrics')
return
}
var url = util.format(this.COLLECTOR_API_CUSTOM_METRICS, this.serviceKey)
this._send(url, this._withInstanceInfo({
timestamp: (new Date()).toISOString(),
data: data
}))
}
CollectorApi.prototype.sendSamples = function (samples, sync) {
var url = this.COLLECTOR_API_SAMPLE
var metadata = {
version: '2',
instance: this._withInstanceInfo({}),
service: {
key: this.serviceKey ? this.serviceKey : undefined,
name: this.serviceName
}
}
var data = assign({}, samples, metadata)
sync ? this._sendSync(url, data) : this._send(url, data)
}
CollectorApi.prototype.sendDependencies = function (dependencies) {
var url = util.format(this.COLLECTOR_API_SECURITY_DEPENDENCIES, this.serviceKey)
this._send(url, dependencies)
}
CollectorApi.prototype._getRetryInterval = function () {
debug('retrying with %d ms', this.baseRetryInterval)
return this.baseRetryInterval
}
CollectorApi.prototype.getService = function (cb) {
debug('getting service id from the trace servers')
var opts = url.parse(this.COLLECTOR_API_SERVICE)
var _this = this
cb = cb || function () {}
var payload = JSON.stringify({
name: _this.serviceName,
version: '2',
collector: {
language: _this.collectorLanguage,
version: libPackage.version
},
runtime: {
name: _this.system.processName,
version: _this.system.processVersion,
pid: _this.system.processId
},
machine: {
arch: _this.system.osArch,
platform: _this.system.osPlatform,
release: _this.system.osRelease,
hostname: _this.system.hostname,
cpus: _this.system.cpus
}
})
var req = https.request({
hostname: opts.hostname,
port: opts.port,
path: opts.path,
method: 'POST',
headers: {
'Authorization': 'Bearer ' + this.apiKey,
'Content-Type': 'application/json',
'X-Reporter-Version': libPackage.version,
'X-Reporter-Language': this.collectorLanguage,
'Content-Length': payload.length
}
}, function (res) {
res.setEncoding('utf8')
res.pipe(bl(function (err, resBuffer) {
var response
if (err) {
debug('There was an error when connecting to the Trace API, retrying', err)
return setTimeout(function () {
_this.getService()
}, _this._getRetryInterval())
}
var resText = resBuffer.toString('utf8')
debug('raw response from trace servers: ', resText)
if (res.statusCode === 401) {
return console.error('error: [trace]', 'Trace API key is rejected - are you sure you are using the right one?')
}
if (res.statusCode > 399) {
return setTimeout(function () {
_this.getService(cb)
}, _this._getRetryInterval())
}
try {
response = JSON.parse(resText)
} catch (ex) {
debug('Error parsing JSON:', ex)
return debug(ex)
}
_this.serviceKey = response.key
cb(null, response.key)
}))
})
debug('getting serviceKey with payload:', payload)
req.on('error', function (error) {
console.error('error: [trace]', 'There was an error connecting to the Trace servers. Make sure your servers can reach', opts.hostname)
debug('error connecting to the Trace servers', error)
})
req.write(payload)
req.end()
}
function create (options) {
return new CollectorApi(options)
}
module.exports.create = create