Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Removed children from segments. #2689

Merged
merged 2 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/db/parsed-statement.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function ParsedStatement(type, operation, collection, raw) {

ParsedStatement.prototype.recordMetrics = function recordMetrics(segment, scope, transaction) {
const duration = segment.getDurationInMillis()
const exclusive = segment.getExclusiveDurationInMillis()
const exclusive = segment.getExclusiveDurationInMillis(transaction.trace)
const type = transaction.isWeb() ? DB.WEB : DB.OTHER
const thisTypeSlash = this.type + '/'
const operation = DB.OPERATION + '/' + thisTypeSlash + this.operation
Expand Down
13 changes: 0 additions & 13 deletions lib/instrumentation/nextjs/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,3 @@ utils.isMiddlewareInstrumentationSupported = function isMiddlewareInstrumentatio
semver.gte(version, MIN_MW_SUPPORTED_VERSION) && semver.lte(version, MAX_MW_SUPPORTED_VERSION)
)
}

/**
* Depending on the Next.js version the segment tree varies as it adds setTimeout segments.
* This util will find the segment that has `getServerSideProps` in the name
*
* @param {object} rootSegment trace root
* @returns {object} getServerSideProps segment
*/
utils.getServerSidePropsSegment = function getServerSidePropsSegment(rootSegment) {
return rootSegment.children[0].children.find((segment) =>
segment.name.includes('getServerSideProps')
)
}
2 changes: 1 addition & 1 deletion lib/metrics/recorders/custom.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const NAMES = require('../names')

function record(segment, scope, transaction) {
const duration = segment.getDurationInMillis()
const exclusive = segment.getExclusiveDurationInMillis()
const exclusive = segment.getExclusiveDurationInMillis(transaction.trace)
const name = NAMES.CUSTOM + NAMES.ACTION_DELIMITER + segment.name

if (scope) {
Expand Down
2 changes: 1 addition & 1 deletion lib/metrics/recorders/generic.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

function record(segment, scope, transaction) {
const duration = segment.getDurationInMillis()
const exclusive = segment.getExclusiveDurationInMillis()
const exclusive = segment.getExclusiveDurationInMillis(transaction.trace)

if (scope) {
transaction.measure(segment.name, scope, duration, exclusive)
Expand Down
2 changes: 1 addition & 1 deletion lib/metrics/recorders/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function recordWeb(segment, scope, tx) {

const duration = segment.getDurationInMillis()
const totalTime = tx.trace.getTotalTimeDurationInMillis()
const exclusive = segment.getExclusiveDurationInMillis()
const exclusive = segment.getExclusiveDurationInMillis(tx.trace)
const partial = segment.partialName
const config = tx.agent.config
// named / key transaction support requires per-name apdexT
Expand Down
2 changes: 1 addition & 1 deletion lib/metrics/recorders/http_external.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const EXTERNAL = require('../../metrics/names').EXTERNAL
function recordExternal(host, library) {
return function externalRecorder(segment, scope, transaction) {
const duration = segment.getDurationInMillis()
const exclusive = segment.getExclusiveDurationInMillis()
const exclusive = segment.getExclusiveDurationInMillis(transaction.trace)
const metricName = EXTERNAL.PREFIX + host + '/' + library
const rollupType = transaction.isWeb() ? EXTERNAL.WEB : EXTERNAL.OTHER
const rollupHost = EXTERNAL.PREFIX + host + '/all'
Expand Down
2 changes: 1 addition & 1 deletion lib/metrics/recorders/message-transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function recordMessageTransaction(segment, scope, tx) {
}

const duration = segment.getDurationInMillis()
const exclusive = segment.getExclusiveDurationInMillis()
const exclusive = segment.getExclusiveDurationInMillis(tx.trace)
const totalTime = tx.trace.getTotalTimeDurationInMillis()

if (scope) {
Expand Down
2 changes: 1 addition & 1 deletion lib/metrics/recorders/other.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function recordBackground(segment, scope, tx) {
}

const duration = segment.getDurationInMillis()
const exclusive = segment.getExclusiveDurationInMillis()
const exclusive = segment.getExclusiveDurationInMillis(tx.trace)
const totalTime = tx.trace.getTotalTimeDurationInMillis()
const name = segment.partialName

Expand Down
2 changes: 1 addition & 1 deletion lib/shim/datastore-shim.js
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ function _recordOperationMetrics(segment, scope, transaction) {
}

const duration = segment.getDurationInMillis()
const exclusive = segment.getExclusiveDurationInMillis()
const exclusive = segment.getExclusiveDurationInMillis(transaction.trace)
const type = transaction.isWeb() ? 'allWeb' : 'allOther'
const operation = segment.name

Expand Down
3 changes: 1 addition & 2 deletions lib/shim/shim.js
Original file line number Diff line number Diff line change
Expand Up @@ -1325,8 +1325,7 @@ function createSegment(name, recorder, parent) {
* @param {Shim} params.shim instance of shim
* @param {Transaction} params.transaction active transaction
* @param {TraceSegment} params.parent the segment that will be the parent of the newly created segment
* @param params.spec
* @param {string|specs.SegmentSpec} spec options for creating segment
* @param {string|specs.SegmentSpec} params.spec options for creating segment
* @returns {?TraceSegment} A new trace segment if a transaction is active, else
* `null` is returned.
*/
Expand Down
2 changes: 1 addition & 1 deletion lib/shim/webframework-shim/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ module.exports._recordMiddleware = function _recordMiddleware(shim, middleware,
function _makeMiddlewareRecorder(_shim, metricName) {
return function middlewareMetricRecorder(segment, scope, transaction) {
const duration = segment.getDurationInMillis()
const exclusive = segment.getExclusiveDurationInMillis()
const exclusive = segment.getExclusiveDurationInMillis(transaction.trace)

if (scope) {
transaction.measure(metricName, scope, duration, exclusive)
Expand Down
6 changes: 4 additions & 2 deletions lib/transaction/trace/exclusive-time-calculator.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
'use strict'

class ExclusiveCalculator {
constructor(root) {
constructor(root, trace) {
this.trace = trace
this.id = root.id
this.toProcess = [root]
// use a second stack to do a post-order traversal
this.parentStack = []
Expand All @@ -19,7 +21,7 @@ class ExclusiveCalculator {
process() {
while (this.toProcess.length) {
const segment = this.toProcess.pop()
const children = segment.getChildren()
const children = this.trace.getChildren(segment.id)
// when we hit a leaf, calc the exclusive time and report the time
// range to the parent
if (children.length === 0) {
Expand Down
175 changes: 125 additions & 50 deletions lib/transaction/trace/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function Trace(transaction) {
this.root.start()

this.intrinsics = Object.create(null)
this.segmentsSeen = 0
this.segments = []
this.totalTimeCache = null

this.custom = new Attributes(ATTRIBUTE_SCOPE, MAXIMUM_CUSTOM_ATTRIBUTES)
Expand All @@ -63,16 +63,11 @@ function Trace(transaction) {
* segments that support recording.
*/
Trace.prototype.end = function end() {
const segments = [this.root]
this.root.finalize(this)
const segments = this.segments

while (segments.length) {
const segment = segments.pop()
segment.finalize()

const children = segment.getChildren()
for (let i = 0; i < children.length; ++i) {
segments.push(children[i])
}
for (let i = 0; i < segments.length; i++) {
segments[i].finalize(this)
}
}

Expand All @@ -85,14 +80,13 @@ Trace.prototype.generateSpanEvents = function generateSpanEvents() {
if (!shouldGenerateSpanEvents(config, this.transaction)) {
return
}
const toProcess = []

// Root segment does not become a span, so we need to process it separately.
const spanAggregator = this.transaction.agent.spanEventAggregator

const children = this.root.getChildren()
const segments = this.segments

if (children.length > 0) {
if (segments.length > 0) {
// At the point where these attributes are available, we only have a
// root span. Adding attributes to first non-root span here.
const attributeMap = {
Expand All @@ -106,54 +100,35 @@ Trace.prototype.generateSpanEvents = function generateSpanEvents() {

for (const [key, value] of Object.entries(attributeMap)) {
if (value !== null) {
children[0].addSpanAttribute(key, value)
segments[0].addSpanAttribute(key, value)
}
}
}

for (let i = 0; i < children.length; ++i) {
toProcess.push(new DTTraceNode(children[i], this.transaction.parentSpanId, true))
}

while (toProcess.length) {
const segmentInfo = toProcess.pop()
const segment = segmentInfo.segment

for (let i = 0; i < segments.length; ++i) {
const segment = segments[i]
const isRoot = segment.parentId === this.root.id
const parentId = isRoot ? this.transaction.parentSpanId : segment.parentId
// Even though at some point we might want to stop adding events because all the priorities
// should be the same, we need to count the spans as seen.
spanAggregator.addSegment({
segment,
transaction: this.transaction,
parentId: segmentInfo.parentId,
isRoot: segmentInfo.isRoot
parentId,
isRoot
})

const nodes = segment.getChildren()
for (let i = 0; i < nodes.length; ++i) {
const node = new DTTraceNode(nodes[i], segment.id)
toProcess.push(node)
}
}
}

function shouldGenerateSpanEvents(config, txn) {
if (!config.distributed_tracing.enabled) {
return false
}
if (!config.span_events.enabled) {
if (!(config.distributed_tracing.enabled && config.span_events.enabled)) {
return false
}

const infiniteTracingConfigured = Boolean(config.infinite_tracing.trace_observer.host)
return infiniteTracingConfigured || txn.sampled
}

function DTTraceNode(segment, parentId, isRoot = false) {
this.segment = segment
this.parentId = parentId
this.isRoot = isRoot
}

/**
* Add a child to the list of segments.
*
Expand Down Expand Up @@ -220,7 +195,7 @@ Trace.prototype.addCustomAttribute = function addCustomAttribute(key, value) {
* traces, in milliseconds.
*/
Trace.prototype.getExclusiveDurationInMillis = function getExclusiveDurationInMillis() {
return this.root.getExclusiveDurationInMillis()
return this.root.getExclusiveDurationInMillis(this)
}

/**
Expand All @@ -234,16 +209,14 @@ Trace.prototype.getTotalTimeDurationInMillis = function getTotalTimeDurationInMi
if (this.totalTimeCache !== null) {
return this.totalTimeCache
}
if (this.root.children.length === 0) {
const segments = this.segments
if (segments.length === 0) {
return 0
}
const segments = this.root.getChildren()
let totalTimeInMillis = 0

while (segments.length !== 0) {
const segment = segments.pop()
totalTimeInMillis += segment.getExclusiveDurationInMillis()
segment.getChildren().forEach((childSegment) => segments.push(childSegment))
let totalTimeInMillis = 0
for (let i = 0; i < segments.length; i++) {
totalTimeInMillis += segments[i].getExclusiveDurationInMillis(this)
}

if (!this.transaction.isActive()) {
Expand Down Expand Up @@ -366,6 +339,104 @@ Trace.prototype._getRequestUri = function _getRequestUri() {
return requestUri
}

/**
* Gets all children of a segment.
*
* @param {number} id of segment
* @returns {Array.<TraceSegment>} list of all segments that have the parentId of the segment
*/
Trace.prototype.getChildren = function getChildren(id) {
return this.segments.filter((segment) => segment.parentId === id)
}

/**
* Gets all children of a segment that should be collected and not ignored.
*
* @param {number} id of segment
* @returns {Array.<TraceSegment>} list of all segments that have the parentId of the segment
*/
Trace.prototype.getCollectedChildren = function getCollectedChildren(id) {
return this.segments.filter(
(segment) => segment.parentId === id && segment._collect && !segment.ignore
)
}

/**
* Gets the parent segment from list of segments on trace by passing in the `parentId`
* and matching on the `segment.id`
*
* @param {number} parentId id of parent segment you want to retrieve
* @returns {TraceSegment} parent segment
*/
Trace.prototype.getParent = function getParent(parentId) {
return this.segments.filter((segment) => segment.id === parentId)[0]
}

/**
* This is perhaps the most poorly-documented element of transaction traces:
bizob2828 marked this conversation as resolved.
Show resolved Hide resolved
* 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
*/
Trace.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.root,
destination: resultDest
}
]

while (segmentsToProcess.length !== 0) {
const { segment, destination } = segmentsToProcess.pop()
const start = segment.timer.startedRelativeTo(this.root.timer)
const duration = segment.getDurationInMillis()

const segmentChildren = this.getCollectedChildren(segment.id)
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 (let 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]
}

/**
* Serializes the trace into the expected JSON format to be sent.
*
Expand All @@ -379,17 +450,21 @@ Trace.prototype._serializeTrace = function _serializeTrace() {
intrinsics: this.intrinsics
}

return [
const trace = [
this.root.timer.start * FROM_MILLIS,
{}, // moved to agentAttributes
{
// hint to RPM for how to display this trace's segments
nr_flatten_leading: false
}, // moved to userAttributes
this.root.toJSON(),
this.toJSON(),
attributes,
[] // FIXME: parameter groups
]

// clear out segments
this.segments = []
return trace
}

module.exports = Trace
Loading
Loading