@risingstack/trace
Version:
RisingStack Trace Node.js collector
537 lines (445 loc) • 12.5 kB
JavaScript
var EventEmitter = require('events').EventEmitter
var debug = require('debug')('risingstack/trace')
var uuid = require('node-uuid')
var cls = require('continuation-local-storage')
var microtime = require('../optionalDependencies/microtime')
var assign = require('lodash.assign')
var CollectorApi = require('./api')
var Metrics = require('./metrics')
var Healthcheck = require('./healthcheck')
var consts = require('../consts')
var ReservoirSampler = require('./reservoir_sampler')
var Profiler = require('./profiler')
var Control = require('./control')
var Security = require('./security')
var Timer = require('./timer')
var controlBus = new EventEmitter()
var REQUEST_ID = 'request-id'
var PARENT_COMM_ID = 'parent-comm-id'
var MUST_COLLECT_LIMIT = 100
function Agent (options) {
debug('Agent is initializing...')
var _this = this
this.cls = cls.createNamespace('trace')
this.collectorApi = CollectorApi.create(options.config)
// config
this.config = options.config
this.apmMetrics = Metrics.apm.create({
collectorApi: this.collectorApi,
config: this.config
})
this.healthcheck = Healthcheck.create({
collectorApi: this.collectorApi,
config: this.config
})
this.rpmMetrics = Metrics.rpm.create({
collectorApi: this.collectorApi,
config: this.config
})
this.externalEdgeMetrics = Metrics.externalEdge.create({
collectorApi: this.collectorApi,
config: this.config
})
this.incomingEdgeMetrics = Metrics.incomingEdge.create({
collectorApi: this.collectorApi,
config: this.config
})
this.memoryProfiler = Profiler.memory.create({
collectorApi: this.collectorApi,
config: this.config,
controlBus: controlBus
})
this.cpuProfiler = Profiler.cpu.create({
collectorApi: this.collectorApi,
config: this.config,
controlBus: controlBus
})
this.control = Control.create({
collectorApi: this.collectorApi,
config: this.config,
controlBus: controlBus
})
this.customMetrics = Metrics.customMetrics.create({
collectorApi: this.collectorApi,
config: this.config
})
this.security = Security.create({
collectorApi: this.collectorApi,
config: this.config
})
// TODO: The Tracer agent, to be extracted
this.name = 'Tracer'
this.collectInterval = this.config.collectInterval
this.totalRequestCount = 0
this.mustCollectCount = 0
// init required variables
this.partials = {}
this.reservoirSampler = new ReservoirSampler(MUST_COLLECT_LIMIT)
this.timer = new Timer(function () {
_this._send()
}, this.collectInterval)
this.tracer = this
//
this.agents = [
this.tracer,
this.apmMetrics,
this.healthcheck,
this.rpmMetrics,
this.externalEdgeMetrics,
this.incomingEdgeMetrics,
this.customMetrics,
this.memoryProfiler,
this.cpuProfiler,
this.control,
this.security
]
}
Agent.prototype.start = function () {
var _this = this
this.collectorApi.getService(function (err, serviceKey) {
if (err) {
return debug(err.message)
}
debug('Agent serviceKey is set to: ', serviceKey)
_this.serviceKey = serviceKey
_this._startAll()
})
}
Agent.prototype._startAll = function () {
this.agents.forEach(function (agent) {
debug(agent.name + ' started')
if (agent.timer != null) {
agent.timer.start()
}
})
}
Agent.prototype._stopAll = function () {
this.agents.forEach(function (agent) {
debug(agent.name + ' stopped')
if (agent.timer != null) {
agent.timer.end()
}
})
}
Agent.prototype.getServiceKey = function () {
return this.serviceKey
}
Agent.prototype.getConfig = function () {
return this.config
}
Agent.prototype.serverReceive = function (data) {
this.totalRequestCount++
var parentCommId = data.parentCommId
var span = this.openSpan(data.requestId)
var parentServiceId
if (!isNaN(data.parentServiceId)) {
parentServiceId = parseInt(data.parentServiceId, 10)
}
var transportDelay = data.time - data.originTime
this.incomingEdgeMetrics.report({
serviceKey: data.parentServiceId,
protocol: data.protocol,
transportDelay: transportDelay
})
span.events.push({
time: data.time || microtime.now(),
type: Agent.SERVER_RECV,
data: assign({}, data.protocolData
? data.protocolData
: {}, {
host: data.host,
protocol: data.protocol,
rpcId: parentCommId,
endpoint: data.url,
method: data.method,
parent: parentServiceId,
originTime: data.originTime
})
})
}
Agent.prototype.serverSend = function (data) {
var parentCommId = data.parentCommId
var span = this.findSpan(data.requestId)
if (!span) {
debug('span was not found for serverSend - it shouldn\'t happen', data.requestId, data.parentCommId)
return
}
span.events.push({
time: data.time || microtime.now(),
type: Agent.SERVER_SEND,
data: assign({}, data.protocolData
? data.protocolData
: {}, {
protocol: data.protocol,
rpcId: parentCommId,
status: data.status === consts.EDGE_STATUS.OK ? 'ok' : 'bad',
statusDescription: data.statusDescription
})
})
span.isForceSampled = span.isForceSampled || data.mustCollect === consts.MUST_COLLECT.ERROR
if (span.isForceSampled) {
this.reservoirSampler.addReturnsSuccess(span)
}
delete this.partials[data.requestId]
}
Agent.prototype.clientSend = function (data) {
var span = this.findSpan(data.requestId)
if (!span) {
return
}
if (data.err) {
span.isForceSampled = true
}
var event = {
time: data.time || microtime.now()
}
if (data.err) {
assign(event, {
type: Agent.ERROR,
data: {
rpcId: data.childCommId,
type: data.err.type,
message: data.err.message,
raw: data.err.raw
}
})
} else {
assign(event, {
type: Agent.CLIENT_SEND,
data: assign({}, data.protocolData
? data.protocolData
: {}, {
protocol: data.protocol,
rpcId: data.childCommId,
host: data.host,
endpoint: data.url,
method: data.method
})
})
}
span.events.push(event)
var hasServerReceive = span.events.some(function (event) {
return event.type === Agent.SERVER_RECV
})
var hasClientSend = span.events.some(function (event) {
return event.type === Agent.CLIENT_SEND
})
// for now worker requests are not supported, clean them up
if (!hasServerReceive && hasClientSend) {
delete this.partials[data.requestId]
}
}
Agent.prototype.clientReceive = function (data) {
var span = this.findSpan(data.requestId)
// report external edges only if service is not instrumented by Trace
if (typeof data.targetServiceKey === 'undefined') {
if (!data.statusCode || data.statusCode < 500) {
this.externalEdgeMetrics.report({
targetHost: data.host,
protocol: data.protocol,
status: data.status,
responseTime: data.responseTime
})
}
}
if (!span) {
return
}
data.time = data.time || this.getMicrotime()
span.events.push({
time: data.time,
type: Agent.CLIENT_RECV,
data: assign({}, data.protocolData
? data.protocolData
: {}, {
protocol: data.protocol,
rpcId: data.childCommId,
status: data.status === consts.EDGE_STATUS.OK ? 'ok' : 'bad',
statusDescription: data.statusDescription
})
})
}
Agent.prototype.onCrash = function (data) {
var span = this.findSpan(this.getRequestId())
var parentCommId = this.getParentCommId()
var crashWithoutSpan
if (!span) {
// we have a crash which does not belong to an open transaction
// creating one
crashWithoutSpan = true
parentCommId = this.generateCommId()
span = this.openSpan(this.generateRequestId())
span.events.push({
type: Agent.SERVER_RECV,
time: this.getMicrotime(),
data: {
method: 'ERROR',
endpoint: 'stacktrace',
rpcId: parentCommId
}
})
}
span.isForceSampled = true
span.events.push({
time: this.getMicrotime(),
type: Agent.ERROR,
data: {
rpcId: parentCommId,
type: 'system-error',
message: data.stackTrace.message,
raw: {
stack: this.config.disableStackTrace ? undefined : data.stackTrace.stack
}
}
})
if (crashWithoutSpan) {
// faking the server send event
span.events.push({
time: microtime.now(),
type: Agent.SERVER_SEND,
data: {
rpcId: parentCommId,
statusCode: 500
}
})
}
this.reservoirSampler.addReturnsSuccess(span)
this._send({
isSync: true
})
}
Agent.prototype.report = function (name, data, error) {
var requestId = this.getRequestId()
var parentCommId = this.getParentCommId()
var span = this.findSpan(requestId)
if (!span) {
return
}
var event = {
time: microtime.now()
}
if (error) {
span.isForceSampled = true
assign(event, {
type: Agent.ERROR,
data: {
rpcId: parentCommId,
name: name,
type: 'user-sent-error',
message: data.message,
raw: data
}
})
} else {
assign(event, {
type: Agent.USER_SENT,
data: {
rpcId: parentCommId,
name: name,
raw: data
}
})
}
// check if we already have a user-sent event with the same name
var isFound = span.events.some(function (event) {
return event.data && event.data.name === name
})
if (!isFound) {
span.events.push(event)
this.partials[requestId] = span
} else {
debug('Already has a user-sent event with the name "%s"', name)
}
}
Agent.prototype.reportError = function (errorName, errorData) {
this.report(errorName, errorData, true)
}
Agent.prototype.findSpan = function (requestId) {
var span
// check if we have a partial with the id
if (!requestId || !this.partials[requestId] || !this.partials[requestId].events) {
debug('missing transaction', requestId)
return
}
span = this.partials[requestId]
// check if we had an SR before, if not, discard the span
var hasServerReceive = span.events.some(function (event) {
return event.type === Agent.SERVER_RECV
})
if (!hasServerReceive) {
debug('no SR found in findSpan, returning')
return
}
return span
}
Agent.prototype.openSpan = function (requestId) {
if (!requestId) {
return
}
var newSpan = {
requestId: requestId,
isSampled: false,
isForceSampled: false,
events: []
}
this.partials[requestId] = this.partials[requestId] || newSpan
return this.partials[requestId]
}
Agent.prototype.setRequestId = function (value) {
return this.cls.set(REQUEST_ID, value)
}
Agent.prototype.getRequestId = function () {
return this.cls.get(REQUEST_ID)
}
Agent.prototype.setParentCommId = function (value) {
return this.cls.set(PARENT_COMM_ID, value)
}
Agent.prototype.getParentCommId = function () {
return this.cls.get(PARENT_COMM_ID)
}
Agent.prototype.clearRequest = function (requestId) {
delete this.partials[requestId]
}
Agent.prototype.bind = function (fn) {
return this.cls.bind(fn)
}
Agent.prototype.bindEmitter = function (emitter) {
return this.cls.bindEmitter(emitter)
}
Agent.prototype.generateCommId = function () {
return this.generateId()
}
Agent.prototype.generateRequestId = function () {
return this.generateId()
}
Agent.prototype.generateId = function () {
return uuid.v4()
}
Agent.prototype.getMicrotime = function () {
return microtime.now()
}
Agent.prototype._send = function (options) {
debug('sending logs to the trace service')
var spans = this.reservoirSampler.getItems()
if (spans.length > 0) {
var dataBag = {
sample: {
rate: 1,
totalRequestCount: 1
},
spans: spans
}
this.totalRequestCount = 0
this.reservoirSampler = new ReservoirSampler(MUST_COLLECT_LIMIT)
this.collectorApi.sendSamples(dataBag, options && options.isSync)
}
}
module.exports.CLIENT_SEND = Agent.CLIENT_SEND = 'cs'
module.exports.CLIENT_RECV = Agent.CLIENT_RECV = 'cr'
module.exports.SERVER_SEND = Agent.SERVER_SEND = 'ss'
module.exports.SERVER_RECV = Agent.SERVER_RECV = 'sr'
module.exports.ERROR = Agent.ERROR = 'err'
module.exports.USER_SENT = Agent.USER_SENT = 'us'
module.exports.create = function (options) {
return new Agent(options)
}