Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 78 additions & 28 deletions packages/dd-trace/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,6 @@ class Config {
this.#applyStableConfig(this.stableConfig?.fleetEntries ?? {}, this.#fleetStableConfig)
this.#applyOptions(options)
this.#applyCalculated()
this.#applyRemote({})
this.#merge()

tagger.add(this.tags, {
Expand All @@ -350,15 +349,26 @@ class Config {
return this.#parsedDdTags
}

// Supports only a subset of options for now.
configure (options, remote) {
if (remote) {
this.#applyRemote(options)
} else {
this.#applyOptions(options)
}
/**
* Updates the configuration with remote config settings.
* Applies remote configuration, recalculates derived values, and merges all configuration sources.
*
* @param {import('./config/remote_config').RemoteConfigOptions|null} options - Remote config
* lib_config object or null to reset all remote configuration
*/
updateRemoteConfig (options) {
this.#applyRemoteConfig(options)
this.#applyCalculated()
this.#merge()
}

// TODO: test
/**
* Updates the configuration with new programmatic options.
*
* @param {Object} options - Configuration options to apply (same format as tracer init options)
*/
updateOptions (options) {
this.#applyOptions(options)
this.#applyCalculated()
this.#merge()
}
Expand Down Expand Up @@ -1284,31 +1294,71 @@ class Config {
}
}

#applyRemote (options) {
const opts = this.#remote
const tags = {}
const headerTags = options.tracing_header_tags
? options.tracing_header_tags.map(tag => {
return tag.tag_name ? `${tag.header}:${tag.tag_name}` : tag.header
})
: undefined
/**
* Applies remote configuration options from APM_TRACING configs.
*
* This method uses field-guarding to support multi-config merging:
* - Only fields present in options are updated
* - Missing fields retain their previous values
* - Fields with null values are explicitly reset (converted to undefined in merge)
* - When options is null, all RC-managed fields are reset to defaults
*
* @param {import('./config/remote_config').RemoteConfigOptions|null} options - Remote config
* lib_config object or null to reset all
*/
#applyRemoteConfig (options) {
// Special case: if options is null, reset all RC-managed fields
// This happens when all remote configs are removed
if (options === null) {
this.#remote = {}
return
}

tagger.add(tags, options.tracing_tags)
if (Object.keys(tags).length) tags['runtime-id'] = runtimeId
const opts = this.#remote

this.#setUnit(opts, 'sampleRate', options.tracing_sampling_rate)
this.#setBoolean(opts, 'logInjection', options.log_injection_enabled)
opts.headerTags = headerTags
this.#setTags(opts, 'tags', tags)
this.#setBoolean(opts, 'tracing', options.tracing_enabled)
this.#remoteUnprocessed['sampler.rules'] = options.tracing_sampling_rules
this.#setSamplingRule(opts, 'sampler.rules', this.#reformatTags(options.tracing_sampling_rules))
if ('dynamic_instrumentation_enabled' in options || 'live_debugging_enabled' in options) {
this.#setBoolean(
opts,
'dynamicInstrumentation.enabled',
options.dynamic_instrumentation_enabled ?? options.live_debugging_enabled
)
}
if ('code_origin_enabled' in options) {
this.#setBoolean(opts, 'codeOriginForSpans.enabled', options.code_origin_enabled)
}
if ('tracing_header_tags' in options) {
const headerTags = options.tracing_header_tags
? options.tracing_header_tags.map(tag => {
return tag.tag_name ? `${tag.header}:${tag.tag_name}` : tag.header
})
: undefined
opts.headerTags = headerTags
}
if ('tracing_tags' in options) {
const tags = {}
tagger.add(tags, options.tracing_tags)
if (Object.keys(tags).length) tags['runtime-id'] = runtimeId
this.#setTags(opts, 'tags', tags)
}
if ('tracing_sampling_rate' in options) {
this.#setUnit(opts, 'sampleRate', options.tracing_sampling_rate)
}
if ('log_injection_enabled' in options) {
this.#setBoolean(opts, 'logInjection', options.log_injection_enabled)
}
if ('tracing_enabled' in options) {
this.#setBoolean(opts, 'tracing', options.tracing_enabled)
}
if ('tracing_sampling_rules' in options) {
this.#remoteUnprocessed['sampler.rules'] = options.tracing_sampling_rules
this.#setSamplingRule(opts, 'sampler.rules', this.#reformatTagsFromRC(options.tracing_sampling_rules))
}
}

#reformatTags (samplingRules) {
#reformatTagsFromRC (samplingRules) {
for (const rule of (samplingRules || [])) {
const reformattedTags = {}
if (rule.tags) {
const reformattedTags = {}
for (const tag of rule.tags) {
reformattedTags[tag.key] = tag.value_glob
}
Expand Down
220 changes: 202 additions & 18 deletions packages/dd-trace/src/config/remote_config.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,218 @@
'use strict'

const RemoteConfigCapabilities = require('../remote_config/capabilities')
const log = require('../log')

module.exports = {
enable
}

/**
* @typedef {object} RemoteConfigOptions
* @property {boolean} [dynamic_instrumentation_enabled] - Enable Dynamic Instrumentation
* @property {boolean} [live_debugging_enabled] - Enable Live Debugging (deprecated alias)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You didn't add exception_replay field. Because I assume it is not supported yet. Live debugging is also not used as it should enable trigger probes and capture code origin spans variables - see the spec for distributed debugger.

So, for now we only need to look at code origin and dynamic instrumentation flags.

* @property {boolean} [code_origin_enabled] - Enable code origin tagging for spans
* @property {Array<{header: string, tag_name?: string}>} [tracing_header_tags] - HTTP headers to tag
* @property {Array<string>} [tracing_tags] - Global tags (format: "key:value")
* @property {number} [tracing_sampling_rate] - Global sampling rate (0.0-1.0)
* @property {boolean} [log_injection_enabled] - Enable trace context log injection
* @property {boolean} [tracing_enabled] - Enable/disable tracing globally
* @property {Array<object>} [tracing_sampling_rules] - Trace sampling rules configuration
*/

/**
* @typedef {ReturnType<import('../config')>} Config
*/

/**
* Manages multiple APM_TRACING configurations with priority-based merging
*/
class RCClientLibConfigManager {
/**
* @param {string} currentService - Current service name
* @param {string} currentEnv - Current environment name
*/
constructor (currentService, currentEnv) {
this.configs = new Map() // config_id -> { conf, priority }
this.currentService = currentService
this.currentEnv = currentEnv
}

/**
* Calculate priority based on target specificity
* Priority order: Service+Env (5) > Service (4) > Env (3) > Cluster (2) > Org (1)
*
* @param {object} conf - Remote config object with service_target and k8s_target_v2 properties
* @returns {number} Priority value from 1 (org-level) to 5 (service+env specific)
*/
calculatePriority (conf) {
const serviceTarget = conf.service_target
const k8sTarget = conf.k8s_target_v2

if (serviceTarget) {
const service = serviceTarget.service
const env = serviceTarget.env

const hasSpecificService = service && service !== '*'
const hasSpecificEnv = env && env !== '*'

if (hasSpecificService && hasSpecificEnv) return 5
if (hasSpecificService) return 4
if (hasSpecificEnv) return 3
}

if (k8sTarget) return 2

return 1 // Org level
}

/**
* Check if config matches current service/env
*
* @param {object} conf - Remote config object with service_target property
* @returns {boolean} True if config matches current service/env or has no filter
*/
matchesCurrentServiceEnv (conf) {
const serviceTarget = conf.service_target
if (!serviceTarget) return true // No filter means match all

const service = serviceTarget.service
const env = serviceTarget.env

// Check service match
if (service && service !== '*' && service !== this.currentService) {
log.debug('[config/remote_config] Ignoring config for service: %s (current: %s)',
service, this.currentService)
return false
}

// Check env match
if (env && env !== '*' && env !== this.currentEnv) {
log.debug('[config/remote_config] Ignoring config for env: %s (current: %s)',
env, this.currentEnv)
return false
}

return true
}

/**
* Add or update a config
*
* @param {string} configId - Unique identifier for the config
* @param {object} conf - Remote config object to add
*/
addConfig (configId, conf) {
if (!this.matchesCurrentServiceEnv(conf)) {
return
}

const priority = this.calculatePriority(conf)
this.configs.set(configId, { conf, priority })

log.debug('[config/remote_config] Added config %s with priority %d', configId, priority)
}

/**
* Remove a config
*
* @param {string} configId - Unique identifier for the config to remove
*/
removeConfig (configId) {
const removed = this.configs.delete(configId)
if (removed) {
log.debug('[config/remote_config] Removed config %s', configId)
}
}

/**
* Get merged lib_config by taking first non-null value for each field
* Configs are sorted by priority (highest first)
*
* @returns {RemoteConfigOptions|null} Merged config object or null if no configs present
*/
getMergedLibConfig () {
if (this.configs.size === 0) {
// When no configs are present, return null to signal config.js to reset all RC fields
return null
}

// Sort configs by priority (highest first)
const sortedConfigs = [...this.configs.values()]
.sort((a, b) => b.priority - a.priority)

const merged = {}
let libConfigCount = 0

// Merge configs: take first non-null/undefined value for each field
// If a field is explicitly set to null, that means "reset to default"
for (const { conf } of sortedConfigs) {
const libConfig = conf.lib_config
if (libConfig == null) continue
libConfigCount++

for (const [key, value] of Object.entries(libConfig)) {
if (key in merged) continue

// Set the value even if it's null (to reset) but not if it's undefined (missing)
if (value === null) {
merged[key] = undefined // TODO: Should this be null?
} else if (value !== undefined) {
merged[key] = value
}
}
}

log.debug('[config/remote_config] Merged %d configs into lib_config', libConfigCount)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: if all libConfigs were null, we didn't merged anything, should we return {} or null.

Not sure if there is a difference,

return merged
}
}

/**
* Configures remote config for core APM tracing functionality
*
* @param {Object} rc - RemoteConfig instance
* @param {Object} config - Tracer config
* @param {Function} enableOrDisableTracing - Function to enable/disable tracing based on config
* @param {import('../remote_config')} rc - RemoteConfig instance
* @param {Config} config - Tracer config
* @param {(config: Config) => void} updateTracing - Function to update tracing state based on config
* @param {(config: Config, rc: import('../remote_config')) => void} updateDebugger - Function to update debugger state
* based on config
*/
function enable (rc, config, enableOrDisableTracing) {
// Register core APM tracing capabilities
function enable (rc, config, updateTracing, updateDebugger) {
// This tracer supports receiving config subsets via the APM_TRACING product handler.
rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_MULTICONFIG, true)

// Tracing
rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_ENABLED, true)
rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_SAMPLE_RATE, true)
rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_SAMPLE_RULES, true)
rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_CUSTOM_TAGS, true)
rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_HTTP_HEADER_TAGS, true)

// Log Management
rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_LOGS_INJECTION, true)
rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_SAMPLE_RATE, true)
rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_ENABLED, true)
rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_SAMPLE_RULES, true)

// APM_TRACING product handler - manages tracer configuration
rc.setProductHandler('APM_TRACING', (action, conf) => {
// Debugger
rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_ENABLE_DYNAMIC_INSTRUMENTATION, true)
rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_ENABLE_LIVE_DEBUGGING, true)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this one


// Code Origin
rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_ENABLE_CODE_ORIGIN, true)

const rcClientLibConfigManager = new RCClientLibConfigManager(config.service, config.env)

rc.setProductHandler('APM_TRACING', (action, conf, configId) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this run on every config we get? this mean are calling config.updateRemoteConfig multiple times. Maybe there is a better way to only call updateRemoteConfig only once we receive all the configs. I assume we would get all of the relevant configs on the first request.

if (action === 'unapply') {
config.configure({}, true)
} else {
config.configure(conf.lib_config, true)
rcClientLibConfigManager.removeConfig(configId)
} else { // apply or modify
rcClientLibConfigManager.addConfig(configId, conf)
}
enableOrDisableTracing(config)
})
}

module.exports = {
enable
// Get merged config and apply it
const mergedLibConfig = rcClientLibConfigManager.getMergedLibConfig()
config.updateRemoteConfig(mergedLibConfig)

// Update features based on merged config
updateTracing(config)
updateDebugger(config, rc)
})
}
Loading