UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

336 lines (304 loc) 11 kB
'use strict' const dns = require('dns') const util = require('util') const { DD_MAJOR } = require('../../../../version') const { parsers, transformers, telemetryTransformers, setWarnInvalidValue } = require('./parsers') const applyMajorOverrides = require('./major-overrides') const { supportedConfigurations, } = /** @type {import('./helper').SupportedConfigurationsJson} */ (require('./supported-configurations.json')) applyMajorOverrides(supportedConfigurations, DD_MAJOR) let log let seqId = 0 const configWithOrigin = new Map() const parseErrors = new Map() /** * Warns about an invalid value for an option and adds the error to the last telemetry entry if it is not already set. * Logging happens only if the error is not already set or the option name is different from the last telemetry entry. * * @param {unknown} value - The value that is invalid. * @param {string} optionName - The name of the option. * @param {string} source - The source of the value. * @param {string} baseMessage - The base message to use for the warning. * @param {Error} [error] - An error that was thrown while parsing the value. */ function warnInvalidValue (value, optionName, source, baseMessage, error) { const canonicalName = (optionsTable[optionName]?.canonicalName ?? optionName) + source // Lazy load log module to avoid circular dependency if (!parseErrors.has(canonicalName)) { // TODO: Rephrase: It will fallback to former source (or default if not set) let message = `${baseMessage}: ${util.inspect(value)} for ${optionName} (source: ${source}), picked default` if (error) { error.stack = error.toString() message += `\n\n${util.inspect(error)}` } parseErrors.set(canonicalName, { message }) log ??= require('../log') const logLevel = error ? 'error' : 'warn' log[logLevel](message) } } setWarnInvalidValue(warnInvalidValue) /** @type {import('./config-types').ConfigDefaults} */ const defaults = { instrumentationSource: 'manual', isServiceUserProvided: false, plugins: true, isCiVisibility: false, lookup: dns.lookup, logger: undefined, } for (const [name, value] of Object.entries(defaults)) { configWithOrigin.set(`${name}default`, { name, value: value ?? null, origin: 'default', seq_id: seqId++, }) } /** * @param {unknown} value * @param {string} origin * @param {string} optionName */ function generateTelemetry (value = null, origin, optionName) { const tableEntry = configurationsTable[optionName] const { type, canonicalName = optionName } = tableEntry ?? { type: typeof value } // TODO: Should we not send defaults to telemetry to reduce size? // TODO: How to handle aliases/actual names in the future? Optional fields? Normalize the name at intake? // TODO: Validate that space separated tags are parsed by the backend. Optimizations would be possible with that. // TODO: How to handle telemetry reporting for aliases? if (value !== null) { if (telemetryTransformers[type]) { value = telemetryTransformers[type](value) } else if (typeof value === 'object' && value !== null) { // Custom optionsTable entries (no `configurationsTable` row, e.g. `logger`) // hold opaque user-supplied references that may carry cycles, so avoid // traversing them via JSON.stringify. value = tableEntry === undefined ? util.inspect(value, { depth: -1 }) : value instanceof URL ? String(value) : JSON.stringify(value) } else if (typeof value === 'function') { value = value.name || 'function' } } const telemetryEntry = { name: canonicalName, value, origin, seq_id: seqId++, } const error = parseErrors.get(`${canonicalName}${origin}`) if (error) { parseErrors.delete(`${canonicalName}${origin}`) telemetryEntry.error = error } configWithOrigin.set(`${canonicalName}${origin}`, telemetryEntry) } // Iterate over the object and always handle the leaf properties as lookup. // Example entries: // // cloudPayloadTagging: { // nestedProperties: [ // 'rules', // 'requestsEnabled', // 'responses', // ], // option: { // property: 'rules', // parser: parsers.JSON, // canonicalName: 'DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING', // transformer: transformers.toCamelCase, // }, // }, // 'cloudPayloadTagging.responses': { // nestedProperties: [ // 'enabled', // ], // }, // 'cloudPayloadTagging.rules': {}, // 'cloudPayloadTagging.requestsEnabled': {}, // 'cloudPayloadTagging.responses.enabled': {} const optionsTable = { // Additional properties that are not supported by the supported-configurations.json file. lookup: { transformer (value) { if (typeof value === 'function') { return value } }, property: 'lookup', }, logger: { transformer (object) { // Create lazily to avoid the overhead when not used. // Match at least one log level. const knownLogLevels = new Set(supportedConfigurations.DD_TRACE_LOG_LEVEL[0].allowed?.split('|')) if (typeof object !== 'object' || object === null) { return object } let matched = false for (const logLevel of knownLogLevels) { if (object[logLevel] !== undefined) { if (typeof object[logLevel] !== 'function') { warnInvalidValue(object[logLevel], 'logger', 'default', `Invalid log level ${logLevel}`) return } matched = true } } if (matched) { return object } }, property: 'logger', }, isCiVisibility: { property: 'isCiVisibility', }, plugins: { property: 'plugins', }, } const parser = (value, optionName, source) => { const { type, canonicalName = optionName } = configurationsTable[optionName] const parsed = parsers[type](value, canonicalName) if (parsed === undefined) { warnInvalidValue(value, optionName, source, `Invalid ${type} input`) } return parsed } /** * @template {import('./config-types').ConfigPath} TPath * @type {Partial<Record<TPath, { * property?: string, * parser: (value: unknown, optionName: string, source: string) => unknown, * canonicalName?: string, * transformer?: (value: unknown, optionName: string, source: string) => unknown, * telemetryTransformer?: (value: unknown) => unknown * }>>} ConfigurationsTable */ const configurationsTable = {} // One way aliases. Must be applied in apply calculated entries. const fallbackConfigurations = new Map() const regExps = {} for (const [canonicalName, entries] of Object.entries(supportedConfigurations)) { if (entries.length !== 1) { // TODO: Determine if we really want to support multiple entries for a canonical name. // This would be needed to show official support for multiple diverging implementations // at a time with by checking for another configuration that is not the canonical name. throw new Error( `Multiple entries found for canonical name: ${canonicalName}. ` + 'This is currently not supported and must be implemented, if needed.' ) } for (const entry of entries) { const configurationNames = entry.internalPropertyName ? [entry.internalPropertyName] : entry.configurationNames const fullPropertyName = configurationNames?.[0] ?? canonicalName const type = entry.type.toUpperCase() let transformer = transformers[entry.transform] if (entry.allowed) { regExps[entry.allowed] ??= new RegExp(`^(${entry.allowed})$`, 'i') const allowed = regExps[entry.allowed] const originalTransform = transformer transformer = (value, optionName, source) => { if (!allowed.test(value)) { warnInvalidValue(value, optionName, source, 'Invalid value') return } if (originalTransform) { value = originalTransform(value) } return value } } const option = { parser, type } if (fullPropertyName !== canonicalName) { option.property = fullPropertyName option.canonicalName = canonicalName configurationsTable[fullPropertyName] = option } if (transformer) { option.transformer = transformer } if (entry.configurationNames) { addOption(option, entry.configurationNames) } configurationsTable[canonicalName] = option if (entry.default === null) { defaults[fullPropertyName] = undefined } else { let parsedDefault = parser(entry.default, fullPropertyName, 'default') if (entry.transform) { parsedDefault = transformer(parsedDefault, fullPropertyName, 'default') } defaults[fullPropertyName] = parsedDefault } generateTelemetry(defaults[fullPropertyName], 'default', fullPropertyName) if (entry.aliases) { for (const alias of entry.aliases) { if (!supportedConfigurations[alias]) { // An actual alias has no matching entry continue } if (!supportedConfigurations[alias].aliases?.includes(canonicalName)) { // Alias will be replaced with the full property name of the alias, if it exists. fallbackConfigurations.set(fullPropertyName, alias) } } } } } // Replace the alias with the canonical property name. for (const [fullPropertyName, alias] of fallbackConfigurations) { if (configurationsTable[alias].property) { fallbackConfigurations.set(fullPropertyName, configurationsTable[alias].property) } } function addOption (option, configurationNames) { for (const name of configurationNames) { let index = -1 let lastNestedProperties while (true) { const nextIndex = name.indexOf('.', index + 1) const intermediateName = nextIndex === -1 ? name : name.slice(0, nextIndex) if (lastNestedProperties) { lastNestedProperties.add(intermediateName.slice(index + 1)) } if (nextIndex === -1) { if (optionsTable[name]) { if (optionsTable[name].nestedProperties && !optionsTable[name].option) { optionsTable[name].option = option break } throw new Error(`Duplicate configuration name: ${name}`) } optionsTable[name] = option break } lastNestedProperties = new Set() index = nextIndex if (!optionsTable[intermediateName]) { optionsTable[intermediateName] = { nestedProperties: lastNestedProperties, } } else if (optionsTable[intermediateName].nestedProperties) { lastNestedProperties = optionsTable[intermediateName].nestedProperties } else { optionsTable[intermediateName] = { nestedProperties: lastNestedProperties, option: optionsTable[intermediateName], } } } } } module.exports = { configurationsTable, defaults, fallbackConfigurations, optionsTable, configWithOrigin, parseErrors, generateTelemetry, }