elastic-apm-node
Version:
The official Elastic APM agent for Node.js
221 lines (185 loc) • 7.21 kB
JavaScript
'use strict'
var util = require('util')
var getPathFromRequest = require('./express-utils').getPathFromRequest
var GenericSpan = require('./generic-span')
var parsers = require('../parsers')
var Span = require('./span')
var symbols = require('../symbols')
module.exports = Transaction
util.inherits(Transaction, GenericSpan)
function Transaction (agent, name, type, opts = {}) {
GenericSpan.call(this, agent, type, opts)
const verb = this.parentId ? 'continue' : 'start'
agent.logger.debug('%s trace %o', verb, { trans: this.id, parent: this.parentId, trace: this.traceId, name: name, type: type })
agent._instrumentation.currentTransaction = this
agent._instrumentation.activeSpan = null
this._defaultName = name || ''
this._customName = ''
this._user = null
this._custom = null
this._result = 'success'
this._builtSpans = 0
this._droppedSpans = 0
this._contextLost = false // TODO: Send this up to the server some how
this._abortTime = 0
}
Object.defineProperty(Transaction.prototype, 'name', {
configurable: true,
enumerable: true,
get () {
// Fall back to a somewhat useful name in case no _defaultName is set.
// This might happen if res.writeHead wasn't called.
return this._customName ||
this._defaultName ||
(this.req ? this.req.method + ' unknown route (unnamed)' : 'unnamed')
},
set (name) {
if (this.ended) {
this._agent.logger.debug('tried to set transaction.name on already ended transaction %o', { trans: this.id, parent: this.parentId, trace: this.traceId })
return
}
this._agent.logger.debug('setting transaction name %o', { trans: this.id, parent: this.parentId, trace: this.traceId, name: name })
this._customName = name
}
})
Object.defineProperty(Transaction.prototype, 'result', {
configurable: true,
enumerable: true,
get () {
return this._result
},
set (result) {
if (this.ended) {
this._agent.logger.debug('tried to set transaction.result on already ended transaction %o', { trans: this.id, parent: this.parentId, trace: this.traceId })
return
}
this._agent.logger.debug('setting transaction result %o', { trans: this.id, parent: this.parentId, trace: this.traceId, result: result })
this._result = result
}
})
Transaction.prototype.setUserContext = function (context) {
if (!context) return
this._user = Object.assign(this._user || {}, context)
}
Transaction.prototype.setCustomContext = function (context) {
if (!context) return
this._custom = Object.assign(this._custom || {}, context)
}
Transaction.prototype.startSpan = function (name, type, opts) {
if (!this.sampled) {
return null
}
if (this.ended) {
this._agent.logger.debug('transaction already ended - cannot build new span %o', { trans: this.id, parent: this.parentId, trace: this.traceId }) // TODO: Should this be supported in the new API?
return null
}
if (this._builtSpans >= this._agent._conf.transactionMaxSpans) {
this._droppedSpans++
return null
}
this._builtSpans++
// To be backwards compatible with the old API, we also accept a `traceparent` string
if (typeof opts === 'string') opts = { childOf: opts }
return new Span(this, name, type, opts)
}
Transaction.prototype.toJSON = function () {
var payload = {
id: this.id,
trace_id: this.traceId,
parent_id: this.parentId,
name: this.name,
type: this.type,
duration: this.duration(),
timestamp: this.timestamp,
result: String(this.result),
sampled: this.sampled,
context: undefined,
sync: this.sync,
span_count: {
started: this._builtSpans
}
}
if (this.sampled) {
payload.context = {
user: Object.assign(
{},
this.req && parsers.getUserContextFromRequest(this.req),
this._user
),
tags: this._labels || {},
custom: this._custom || {}
}
// Only include dropped count when spans have been dropped.
if (this._droppedSpans > 0) {
payload.span_count.dropped = this._droppedSpans
}
var conf = this._agent._conf
if (this.req) {
payload.context.request = parsers.getContextFromRequest(this.req, conf, 'transactions')
}
if (this.res) {
payload.context.response = parsers.getContextFromResponse(this.res, conf)
}
}
return payload
}
Transaction.prototype._encode = function () {
if (!this.ended) {
this._agent.logger.error('cannot encode un-ended transaction: %o', { trans: this.id, parent: this.parentId, trace: this.traceId })
return null
}
return this.toJSON()
}
Transaction.prototype.setDefaultName = function (name) {
this._agent.logger.debug('setting default transaction name: %s %o', name, { trans: this.id, parent: this.parentId, trace: this.traceId })
this._defaultName = name
}
Transaction.prototype.setDefaultNameFromRequest = function () {
var req = this.req
var path = getPathFromRequest(req, false, this._agent._conf.usePathAsTransactionName)
if (!path) {
this._agent.logger.debug('could not extract route name from request %o', {
url: req.url,
type: typeof path,
null: path === null, // because typeof null === 'object'
route: !!req.route,
regex: req.route ? !!req.route.regexp : false,
mountstack: req[symbols.expressMountStack] ? req[symbols.expressMountStack].length : false,
trans: this.id,
parent: this.parentId,
trace: this.traceId
})
path = 'unknown route'
}
this.setDefaultName(req.method + ' ' + path)
}
Transaction.prototype.ensureParentId = function () {
return this._context.ensureParentId()
}
Transaction.prototype.end = function (result, endTime) {
if (this.ended) {
this._agent.logger.debug('tried to call transaction.end() on already ended transaction %o', { trans: this.id, parent: this.parentId, trace: this.traceId })
return
}
if (result !== undefined && result !== null) {
this.result = result
}
if (!this._defaultName && this.req) this.setDefaultNameFromRequest()
this._timer.end(endTime)
this.ended = true
var trans = this._agent._instrumentation.currentTransaction
// These two edge-cases should normally not happen, but if the hooks into
// Node.js doesn't work as intended it might. In that case we want to
// gracefully handle it. That involves ignoring all spans under the given
// transaction as they will most likely be incomplete. We still want to send
// the transaction without any spans as it's still valuable data.
if (!trans) {
this._agent.logger.debug('WARNING: no currentTransaction found %o', { current: trans, spans: this._builtSpans, trans: this.id, parent: this.parentId, trace: this.traceId })
this._contextLost = true
} else if (trans !== this) {
this._agent.logger.debug('WARNING: transaction is out of sync %o', { other: trans.id, spans: this._builtSpans, trans: this.id, parent: this.parentId, trace: this.traceId })
this._contextLost = true
}
this._agent._instrumentation.addEndedTransaction(this)
this._agent.logger.debug('ended transaction %o', { trans: this.id, parent: this.parentId, trace: this.traceId, type: this.type, result: this.result, name: this.name })
}