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

add span events as a top level field for v0.4 encoding #5229

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
7 changes: 6 additions & 1 deletion packages/dd-trace/src/encode/0.4.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class AgentEncoder {
process.env.DD_TRACE_ENCODING_DEBUG,
false
))
this._format = 'v0.4'
}

count () {
Expand Down Expand Up @@ -112,6 +113,10 @@ class AgentEncoder {
this._encodeMap(bytes, span.meta)
this._encodeString(bytes, 'metrics')
this._encodeMap(bytes, span.metrics)
if (span.span_events) {
this._encodeString(bytes, 'span_events')
this._encodeObjectAsArray(bytes, span.span_events, new Set())
}
if (span.meta_struct) {
this._encodeString(bytes, 'meta_struct')
this._encodeMetaStruct(bytes, span.meta_struct)
Expand Down Expand Up @@ -319,4 +324,4 @@ class AgentEncoder {
}
}

module.exports = { AgentEncoder }
module.exports = { AgentEncoder, SOFT_LIMIT }
7 changes: 6 additions & 1 deletion packages/dd-trace/src/encode/0.5.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict'

const { truncateSpan, normalizeSpan } = require('./tags-processors')
const { AgentEncoder: BaseEncoder } = require('./0.4')
const { AgentEncoder: BaseEncoder, SOFT_LIMIT } = require('./0.4')

const ARRAY_OF_TWO = 0x92
const ARRAY_OF_TWELVE = 0x9c
Expand All @@ -11,6 +11,11 @@ function formatSpan (span) {
}

class AgentEncoder extends BaseEncoder {
constructor (writer, limit = SOFT_LIMIT) {
super(writer, limit)
this._format = 'v0.5'
}

makePayload () {
const prefixSize = 1
const stringSize = this._stringBytes.length + 5
Expand Down
8 changes: 8 additions & 0 deletions packages/dd-trace/src/exporters/agent/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const { URL, format } = require('url')
const log = require('../../log')
const Writer = require('./writer')
const { fetchAgentInfo } = require('../common/util')

class AgentExporter {
constructor (config, prioritySampler) {
Expand All @@ -14,6 +15,13 @@ class AgentExporter {
port
}))

this._agentSupportsTopLevelSpanEvents = fetchAgentInfo(this._url, (err, agentInfo) => {
if (err) {
return false
}
return agentInfo?.span_events === 'true'
})

const headers = {}
if (stats.enabled || appsec?.standalone?.enabled) {
headers['Datadog-Client-Computed-Stats'] = 'yes'
Expand Down
19 changes: 1 addition & 18 deletions packages/dd-trace/src/exporters/common/agent-info-exporter.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,7 @@
const { URL, format } = require('url')

const request = require('./request')
const { incrementCountMetric, TELEMETRY_EVENTS_ENQUEUED_FOR_SERIALIZATION } = require('../../ci-visibility/telemetry')

function fetchAgentInfo (url, callback) {
request('', {
path: '/info',
url
}, (err, res) => {
if (err) {
return callback(err)
}
try {
const response = JSON.parse(res)
return callback(null, response)
} catch (e) {
return callback(e)
}
})
}
const { fetchAgentInfo } = require('./util')

/**
* Exporter that exposes a way to query /info endpoint from the agent and gives you the response.
Expand Down
21 changes: 20 additions & 1 deletion packages/dd-trace/src/exporters/common/util.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const request = require('./request')

function safeJSONStringify (value) {
return JSON.stringify(
value,
Expand All @@ -6,4 +8,21 @@ function safeJSONStringify (value) {
)
}

module.exports = { safeJSONStringify }
function fetchAgentInfo (url, callback) {
request('', {
path: '/info',
url
}, (err, res) => {
if (err) {
return callback(err)
}
try {
const response = JSON.parse(res)
return callback(null, response)
} catch (e) {
return callback(e)
}
})
}

module.exports = { safeJSONStringify, fetchAgentInfo }
78 changes: 44 additions & 34 deletions packages/dd-trace/src/format.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ const map = {
'resource.name': 'resource'
}

function format (span) {
function format (span, exporter) {
const encoder = exporter?._writer?._encoder

const formatted = formatSpan(span)

extractSpanLinks(formatted, span)
extractSpanEvents(formatted, span)
extractSpanEvents(formatted, span, encoder, exporter?._agentSupportsTopLevelSpanEvents)
extractRootTags(formatted, span)
extractChunkTags(formatted, span)
extractTags(formatted, span)
Expand Down Expand Up @@ -68,7 +70,7 @@ function setSingleSpanIngestionTags (span, options) {
addTag({}, span.metrics, SPAN_SAMPLING_MAX_PER_SECOND, options.maxPerSecond)
}

function extractSpanLinks (trace, span) {
function extractSpanLinks (formattedSpan, span) {
const links = []
if (span._links) {
for (const link of span._links) {
Expand All @@ -87,10 +89,10 @@ function extractSpanLinks (trace, span) {
links.push(formattedLink)
}
}
if (links.length > 0) { trace.meta['_dd.span_links'] = JSON.stringify(links) }
if (links.length > 0) { formattedSpan.meta['_dd.span_links'] = JSON.stringify(links) }
}

function extractSpanEvents (trace, span) {
function extractSpanEvents (formattedSpan, span, encoder, agentSupportsTopLevelSpanEvents) {
const events = []
if (span._events) {
for (const event of span._events) {
Expand All @@ -103,18 +105,26 @@ function extractSpanEvents (trace, span) {
events.push(formattedEvent)
}
}
if (events.length > 0) { trace.meta.events = JSON.stringify(events) }
if (events.length > 0) {
if (encoder._format === 'v0.4' && agentSupportsTopLevelSpanEvents) {
// the agent supports formattedSpan.span_events natively as a top-level field as of 7.63.0
formattedSpan.span_events = events
} else {
// v0.5 case && backwards compatibility for v0.4 where the agent is older than 7.63.0
formattedSpan.meta.events = JSON.stringify(events)
}
}
}

function extractTags (trace, span) {
function extractTags (formattedSpan, span) {
const context = span.context()
const origin = context._trace.origin
const tags = context._tags
const hostname = context._hostname
const priority = context._sampling.priority

if (tags['span.kind'] && tags['span.kind'] !== 'internal') {
addTag({}, trace.metrics, MEASURED, 1)
addTag({}, formattedSpan.metrics, MEASURED, 1)
}

const tracerService = span.tracer()._service.toLowerCase()
Expand All @@ -129,83 +139,83 @@ function extractTags (trace, span) {
case 'service.name':
case 'span.type':
case 'resource.name':
addTag(trace, {}, map[tag], tags[tag])
addTag(formattedSpan, {}, map[tag], tags[tag])
break
// HACK: remove when Datadog supports numeric status code
case 'http.status_code':
addTag(trace.meta, {}, tag, tags[tag] && String(tags[tag]))
addTag(formattedSpan.meta, {}, tag, tags[tag] && String(tags[tag]))
break
case 'analytics.event':
addTag({}, trace.metrics, ANALYTICS, tags[tag] === undefined || tags[tag] ? 1 : 0)
addTag({}, formattedSpan.metrics, ANALYTICS, tags[tag] === undefined || tags[tag] ? 1 : 0)
break
case HOSTNAME_KEY:
case MEASURED:
addTag({}, trace.metrics, tag, tags[tag] === undefined || tags[tag] ? 1 : 0)
addTag({}, formattedSpan.metrics, tag, tags[tag] === undefined || tags[tag] ? 1 : 0)
break
case 'error':
if (context._name !== 'fs.operation') {
extractError(trace, tags[tag])
extractError(formattedSpan, tags[tag])
}
break
case ERROR_TYPE:
case ERROR_MESSAGE:
case ERROR_STACK:
// HACK: remove when implemented in the backend
if (context._name !== 'fs.operation') {
// HACK: to ensure otel.recordException does not influence trace.error
// HACK: to ensure otel.recordException does not influence formattedSpan.error
if (tags.setTraceError) {
trace.error = 1
formattedSpan.error = 1
}
} else {
break
}
default: // eslint-disable-line no-fallthrough
addTag(trace.meta, trace.metrics, tag, tags[tag])
addTag(formattedSpan.meta, formattedSpan.metrics, tag, tags[tag])
}
}
setSingleSpanIngestionTags(trace, context._spanSampling)
setSingleSpanIngestionTags(formattedSpan, context._spanSampling)

addTag(trace.meta, trace.metrics, 'language', 'javascript')
addTag(trace.meta, trace.metrics, PROCESS_ID, process.pid)
addTag(trace.meta, trace.metrics, SAMPLING_PRIORITY_KEY, priority)
addTag(trace.meta, trace.metrics, ORIGIN_KEY, origin)
addTag(trace.meta, trace.metrics, HOSTNAME_KEY, hostname)
addTag(formattedSpan.meta, formattedSpan.metrics, 'language', 'javascript')
addTag(formattedSpan.meta, formattedSpan.metrics, PROCESS_ID, process.pid)
addTag(formattedSpan.meta, formattedSpan.metrics, SAMPLING_PRIORITY_KEY, priority)
addTag(formattedSpan.meta, formattedSpan.metrics, ORIGIN_KEY, origin)
addTag(formattedSpan.meta, formattedSpan.metrics, HOSTNAME_KEY, hostname)
}

function extractRootTags (trace, span) {
function extractRootTags (formattedSpan, span) {
const context = span.context()
const isLocalRoot = span === context._trace.started[0]
const parentId = context._parentId

if (!isLocalRoot || (parentId && parentId.toString(10) !== '0')) return

addTag({}, trace.metrics, SAMPLING_RULE_DECISION, context._trace[SAMPLING_RULE_DECISION])
addTag({}, trace.metrics, SAMPLING_LIMIT_DECISION, context._trace[SAMPLING_LIMIT_DECISION])
addTag({}, trace.metrics, SAMPLING_AGENT_DECISION, context._trace[SAMPLING_AGENT_DECISION])
addTag({}, trace.metrics, TOP_LEVEL_KEY, 1)
addTag({}, formattedSpan.metrics, SAMPLING_RULE_DECISION, context._trace[SAMPLING_RULE_DECISION])
addTag({}, formattedSpan.metrics, SAMPLING_LIMIT_DECISION, context._trace[SAMPLING_LIMIT_DECISION])
addTag({}, formattedSpan.metrics, SAMPLING_AGENT_DECISION, context._trace[SAMPLING_AGENT_DECISION])
addTag({}, formattedSpan.metrics, TOP_LEVEL_KEY, 1)
}

function extractChunkTags (trace, span) {
function extractChunkTags (formattedSpan, span) {
const context = span.context()
const isLocalRoot = span === context._trace.started[0]

if (!isLocalRoot) return

for (const key in context._trace.tags) {
addTag(trace.meta, trace.metrics, key, context._trace.tags[key])
addTag(formattedSpan.meta, formattedSpan.metrics, key, context._trace.tags[key])
}
}

function extractError (trace, error) {
function extractError (formattedSpan, error) {
if (!error) return

trace.error = 1
formattedSpan.error = 1

if (isError(error)) {
// AggregateError only has a code and no message.
addTag(trace.meta, trace.metrics, ERROR_MESSAGE, error.message || error.code)
addTag(trace.meta, trace.metrics, ERROR_TYPE, error.name)
addTag(trace.meta, trace.metrics, ERROR_STACK, error.stack)
addTag(formattedSpan.meta, formattedSpan.metrics, ERROR_MESSAGE, error.message || error.code)
addTag(formattedSpan.meta, formattedSpan.metrics, ERROR_TYPE, error.name)
addTag(formattedSpan.meta, formattedSpan.metrics, ERROR_STACK, error.stack)
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/dd-trace/src/span_processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class SpanProcessor {

for (const span of started) {
if (span._duration !== undefined) {
const formattedSpan = format(span)
const formattedSpan = format(span, this._exporter)
this._stats.onSpanFinished(formattedSpan)
formatted.push(formattedSpan)

Expand Down
Loading