newrelic
Version:
New Relic agent
302 lines (266 loc) • 11.7 kB
JavaScript
/*
* Copyright 2025 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
'use strict'
const logger = require('../../logger').child({ component: 'partial-trace' })
const { PARTIAL_TRACE } = require('../../metrics/names')
const { SPAN_EVENTS } = require('#agentlib/metrics/names.js')
/**
* A PartialTrace manages span processing for partial granularity traces.
* It handles span reparenting, compaction logic, and finalization of span events.
*/
class PartialTrace {
constructor(transaction) {
this.transaction = transaction
this.metrics = transaction.metrics
this.spans = []
this.droppedSpans = new Map()
this.compactSpanGroups = {}
this.type = transaction.partialType
}
/**
* Adds necessary partial tracing metrics
* - `Supportability/Nodejs/PartialGranularity/<partial granularity type>`
* - `Supportability/DistributedTrace/PartialGranularity/<partial granularity type>/Span/Instrumented`
* - `Supportability/DistributedTrace/PartialGranularity/<partial granularity type>/Span/Kept`
*
* @param {boolean} spanKept flag to tell if we should increment Kept metric
*/
createMetrics(spanKept) {
this.metrics.getOrCreateMetric(`${PARTIAL_TRACE.PREFIX}/${this.type}`).incrementCallCount()
const prefix = `${PARTIAL_TRACE.SPAN_PREFIX}/${this.type}`
this.metrics.getOrCreateMetric(`${prefix}${PARTIAL_TRACE.INSTRUMENTED}`).incrementCallCount()
if (spanKept) {
this.metrics.getOrCreateMetric(`${prefix}${PARTIAL_TRACE.KEPT}`).incrementCallCount()
}
}
/**
* Called in `lib/spans/span-event-aggregator` which creates the spans from trace
* Called when a transaction ends. Generates all the spans from trace.
* This eventually calls `SpanEventAggregator.addSegment`. instead of enqueuing
* span to SpanEventAggregator it will call `this.addSpan`
* When all spans have been generated and stored on partial trace, it will call
* `this.finalize`. Which takes care of reparenting or if compact will compact spans
*/
generateSpanEvents() {
this.transaction.trace.generateSpanEvents(this.transaction.trace.segments.root)
this.finalize()
}
/**
* Runs a span through partial tracing rules.
* If the span is null, it indicates that the span was dropped,
* and we must keep track of its id and parentId for potential reparenting.
* Also reparents span links from dropped spans.
*
* @param {object} params to function
* @param {string} params.span to apply partial rules to
* @param {boolean} params.isEntry flag indicating span is entry point span
*/
addSpan({ span, isEntry }) {
const id = span.id
const parentId = span.parentId
// capture necessary information for reparenting span links before span is potentially dropped
const spanLinks = span.spanLinks
const exitSpan = span.isExitSpan
const hasEntityAttrs = span.hasEntityRelationshipAttrs
span = span.applyPartialTraceRules({ isEntry, partialTrace: this })
this.createMetrics(!!span)
if (span) {
// span was not dropped, add to trace until all spans have been processed
this.spans.push(span)
} else if (this.type !== 'compact') {
// span was dropped so keep track of its id and parent as any spans
// whose parent id was dropped needs to update to this new id
// unless compact where all parentIds are assigned to the entry span
// in finalizeSpanEvents
this.droppedSpans.set(id, parentId)
// span was dropped but we still need to move its span links to the last kept span
// spanLinks were captured before the span was dropped
this.reparentSpanLinks(this.spans.at(-1), spanLinks)
} else if ((this.type === 'compact') && (!exitSpan || !hasEntityAttrs)) {
// we still need to reparent links for dropped spans in compact mode that are not exit
// span or spans without entity relationship attributes
this.reparentSpanLinks(this.spans.at(-1), spanLinks)
}
}
/**
* Iterates over the span links from a dropped span and reassigns them to the span to reparent to.
* The id intrinsic attribute will also be updated to the value of the last kept span id.
*
* @param {SpanEvent} span the span to reparent span links to
* @param {SpanLink[]} spanLinksToReparent an array of span links to reparent to last kept span
*/
reparentSpanLinks(span, spanLinksToReparent = []) {
// The id intrinsics attribute needs to be updated to equal the id of the new span the
// span links are moving to.
for (const link of spanLinksToReparent) {
if (span.spanLinks.length === 100) {
logger.trace('Span links limit reached. Not moving additional span links.')
this.transaction.agent.metrics.getOrCreateMetric(SPAN_EVENTS.LINKS_DROPPED).incrementCallCount()
return
}
link.intrinsics.id = span.id
span.spanLinks.push(link)
}
}
/**
* Iterates over dropped spans and reparents span if its current parent was dropped.
* It'll traverse until it finds a parent that wasn't dropped or there are no more parents to check.
*
* @param {SpanEvent} span the span to potentially reparent
*/
maybeReparentSpan(span) {
let result = this.droppedSpans.get(span.parentId)
let count = 0
while (this.droppedSpans.has(result) && count < this.droppedSpans.size) {
result = this.droppedSpans.get(result)
count++
}
if (result) {
logger.debug(`Reparenting span ${span.id} from parent ${span.parentId} to ${result}`)
span.addIntrinsicAttribute('parentId', result)
}
}
/**
* Checks if span has error attributes. If no error has been stored metadata,
* store incoming one. otherwise check if the incoming span started later, if so store
*
* @param {object} meta metadata for a given applyCompaction run
* @param {SpanEvent} span an exit span to the same entity as retained span
*/
compactionError(meta, span) {
// store the error that occurs in all exit spans
if (span.hasErrorAttrs) {
if (!meta.errorSpan) {
meta.errorSpan = span
logger.trace('Partial trace is compact, found an error to use from span %s, error attrs %s', span.intrinsics.name, span.errorAttrs)
} else if (span.intrinsics.timestamp > meta.errorSpan.intrinsics.timestamp) {
meta.errorSpan = span
logger.trace('Partial trace is compact, span occurred after exiting error, re-assigning error to use from span %s, error attrs %s', span.intrinsics.name, span.errorAttrs)
}
}
}
/**
* Compares the metadata to decide if it needs to re-assign currentStart, currentEnd
* and/or increment totalDuration
*
* @param {object} meta metadata for a given applyCompaction run
* @param {SpanEvent} span an exit span to the same entity as retained span
*/
calculateDuration(meta, span) {
const start = span.intrinsics.timestamp / 1000
// duration is captured in seconds, need to convert to milliseconds as timestamp is in millis
const end = start + span.intrinsics.duration
if (meta.currentStart === null) {
// first interval
meta.currentStart = start
meta.currentEnd = end
} else if (meta.currentEnd >= start) {
// interval overlaps, extend the current end
meta.currentEnd = Math.max(meta.currentEnd, end)
} else {
// non-overlapping, add current interval duration and start new interval
meta.totalDuration += meta.currentEnd - meta.currentStart
meta.currentStart = start
meta.currentEnd = end
}
}
/**
* Checks if span had a parentId, if so it will reparent to entry span.
* If span was the retained exit span for a given entity and it has other spans that talked to the same entity, it will:
* sort all timestamps for spans that got dropped and calculate the `nr.durations`
* assign `nr.ids` for all dropped spans to same entity.
* reparent the span links from dropped spans to the retained exit span.
*
* @param {SpanEvent} span to check if it has to calculate `nr.durations` and `nr.ids`
*/
applyCompaction(span) {
if (span.parentId) {
logger.debug(`Partial trace is compact, reparenting span ${span.id} from parent ${span.parentId} to entry span ${this.transaction.baseSegment.name}${this.transaction.baseSegment.id}`)
span.addIntrinsicAttribute('parentId', this.transaction.baseSegment.id)
}
const sameEntitySpans = this.compactSpanGroups[span.id]
if (!sameEntitySpans) {
logger.trace('Partial trace is compact, but not an exit span, not assigning `nr.ids` nor `nr.durations` to span %s', span.intrinsics.name)
return
}
if (sameEntitySpans?.length < 2) {
logger.trace('Partial trace is compact, but no exit spans were dropped, not assigning `nr.ids` nor `nr.durations` to span %s', span.intrinsics.name)
return
}
const meta = {
ids: [],
droppedIds: 0,
totalDuration: 0,
currentStart: null,
currentEnd: null,
errorSpan: null,
spanLinks: []
}
// timestamps must be sorted to accurately calculate overlapping durations
sameEntitySpans.sort((a, b) => a.intrinsics.timestamp - b.intrinsics.timestamp)
for (let i = 0; i < sameEntitySpans.length; i++) {
const sameEntitySpan = sameEntitySpans[i]
// do not push its own id to `nr.ids`
// first span in array is always the retained exit span
if (i !== 0) {
// Max size for `nr.ids` = 1024. Max length = 63 (each span id is 16 bytes + 8 bytes for list type).
if (meta.ids.length < 63) {
meta.ids.push(sameEntitySpan.id)
} else {
meta.droppedIds++
}
Array.prototype.push.apply(meta.spanLinks, sameEntitySpan.spanLinks)
}
this.compactionError(meta, sameEntitySpan)
this.calculateDuration(meta, sameEntitySpan)
}
this.assignCompactAttrs({ span, meta })
this.reparentSpanLinks(span, meta.spanLinks)
}
assignCompactAttrs({ span, meta }) {
logger.trace('Partial trace is compact, assigning `nr.ids`: %s, `nr.durations`: %s, to span %s', meta.ids, meta.totalDuration, span.intrinsics.name)
// add the final interval duration
if (meta.currentStart !== null) {
meta.totalDuration += meta.currentEnd - meta.currentStart
}
if (meta.droppedIds > 0) {
this.transaction.metrics.getOrCreateMetric(PARTIAL_TRACE.DROPPED).incrementCallCount(meta.droppedIds)
}
span.addIntrinsicAttribute('nr.ids', meta.ids)
span.addIntrinsicAttribute('nr.durations', meta.totalDuration)
if (meta.errorSpan) {
for (const [key, value] of Object.entries(meta.errorSpan.errorAttrs)) {
// value of `error.expected` is a boolean, cannot truncate it
const truncateExempt = key === 'error.expected' ? true : false
span.addAttribute(key, value, truncateExempt)
}
}
}
reset() {
this.spans.length = 0
this.droppedSpans.clear()
this.compactSpanGroups = {}
}
/**
* Finalizes span events for partial traces by reparenting spans
* if their parent was dropped, associates `nr.ids` and `nr.durations` intrinsics for
* partial traces of type `compact`. Lastly, adds the span events to the span event
* aggregator.
*
* Note: This is a no-op for full traces and traces using infinite tracing.
*/
finalize() {
for (const span of this.spans) {
if (this.type === 'compact') {
this.applyCompaction(span)
} else {
this.maybeReparentSpan(span)
}
this.transaction.agent.spanEventAggregator.add(span, this.transaction.priority)
}
this.reset()
}
}
module.exports = PartialTrace