newrelic
Version:
New Relic agent
416 lines (357 loc) • 14 kB
JavaScript
/*
* Copyright 2020 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
'use strict'
const Config = require('../config')
const { truncate } = require('../util/byte-limit')
const { DESTINATIONS } = require('../config/attribute-filter')
const { addSpanKind, isEntryPointSpan, reparentSpan, shouldCreateSpan, HTTP_LIBRARY, REGEXS, SPAN_KIND, CATEGORIES } = require('./helpers')
const EMPTY_USER_ATTRS = Object.freeze(Object.create(null))
const SERVER_ADDRESS = 'server.address'
const logger = require('../logger').child({ component: 'span-event' })
const { PARTIAL_TYPES } = require('../transaction')
/**
* This keeps a static list of attributes that are used by
* one or more entity relationship rules to synthesize an entity relationship.
*/
const SPAN_ENTITY_RELATIONSHIP_ATTRIBUTES = [
'cloud.account.id',
'cloud.platform',
'cloud.region',
'cloud.resource_id',
'db.instance',
'db.system',
'http.url',
'messaging.destination.name',
'messaging.system',
'peer.hostname',
'server.address',
'server.port',
'span.kind',
]
/**
* All the intrinsic attributes for span events, regardless of kind.
*/
class SpanIntrinsics {
constructor() {
this.type = 'Span'
this.traceId = null
this.guid = null
this.parentId = null
this.transactionId = null
this.sampled = null
this.priority = null
this.name = null
this.category = CATEGORIES.GENERIC
this.component = null
this.timestamp = null
this.duration = null
this['nr.entryPoint'] = null
this['nr.pg'] = null
this['span.kind'] = null
this.trustedParentId = null
this.tracingVendors = null
}
}
/**
* General span event class.
*
* Do not construct directly, instead use one of the static `from*` methods such
* as `SpanEvent.fromSegment`.
*
* @private
* @class
*/
class SpanEvent {
constructor(attributes, customAttributes) {
this.customAttributes = customAttributes
this.attributes = attributes
this.intrinsics = new SpanIntrinsics()
if (attributes.host) {
this.addAttribute(SERVER_ADDRESS, attributes.host)
attributes.host = null
}
if (attributes.port) {
this.addAttribute('server.port', attributes.port, true)
attributes.port = null
}
}
get parentId() {
return this.intrinsics.parentId
}
getIntrinsicAttributes() {
return this.intrinsics
}
addIntrinsics({ segment, spanContext, transaction, parentId, isRoot, inProcessSpans, entryPoint }) {
for (const [key, value] of Object.entries(spanContext.intrinsicAttributes)) {
this.addIntrinsicAttribute(key, value)
}
this.addIntrinsicAttribute('traceId', transaction.traceId)
this.addIntrinsicAttribute('transactionId', transaction.id)
this.addIntrinsicAttribute('sampled', transaction.sampled)
this.addIntrinsicAttribute('priority', transaction.priority)
this.addIntrinsicAttribute('name', segment.name)
this.addIntrinsicAttribute('guid', segment.id)
this.addIntrinsicAttribute('parentId', reparentSpan({ inProcessSpans, isRoot, segment, transaction, parentId }))
if (isRoot) {
this.addIntrinsicAttribute('trustedParentId', transaction.traceContext.trustedParentId)
if (transaction.traceContext.tracingVendors) {
this.addIntrinsicAttribute('tracingVendors', transaction.traceContext.tracingVendors)
}
}
// Only set this if it will be `true`. Must be `null` otherwise.
if (entryPoint) {
this.addIntrinsicAttribute('nr.entryPoint', true)
if (transaction.partialType) {
this.addIntrinsicAttribute('nr.pg', true)
}
}
// Timestamp in milliseconds, duration in seconds. Yay consistency!
this.addIntrinsicAttribute('timestamp', segment.timer.start)
this.addIntrinsicAttribute('duration', segment.timer.getDurationInMillis() / 1000)
}
addIntrinsicAttribute(key, value) {
this.intrinsics[key] = value
}
static get CATEGORIES() {
return CATEGORIES
}
static get DatastoreSpanEvent() {
return DatastoreSpanEvent
}
static get HttpSpanEvent() {
return HttpSpanEvent
}
static isExitSpan(span) {
const name = span.intrinsics?.name
return REGEXS.CLIENT.EXTERNAL.test(name) || REGEXS.CLIENT.DATASTORE.test(name) || REGEXS.PRODUCER.test(name)
}
static isLlmSpan(span) {
return span.intrinsics?.name?.startsWith('Llm/')
}
/**
* Drops span or filters attributes based on partial trace rules for a given mode.
* The rules are as such:
* - If not a partial trace, return span untouched
* - If an entry point span, return span untouched
* - If an LLM span, return span untouched
* - If not an exit span, return null(aka drop span)
* - If mode is 'reduced' and there are entity relationship attributes, return span untouched
* - If mode is 'essential' and there are entity relationship attributes or error attributes, return span with only those attributes, drop custom attributes
* - Otherwise return null(aka drop span)
*
* @param {object} params to function
* @param {SpanEvent} params.span span to apply rules to
* @param {boolean} params.entryPoint whether the span is an entry point
* @param {boolean} params.partialType mode of partial trace ('reduced', 'essential', 'compact') if the trace is partial
* @returns {SpanEvent|null} the span after applying the rules, or null if dropped
*/
static applyPartialTraceRules({ span, entryPoint, partialType }) {
const isLlmSpan = SpanEvent.isLlmSpan(span)
if (!partialType || entryPoint || isLlmSpan) {
logger.trace('Span %s is either not a partial trace, an entry point: %s, or an LLM span: %s, keeping span unchanged.', span.intrinsics.name, entryPoint, isLlmSpan)
return span
}
if (!SpanEvent.isExitSpan(span)) {
logger.trace('Span %s is not an exit span and trace is partial granularity type: %s.', span.intrinsics.name, partialType)
return null
}
const attributes = span.attributes
const attrKeys = Object.keys(attributes)
const entityRelationshipAttrs = SPAN_ENTITY_RELATIONSHIP_ATTRIBUTES.filter((item) => attrKeys.includes(item))
if (partialType === PARTIAL_TYPES.REDUCED) {
if (entityRelationshipAttrs.length === 0) {
logger.trace('Span %s does not contain any entity relationship attributes %j and trace is partial granularity type: %s, dropping span.', span.intrinsics.name, span.attributes, partialType)
return null
}
logger.trace('Span %s contains entity relationship attributes and trace is partial granularity type: %s, keeping span unchanged.', span.intrinsics.name, partialType)
} else if (partialType === PARTIAL_TYPES.ESSENTIAL) {
const attributesToKeep = Object.create(null)
for (const item in attributes) {
if (entityRelationshipAttrs.includes(item) || item.startsWith('error.')) {
attributesToKeep[item] = attributes[item]
}
}
if (Object.keys(attributesToKeep).length === 0) {
logger.trace('Span %s does not contain any entity relationship attributes %j and trace is partial granularity type: %s, dropping span.', span.intrinsics.name, span.attributes, partialType)
return null
}
span.attributes = attributesToKeep
span.customAttributes = Object.create(null)
logger.trace('Span %s contains entity relationship attributes and trace is partial granularity type: %s, only keeping entity relationship attributes and removing custom attributes.', span.intrinsics.name, partialType)
}
return span
}
static createSpan({ segment, attributes, customAttributes }) {
let span = null
if (HttpSpanEvent.testSegment(segment)) {
span = new HttpSpanEvent(attributes, customAttributes)
} else if (DatastoreSpanEvent.testSegment(segment)) {
span = new DatastoreSpanEvent(attributes, customAttributes)
} else {
span = new SpanEvent(attributes, customAttributes)
}
span.spanLinks = segment.spanLinks ?? []
return span
}
/**
* Constructs a `SpanEvent` from the given segment.
*
* The constructed span event will contain extra data depending on the
* category of the segment.
*
* @param {object} params params object
* @param {TraceSegment} params.segment segment to turn into a span event.
* @param {Transaction} params.transaction active transaction
* @param {?string} [params.parentId] ID of the segment's parent.
* @param {boolean} [params.isRoot] if segment is root segment; defaults to `false`
* @param {boolean} params.inProcessSpans if the segment is in-process, create span
* @returns {SpanEvent} The constructed event.
*/
static fromSegment({ segment, transaction, parentId = null, isRoot = false, inProcessSpans }) {
const entryPoint = isEntryPointSpan({ segment, transaction })
if (!inProcessSpans && !shouldCreateSpan({ entryPoint, segment, transaction })) {
return null
}
const spanContext = segment.getSpanContext()
// Since segments already hold span agent attributes and we want to leverage
// filtering, we add to the segment attributes prior to processing.
if (spanContext.hasError && !transaction.hasIgnoredErrorStatusCode()) {
const details = spanContext.errorDetails
segment.addSpanAttribute('error.message', details.message)
segment.addSpanAttribute('error.class', details.type)
if (details.expected) {
segment.addSpanAttribute('error.expected', details.expected)
}
}
const attributes = segment.attributes.get(DESTINATIONS.SPAN_EVENT)
const customAttributes = spanContext.customAttributes.get(DESTINATIONS.SPAN_EVENT)
const span = SpanEvent.createSpan({ segment, attributes, customAttributes })
span.addIntrinsics({ segment, spanContext, transaction, parentId, isRoot, inProcessSpans, entryPoint })
addSpanKind({ segment, span })
return SpanEvent.applyPartialTraceRules({ span, entryPoint, partialType: transaction.partialType })
}
toJSON() {
return [
_filterNulls(this.intrinsics),
this.customAttributes ? _filterNulls(this.customAttributes) : EMPTY_USER_ATTRS,
_filterNulls(this.attributes)
]
}
addCustomAttribute(key, value, truncateExempt = false) {
const { attributeFilter } = Config.getInstance()
const dest = attributeFilter.filterSegment(DESTINATIONS.SPAN_EVENT, key)
if (dest & DESTINATIONS.SPAN_EVENT) {
this.customAttributes[key] = truncateExempt ? value : _truncate(value)
}
}
addAttribute(key, value, truncateExempt = false) {
const { attributeFilter } = Config.getInstance()
const dest = attributeFilter.filterSegment(DESTINATIONS.SPAN_EVENT, key)
if (dest & DESTINATIONS.SPAN_EVENT) {
this.attributes[key] = truncateExempt ? value : _truncate(value)
}
}
}
/**
* Span event class for external requests.
*
* @private
* @class
*/
class HttpSpanEvent extends SpanEvent {
constructor(attributes, customAttributes) {
super(attributes, customAttributes)
this.addIntrinsicAttribute('category', CATEGORIES.HTTP)
this.addIntrinsicAttribute('component', attributes.library || HTTP_LIBRARY)
this.addIntrinsicAttribute('span.kind', SPAN_KIND.CLIENT)
if (attributes.library) {
attributes.library = null
}
if (attributes.url) {
this.addAttribute('http.url', attributes.url)
attributes.url = null
}
if (attributes.hostname) {
this.addAttribute(SERVER_ADDRESS, attributes.hostname)
attributes.hostname = null
}
if (attributes.procedure) {
this.addAttribute('http.method', attributes.procedure)
this.addAttribute('http.request.method', attributes.procedure)
attributes.procedure = null
}
}
static testSegment(segment) {
return REGEXS.CLIENT.EXTERNAL.test(segment.name)
}
}
/**
* Span event class for datastore operations and queries.
*
* @private
* @class
*/
class DatastoreSpanEvent extends SpanEvent {
constructor(attributes, customAttributes) {
super(attributes, customAttributes)
this.addIntrinsicAttribute('category', CATEGORIES.DATASTORE)
this.addIntrinsicAttribute('span.kind', SPAN_KIND.CLIENT)
if (attributes.product) {
this.addIntrinsicAttribute('component', attributes.product)
this.addAttribute('db.system', attributes.product)
attributes.product = null
}
if (attributes.collection) {
this.addAttribute('db.collection', attributes.collection)
attributes.collection = null
}
if (attributes.sql || attributes.sql_obfuscated) {
let sql = null
if (attributes.sql_obfuscated) {
sql = _truncate(attributes.sql_obfuscated)
attributes.sql_obfuscated = null
} else if (attributes.sql) {
sql = _truncate(attributes.sql)
attributes.sql = null
}
// Flag as exempt from normal attribute truncation
this.addAttribute('db.statement', sql, true)
}
if (attributes.database_name) {
this.addAttribute('db.instance', attributes.database_name)
attributes.database_name = null
}
const serverAddress = attributes[SERVER_ADDRESS]
if (serverAddress) {
this.addAttribute('peer.hostname', serverAddress)
if (attributes.port_path_or_id) {
const address = `${serverAddress}:${attributes.port_path_or_id}`
this.addAttribute('peer.address', address)
this.addAttribute('server.port', attributes.port_path_or_id, true)
attributes.port_path_or_id = null
}
}
}
static testSegment(segment) {
return REGEXS.CLIENT.DATASTORE.test(segment.name)
}
}
function _truncate(val) {
let truncated = truncate(val, 1997)
if (truncated !== val) {
truncated += '...'
}
return truncated
}
function _filterNulls(obj) {
const out = Object.create(null)
for (const key in obj) {
if (obj[key] != null) {
out[key] = obj[key]
}
}
return out
}
module.exports = SpanEvent