newrelic
Version:
New Relic agent
494 lines (420 loc) • 14.2 kB
JavaScript
/*
* Copyright 2020 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
'use strict'
const {DESTINATIONS} = require('../../config/attribute-filter')
const logger = require('../../logger').child({component: 'segment'})
const Timer = require('../../timer')
const urltils = require('../../util/urltils')
const hashes = require('../../util/hashes')
const {Attributes} = require('../../attributes')
const ExclusiveCalculator = require('./exclusive-time-calculator')
const SpanContext = require('../../spans/span-context')
const NAMES = require('../../metrics/names')
const INSTANCE_UNKNOWN = 'unknown'
const STATE = {
EXTERNAL: 'EXTERNAL',
CALLBACK: 'CALLBACK'
}
const ATTRIBUTE_SCOPE = 'segment'
/**
* Initializes the segment and binds the recorder to itself, if provided.
*
* @constructor
* @classdesc
* TraceSegments are inserted to track instrumented function calls. Each one is
* bound to a transaction, given a name (used only internally to the framework
* for now), and has one or more children (that are also part of the same
* transaction), as well as an associated timer.
*
* @param {Transaction} transaction
* The transaction to which this segment will be bound.
*
* @param {string} name
* Human-readable name for this segment (e.g. 'http', 'net', 'express',
* 'mysql', etc).
*
* @param {?function} recorder
* Callback that takes a segment and a scope name as attributes (intended to be
* used to record metrics related to the segment).
*/
function TraceSegment(transaction, name, recorder) {
this.name = name
this.transaction = transaction
++transaction.numSegments
++transaction.agent.totalActiveSegments
++transaction.agent.segmentsCreatedInHarvest
if (recorder) {
transaction.addRecorder(recorder.bind(null, this))
}
this.attributes = new Attributes(ATTRIBUTE_SCOPE)
this.children = []
// Generate a unique id for use in span events.
this.id = hashes.makeId()
this.timer = new Timer()
this.internal = false
this.opaque = false
this.shim = null
// hidden class optimization
this.partialName = null
this._exclusiveDuration = null
this._collect = true
this.host = null
this.port = null
this.state = STATE.EXTERNAL
this.async = true
this.ignore = false
this.probe('new TraceSegment')
}
TraceSegment.prototype.getSpanContext = function getSpanContext() {
const config = this.transaction.agent.config
const spansEnabled = config.distributed_tracing.enabled && config.span_events.enabled
if (!this._spanContext && spansEnabled) {
this._spanContext = new SpanContext()
}
return this._spanContext
}
TraceSegment.prototype.addAttribute =
function addAttribute(key, value, truncateExempt = false) {
this.attributes.addAttribute(
DESTINATIONS.SEGMENT_SCOPE,
key,
value,
truncateExempt
)
}
TraceSegment.prototype.addSpanAttribute =
function addSpanAttribute(key, value, truncateExempt = false) {
this.attributes.addAttribute(
DESTINATIONS.SPAN_EVENT,
key,
value,
truncateExempt
)
}
TraceSegment.prototype.addSpanAttributes =
function addSpanAttributes(attributes) {
this.attributes.addAttributes(
DESTINATIONS.SPAN_EVENT,
attributes
)
}
TraceSegment.prototype.getAttributes = function getAttributes() {
return this.attributes.get(DESTINATIONS.TRANS_SEGMENT)
}
TraceSegment.prototype.getSpanId = function getSpanId() {
const conf = this.transaction.agent.config
const enabled = conf.span_events.enabled && conf.distributed_tracing.enabled
if (enabled) {
return this.id
}
return null
}
/**
* @param {string} host
* The name of the host of the database. This will be normalized if the string
* represents localhost.
*
* @param {string|number} port
* The database's port, path to unix socket, or id.
*
* @param {string|number|bool} database
* The name or ID of the database that was connected to. Or `false` if there is
* no database name (i.e. Redis has no databases, only hosts).
*/
TraceSegment.prototype.captureDBInstanceAttributes = captureDBInstanceAttributes
function captureDBInstanceAttributes(host, port, database) {
var config = this.transaction.agent.config
var dsTracerConf = config.datastore_tracer
// Add database name if provided and enabled.
if (database !== false && dsTracerConf.database_name_reporting.enabled) {
this.addAttribute(
'database_name',
typeof database === 'number' ? database : (database || INSTANCE_UNKNOWN)
)
}
// Add instance information if enabled.
if (dsTracerConf.instance_reporting.enabled) {
// Determine appropriate defaults for host and port.
port = port || INSTANCE_UNKNOWN
if (host && urltils.isLocalhost(host)) {
host = config.getHostnameSafe(host)
}
if (!host || host === 'UNKNOWN_BOX') { // Config's default name of a host.
host = INSTANCE_UNKNOWN
}
this.addAttribute('host', host)
this.addAttribute('port_path_or_id', String(port))
}
}
TraceSegment.prototype.moveToCallbackState = function moveToCallbackState() {
this.state = STATE.CALLBACK
}
TraceSegment.prototype.isInCallbackState = function isInCallbackState() {
return this.state === STATE.CALLBACK
}
TraceSegment.prototype.probe = function probe(action) {
if (this.transaction.traceStacks) {
this.transaction.probe(action, {segment: this.name})
}
}
/**
* For use when a transaction is ending. The transaction segment should
* be named after the transaction it belongs to (which is only known by
* the end).
*/
TraceSegment.prototype.setNameFromTransaction = function setNameFromTransaction() {
var transaction = this.transaction
// transaction name and transaciton segment name must match
this.name = transaction.getFullName()
// partialName is used to name apdex metrics when recording
this.partialName = transaction._partialName
}
/**
* Once a transaction is named, the web segment also needs to be updated to
* match it (which implies this method must be called subsequent to
* transaction.finalizeNameFromUri). To properly name apdex metrics during metric
* recording, it's also necessary to copy the transaction's partial name. And
* finally, marking the trace segment as being a web segment copies the
* segment's parameters onto the transaction.
*/
TraceSegment.prototype.markAsWeb = function markAsWeb() {
var transaction = this.transaction
this.setNameFromTransaction()
var traceAttrs = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE)
Object.keys(traceAttrs).forEach((key) => {
if (!this.attributes.has(key)) {
this.addAttribute(key, traceAttrs[key])
}
})
}
/**
* A segment attached to something evented (such as a database
* cursor) just finished an action, so set the timer to mark
* the timer as having a stop time.
*/
TraceSegment.prototype.touch = function touch() {
this.probe('Touched')
this.timer.touch()
this._updateRootTimer()
}
TraceSegment.prototype.overwriteDurationInMillis = overwriteDurationInMillis
function overwriteDurationInMillis(duration, start) {
this.timer.overwriteDurationInMillis(duration, start)
}
TraceSegment.prototype.start = function start() {
this.timer.begin()
}
/**
* Stop timing the related action.
*/
TraceSegment.prototype.end = function end() {
if (!this.timer.isActive()) return
this.probe('Ended')
this.timer.end()
this._updateRootTimer()
}
TraceSegment.prototype.finalize = function finalize() {
if (this.timer.softEnd()) {
this._updateRootTimer()
// timer.softEnd() returns true if the timer was ended prematurely, so
// in that case we can name the segment as truncated
this.name = NAMES.TRUNCATED.PREFIX + this.name
}
this.addAttribute(
'nr_exclusive_duration_millis',
this.getExclusiveDurationInMillis()
)
}
/**
* Helper to set the end of the root timer to this segment's root if it is later
* in time.
*/
TraceSegment.prototype._updateRootTimer = function _updateRootTimer() {
var root = this.transaction.trace.root
if (this.timer.endsAfter(root.timer)) {
var newDuration = (
this.timer.start +
this.getDurationInMillis() -
root.timer.start
)
root.overwriteDurationInMillis(newDuration)
}
}
/**
* Test to see if underlying timer is still active
*
* @returns {boolean} true if no longer active, else false.
*/
TraceSegment.prototype._isEnded = function _isEnded() {
return !this.timer.isActive() || this.timer.touched
}
/**
* Add a new segment to a scope implicitly bounded by this segment.
*
* @param {string} childName New human-readable name for the segment.
* @returns {TraceSegment} New nested TraceSegment.
*/
TraceSegment.prototype.add = function add(childName, recorder) {
if (this.opaque) {
logger.trace('Skipping child addition on opaque segment')
return this
}
logger.trace('Adding segment %s to %s in %s', childName, this.name, this.transaction.id)
var segment = new TraceSegment(this.transaction, childName, recorder)
var config = this.transaction.agent.config
if (this.transaction.trace.segmentsSeen++ >= config.max_trace_segments) {
segment._collect = false
}
this.children.push(segment)
if (config.debug && config.debug.double_linked_transactions) {
segment.parent = this
}
return segment
}
/**
* Set the duration of the segment explicitly.
*
* @param {Number} duration Duration in milliseconds.
*/
TraceSegment.prototype.setDurationInMillis = setDurationInMillis
function setDurationInMillis(duration, start) {
this.timer.setDurationInMillis(duration, start)
}
TraceSegment.prototype.getDurationInMillis = function getDurationInMillis() {
return this.timer.getDurationInMillis()
}
/**
* Only for testing!
*
* @param {number} duration Milliseconds of exclusive duration.
*/
TraceSegment.prototype._setExclusiveDurationInMillis = _setExclusiveDurationInMillis
function _setExclusiveDurationInMillis(duration) {
this._exclusiveDuration = duration
}
/**
* The duration of the transaction trace tree that only this level accounts
* for.
*
* @return {integer} The amount of time the trace took, minus any child
* segments, in milliseconds.
*/
TraceSegment.prototype.getExclusiveDurationInMillis = getExclusiveDurationInMillis
function getExclusiveDurationInMillis() {
if (this._exclusiveDuration == null) {
// Calculate the exclusive time for the subtree rooted at `this`
const calculator = new ExclusiveCalculator(this)
calculator.process()
}
return this._exclusiveDuration
}
TraceSegment.prototype.getChildren = function getChildren() {
var children = []
for (var i = 0, len = this.children.length; i < len; ++i) {
if (!this.children[i].ignore) {
children.push(this.children[i])
}
}
return children
}
TraceSegment.prototype.getCollectedChildren = function getCollectedChildren() {
var children = []
for (var i = 0, len = this.children.length; i < len; ++i) {
if (this.children[i]._collect && !this.children[i].ignore) {
children.push(this.children[i])
}
}
return children
}
/**
* Enumerate the timings of this segment's descendants.
*
* @param {Number} end The end of this segment, to keep the calculated
* duration from exceeding the duration of the
* parent. Defaults to Infinity.
*
* @returns {Array} Unsorted list of [start, end] pairs, with no pair
* having an end greater than the passed in end time.
*/
TraceSegment.prototype._getChildPairs = function _getChildPairs(end) {
// quick optimization
if (this.children.length < 1) return []
if (!end) end = Infinity
var children = this.getChildren()
var childPairs = []
while (children.length) {
var child = children.pop()
var pair = child.timer.toRange()
if (pair[0] >= end) continue
children = children.concat(child.getChildren())
pair[1] = Math.min(pair[1], end)
childPairs.push(pair)
}
return childPairs
}
/**
* This is perhaps the most poorly-documented element of transaction traces:
* what do each of the segment representations look like prior to encoding?
* Spelunking in the code for the other agents has revealed that each child
* node is an array with the following field in the following order:
*
* 0: entry timestamp relative to transaction start time
* 1: exit timestamp
* 2: metric name
* 3: parameters as a name -> value JSON dictionary
* 4: any child segments
*
* Other agents include further fields in this. I haven't gotten to the bottom
* of all of them (and Ruby, of course, sends marshalled Ruby object), but
* here's what I know so far:
*
* in Java:
* 5: class name
* 6: method name
*
* in Python:
* 5: a "label"
*
* FIXME: I don't know if it makes sense to add custom fields for Node. TBD
*/
TraceSegment.prototype.toJSON = function toJSON() {
// use depth-first search on the segment tree using stack
const resultDest = []
// array of objects relating a segment and the destination for its
// serialized data.
const segmentsToProcess = [{
segment: this,
destination: resultDest
}]
while (segmentsToProcess.length !== 0) {
const {segment, destination} = segmentsToProcess.pop()
const start = segment.timer.startedRelativeTo(segment.transaction.trace.root.timer)
const duration = segment.getDurationInMillis()
const segmentChildren = segment.getCollectedChildren()
const childArray = []
// push serialized data into the specified destination
destination.push([
start,
start + duration,
segment.name,
segment.getAttributes(),
childArray
])
if (segmentChildren.length) {
// push the children and the parent's children array into the stack.
// to preserve the chronological order of the children, push them
// onto the stack backwards (so the first one created is on top).
for (var i = segmentChildren.length - 1; i >= 0; --i) {
segmentsToProcess.push({
segment: segmentChildren[i],
destination: childArray
})
}
}
}
// pull the result out of the array we serialized it into
return resultDest[0]
}
module.exports = TraceSegment