dd-trace
Version:
Datadog APM tracing client for JavaScript
280 lines (268 loc) • 7.6 kB
JavaScript
'use strict'
const fs = require('fs')
const { DD_MAJOR } = require('../../../../version')
const tagger = require('../tagger')
let warnInvalidValue
function setWarnInvalidValue (fn) {
warnInvalidValue = fn
}
// `'b3 single header'` is the legacy spelling of `'b3'`; on v6 it is normalised below.
const VALID_PROPAGATION_STYLES = new Set([
'datadog', 'tracecontext', 'b3', 'b3 single header', 'b3multi', 'baggage', 'none',
])
const RENAMED_OTEL_TAGS = new Map(
[
['deployment.environment.name', 'env'],
['deployment.environment', 'env'],
['service.name', 'service'],
['service.version', 'version'],
]
)
function toCase (value, methodName) {
if (Array.isArray(value)) {
return value.map(item => {
return transformers[methodName](item)
})
}
return value[methodName]()
}
const transformers = {
setGRPCRange (value) {
if (value == null) {
return
}
value = value.split(',')
const result = []
for (const val of value) {
const dashIndex = val.indexOf('-')
if (dashIndex === -1) {
result.push(Number(val))
} else {
const start = Number(val.slice(0, dashIndex))
const end = Number(val.slice(dashIndex + 1))
for (let i = start; i <= end; i++) {
result.push(i)
}
}
}
return result
},
toLowerCase (value) {
return toCase(value, 'toLowerCase')
},
toUpperCase (value) {
return toCase(value, 'toUpperCase')
},
toCamelCase (value) {
if (Array.isArray(value)) {
return value.map(item => {
return transformers.toCamelCase(item)
})
}
if (typeof value === 'object' && value !== null) {
const result = {}
for (const [key, innerValue] of Object.entries(value)) {
const camelCaseKey = key.replaceAll(/_(\w)/g, (_, letter) => letter.toUpperCase())
result[camelCaseKey] = transformers.toCamelCase(innerValue)
}
return result
}
return value
},
parseOtelTags (object) {
const tags = {}
for (const [key, value] of Object.entries(object)) {
tags[RENAMED_OTEL_TAGS.get(key) ?? key] = value
}
return tags
},
normalizeProfilingEnabled (configValue) {
if (configValue == null) {
return
}
if (configValue === 'true' || configValue === '1') {
return 'true'
}
if (configValue === 'false' || configValue === '0') {
return 'false'
}
const lowercased = String(configValue).toLowerCase()
if (lowercased !== configValue) {
return transformers.normalizeProfilingEnabled(lowercased)
}
return configValue
},
sampleRate (value, optionName, source) {
const number = Number(value)
if (Number.isNaN(number) || value === null) {
warnInvalidValue(value, optionName, source, 'Sample rate invalid')
return
}
const clamped = Math.min(Math.max(number, 0), 1)
if (clamped !== number) {
warnInvalidValue(value, optionName, source, 'Sample rate out of range between 0 and 1')
return clamped
}
return number
},
readFilePath (raw, optionName, source) {
const { stackTraceLimit } = Error
Error.stackTraceLimit = 0
try {
return fs.readFileSync(raw, 'utf8')
} catch (error) {
warnInvalidValue(raw, optionName, source, 'Error reading path', error)
} finally {
Error.stackTraceLimit = stackTraceLimit
}
},
/**
* Given a string of comma-separated paths, return the array of paths.
* If a blank path is provided a null is returned to signal that the feature is disabled.
* An empty array means the feature is enabled but that no rules need to be applied.
*
* @param {string | string[]} input
*/
splitJSONPathRules (input) {
if (!input || input === '$') return
if (Array.isArray(input)) return input
if (input === 'all') return []
return input.split(',')
},
stripColonWhitespace (value) {
if (Array.isArray(value)) {
return value.map(item => {
return transformers.stripColonWhitespace(item)
})
}
return value.replaceAll(/\s*:\s*/g, ':')
},
validatePropagationStyles (value, optionName) {
value = transformers.toLowerCase(value)
for (let index = 0; index < value.length; index++) {
const propagator = value[index]
if (!VALID_PROPAGATION_STYLES.has(propagator)) {
warnInvalidValue(propagator, optionName, optionName, 'Invalid propagator')
return
}
if (DD_MAJOR >= 6 && propagator === 'b3 single header') {
value[index] = 'b3'
}
}
return value
},
}
const telemetryTransformers = {
JSON (object) {
return (typeof object !== 'object' || object === null) ? object : JSON.stringify(object)
},
MAP (object) {
if (typeof object !== 'object' || object === null) {
return object
}
let result = ''
for (const [key, value] of Object.entries(object)) {
result += `${key}:${value},`
}
return result.slice(0, -1)
},
ARRAY (array) {
return Array.isArray(array) ? array.join(',') : array
},
}
const parsers = {
BOOLEAN (raw) {
if (raw === 'true' || raw === '1') {
return true
}
if (raw === 'false' || raw === '0') {
return false
}
const lowercased = raw.toLowerCase()
if (lowercased !== raw) {
return parsers.BOOLEAN(lowercased)
}
},
INT (raw) {
const parsed = Math.trunc(raw)
if (Number.isNaN(parsed)) {
return
}
return parsed
},
DECIMAL (raw) {
const parsed = Number(raw)
if (Number.isNaN(parsed)) {
return
}
return parsed
},
ARRAY (raw) {
// TODO: Make the parsing a helper that is reused everywhere.
const result = []
if (!raw) {
return result
}
let valueStart = 0
for (let i = 0; i < raw.length; i++) {
const char = raw[i]
if (char === ',') {
const value = raw.slice(valueStart, i).trim()
// Auto filter empty entries.
if (value.length > 0) {
result.push(value)
}
valueStart = i + 1
}
}
if (valueStart < raw.length) {
const value = raw.slice(valueStart).trim()
// Auto filter empty entries.
if (value.length > 0) {
result.push(value)
}
}
return result
},
MAP (raw, optionName) {
/** @type {Record<string, string>} */
const entries = {}
if (!raw) {
return entries
}
let valueSeparator = ':'
if (optionName.startsWith('OTEL_')) {
// OTEL spec uses `key=value,key=value`
// (https://opentelemetry.io/docs/specs/otel/protocol/exporter/#specifying-headers-via-environment-variables),
// while DD uses `key:value,key:value`. Parse OTEL-prefixed options with `=` so downstream code
// receives a proper map and telemetry reports the parsed entries. The char-by-char loop
// avoids the allocations that `split(',')` + `indexOf('=')` do per pair.
valueSeparator = '='
} else if (optionName === 'DD_TAGS' && !raw.includes(',')) {
// DD_TAGS is a special case. It may be a map of key-value pairs separated by spaces.
raw = raw.replaceAll(/\s+/g, ',')
}
tagger.add(entries, raw, valueSeparator)
return entries
},
JSON (raw) {
const { stackTraceLimit } = Error
Error.stackTraceLimit = 0
try {
return JSON.parse(raw)
} catch {
// ignore
} finally {
Error.stackTraceLimit = stackTraceLimit
}
},
STRING (raw) {
return raw
},
}
module.exports = {
parsers,
transformers,
telemetryTransformers,
setWarnInvalidValue,
}