diff --git a/integration-tests/opentelemetry.spec.js b/integration-tests/opentelemetry.spec.js index 973d7f965ac..755486f362e 100644 --- a/integration-tests/opentelemetry.spec.js +++ b/integration-tests/opentelemetry.spec.js @@ -60,7 +60,7 @@ describe('opentelemetry', () => { '@opentelemetry/api@1.8.0', '@opentelemetry/instrumentation', '@opentelemetry/instrumentation-http', - '@opentelemetry/instrumentation-express', + '@opentelemetry/instrumentation-express@0.47.1', 'express' ] if (satisfies(process.version.slice(1), '>=14')) { diff --git a/package.json b/package.json index c4af405c115..8fbf26b9aea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dd-trace", - "version": "5.42.0", + "version": "5.43.0", "description": "Datadog APM tracing client for JavaScript", "main": "index.js", "typings": "index.d.ts", @@ -89,7 +89,7 @@ "@datadog/native-iast-rewriter": "2.8.0", "@datadog/native-iast-taint-tracking": "3.3.0", "@datadog/native-metrics": "^3.1.0", - "@datadog/pprof": "5.5.1", + "@datadog/pprof": "5.6.0", "@datadog/sketches-js": "^2.1.0", "@isaacs/ttlcache": "^1.4.1", "@opentelemetry/api": ">=1.0.0 <1.9.0", diff --git a/packages/datadog-instrumentations/src/vitest.js b/packages/datadog-instrumentations/src/vitest.js index 2f5f18e7a27..4ac4062b46c 100644 --- a/packages/datadog-instrumentations/src/vitest.js +++ b/packages/datadog-instrumentations/src/vitest.js @@ -94,6 +94,10 @@ function isReporterPackageNewest (vitestPackage) { return vitestPackage.h?.name === 'BaseSequencer' } +function isBaseSequencer (vitestPackage) { + return vitestPackage.b?.name === 'BaseSequencer' +} + function getChannelPromise (channelToPublishTo) { return new Promise(resolve => { sessionAsyncResource.runInAsyncScope(() => { @@ -615,11 +619,22 @@ addHook({ addHook({ name: 'vitest', - versions: ['>=3.0.0'], + versions: ['>=3.0.9'], + filePattern: 'dist/chunks/coverage.*' +}, (coveragePackage) => { + if (isBaseSequencer(coveragePackage)) { + shimmer.wrap(coveragePackage.b.prototype, 'sort', getSortWrapper) + } + return coveragePackage +}) + +addHook({ + name: 'vitest', + versions: ['>=3.0.0 <3.0.9'], filePattern: 'dist/chunks/resolveConfig.*' -}, (randomSequencerPackage) => { - shimmer.wrap(randomSequencerPackage.B.prototype, 'sort', getSortWrapper) - return randomSequencerPackage +}, (resolveConfigPackage) => { + shimmer.wrap(resolveConfigPackage.B.prototype, 'sort', getSortWrapper) + return resolveConfigPackage }) // Can't specify file because compiled vitest includes hashes in their files diff --git a/packages/dd-trace/src/appsec/telemetry/common.js b/packages/dd-trace/src/appsec/telemetry/common.js index 2b5b93801c3..a8ed471bd10 100644 --- a/packages/dd-trace/src/appsec/telemetry/common.js +++ b/packages/dd-trace/src/appsec/telemetry/common.js @@ -7,7 +7,8 @@ const tags = { RULE_TRIGGERED: 'rule_triggered', WAF_TIMEOUT: 'waf_timeout', WAF_VERSION: 'waf_version', - EVENT_RULES_VERSION: 'event_rules_version' + EVENT_RULES_VERSION: 'event_rules_version', + INPUT_TRUNCATED: 'input_truncated' } function getVersionsTags (wafVersion, rulesVersion) { diff --git a/packages/dd-trace/src/appsec/telemetry/waf.js b/packages/dd-trace/src/appsec/telemetry/waf.js index 31b4ecafec6..c79ed04c243 100644 --- a/packages/dd-trace/src/appsec/telemetry/waf.js +++ b/packages/dd-trace/src/appsec/telemetry/waf.js @@ -7,6 +7,12 @@ const appsecMetrics = telemetryMetrics.manager.namespace('appsec') const DD_TELEMETRY_WAF_RESULT_TAGS = Symbol('_dd.appsec.telemetry.waf.result.tags') +const TRUNCATION_FLAGS = { + STRING: 1, + CONTAINER_SIZE: 2, + CONTAINER_DEPTH: 4 +} + function addWafRequestMetrics (store, { duration, durationExt, wafTimeout, errorCode }) { store[DD_TELEMETRY_REQUEST_METRICS].duration += duration || 0 store[DD_TELEMETRY_REQUEST_METRICS].durationExt += durationExt || 0 @@ -58,6 +64,12 @@ function trackWafMetrics (store, metrics) { metricTags[tags.WAF_TIMEOUT] = true } + const truncationReason = getTruncationReason(metrics) + if (truncationReason > 0) { + metricTags[tags.INPUT_TRUNCATED] = true + incrementTruncatedMetrics(metrics, truncationReason) + } + return metricTags } @@ -69,6 +81,7 @@ function getOrCreateMetricTags (store, versionsTags) { [tags.REQUEST_BLOCKED]: false, [tags.RULE_TRIGGERED]: false, [tags.WAF_TIMEOUT]: false, + [tags.INPUT_TRUNCATED]: false, ...versionsTags } @@ -98,6 +111,39 @@ function incrementWafRequests (store) { } } +function incrementTruncatedMetrics (metrics, truncationReason) { + const truncationTags = { truncation_reason: truncationReason } + appsecMetrics.count('waf.input_truncated', truncationTags).inc(1) + + if (metrics?.maxTruncatedString) { + appsecMetrics.distribution('waf.truncated_value_size', { + truncation_reason: TRUNCATION_FLAGS.STRING + }).track(metrics.maxTruncatedString) + } + + if (metrics?.maxTruncatedContainerSize) { + appsecMetrics.distribution('waf.truncated_value_size', { + truncation_reason: TRUNCATION_FLAGS.CONTAINER_SIZE + }).track(metrics.maxTruncatedContainerSize) + } + + if (metrics?.maxTruncatedContainerDepth) { + appsecMetrics.distribution('waf.truncated_value_size', { + truncation_reason: TRUNCATION_FLAGS.CONTAINER_DEPTH + }).track(metrics.maxTruncatedContainerDepth) + } +} + +function getTruncationReason ({ maxTruncatedString, maxTruncatedContainerSize, maxTruncatedContainerDepth }) { + let reason = 0 + + if (maxTruncatedString) reason |= TRUNCATION_FLAGS.STRING + if (maxTruncatedContainerSize) reason |= TRUNCATION_FLAGS.CONTAINER_SIZE + if (maxTruncatedContainerDepth) reason |= TRUNCATION_FLAGS.CONTAINER_DEPTH + + return reason +} + module.exports = { addWafRequestMetrics, trackWafMetrics, diff --git a/packages/dd-trace/src/config.js b/packages/dd-trace/src/config.js index 4f85b485a01..e85fe1ceeb6 100644 --- a/packages/dd-trace/src/config.js +++ b/packages/dd-trace/src/config.js @@ -595,6 +595,7 @@ class Config { this._setValue(defaults, 'vertexai.spanCharLimit', 128) this._setValue(defaults, 'vertexai.spanPromptCompletionSampleRate', 1.0) this._setValue(defaults, 'trace.aws.addSpanPointers', true) + this._setValue(defaults, 'trace.nativeSpanEvents', false) } _applyLocalStableConfig () { @@ -765,6 +766,7 @@ class Config { DD_VERTEXAI_SPAN_PROMPT_COMPLETION_SAMPLE_RATE, DD_VERTEXAI_SPAN_CHAR_LIMIT, DD_TRACE_INFERRED_PROXY_SERVICES_ENABLED, + DD_TRACE_NATIVE_SPAN_EVENTS, OTEL_METRICS_EXPORTER, OTEL_PROPAGATORS, OTEL_RESOURCE_ATTRIBUTES, @@ -977,6 +979,7 @@ class Config { this._setBoolean(env, 'trace.aws.addSpanPointers', DD_TRACE_AWS_ADD_SPAN_POINTERS) this._setString(env, 'trace.dynamoDb.tablePrimaryKeys', DD_TRACE_DYNAMODB_TABLE_PRIMARY_KEYS) this._setArray(env, 'graphqlErrorExtensions', DD_TRACE_GRAPHQL_ERROR_EXTENSIONS) + this._setBoolean(env, 'trace.nativeSpanEvents', DD_TRACE_NATIVE_SPAN_EVENTS) this._setValue( env, 'vertexai.spanPromptCompletionSampleRate', @@ -1114,6 +1117,7 @@ class Config { this._setString(opts, 'version', options.version || tags.version) this._setBoolean(opts, 'inferredProxyServicesEnabled', options.inferredProxyServicesEnabled) this._setBoolean(opts, 'graphqlErrorExtensions', options.graphqlErrorExtensions) + this._setBoolean(opts, 'trace.nativeSpanEvents', options.trace?.nativeSpanEvents) // For LLMObs, we want the environment variable to take precedence over the options. // This is reliant on environment config being set before options. diff --git a/packages/dd-trace/src/dogstatsd.js b/packages/dd-trace/src/dogstatsd.js index f97a95b75eb..038f4a043f0 100644 --- a/packages/dd-trace/src/dogstatsd.js +++ b/packages/dd-trace/src/dogstatsd.js @@ -234,7 +234,7 @@ class MetricsAggregationClient { this._histograms[name].get(tag).record(value) } - count (name, count, tag, monotonic = false) { + count (name, count, tag, monotonic = true) { if (typeof tag === 'boolean') { monotonic = tag tag = undefined @@ -254,8 +254,8 @@ class MetricsAggregationClient { this._gauges[name].set(tag, value) } - increment (name, count = 1, tag, monotonic) { - this.count(name, count, tag, monotonic) + increment (name, count = 1, tag) { + this.count(name, count, tag) } decrement (name, count = 1, tag) { diff --git a/packages/dd-trace/src/encode/0.4.js b/packages/dd-trace/src/encode/0.4.js index d5c72bdb575..42c1fd3218b 100644 --- a/packages/dd-trace/src/encode/0.4.js +++ b/packages/dd-trace/src/encode/0.4.js @@ -5,11 +5,22 @@ const { Chunk, MsgpackEncoder } = require('../msgpack') const log = require('../log') const { isTrue } = require('../util') const coalesce = require('koalas') +const { memoize } = require('../log/utils') const SOFT_LIMIT = 8 * 1024 * 1024 // 8MB -function formatSpan (span) { - return normalizeSpan(truncateSpan(span, false)) +function formatSpan (span, config) { + span = normalizeSpan(truncateSpan(span, false)) + if (span.span_events) { + // ensure span events are encoded as tags if agent doesn't support native top level span events + if (!config?.trace?.nativeSpanEvents) { + span.meta.events = JSON.stringify(span.span_events) + delete span.span_events + } else { + formatSpanEvents(span) + } + } + return span } class AgentEncoder { @@ -24,6 +35,7 @@ class AgentEncoder { process.env.DD_TRACE_ENCODING_DEBUG, false )) + this._config = this._writer?._config } count () { @@ -74,16 +86,18 @@ class AgentEncoder { this._encodeArrayPrefix(bytes, trace) for (let span of trace) { - span = formatSpan(span) + span = formatSpan(span, this._config) bytes.reserve(1) - if (span.type && span.meta_struct) { - bytes.buffer[bytes.length - 1] = 0x8d - } else if (span.type || span.meta_struct) { - bytes.buffer[bytes.length - 1] = 0x8c - } else { - bytes.buffer[bytes.length - 1] = 0x8b - } + // this is the original size of the fixed map for span attributes that always exist + let mapSize = 11 + + // increment the payload map size depending on if some optional fields exist + if (span.type) mapSize += 1 + if (span.meta_struct) mapSize += 1 + if (span.span_events) mapSize += 1 + + bytes.buffer[bytes.length - 1] = 0x80 + mapSize if (span.type) { this._encodeString(bytes, 'type') @@ -112,6 +126,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) @@ -200,6 +218,9 @@ class AgentEncoder { case 'number': this._encodeFloat(bytes, value) break + case 'boolean': + this._encodeBool(bytes, value) + break default: // should not happen } @@ -258,7 +279,7 @@ class AgentEncoder { this._encodeObjectAsArray(bytes, value, circularReferencesDetector) } else if (value !== null && typeof value === 'object') { this._encodeObjectAsMap(bytes, value, circularReferencesDetector) - } else if (typeof value === 'string' || typeof value === 'number') { + } else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { this._encodeValue(bytes, value) } } @@ -268,7 +289,7 @@ class AgentEncoder { const validKeys = keys.filter(key => { const v = value[key] return typeof v === 'string' || - typeof v === 'number' || + typeof v === 'number' || typeof v === 'boolean' || (v !== null && typeof v === 'object' && !circularReferencesDetector.has(v)) }) @@ -319,4 +340,79 @@ class AgentEncoder { } } +const memoizedLogDebug = memoize((key, message) => { + log.debug(message) + // return something to store in memoize cache + return true +}) + +function formatSpanEvents (span) { + for (const spanEvent of span.span_events) { + if (spanEvent.attributes) { + for (const [key, value] of Object.entries(spanEvent.attributes)) { + const newValue = convertSpanEventAttributeValues(key, value) + if (newValue !== undefined) { + spanEvent.attributes[key] = newValue + } else { + delete spanEvent.attributes[key] // delete from attributes if undefined + } + } + if (Object.entries(spanEvent.attributes).length === 0) { + delete spanEvent.attributes + } + } + } +} + +function convertSpanEventAttributeValues (key, value, depth = 0) { + if (typeof value === 'string') { + return { + type: 0, + string_value: value + } + } else if (typeof value === 'boolean') { + return { + type: 1, + bool_value: value + } + } else if (Number.isInteger(value)) { + return { + type: 2, + int_value: value + } + } else if (typeof value === 'number') { + return { + type: 3, + double_value: value + } + } else if (Array.isArray(value)) { + if (depth === 0) { + const convertedArray = value + .map((val) => convertSpanEventAttributeValues(key, val, 1)) + .filter((convertedVal) => convertedVal !== undefined) + + // Only include array_value if there are valid elements + if (convertedArray.length > 0) { + return { + type: 4, + array_value: convertedArray + } + } else { + // If all elements were unsupported, return undefined + return undefined + } + } else { + memoizedLogDebug(key, 'Encountered nested array data type for span event v0.4 encoding. ' + + `Skipping encoding key: ${key}: with value: ${typeof value}.` + ) + return undefined + } + } else { + memoizedLogDebug(key, 'Encountered unsupported data type for span event v0.4 encoding, key: ' + + `${key}: with value: ${typeof value}. Skipping encoding of pair.` + ) + return undefined + } +} + module.exports = { AgentEncoder } diff --git a/packages/dd-trace/src/encode/0.5.js b/packages/dd-trace/src/encode/0.5.js index a0a042f3f7d..b61618aee94 100644 --- a/packages/dd-trace/src/encode/0.5.js +++ b/packages/dd-trace/src/encode/0.5.js @@ -7,7 +7,13 @@ const ARRAY_OF_TWO = 0x92 const ARRAY_OF_TWELVE = 0x9c function formatSpan (span) { - return normalizeSpan(truncateSpan(span, false)) + span = normalizeSpan(truncateSpan(span, false)) + // ensure span events are encoded as tags + if (span.span_events) { + span.meta.events = JSON.stringify(span.span_events) + delete span.span_events + } + return span } class AgentEncoder extends BaseEncoder { diff --git a/packages/dd-trace/src/exporters/agent/index.js b/packages/dd-trace/src/exporters/agent/index.js index ee26726c137..698e3bd00b0 100644 --- a/packages/dd-trace/src/exporters/agent/index.js +++ b/packages/dd-trace/src/exporters/agent/index.js @@ -24,7 +24,8 @@ class AgentExporter { prioritySampler, lookup, protocolVersion, - headers + headers, + config }) this._timer = undefined diff --git a/packages/dd-trace/src/exporters/agent/writer.js b/packages/dd-trace/src/exporters/agent/writer.js index 8fac323e614..056f1b86140 100644 --- a/packages/dd-trace/src/exporters/agent/writer.js +++ b/packages/dd-trace/src/exporters/agent/writer.js @@ -10,15 +10,16 @@ const BaseWriter = require('../common/writer') const METRIC_PREFIX = 'datadog.tracer.node.exporter.agent' class Writer extends BaseWriter { - constructor ({ prioritySampler, lookup, protocolVersion, headers }) { + constructor ({ prioritySampler, lookup, protocolVersion, headers, config = {} }) { super(...arguments) const AgentEncoder = getEncoder(protocolVersion) this._prioritySampler = prioritySampler this._lookup = lookup this._protocolVersion = protocolVersion - this._encoder = new AgentEncoder(this) this._headers = headers + this._config = config + this._encoder = new AgentEncoder(this) } _sendPayload (data, count, done) { diff --git a/packages/dd-trace/src/format.js b/packages/dd-trace/src/format.js index 516f1894975..a322b211662 100644 --- a/packages/dd-trace/src/format.js +++ b/packages/dd-trace/src/format.js @@ -68,7 +68,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) { @@ -87,10 +87,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) { const events = [] if (span._events) { for (const event of span._events) { @@ -103,10 +103,12 @@ function extractSpanEvents (trace, span) { events.push(formattedEvent) } } - if (events.length > 0) { trace.meta.events = JSON.stringify(events) } + if (events.length > 0) { + formattedSpan.span_events = events + } } -function extractTags (trace, span) { +function extractTags (formattedSpan, span) { const context = span.context() const origin = context._trace.origin const tags = context._tags @@ -114,7 +116,7 @@ function extractTags (trace, span) { 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() @@ -129,22 +131,22 @@ 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: @@ -152,60 +154,60 @@ function extractTags (trace, span) { 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) } } diff --git a/packages/dd-trace/src/llmobs/sdk.js b/packages/dd-trace/src/llmobs/sdk.js index 64c4bd2ab87..f14ee698d09 100644 --- a/packages/dd-trace/src/llmobs/sdk.js +++ b/packages/dd-trace/src/llmobs/sdk.js @@ -105,12 +105,12 @@ class LLMObs extends NoopLLMObs { if (fn.length > 1) { return this._tracer.trace(name, spanOptions, (span, cb) => - this._activate(span, { kind, options: llmobsOptions }, () => fn(span, cb)) + this._activate(span, { kind, ...llmobsOptions }, () => fn(span, cb)) ) } return this._tracer.trace(name, spanOptions, span => - this._activate(span, { kind, options: llmobsOptions }, () => fn(span)) + this._activate(span, { kind, ...llmobsOptions }, () => fn(span)) ) } @@ -166,7 +166,7 @@ class LLMObs extends NoopLLMObs { } try { - const result = llmobs._activate(span, { kind, options: llmobsOptions }, () => fn.apply(this, fnArgs)) + const result = llmobs._activate(span, { kind, ...llmobsOptions }, () => fn.apply(this, fnArgs)) if (result && typeof result.then === 'function') { return result.then( diff --git a/packages/dd-trace/src/log/index.js b/packages/dd-trace/src/log/index.js index fe2d5d4a607..1fd11122991 100644 --- a/packages/dd-trace/src/log/index.js +++ b/packages/dd-trace/src/log/index.js @@ -6,19 +6,7 @@ const { isTrue } = require('../util') const { traceChannel, debugChannel, infoChannel, warnChannel, errorChannel } = require('./channels') const logWriter = require('./writer') const { Log } = require('./log') - -const memoize = func => { - const cache = {} - const memoized = function (key) { - if (!cache[key]) { - cache[key] = func.apply(this, arguments) - } - - return cache[key] - } - - return memoized -} +const { memoize } = require('./utils') const config = { enabled: false, diff --git a/packages/dd-trace/src/log/utils.js b/packages/dd-trace/src/log/utils.js new file mode 100644 index 00000000000..96b8d109d24 --- /dev/null +++ b/packages/dd-trace/src/log/utils.js @@ -0,0 +1,16 @@ +'use strict' + +const memoize = func => { + const cache = {} + const memoized = function (key) { + if (!cache[key]) { + cache[key] = func.apply(this, arguments) + } + + return cache[key] + } + + return memoized +} + +module.exports = { memoize } diff --git a/packages/dd-trace/src/runtime_metrics/runtime_metrics.js b/packages/dd-trace/src/runtime_metrics/runtime_metrics.js index 4f92afea90c..57177bef16a 100644 --- a/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +++ b/packages/dd-trace/src/runtime_metrics/runtime_metrics.js @@ -111,11 +111,11 @@ const runtimeMetrics = module.exports = { }, increment (name, tag, monotonic) { - client && client.increment(name, 1, tag, monotonic) + this.count(name, 1, tag, monotonic) }, decrement (name, tag) { - client && client.decrement(name, 1, tag) + this.count(name, -1, tag) } } @@ -211,7 +211,7 @@ function captureGCMetrics () { histogram('runtime.node.gc.pause', pauseAll) for (const type in pause) { - histogram('runtime.node.gc.pause.by.type', pause[type], [`gc_type:${type}`]) + histogram('runtime.node.gc.pause.by.type', pause[type], `gc_type:${type}`) } gcProfiler.start() @@ -265,7 +265,7 @@ function captureNativeMetrics () { if (type === 'all') { histogram('runtime.node.gc.pause', stats.gc[type]) } else { - histogram('runtime.node.gc.pause.by.type', stats.gc[type], [`gc_type:${type}`]) + histogram('runtime.node.gc.pause.by.type', stats.gc[type], `gc_type:${type}`) } }) @@ -279,16 +279,15 @@ function captureNativeMetrics () { } } -function histogram (name, stats, tags) { - tags = tags ? [].concat(tags) : [] - - if (tags.length > 0) { - for (const tag of tags) { - client.histogram(name, stats, tag) - } - } else { - client.histogram(name, stats) - } +function histogram (name, stats, tag) { + client.gauge(`${name}.min`, stats.min, tag) + client.gauge(`${name}.max`, stats.max, tag) + client.increment(`${name}.sum`, stats.sum, tag) + client.increment(`${name}.total`, stats.sum, tag) + client.gauge(`${name}.avg`, stats.avg, tag) + client.increment(`${name}.count`, stats.count, tag) + client.gauge(`${name}.median`, stats.median, tag) + client.gauge(`${name}.95percentile`, stats.p95, tag) } function startGCObserver () { diff --git a/packages/dd-trace/test/appsec/resources/index.js b/packages/dd-trace/test/appsec/resources/index.js new file mode 100644 index 00000000000..575e4724ec7 --- /dev/null +++ b/packages/dd-trace/test/appsec/resources/index.js @@ -0,0 +1,21 @@ +'use strict' + +const tracer = require('dd-trace') +tracer.init({ + flushInterval: 1 +}) + +const express = require('express') +const body = require('body-parser') + +const app = express() +app.use(body.json()) +const port = process.env.APP_PORT || 3000 + +app.post('/', async (req, res) => { + res.end('OK') +}) + +app.listen(port, () => { + process.send({ port }) +}) diff --git a/packages/dd-trace/test/appsec/telemetry/rasp.spec.js b/packages/dd-trace/test/appsec/telemetry/rasp.spec.js index 3640fb3e9d4..c36ffd274c0 100644 --- a/packages/dd-trace/test/appsec/telemetry/rasp.spec.js +++ b/packages/dd-trace/test/appsec/telemetry/rasp.spec.js @@ -129,6 +129,7 @@ describe('Appsec Rasp Telemetry metrics', () => { blockTriggered: true, ruleTriggered: true, wafTimeout: true, + input_truncated: true, wafVersion, rulesVersion }, req, { type: 'rule-type' }) @@ -141,7 +142,8 @@ describe('Appsec Rasp Telemetry metrics', () => { rule_triggered: false, waf_timeout: false, waf_version: wafVersion, - event_rules_version: rulesVersion + event_rules_version: rulesVersion, + input_truncated: false }) }) }) diff --git a/packages/dd-trace/test/appsec/telemetry/waf.spec.js b/packages/dd-trace/test/appsec/telemetry/waf.spec.js index 3e16714c234..4962d7fccf2 100644 --- a/packages/dd-trace/test/appsec/telemetry/waf.spec.js +++ b/packages/dd-trace/test/appsec/telemetry/waf.spec.js @@ -30,6 +30,11 @@ describe('Appsec Waf Telemetry metrics', () => { afterEach(sinon.restore) describe('if enabled', () => { + const metrics = { + wafVersion, + rulesVersion + } + beforeEach(() => { appsecTelemetry.enable({ enabled: true, @@ -38,11 +43,6 @@ describe('Appsec Waf Telemetry metrics', () => { }) describe('updateWafRequestsMetricTags', () => { - const metrics = { - wafVersion, - rulesVersion - } - it('should skip update if no request is provided', () => { const result = appsecTelemetry.updateWafRequestsMetricTags(metrics) @@ -57,7 +57,8 @@ describe('Appsec Waf Telemetry metrics', () => { event_rules_version: rulesVersion, request_blocked: false, rule_triggered: false, - waf_timeout: false + waf_timeout: false, + input_truncated: false }) }) @@ -66,6 +67,7 @@ describe('Appsec Waf Telemetry metrics', () => { blockTriggered: true, ruleTriggered: true, wafTimeout: true, + maxTruncatedString: 5000, ...metrics }, req) @@ -74,7 +76,8 @@ describe('Appsec Waf Telemetry metrics', () => { event_rules_version: rulesVersion, request_blocked: true, rule_triggered: true, - waf_timeout: true + waf_timeout: true, + input_truncated: true }) }) @@ -93,7 +96,8 @@ describe('Appsec Waf Telemetry metrics', () => { event_rules_version: rulesVersion, request_blocked: false, rule_triggered: true, - waf_timeout: false + waf_timeout: false, + input_truncated: false }) }) @@ -102,6 +106,7 @@ describe('Appsec Waf Telemetry metrics', () => { blockTriggered: true, ruleTriggered: true, wafTimeout: true, + maxTruncatedContainerSize: 300, ...metrics }, req) @@ -120,7 +125,8 @@ describe('Appsec Waf Telemetry metrics', () => { event_rules_version: rulesVersion, request_blocked: true, rule_triggered: true, - waf_timeout: true + waf_timeout: true, + input_truncated: true }) }) @@ -250,7 +256,8 @@ describe('Appsec Waf Telemetry metrics', () => { rule_triggered: false, waf_timeout: true, waf_version: wafVersion, - event_rules_version: rulesVersion + event_rules_version: rulesVersion, + input_truncated: false }) }) @@ -260,6 +267,63 @@ describe('Appsec Waf Telemetry metrics', () => { expect(count).to.not.have.been.called }) }) + + describe('WAF Truncation metrics', () => { + it('should report truncated string metrics', () => { + const result = appsecTelemetry.updateWafRequestsMetricTags({ maxTruncatedString: 5000 }, req) + expect(result).to.have.property('input_truncated', true) + + expect(count).to.have.been.calledWith('waf.input_truncated', { truncation_reason: 1 }) + expect(inc).to.have.been.calledWith(1) + + expect(distribution).to.have.been.calledWith('waf.truncated_value_size', { truncation_reason: 1 }) + expect(track).to.have.been.calledWith(5000) + }) + + it('should report truncated container size metrics', () => { + const result = appsecTelemetry.updateWafRequestsMetricTags({ maxTruncatedContainerSize: 300 }, req) + expect(result).to.have.property('input_truncated', true) + + expect(count).to.have.been.calledWith('waf.input_truncated', { truncation_reason: 2 }) + expect(inc).to.have.been.calledWith(1) + + expect(distribution).to.have.been.calledWith('waf.truncated_value_size', { truncation_reason: 2 }) + expect(track).to.have.been.calledWith(300) + }) + + it('should report truncated container depth metrics', () => { + const result = appsecTelemetry.updateWafRequestsMetricTags({ maxTruncatedContainerDepth: 20 }, req) + expect(result).to.have.property('input_truncated', true) + + expect(count).to.have.been.calledWith('waf.input_truncated', { truncation_reason: 4 }) + expect(inc).to.have.been.calledWith(1) + + expect(distribution).to.have.been.calledWith('waf.truncated_value_size', { truncation_reason: 4 }) + expect(track).to.have.been.calledWith(20) + }) + + it('should combine truncation reasons when multiple truncations occur', () => { + const result = appsecTelemetry.updateWafRequestsMetricTags({ + maxTruncatedString: 5000, + maxTruncatedContainerSize: 300, + maxTruncatedContainerDepth: 20 + }, req) + expect(result).to.have.property('input_truncated', true) + + expect(count).to.have.been.calledWith('waf.input_truncated', { truncation_reason: 7 }) + expect(distribution).to.have.been.calledWith('waf.truncated_value_size', { truncation_reason: 1 }) + expect(distribution).to.have.been.calledWith('waf.truncated_value_size', { truncation_reason: 2 }) + expect(distribution).to.have.been.calledWith('waf.truncated_value_size', { truncation_reason: 4 }) + }) + + it('should not report truncation metrics when no truncation occurs', () => { + const result = appsecTelemetry.updateWafRequestsMetricTags(metrics, req) + expect(result).to.have.property('input_truncated', false) + + expect(count).to.not.have.been.calledWith('waf.input_truncated') + expect(distribution).to.not.have.been.calledWith('waf.truncated_value_size') + }) + }) }) describe('if disabled', () => { diff --git a/packages/dd-trace/test/appsec/waf-metrics.integration.spec.js b/packages/dd-trace/test/appsec/waf-metrics.integration.spec.js new file mode 100644 index 00000000000..322ce7a2fb2 --- /dev/null +++ b/packages/dd-trace/test/appsec/waf-metrics.integration.spec.js @@ -0,0 +1,130 @@ +'use strict' + +const { createSandbox, FakeAgent, spawnProc } = require('../../../../integration-tests/helpers') +const getPort = require('get-port') +const path = require('path') +const Axios = require('axios') +const { assert } = require('chai') + +describe('WAF truncation metrics', () => { + let axios, sandbox, cwd, appPort, appFile, agent, proc + + before(async function () { + this.timeout(process.platform === 'win32' ? 90000 : 30000) + + sandbox = await createSandbox( + ['express'], + false, + [path.join(__dirname, 'resources')] + ) + + appPort = await getPort() + cwd = sandbox.folder + appFile = path.join(cwd, 'resources', 'index.js') + + axios = Axios.create({ + baseURL: `http://localhost:${appPort}` + }) + }) + + after(async function () { + this.timeout(60000) + await sandbox.remove() + }) + + beforeEach(async () => { + agent = await new FakeAgent().start() + proc = await spawnProc(appFile, { + cwd, + env: { + DD_TRACE_AGENT_PORT: agent.port, + APP_PORT: appPort, + DD_APPSEC_ENABLED: 'true', + DD_TELEMETRY_HEARTBEAT_INTERVAL: 1 + } + }) + }) + + afterEach(async () => { + proc.kill() + await agent.stop() + }) + + it('should report tuncation metrics', async () => { + let appsecTelemetryMetricsReceived = false + let appsecTelemetryDistributionsReceived = false + + const longValue = 'testattack'.repeat(500) + const largeObject = {} + for (let i = 0; i < 300; ++i) { + largeObject[`key${i}`] = `value${i}` + } + const deepObject = createNestedObject(25, { value: 'a' }) + const complexPayload = { + deepObject, + longValue, + largeObject + } + + await axios.post('/', { complexPayload }) + + const checkMessages = agent.assertMessageReceived(({ payload }) => { + assert.strictEqual(payload[0][0].metrics['_dd.appsec.enabled'], 1) + assert.strictEqual(payload[0][0].metrics['_dd.appsec.truncated.container_depth'], 20) + assert.strictEqual(payload[0][0].metrics['_dd.appsec.truncated.container_size'], 300) + assert.strictEqual(payload[0][0].metrics['_dd.appsec.truncated.string_length'], 5000) + }) + + const checkTelemetryMetrics = agent.assertTelemetryReceived(({ payload }) => { + const namespace = payload.payload.namespace + + if (namespace === 'appsec') { + appsecTelemetryMetricsReceived = true + const series = payload.payload.series + const inputTruncated = series.find(s => s.metric === 'waf.input_truncated') + + assert.exists(inputTruncated, 'input truncated serie should exist') + assert.strictEqual(inputTruncated.type, 'count') + assert.include(inputTruncated.tags, 'truncation_reason:7') + + const wafRequests = series.find(s => s.metric === 'waf.requests') + assert.exists(wafRequests, 'waf requests serie should exist') + assert.include(wafRequests.tags, 'input_truncated:true') + } + }, 30_000, 'generate-metrics', 2) + + const checkTelemetryDistributions = agent.assertTelemetryReceived(({ payload }) => { + const namespace = payload.payload.namespace + + if (namespace === 'appsec') { + appsecTelemetryDistributionsReceived = true + const series = payload.payload.series + const wafDuration = series.find(s => s.metric === 'waf.duration') + const wafDurationExt = series.find(s => s.metric === 'waf.duration_ext') + const wafTuncated = series.filter(s => s.metric === 'waf.truncated_value_size') + + assert.exists(wafDuration, 'waf duration serie should exist') + assert.exists(wafDurationExt, 'waf duration ext serie should exist') + + assert.equal(wafTuncated.length, 3) + assert.include(wafTuncated[0].tags, 'truncation_reason:1') + assert.include(wafTuncated[1].tags, 'truncation_reason:2') + assert.include(wafTuncated[2].tags, 'truncation_reason:4') + } + }, 30_000, 'distributions', 1) + + return Promise.all([checkMessages, checkTelemetryMetrics, checkTelemetryDistributions]).then(() => { + assert.equal(appsecTelemetryMetricsReceived, true) + assert.equal(appsecTelemetryDistributionsReceived, true) + + return true + }) + }) +}) + +const createNestedObject = (n, obj) => { + if (n > 0) { + return { a: createNestedObject(n - 1, obj) } + } + return obj +} diff --git a/packages/dd-trace/test/custom-metrics.spec.js b/packages/dd-trace/test/custom-metrics.spec.js index 7ef809b60ca..802fa01e3e7 100644 --- a/packages/dd-trace/test/custom-metrics.spec.js +++ b/packages/dd-trace/test/custom-metrics.spec.js @@ -53,7 +53,7 @@ describe('Custom Metrics', () => { if (stdout) console.log(stdout) if (stderr) console.error(stderr) - expect(metricsData.split('#')[0]).to.equal('page.views.data:1|g|') + expect(metricsData.split('#')[0]).to.equal('page.views.data:1|c|') done() }) diff --git a/packages/dd-trace/test/dogstatsd.spec.js b/packages/dd-trace/test/dogstatsd.spec.js index c34c80966d9..11f432167fb 100644 --- a/packages/dd-trace/test/dogstatsd.spec.js +++ b/packages/dd-trace/test/dogstatsd.spec.js @@ -382,7 +382,7 @@ describe('dogstatsd', () => { client.flush() expect(udp4.send).to.have.been.called - expect(udp4.send.firstCall.args[0].toString()).to.equal('test.count:20|g\n') + expect(udp4.send.firstCall.args[0].toString()).to.equal('test.count:20|c\n') }) it('.increment() with default', () => { @@ -393,7 +393,7 @@ describe('dogstatsd', () => { client.flush() expect(udp4.send).to.have.been.called - expect(udp4.send.firstCall.args[0].toString()).to.equal('test.count:2|g\n') + expect(udp4.send.firstCall.args[0].toString()).to.equal('test.count:2|c\n') }) it('.decrement()', () => { @@ -404,7 +404,7 @@ describe('dogstatsd', () => { client.flush() expect(udp4.send).to.have.been.called - expect(udp4.send.firstCall.args[0].toString()).to.equal('test.count:-20|g\n') + expect(udp4.send.firstCall.args[0].toString()).to.equal('test.count:-20|c\n') }) it('.decrement() with default', () => { @@ -415,7 +415,7 @@ describe('dogstatsd', () => { client.flush() expect(udp4.send).to.have.been.called - expect(udp4.send.firstCall.args[0].toString()).to.equal('test.count:-2|g\n') + expect(udp4.send.firstCall.args[0].toString()).to.equal('test.count:-2|c\n') }) it('.distribution()', () => { diff --git a/packages/dd-trace/test/encode/0.4.spec.js b/packages/dd-trace/test/encode/0.4.spec.js index ea43c84828b..0d9aaa437a8 100644 --- a/packages/dd-trace/test/encode/0.4.spec.js +++ b/packages/dd-trace/test/encode/0.4.spec.js @@ -5,6 +5,8 @@ require('../setup/tap') const { expect } = require('chai') const msgpack = require('@msgpack/msgpack') const id = require('../../src/id') +const proxyquire = require('proxyquire') +const sinon = require('sinon') function randString (length) { return Array.from({ length }, () => { @@ -18,400 +20,488 @@ describe('encode', () => { let logger let data - beforeEach(() => { - logger = { - debug: sinon.stub() - } - const { AgentEncoder } = proxyquire('../src/encode/0.4', { - '../log': logger + describe('without configuration', () => { + beforeEach(() => { + logger = { + debug: sinon.stub() + } + const { AgentEncoder } = proxyquire('../../src/encode/0.4', { + '../log': logger + }) + writer = { flush: sinon.spy() } + encoder = new AgentEncoder(writer) + data = [{ + trace_id: id('1234abcd1234abcd'), + span_id: id('1234abcd1234abcd'), + parent_id: id('1234abcd1234abcd'), + name: 'test', + resource: 'test-r', + service: 'test-s', + type: 'foo', + error: 0, + meta: { + bar: 'baz' + }, + metrics: { + example: 1 + }, + start: 123, + duration: 456, + links: [] + }] }) - writer = { flush: sinon.spy() } - encoder = new AgentEncoder(writer) - data = [{ - trace_id: id('1234abcd1234abcd'), - span_id: id('1234abcd1234abcd'), - parent_id: id('1234abcd1234abcd'), - name: 'test', - resource: 'test-r', - service: 'test-s', - type: 'foo', - error: 0, - meta: { - bar: 'baz' - }, - metrics: { - example: 1 - }, - start: 123, - duration: 456, - links: [] - }] - }) - it('should encode to msgpack', () => { - encoder.encode(data) - - const buffer = encoder.makePayload() - const decoded = msgpack.decode(buffer, { useBigInt64: true }) - const trace = decoded[0] - - expect(trace).to.be.instanceof(Array) - expect(trace[0]).to.be.instanceof(Object) - expect(trace[0].trace_id.toString(16)).to.equal(data[0].trace_id.toString()) - expect(trace[0].span_id.toString(16)).to.equal(data[0].span_id.toString()) - expect(trace[0].parent_id.toString(16)).to.equal(data[0].parent_id.toString()) - expect(trace[0].start).to.equal(123n) - expect(trace[0].duration).to.equal(456n) - expect(trace[0].name).to.equal(data[0].name) - expect(trace[0].meta).to.deep.equal({ bar: 'baz' }) - expect(trace[0].metrics).to.deep.equal({ example: 1 }) - }) + it('should encode to msgpack', () => { + encoder.encode(data) - it('should truncate long IDs', () => { - data[0].trace_id = id('ffffffffffffffff1234abcd1234abcd') - data[0].span_id = id('ffffffffffffffff1234abcd1234abcd') - data[0].arent_id = id('ffffffffffffffff1234abcd1234abcd') + const buffer = encoder.makePayload() + const decoded = msgpack.decode(buffer, { useBigInt64: true }) + const trace = decoded[0] - encoder.encode(data) + expect(trace).to.be.instanceof(Array) + expect(trace[0]).to.be.instanceof(Object) + expect(trace[0].trace_id.toString(16)).to.equal(data[0].trace_id.toString()) + expect(trace[0].span_id.toString(16)).to.equal(data[0].span_id.toString()) + expect(trace[0].parent_id.toString(16)).to.equal(data[0].parent_id.toString()) + expect(trace[0].start).to.equal(123n) + expect(trace[0].duration).to.equal(456n) + expect(trace[0].name).to.equal(data[0].name) + expect(trace[0].meta).to.deep.equal({ bar: 'baz' }) + expect(trace[0].metrics).to.deep.equal({ example: 1 }) + }) - const buffer = encoder.makePayload() - const decoded = msgpack.decode(buffer, { useBigInt64: true }) - const trace = decoded[0] + it('should truncate long IDs', () => { + data[0].trace_id = id('ffffffffffffffff1234abcd1234abcd') + data[0].span_id = id('ffffffffffffffff1234abcd1234abcd') + data[0].arent_id = id('ffffffffffffffff1234abcd1234abcd') - expect(trace[0].trace_id.toString(16)).to.equal('1234abcd1234abcd') - expect(trace[0].span_id.toString(16)).to.equal('1234abcd1234abcd') - expect(trace[0].parent_id.toString(16)).to.equal('1234abcd1234abcd') - }) + encoder.encode(data) - it('should report its count', () => { - expect(encoder.count()).to.equal(0) + const buffer = encoder.makePayload() + const decoded = msgpack.decode(buffer, { useBigInt64: true }) + const trace = decoded[0] - encoder.encode(data) + expect(trace[0].trace_id.toString(16)).to.equal('1234abcd1234abcd') + expect(trace[0].span_id.toString(16)).to.equal('1234abcd1234abcd') + expect(trace[0].parent_id.toString(16)).to.equal('1234abcd1234abcd') + }) - expect(encoder.count()).to.equal(1) + it('should report its count', () => { + expect(encoder.count()).to.equal(0) - encoder.encode(data) + encoder.encode(data) - expect(encoder.count()).to.equal(2) - }) + expect(encoder.count()).to.equal(1) - it('should flush when the payload size limit is reached', function () { - // Make 8mb of data - for (let i = 0; i < 8 * 1024; i++) { - data[0].meta[`foo${i}`] = randString(1024) - } + encoder.encode(data) - encoder.encode(data) + expect(encoder.count()).to.equal(2) + }) - expect(writer.flush).to.have.been.called - }) + it('should flush when the payload size limit is reached', function () { + // Make 8mb of data + for (let i = 0; i < 8 * 1024; i++) { + data[0].meta[`foo${i}`] = randString(1024) + } - it('should reset after making a payload', () => { - encoder.encode(data) - encoder.makePayload() + encoder.encode(data) - const payload = encoder.makePayload() + expect(writer.flush).to.have.been.called + }) - expect(encoder.count()).to.equal(0) - expect(payload).to.have.length(5) - expect(payload[0]).to.equal(0xdd) - expect(payload[1]).to.equal(0) - expect(payload[2]).to.equal(0) - expect(payload[3]).to.equal(0) - expect(payload[4]).to.equal(0) - }) + it('should reset after making a payload', () => { + encoder.encode(data) + encoder.makePayload() + + const payload = encoder.makePayload() - it('should log adding an encoded trace to the buffer if enabled', () => { - encoder._debugEncoding = true - encoder.encode(data) + expect(encoder.count()).to.equal(0) + expect(payload).to.have.length(5) + expect(payload[0]).to.equal(0xdd) + expect(payload[1]).to.equal(0) + expect(payload[2]).to.equal(0) + expect(payload[3]).to.equal(0) + expect(payload[4]).to.equal(0) + }) - const message = logger.debug.firstCall.args[0]() + it('should log adding an encoded trace to the buffer if enabled', () => { + encoder._debugEncoding = true + encoder.encode(data) - expect(message).to.match(/^Adding encoded trace to buffer:(\s[a-f\d]{2})+$/) - }) + const message = logger.debug.firstCall.args[0]() - it('should not log adding an encoded trace to the buffer by default', () => { - encoder.encode(data) + expect(message).to.match(/^Adding encoded trace to buffer:(\s[a-f\d]{2})+$/) + }) - expect(logger.debug).to.not.have.been.called - }) + it('should not log adding an encoded trace to the buffer by default', () => { + encoder.encode(data) - it('should work when the buffer is resized', function () { - // big enough to trigger a resize - const dataToEncode = Array(15000).fill({ - trace_id: id('1234abcd1234abcd'), - span_id: id('1234abcd1234abcd'), - parent_id: id('1234abcd1234abcd'), - name: 'bigger name than expected', - resource: 'test-r', - service: 'test-s', - type: 'foo', - error: 0, - meta: { - bar: 'baz' - }, - metrics: { - example: 1, - moreExample: 2 - }, - start: 123, - duration: 456 + expect(logger.debug).to.not.have.been.called }) - encoder.encode(dataToEncode) - const buffer = encoder.makePayload() - const [decodedPayload] = msgpack.decode(buffer, { useBigInt64: true }) - decodedPayload.forEach(decodedData => { - expect(decodedData).to.include({ + it('should work when the buffer is resized', function () { + // big enough to trigger a resize + const dataToEncode = Array(15000).fill({ + trace_id: id('1234abcd1234abcd'), + span_id: id('1234abcd1234abcd'), + parent_id: id('1234abcd1234abcd'), name: 'bigger name than expected', resource: 'test-r', service: 'test-s', type: 'foo', - error: 0 - }) - expect(decodedData.start).to.equal(123n) - expect(decodedData.duration).to.equal(456n) - expect(decodedData.meta).to.eql({ - bar: 'baz' + error: 0, + meta: { + bar: 'baz' + }, + metrics: { + example: 1, + moreExample: 2 + }, + start: 123, + duration: 456 }) - expect(decodedData.metrics).to.eql({ - example: 1, - moreExample: 2 + encoder.encode(dataToEncode) + + const buffer = encoder.makePayload() + const [decodedPayload] = msgpack.decode(buffer, { useBigInt64: true }) + decodedPayload.forEach(decodedData => { + expect(decodedData).to.include({ + name: 'bigger name than expected', + resource: 'test-r', + service: 'test-s', + type: 'foo', + error: 0 + }) + expect(decodedData.start).to.equal(123n) + expect(decodedData.duration).to.equal(456n) + expect(decodedData.meta).to.eql({ + bar: 'baz' + }) + expect(decodedData.metrics).to.eql({ + example: 1, + moreExample: 2 + }) + expect(decodedData.trace_id.toString(16)).to.equal('1234abcd1234abcd') + expect(decodedData.span_id.toString(16)).to.equal('1234abcd1234abcd') + expect(decodedData.parent_id.toString(16)).to.equal('1234abcd1234abcd') }) - expect(decodedData.trace_id.toString(16)).to.equal('1234abcd1234abcd') - expect(decodedData.span_id.toString(16)).to.equal('1234abcd1234abcd') - expect(decodedData.parent_id.toString(16)).to.equal('1234abcd1234abcd') }) - }) - - it('should encode span events', () => { - const encodedLink = '[{"name":"Something went so wrong","time_unix_nano":1000000},' + - '{"name":"I can sing!!! acbdefggnmdfsdv k 2e2ev;!|=xxx","time_unix_nano":1633023102000000,' + - '"attributes":{"emotion":"happy","rating":9.8,"other":[1,9.5,1],"idol":false}}]' - - data[0].meta.events = encodedLink - - encoder.encode(data) - const buffer = encoder.makePayload() - const decoded = msgpack.decode(buffer, { useBigInt64: true }) - const trace = decoded[0] - expect(trace[0].meta.events).to.deep.equal(encodedLink) - }) + it('should encode span events within tags as a fallback to encoding as a top level field', () => { + const topLevelEvents = [ + { name: 'Something went so wrong', time_unix_nano: 1000000 }, + { + name: 'I can sing!!! acbdefggnmdfsdv k 2e2ev;!|=xxx', + time_unix_nano: 1633023102000000, + attributes: { emotion: 'happy', rating: 9.8, other: [1, 9.5, 1], idol: false } + } + ] - it('should encode spanLinks', () => { - const traceIdHigh = id('10') - const traceId = id('1234abcd1234abcd') - const rootTid = traceIdHigh.toString(16).padStart(16, '0') - const rootT64 = traceId.toString(16).padStart(16, '0') - const traceIdVal = `${rootTid}${rootT64}` - - const encodedLink = `[{"trace_id":"${traceIdVal}","span_id":"1234abcd1234abcd",` + - '"attributes":{"foo":"bar"},"tracestate":"dd=s:-1;o:foo;t.dm:-4;t.usr.id:bar","flags":1}]' - - data[0].meta['_dd.span_links'] = encodedLink - - encoder.encode(data) - - const buffer = encoder.makePayload() - const decoded = msgpack.decode(buffer, { useBigInt64: true }) - const trace = decoded[0] - expect(trace).to.be.instanceof(Array) - expect(trace[0]).to.be.instanceof(Object) - expect(trace[0].trace_id.toString(16)).to.equal(data[0].trace_id.toString()) - expect(trace[0].span_id.toString(16)).to.equal(data[0].span_id.toString()) - expect(trace[0].parent_id.toString(16)).to.equal(data[0].parent_id.toString()) - expect(trace[0].start).to.equal(123n) - expect(trace[0].duration).to.equal(456n) - expect(trace[0].name).to.equal(data[0].name) - expect(trace[0].meta).to.deep.equal({ bar: 'baz', '_dd.span_links': encodedLink }) - expect(trace[0].metrics).to.deep.equal({ example: 1 }) - }) + const encodedLink = '[{"name":"Something went so wrong","time_unix_nano":1000000},' + + '{"name":"I can sing!!! acbdefggnmdfsdv k 2e2ev;!|=xxx","time_unix_nano":1633023102000000,' + + '"attributes":{"emotion":"happy","rating":9.8,"other":[1,9.5,1],"idol":false}}]' - it('should encode spanLinks with just span and trace id', () => { - const traceId = '00000000000000001234abcd1234abcd' - const spanId = '1234abcd1234abcd' - const encodedLink = `[{"trace_id":"${traceId}","span_id":"${spanId}"}]` - data[0].meta['_dd.span_links'] = encodedLink - encoder.encode(data) - - const buffer = encoder.makePayload() - const decoded = msgpack.decode(buffer, { useBigInt64: true }) - const trace = decoded[0] - expect(trace).to.be.instanceof(Array) - expect(trace[0]).to.be.instanceof(Object) - expect(trace[0].trace_id.toString(16)).to.equal(data[0].trace_id.toString()) - expect(trace[0].span_id.toString(16)).to.equal(data[0].span_id.toString()) - expect(trace[0].parent_id.toString(16)).to.equal(data[0].parent_id.toString()) - expect(trace[0].start).to.equal(123n) - expect(trace[0].duration).to.equal(456n) - expect(trace[0].name).to.equal(data[0].name) - expect(trace[0].meta).to.deep.equal({ bar: 'baz', '_dd.span_links': encodedLink }) - expect(trace[0].metrics).to.deep.equal({ example: 1 }) - }) + data[0].span_events = topLevelEvents - describe('meta_struct', () => { - it('should encode meta_struct with simple key value object', () => { - const metaStruct = { - foo: 'bar', - baz: 123 - } - data[0].meta_struct = metaStruct encoder.encode(data) const buffer = encoder.makePayload() - const decoded = msgpack.decode(buffer, { useBigInt64: true }) const trace = decoded[0] - - expect(msgpack.decode(trace[0].meta_struct.foo)).to.be.equal(metaStruct.foo) - expect(msgpack.decode(trace[0].meta_struct.baz)).to.be.equal(metaStruct.baz) + expect(trace[0].meta.events).to.deep.equal(encodedLink) }) - it('should ignore array in meta_struct', () => { - const metaStruct = ['one', 2, 'three', 4, 5, 'six'] - data[0].meta_struct = metaStruct + it('should encode spanLinks', () => { + const traceIdHigh = id('10') + const traceId = id('1234abcd1234abcd') + const rootTid = traceIdHigh.toString(16).padStart(16, '0') + const rootT64 = traceId.toString(16).padStart(16, '0') + const traceIdVal = `${rootTid}${rootT64}` + + const encodedLink = `[{"trace_id":"${traceIdVal}","span_id":"1234abcd1234abcd",` + + '"attributes":{"foo":"bar"},"tracestate":"dd=s:-1;o:foo;t.dm:-4;t.usr.id:bar","flags":1}]' + + data[0].meta['_dd.span_links'] = encodedLink + encoder.encode(data) const buffer = encoder.makePayload() - const decoded = msgpack.decode(buffer, { useBigInt64: true }) const trace = decoded[0] - expect(trace[0].meta_struct).to.deep.equal({}) + expect(trace).to.be.instanceof(Array) + expect(trace[0]).to.be.instanceof(Object) + expect(trace[0].trace_id.toString(16)).to.equal(data[0].trace_id.toString()) + expect(trace[0].span_id.toString(16)).to.equal(data[0].span_id.toString()) + expect(trace[0].parent_id.toString(16)).to.equal(data[0].parent_id.toString()) + expect(trace[0].start).to.equal(123n) + expect(trace[0].duration).to.equal(456n) + expect(trace[0].name).to.equal(data[0].name) + expect(trace[0].meta).to.deep.equal({ bar: 'baz', '_dd.span_links': encodedLink }) + expect(trace[0].metrics).to.deep.equal({ example: 1 }) }) - it('should encode meta_struct with empty object and array', () => { - const metaStruct = { - foo: {}, - bar: [] - } - data[0].meta_struct = metaStruct + it('should encode spanLinks with just span and trace id', () => { + const traceId = '00000000000000001234abcd1234abcd' + const spanId = '1234abcd1234abcd' + const encodedLink = `[{"trace_id":"${traceId}","span_id":"${spanId}"}]` + data[0].meta['_dd.span_links'] = encodedLink encoder.encode(data) const buffer = encoder.makePayload() - const decoded = msgpack.decode(buffer, { useBigInt64: true }) const trace = decoded[0] - expect(msgpack.decode(trace[0].meta_struct.foo)).to.deep.equal(metaStruct.foo) - expect(msgpack.decode(trace[0].meta_struct.bar)).to.deep.equal(metaStruct.bar) + expect(trace).to.be.instanceof(Array) + expect(trace[0]).to.be.instanceof(Object) + expect(trace[0].trace_id.toString(16)).to.equal(data[0].trace_id.toString()) + expect(trace[0].span_id.toString(16)).to.equal(data[0].span_id.toString()) + expect(trace[0].parent_id.toString(16)).to.equal(data[0].parent_id.toString()) + expect(trace[0].start).to.equal(123n) + expect(trace[0].duration).to.equal(456n) + expect(trace[0].name).to.equal(data[0].name) + expect(trace[0].meta).to.deep.equal({ bar: 'baz', '_dd.span_links': encodedLink }) + expect(trace[0].metrics).to.deep.equal({ example: 1 }) }) - it('should encode meta_struct with possible real use case', () => { - const metaStruct = { - '_dd.stack': { - exploit: [ - { - type: 'test', - language: 'nodejs', - id: 'someuuid', - message: 'Threat detected', - frames: [ - { - id: 0, - file: 'test.js', - line: 1, - column: 31, - function: 'test' - }, - { - id: 1, - file: 'test2.js', - line: 54, - column: 77, - function: 'test' - }, - { - id: 2, - file: 'test.js', - line: 1245, - column: 41, - function: 'test' - }, - { - id: 3, - file: 'test3.js', - line: 2024, - column: 32, - function: 'test' - } - ] - } - ] + describe('meta_struct', () => { + it('should encode meta_struct with simple key value object', () => { + const metaStruct = { + foo: 'bar', + baz: 123 } - } - data[0].meta_struct = metaStruct + data[0].meta_struct = metaStruct + encoder.encode(data) - encoder.encode(data) + const buffer = encoder.makePayload() - const buffer = encoder.makePayload() + const decoded = msgpack.decode(buffer, { useBigInt64: true }) + const trace = decoded[0] - const decoded = msgpack.decode(buffer, { useBigInt64: true }) - const trace = decoded[0] - expect(msgpack.decode(trace[0].meta_struct['_dd.stack'])).to.deep.equal(metaStruct['_dd.stack']) - }) + expect(msgpack.decode(trace[0].meta_struct.foo)).to.be.equal(metaStruct.foo) + expect(msgpack.decode(trace[0].meta_struct.baz)).to.be.equal(metaStruct.baz) + }) - it('should encode meta_struct ignoring circular references in objects', () => { - const circular = { - bar: 'baz', - deeper: { - foo: 'bar' + it('should ignore array in meta_struct', () => { + const metaStruct = ['one', 2, 'three', 4, 5, 'six'] + data[0].meta_struct = metaStruct + encoder.encode(data) + + const buffer = encoder.makePayload() + + const decoded = msgpack.decode(buffer, { useBigInt64: true }) + const trace = decoded[0] + expect(trace[0].meta_struct).to.deep.equal({}) + }) + + it('should encode meta_struct with empty object and array', () => { + const metaStruct = { + foo: {}, + bar: [] } - } - circular.deeper.circular = circular - const metaStruct = { - foo: circular - } - data[0].meta_struct = metaStruct + data[0].meta_struct = metaStruct + encoder.encode(data) - encoder.encode(data) + const buffer = encoder.makePayload() - const buffer = encoder.makePayload() + const decoded = msgpack.decode(buffer, { useBigInt64: true }) + const trace = decoded[0] + expect(msgpack.decode(trace[0].meta_struct.foo)).to.deep.equal(metaStruct.foo) + expect(msgpack.decode(trace[0].meta_struct.bar)).to.deep.equal(metaStruct.bar) + }) - const decoded = msgpack.decode(buffer, { useBigInt64: true }) - const trace = decoded[0] + it('should encode meta_struct with possible real use case', () => { + const metaStruct = { + '_dd.stack': { + exploit: [ + { + type: 'test', + language: 'nodejs', + id: 'someuuid', + message: 'Threat detected', + frames: [ + { + id: 0, + file: 'test.js', + line: 1, + column: 31, + function: 'test' + }, + { + id: 1, + file: 'test2.js', + line: 54, + column: 77, + function: 'test' + }, + { + id: 2, + file: 'test.js', + line: 1245, + column: 41, + function: 'test' + }, + { + id: 3, + file: 'test3.js', + line: 2024, + column: 32, + function: 'test' + } + ] + } + ] + } + } + data[0].meta_struct = metaStruct + + encoder.encode(data) + + const buffer = encoder.makePayload() + + const decoded = msgpack.decode(buffer, { useBigInt64: true }) + const trace = decoded[0] + expect(msgpack.decode(trace[0].meta_struct['_dd.stack'])).to.deep.equal(metaStruct['_dd.stack']) + }) - const expectedMetaStruct = { - foo: { + it('should encode meta_struct ignoring circular references in objects', () => { + const circular = { bar: 'baz', deeper: { foo: 'bar' } } - } - expect(msgpack.decode(trace[0].meta_struct.foo)).to.deep.equal(expectedMetaStruct.foo) - }) + circular.deeper.circular = circular + const metaStruct = { + foo: circular + } + data[0].meta_struct = metaStruct - it('should encode meta_struct ignoring circular references in arrays', () => { - const circular = [{ - bar: 'baz' - }] - circular.push(circular) - const metaStruct = { - foo: circular - } - data[0].meta_struct = metaStruct + encoder.encode(data) - encoder.encode(data) + const buffer = encoder.makePayload() - const buffer = encoder.makePayload() + const decoded = msgpack.decode(buffer, { useBigInt64: true }) + const trace = decoded[0] - const decoded = msgpack.decode(buffer, { useBigInt64: true }) - const trace = decoded[0] + const expectedMetaStruct = { + foo: { + bar: 'baz', + deeper: { + foo: 'bar' + } + } + } + expect(msgpack.decode(trace[0].meta_struct.foo)).to.deep.equal(expectedMetaStruct.foo) + }) - const expectedMetaStruct = { - foo: [{ + it('should encode meta_struct ignoring circular references in arrays', () => { + const circular = [{ bar: 'baz' }] - } - expect(msgpack.decode(trace[0].meta_struct.foo)).to.deep.equal(expectedMetaStruct.foo) + circular.push(circular) + const metaStruct = { + foo: circular + } + data[0].meta_struct = metaStruct + + encoder.encode(data) + + const buffer = encoder.makePayload() + + const decoded = msgpack.decode(buffer, { useBigInt64: true }) + const trace = decoded[0] + + const expectedMetaStruct = { + foo: [{ + bar: 'baz' + }] + } + expect(msgpack.decode(trace[0].meta_struct.foo)).to.deep.equal(expectedMetaStruct.foo) + }) + + it('should encode meta_struct ignoring undefined properties', () => { + const metaStruct = { + foo: 'bar', + undefinedProperty: undefined + } + data[0].meta_struct = metaStruct + + encoder.encode(data) + + const buffer = encoder.makePayload() + + const decoded = msgpack.decode(buffer, { useBigInt64: true }) + const trace = decoded[0] + + const expectedMetaStruct = { + foo: 'bar' + } + expect(msgpack.decode(trace[0].meta_struct.foo)).to.deep.equal(expectedMetaStruct.foo) + expect(trace[0].meta_struct.undefinedProperty).to.be.undefined + }) + + it('should encode meta_struct ignoring null properties', () => { + const metaStruct = { + foo: 'bar', + nullProperty: null + } + data[0].meta_struct = metaStruct + + encoder.encode(data) + + const buffer = encoder.makePayload() + + const decoded = msgpack.decode(buffer, { useBigInt64: true }) + const trace = decoded[0] + + const expectedMetaStruct = { + foo: 'bar' + } + expect(msgpack.decode(trace[0].meta_struct.foo)).to.deep.equal(expectedMetaStruct.foo) + expect(trace[0].meta_struct.nullProperty).to.be.undefined + }) + + it('should not encode null meta_struct', () => { + data[0].meta_struct = null + + encoder.encode(data) + + const buffer = encoder.makePayload() + + const decoded = msgpack.decode(buffer, { useBigInt64: true }) + const trace = decoded[0] + + expect(trace[0].meta_struct).to.be.undefined + }) }) + }) + + describe('with configuration', () => { + let logger - it('should encode meta_struct ignoring undefined properties', () => { - const metaStruct = { - foo: 'bar', - undefinedProperty: undefined + beforeEach(() => { + // Create a sinon spy for log.debug + logger = { + debug: sinon.spy() } - data[0].meta_struct = metaStruct + + const { AgentEncoder } = proxyquire('../../src/encode/0.4', { + '../log': logger + }) + writer = { flush: sinon.spy(), _config: { trace: { nativeSpanEvents: true } } } + encoder = new AgentEncoder(writer) + }) + + it('should encode span events as a top-level field when the agent version supports this', () => { + const topLevelEvents = [ + { name: 'Something went so wrong', time_unix_nano: 1000000 }, + { + name: 'I can sing!!! acbdefggnmdfsdv k 2e2ev;!|=xxx', + time_unix_nano: 1633023102000000, + attributes: { emotion: 'happy', happiness: 10, rating: 9.8, other: ['hi', false, 1, 1.2], idol: false } + } + ] + + data[0].span_events = topLevelEvents encoder.encode(data) @@ -420,19 +510,48 @@ describe('encode', () => { const decoded = msgpack.decode(buffer, { useBigInt64: true }) const trace = decoded[0] - const expectedMetaStruct = { - foo: 'bar' - } - expect(msgpack.decode(trace[0].meta_struct.foo)).to.deep.equal(expectedMetaStruct.foo) - expect(trace[0].meta_struct.undefinedProperty).to.be.undefined + const formattedTopLevelEvent = [ + { name: 'Something went so wrong', time_unix_nano: 1000000 }, + { + name: 'I can sing!!! acbdefggnmdfsdv k 2e2ev;!|=xxx', + time_unix_nano: 1633023102000000, + attributes: { + emotion: { type: 0, string_value: 'happy' }, + idol: { type: 1, bool_value: false }, + happiness: { type: 2, int_value: 10 }, + rating: { type: 3, double_value: 9.8 }, + other: { + type: 4, + array_value: [ + { type: 0, string_value: 'hi' }, + { type: 1, bool_value: false }, + { type: 2, int_value: 1 }, + { type: 3, double_value: 1.2 } + ] + } + } + } + ] + + expect(trace[0].span_events).to.deep.equal(formattedTopLevelEvent) }) - it('should encode meta_struct ignoring null properties', () => { - const metaStruct = { - foo: 'bar', - nullProperty: null - } - data[0].meta_struct = metaStruct + it('should encode span events as a top-level field when agent supports it ' + + 'but skips encoding unsupported field types', () => { + const topLevelEvents = [ + { + name: 'I can sing!!! acbdefggnmdfsdv k 2e2ev;!|=xxx', + time_unix_nano: 1633023102000000, + attributes: { emotion: { unsupportedNestedObject: 'happiness' }, array: [['nested_array']] } + }, + { + name: 'I can sing!!!', + time_unix_nano: 1633023102000000, + attributes: { emotion: { unsupportedNestedObject: 'happiness' }, array: [['nested_array'], 'valid_value'] } + } + ] + + data[0].span_events = topLevelEvents encoder.encode(data) @@ -441,24 +560,85 @@ describe('encode', () => { const decoded = msgpack.decode(buffer, { useBigInt64: true }) const trace = decoded[0] - const expectedMetaStruct = { - foo: 'bar' - } - expect(msgpack.decode(trace[0].meta_struct.foo)).to.deep.equal(expectedMetaStruct.foo) - expect(trace[0].meta_struct.nullProperty).to.be.undefined + const formattedTopLevelEvent = [ + { + name: 'I can sing!!! acbdefggnmdfsdv k 2e2ev;!|=xxx', + time_unix_nano: 1633023102000000 + }, + { + name: 'I can sing!!!', + time_unix_nano: 1633023102000000, + attributes: { array: { type: 4, array_value: [{ type: 0, string_value: 'valid_value' }] } } + } + ] + + expect(trace[0].span_events).to.deep.equal(formattedTopLevelEvent) }) - it('should not encode null meta_struct', () => { - data[0].meta_struct = null + it('should call log.debug only once for the same unsupported key', () => { + const topLevelEvents = [ + { + name: 'Event 1', + time_unix_nano: 1000000, + attributes: { unsupported_key: { some: 'object' }, other_key: 'valid' } + }, + { + name: 'Event 2', + time_unix_nano: 2000000, + attributes: { unsupported_key: { another: 'object' } } + }, + { + name: 'Event 3', + time_unix_nano: 3000000, + attributes: { unsupported_key: { yet: 'another object' } } + }, + { + name: 'Event 4', + time_unix_nano: 4000000, + attributes: { unsupported_key: { different: 'structure' } } + } + ] + + data[0].span_events = topLevelEvents encoder.encode(data) - const buffer = encoder.makePayload() + // Assert that log.debug was called only once for 'unsupported_key' + sinon.assert.calledOnce(logger.debug) + sinon.assert.calledWith( + logger.debug, + sinon.match(/Encountered unsupported data type for span event v0\.4 encoding, key: unsupported_key/) + ) + }) - const decoded = msgpack.decode(buffer, { useBigInt64: true }) - const trace = decoded[0] + it('should call log.debug once per unique unsupported key', () => { + const topLevelEvents = [ + { + name: 'Event 1', + time_unix_nano: 1000000, + attributes: { unsupported_key1: { some: 'object' }, unsupported_key2: { another: 'object' } } + }, + { + name: 'Event 2', + time_unix_nano: 2000000, + attributes: { unsupported_key1: { different: 'structure' }, unsupported_key3: { more: 'objects' } } + }, + { + name: 'Event 3', + time_unix_nano: 3000000, + attributes: { unsupported_key2: { yet: 'another object' }, unsupported_key3: { extra: 'data' } } + } + ] + + data[0].span_events = topLevelEvents + + encoder.encode(data) - expect(trace[0].meta_struct).to.be.undefined + // Assert that log.debug was called once for each unique unsupported key + expect(logger.debug.callCount).to.equal(3) + expect(logger.debug.getCall(0).args[0]).to.match(/unsupported_key1/) + expect(logger.debug.getCall(1).args[0]).to.match(/unsupported_key2/) + expect(logger.debug.getCall(2).args[0]).to.match(/unsupported_key3/) }) }) }) diff --git a/packages/dd-trace/test/encode/0.5.spec.js b/packages/dd-trace/test/encode/0.5.spec.js index c28ca6fe492..607c0054e91 100644 --- a/packages/dd-trace/test/encode/0.5.spec.js +++ b/packages/dd-trace/test/encode/0.5.spec.js @@ -65,11 +65,20 @@ describe('encode 0.5', () => { }) it('should encode span events', () => { + const topLevelEvents = [ + { name: 'Something went so wrong', time_unix_nano: 1000000 }, + { + name: 'I can sing!!! acbdefggnmdfsdv k 2e2ev;!|=xxx', + time_unix_nano: 1633023102000000, + attributes: { emotion: 'happy', rating: 9.8, other: [1, 9.5, 1], idol: false } + } + ] + const encodedLink = '[{"name":"Something went so wrong","time_unix_nano":1000000},' + '{"name":"I can sing!!! acbdefggnmdfsdv k 2e2ev;!|=xxx","time_unix_nano":1633023102000000,' + '"attributes":{"emotion":"happy","rating":9.8,"other":[1,9.5,1],"idol":false}}]' - data[0].meta.events = encodedLink + data[0].span_events = topLevelEvents encoder.encode(data) diff --git a/packages/dd-trace/test/format.spec.js b/packages/dd-trace/test/format.spec.js index 8b606cf5747..9177cc1e86e 100644 --- a/packages/dd-trace/test/format.spec.js +++ b/packages/dd-trace/test/format.spec.js @@ -98,10 +98,11 @@ describe('format', () => { ] trace = format(span) - const spanEvents = JSON.parse(trace.meta.events) + const spanEvents = trace.span_events expect(spanEvents).to.deep.equal([{ name: 'Something went so wrong', - time_unix_nano: 1000000 + time_unix_nano: 1000000, + attributes: undefined }, { name: 'I can sing!!! acbdefggnmdfsdv k 2e2ev;!|=xxx', time_unix_nano: 1633023102000000, diff --git a/packages/dd-trace/test/llmobs/sdk/index.spec.js b/packages/dd-trace/test/llmobs/sdk/index.spec.js index 0f6a09bf17e..9075b24d4cc 100644 --- a/packages/dd-trace/test/llmobs/sdk/index.spec.js +++ b/packages/dd-trace/test/llmobs/sdk/index.spec.js @@ -327,6 +327,29 @@ describe('sdk', () => { }) }) }) + + it('passes the options to the tagger correctly', () => { + let span + llmobs.trace({ + kind: 'workflow', + name: 'test', + mlApp: 'override', + sessionId: 'sessionId', + modelName: 'modelName', + modelProvider: 'modelProvider' + }, (_span) => { + span = _span + }) + + expect(LLMObsTagger.tagMap.get(span)).to.deep.equal({ + '_ml_obs.meta.span.kind': 'workflow', + '_ml_obs.meta.ml_app': 'override', + '_ml_obs.meta.model_name': 'modelName', + '_ml_obs.meta.model_provider': 'modelProvider', + '_ml_obs.session_id': 'sessionId', + '_ml_obs.llmobs_parent_id': 'undefined' + }) + }) }) describe('wrap', () => { @@ -745,6 +768,32 @@ describe('sdk', () => { wrappedOuter() }) }) + + it('passes the options to the tagger correctly', () => { + let span + + const fn = llmobs.wrap({ + kind: 'workflow', + name: 'test', + mlApp: 'override', + sessionId: 'sessionId', + modelName: 'modelName', + modelProvider: 'modelProvider' + }, () => { + span = llmobs._active() + }) + + fn() + + expect(LLMObsTagger.tagMap.get(span)).to.deep.equal({ + '_ml_obs.meta.span.kind': 'workflow', + '_ml_obs.meta.ml_app': 'override', + '_ml_obs.meta.model_name': 'modelName', + '_ml_obs.meta.model_provider': 'modelProvider', + '_ml_obs.session_id': 'sessionId', + '_ml_obs.llmobs_parent_id': 'undefined' + }) + }) }) }) diff --git a/packages/dd-trace/test/runtime_metrics.spec.js b/packages/dd-trace/test/runtime_metrics.spec.js index a83a5aaf447..612c1e7f423 100644 --- a/packages/dd-trace/test/runtime_metrics.spec.js +++ b/packages/dd-trace/test/runtime_metrics.spec.js @@ -218,31 +218,31 @@ suiteDescribe('runtimeMetrics', () => { expect(client.gauge).to.have.been.calledWith('runtime.node.heap.malloced_memory') expect(client.gauge).to.have.been.calledWith('runtime.node.heap.peak_malloced_memory') - expect(client.gauge).to.have.been.calledWith('runtime.node.event_loop.delay.max') - expect(client.gauge).to.have.been.calledWith('runtime.node.event_loop.delay.min') - expect(client.increment).to.have.been.calledWith('runtime.node.event_loop.delay.sum') - expect(client.gauge).to.have.been.calledWith('runtime.node.event_loop.delay.avg') - expect(client.gauge).to.have.been.calledWith('runtime.node.event_loop.delay.median') - expect(client.gauge).to.have.been.calledWith('runtime.node.event_loop.delay.95percentile') - expect(client.increment).to.have.been.calledWith('runtime.node.event_loop.delay.count') + expect(client.gauge).to.have.been.calledWith('runtime.node.event_loop.delay.max', sinon.match.number) + expect(client.gauge).to.have.been.calledWith('runtime.node.event_loop.delay.min', sinon.match.number) + expect(client.increment).to.have.been.calledWith('runtime.node.event_loop.delay.sum', sinon.match.number) + expect(client.gauge).to.have.been.calledWith('runtime.node.event_loop.delay.avg', sinon.match.number) + expect(client.gauge).to.have.been.calledWith('runtime.node.event_loop.delay.median', sinon.match.number) + expect(client.gauge).to.have.been.calledWith('runtime.node.event_loop.delay.95percentile', sinon.match.number) + expect(client.increment).to.have.been.calledWith('runtime.node.event_loop.delay.count', sinon.match.number) expect(client.gauge).to.have.been.calledWith('runtime.node.event_loop.utilization') - expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.max') - expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.min') - expect(client.increment).to.have.been.calledWith('runtime.node.gc.pause.sum') - expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.avg') - expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.median') - expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.95percentile') - expect(client.increment).to.have.been.calledWith('runtime.node.gc.pause.count') - - expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.by.type.max') - expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.by.type.min') - expect(client.increment).to.have.been.calledWith('runtime.node.gc.pause.by.type.sum') - expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.by.type.avg') - expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.by.type.median') - expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.by.type.95percentile') - expect(client.increment).to.have.been.calledWith('runtime.node.gc.pause.by.type.count') + expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.max', sinon.match.number) + expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.min', sinon.match.number) + expect(client.increment).to.have.been.calledWith('runtime.node.gc.pause.sum', sinon.match.number) + expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.avg', sinon.match.number) + expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.median', sinon.match.number) + expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.95percentile', sinon.match.number) + expect(client.increment).to.have.been.calledWith('runtime.node.gc.pause.count', sinon.match.number) + + expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.by.type.max', sinon.match.number) + expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.by.type.min', sinon.match.number) + expect(client.increment).to.have.been.calledWith('runtime.node.gc.pause.by.type.sum', sinon.match.number) + expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.by.type.avg', sinon.match.number) + expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.by.type.median', sinon.match.number) + expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.by.type.95percentile', sinon.match.number) + expect(client.increment).to.have.been.calledWith('runtime.node.gc.pause.by.type.count', sinon.match.number) expect(client.increment).to.have.been.calledWith( 'runtime.node.gc.pause.by.type.count', sinon.match.any, sinon.match(val => { return val && /^gc_type:[a-z_]+$/.test(val[0]) diff --git a/yarn.lock b/yarn.lock index bf2f3788422..32531ab4f03 100644 --- a/yarn.lock +++ b/yarn.lock @@ -391,10 +391,10 @@ node-addon-api "^6.1.0" node-gyp-build "^3.9.0" -"@datadog/pprof@5.5.1": - version "5.5.1" - resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-5.5.1.tgz#fba8124b6ad537e29326f5f15ed6e64b7a009e96" - integrity sha512-3pZVYqc5YkZJOj9Rc8kQ/wG4qlygcnnwFU/w0QKX6dEdJh+1+dWniuUu+GSEjy/H0jc14yhdT2eJJf/F2AnHNw== +"@datadog/pprof@5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-5.6.0.tgz#b6f5c566512ba5e55c6dbf46e9f0f020cfd5c6b5" + integrity sha512-x7yN0s4wMnRqv3PWQ6eXKH5XE5qvCOwWbOsXqpT2Irbsc7Wcl5w5JrJUcbPCdSJGihpIh6kAeIrS6w/ZCcHy2Q== dependencies: delay "^5.0.0" node-gyp-build "<4.0"