diff --git a/tests/appsec/test_asm_standalone.py b/tests/appsec/test_asm_standalone.py index 43cc73d368e..ce6c7cdf48c 100644 --- a/tests/appsec/test_asm_standalone.py +++ b/tests/appsec/test_asm_standalone.py @@ -737,9 +737,17 @@ def test_telemetry_sca_enabled_propagated(self): payload.sort(key=lambda item: item["seq_id"], reverse=True) assert configuration_by_name - dd_appsec_sca_enabled = TelemetryUtils.get_dd_appsec_sca_enabled_str(context.library) - - cfg_appsec_enabled = configuration_by_name.get(dd_appsec_sca_enabled) + dd_appsec_sca_enabled_names = TelemetryUtils.get_dd_appsec_sca_enabled_names(context.library) + dd_appsec_sca_enabled = " or ".join(dd_appsec_sca_enabled_names) + + cfg_appsec_enabled = next( + ( + configuration_by_name.get(config_name) + for config_name in dd_appsec_sca_enabled_names + if config_name in configuration_by_name + ), + None, + ) assert cfg_appsec_enabled is not None, f"Missing telemetry config item for '{dd_appsec_sca_enabled}'" outcome_value: bool | str = True diff --git a/tests/docker_ssi/test_docker_ssi_appsec.py b/tests/docker_ssi/test_docker_ssi_appsec.py index ae07be16767..18597c58f81 100644 --- a/tests/docker_ssi/test_docker_ssi_appsec.py +++ b/tests/docker_ssi/test_docker_ssi_appsec.py @@ -46,8 +46,8 @@ def test_telemetry_source_ssi(self): # Check that instrumentation source is ssi injection_source = ( - configurations.get("DD_APPSEC_ENABLED") # Python - or configurations.get("appsec.enabled") # Node.js & PHP + configurations.get("DD_APPSEC_ENABLED") # Node.js & Python + or configurations.get("appsec.enabled") # PHP or configurations.get("appsec_enabled") # Java ) assert injection_source, f"instrumentation_source not found in configuration {configurations}" diff --git a/tests/parametric/test_telemetry.py b/tests/parametric/test_telemetry.py index 034b6a9ceb5..90b5ceb2a8c 100644 --- a/tests/parametric/test_telemetry.py +++ b/tests/parametric/test_telemetry.py @@ -18,17 +18,20 @@ telemetry_name_mapping: dict[str, dict[str, str | list[str]]] = { "instrumentation_source": { "java": "DD_INSTRUMENTATION_SOURCE", + "nodejs": ["instrumentationSource", "instrumentation_source"], }, "ssi_injection_enabled": { "python": "DD_INJECTION_ENABLED", "java": "DD_INJECTION_ENABLED", "ruby": "DD_INJECTION_ENABLED", + "nodejs": ["DD_INJECTION_ENABLED", "injectionEnabled", "ssi_injection_enabled"], "golang": ["DD_INJECTION_ENABLED", "injection_enabled"], }, "ssi_forced_injection_enabled": { "python": "DD_INJECT_FORCE", "ruby": "DD_INJECT_FORCE", "java": "DD_INJECT_FORCE", + "nodejs": ["DD_INJECT_FORCE", "injectForce", "ssi_forced_injection_enabled"], "golang": ["DD_INJECT_FORCE", "inject_force"], }, "trace_sample_rate": { @@ -41,7 +44,7 @@ }, "logs_injection_enabled": { "dotnet": "DD_LOGS_INJECTION", - "nodejs": "DD_LOG_INJECTION", # TODO: rename to DD_LOGS_INJECTION in subsequent PR + "nodejs": ["DD_LOGS_INJECTION", "DD_LOG_INJECTION"], "python": "DD_LOGS_INJECTION", "php": "trace.logs_enabled", "ruby": "DD_LOGS_INJECTION", @@ -67,14 +70,14 @@ "trace_enabled": { "dotnet": "DD_TRACE_ENABLED", "java": "DD_TRACE_ENABLED", - "nodejs": "tracing", + "nodejs": ["DD_TRACE_ENABLED", "tracing"], "python": "DD_TRACE_ENABLED", "ruby": "DD_TRACE_ENABLED", "golang": ["DD_TRACE_ENABLED", "trace_enabled"], }, "profiling_enabled": { "dotnet": "DD_PROFILING_ENABLED", - "nodejs": "profiling.enabled", + "nodejs": ["DD_PROFILING_ENABLED", "profiling.enabled"], "python": "DD_PROFILING_ENABLED", "ruby": "DD_PROFILING_ENABLED", "golang": ["DD_PROFILING_ENABLED", "profiling_enabled"], @@ -82,7 +85,7 @@ }, "appsec_enabled": { "dotnet": "DD_APPSEC_ENABLED", - "nodejs": "appsec.enabled", + "nodejs": ["DD_APPSEC_ENABLED", "appsec.enabled"], "python": "DD_APPSEC_ENABLED", "ruby": "DD_APPSEC_ENABLED", "golang": ["DD_APPSEC_ENABLED", "appsec_enabled"], @@ -90,7 +93,7 @@ }, "data_streams_enabled": { "dotnet": "DD_DATA_STREAMS_ENABLED", - "nodejs": "dsmEnabled", + "nodejs": ["DD_DATA_STREAMS_ENABLED", "dsmEnabled"], "python": "DD_DATA_STREAMS_ENABLED", "java": "DD_DATA_STREAMS_ENABLED", "golang": ["DD_DATA_STREAMS_ENABLED", "data_streams_enabled"], @@ -99,7 +102,7 @@ "runtime_metrics_enabled": { "java": "DD_RUNTIME_METRICS_ENABLED", "dotnet": "DD_RUNTIME_METRICS_ENABLED", - "nodejs": "runtime.metrics.enabled", + "nodejs": ["DD_RUNTIME_METRICS_ENABLED", "runtime.metrics.enabled"], "python": "DD_RUNTIME_METRICS_ENABLED", "ruby": "DD_RUNTIME_METRICS_ENABLED", "golang": ["DD_RUNTIME_METRICS_ENABLED", "runtime_metrics_enabled"], @@ -107,7 +110,7 @@ "dynamic_instrumentation_enabled": { "java": "DD_DYNAMIC_INSTRUMENTATION_ENABLED", "dotnet": "DD_DYNAMIC_INSTRUMENTATION_ENABLED", - "nodejs": "dynamicInstrumentation.enabled", + "nodejs": ["DD_DYNAMIC_INSTRUMENTATION_ENABLED", "dynamicInstrumentation.enabled"], "python": "DD_DYNAMIC_INSTRUMENTATION_ENABLED", "php": "dynamic_instrumentation.enabled", "ruby": "DD_DYNAMIC_INSTRUMENTATION_ENABLED", @@ -162,16 +165,22 @@ def _check_propagation_style_with_inject_and_extract( """ # Define the inject and extract key names for each language if library_name in ("python", "ruby"): - inject_key = "DD_TRACE_PROPAGATION_STYLE_INJECT" - extract_key = "DD_TRACE_PROPAGATION_STYLE_EXTRACT" + inject_keys = ["DD_TRACE_PROPAGATION_STYLE_INJECT"] + extract_keys = ["DD_TRACE_PROPAGATION_STYLE_EXTRACT"] elif library_name == "nodejs": - inject_key = "tracePropagationStyle.inject" - extract_key = "tracePropagationStyle.extract" + inject_keys = ["DD_TRACE_PROPAGATION_STYLE_INJECT", "tracePropagationStyle.inject"] + extract_keys = ["DD_TRACE_PROPAGATION_STYLE_EXTRACT", "tracePropagationStyle.extract"] else: raise ValueError(f"Unsupported library for inject/extract propagation style: {library_name}") # Check inject key - inject_item = test_agent.get_telemetry_config_by_origin(configuration_by_name, inject_key, expected_origin) + inject_item = None + inject_key = inject_keys[0] + for candidate in inject_keys: + inject_item = test_agent.get_telemetry_config_by_origin(configuration_by_name, candidate, expected_origin) + if inject_item is not None: + inject_key = candidate + break assert inject_item is not None, ( f"No configuration found for '{inject_key}' with origin '{expected_origin}'. Full configuration_by_name: {configuration_by_name}" ) @@ -182,7 +191,13 @@ def _check_propagation_style_with_inject_and_extract( assert inject_item["value"], f"Expected non-empty value for '{inject_key}'" # Check extract key - extract_item = test_agent.get_telemetry_config_by_origin(configuration_by_name, extract_key, expected_origin) + extract_item = None + extract_key = extract_keys[0] + for candidate in extract_keys: + extract_item = test_agent.get_telemetry_config_by_origin(configuration_by_name, candidate, expected_origin) + if extract_item is not None: + extract_key = candidate + break assert extract_item is not None, ( f"No configuration found for '{extract_key}' with origin '{expected_origin}'. Full configuration_by_name: {configuration_by_name}" ) @@ -320,12 +335,9 @@ def test_library_settings(self, test_agent: TestAgentAPI, test_library: APMLibra ) == "5.2.0" ) - assert ( - test_agent.get_telemetry_config_by_origin( - configuration_by_name, "DD_TRACE_RATE_LIMIT", "env_var", return_value_only=True - ) - == "10" - ) + assert test_agent.get_telemetry_config_by_origin( + configuration_by_name, "DD_TRACE_RATE_LIMIT", "env_var", return_value_only=True + ) in ("10", 10) assert ( test_agent.get_telemetry_config_by_origin( configuration_by_name, "DD_TRACE_HEADER_TAGS", "env_var", return_value_only=True @@ -1084,17 +1096,27 @@ def test_injection_enabled( ssi_enabled_telemetry_names = _mapped_telemetry_name("ssi_injection_enabled") inject_enabled = None for ssi_name in ssi_enabled_telemetry_names: - inject_enabled = test_agent.get_telemetry_config_by_origin( - configuration_by_name, ssi_name, "env_var", fallback_to_first=(expected_value is None) - ) + inject_enabled = test_agent.get_telemetry_config_by_origin(configuration_by_name, ssi_name, "env_var") if inject_enabled is not None: break + if inject_enabled is None and context.library == "nodejs": + for ssi_name in ssi_enabled_telemetry_names: + inject_enabled = test_agent.get_telemetry_config_by_origin( + configuration_by_name, ssi_name, "calculated", fallback_to_first=True + ) + if inject_enabled is not None: + break assert inject_enabled is not None, ( f"No configuration found for any of {' or '.join(ssi_enabled_telemetry_names)}" ) assert isinstance(inject_enabled, dict) - assert inject_enabled.get("value") == expected_value - if expected_value is not None: + expected_values: tuple[object, ...] = (expected_value,) + if context.library == "nodejs": + expected_values += ([item.strip() for item in expected_value.split(",")],) + assert inject_enabled.get("value") in expected_values + # Node.js now derives the SSI source config from canonical config entries. Once PR #7734 + # is fully rolled out, restore the strict env_var origin assertion here. + if expected_value is not None and context.library != "nodejs": assert inject_enabled.get("origin") == "env_var" @pytest.mark.parametrize( @@ -1132,16 +1154,26 @@ def test_inject_force(self, expected_value: str, test_agent: TestAgentAPI, test_ inject_force = None for inject_force_name in inject_force_telemetry_names: inject_force = test_agent.get_telemetry_config_by_origin( - configuration_by_name, inject_force_name, "env_var", fallback_to_first=(expected_value == "none") + configuration_by_name, inject_force_name, "env_var" ) if inject_force is not None: break + if inject_force is None and context.library == "nodejs": + for inject_force_name in inject_force_telemetry_names: + inject_force = test_agent.get_telemetry_config_by_origin( + configuration_by_name, inject_force_name, "calculated", fallback_to_first=True + ) + if inject_force is not None: + break assert inject_force is not None, ( f"No configuration found for any of {' or '.join(inject_force_telemetry_names)}" ) assert isinstance(inject_force, dict) assert str(inject_force.get("value")).lower() == expected_value - assert inject_force.get("origin") == "env_var" + # Node.js now derives the SSI source config from canonical config entries. Once PR #7734 + # is fully rolled out, restore the strict env_var origin assertion here. + if context.library != "nodejs": + assert inject_force.get("origin") == "env_var" @pytest.mark.parametrize("library_env", [{**DEFAULT_ENVVARS, "DD_SERVICE": "service_test"}]) def test_instrumentation_source_non_ssi(self, test_agent: TestAgentAPI, test_library: APMLibrary): @@ -1161,6 +1193,15 @@ def test_instrumentation_source_non_ssi(self, test_agent: TestAgentAPI, test_lib ) if instrumentation_source is not None: break + # Node.js reports instrumentationSource as a calculated value with the new config pipeline. + # Remove this fallback after PR #7734 lands and older values no longer need coverage. + if instrumentation_source is None and context.library == "nodejs": + for instrumentation_source_name in instrumentation_source_telemetry_names: + instrumentation_source = test_agent.get_telemetry_config_by_origin( + configuration_by_name, instrumentation_source_name, "calculated", fallback_to_first=True + ) + if instrumentation_source is not None: + break assert instrumentation_source is not None, ( f"No configuration found for any of {' or '.join(instrumentation_source_telemetry_names)}" ) @@ -1217,7 +1258,8 @@ def _assert_telemetry_sca_enabled_propagated( ): assert test_library.is_alive(), "Library container is not running" configuration_by_name = test_agent.wait_for_telemetry_configurations() - dd_appsec_sca_enabled = TelemetryUtils.get_dd_appsec_sca_enabled_str(context.library) + dd_appsec_sca_enabled_names = TelemetryUtils.get_dd_appsec_sca_enabled_names(context.library) + dd_appsec_sca_enabled = " or ".join(dd_appsec_sca_enabled_names) logger.info(f"""Check that: * the env var DD_APPSEC_SCA_ENABLED={library_env["DD_APPSEC_SCA_ENABLED"]} @@ -1225,7 +1267,14 @@ def _assert_telemetry_sca_enabled_propagated( assert configuration_by_name is not None, "Missing telemetry configuration" - cfg_appsec_enabled = configuration_by_name.get(dd_appsec_sca_enabled) + cfg_appsec_enabled = next( + ( + configuration_by_name.get(config_name) + for config_name in dd_appsec_sca_enabled_names + if config_name in configuration_by_name + ), + None, + ) logger.info(f"Oberved {dd_appsec_sca_enabled}: {cfg_appsec_enabled}") assert cfg_appsec_enabled is not None, f"Missing telemetry config item for '{dd_appsec_sca_enabled}'" @@ -1239,11 +1288,19 @@ def test_telemetry_sca_enabled_not_propagated(self, test_agent: TestAgentAPI, te assert configuration_by_name is not None, "Missing telemetry configuration" - dd_appsec_sca_enabled = TelemetryUtils.get_dd_appsec_sca_enabled_str(context.library) + dd_appsec_sca_enabled_names = TelemetryUtils.get_dd_appsec_sca_enabled_names(context.library) + dd_appsec_sca_enabled = " or ".join(dd_appsec_sca_enabled_names) if context.library in ("java", "nodejs", "python", "ruby"): - cfg_appsec_enabled = configuration_by_name.get(dd_appsec_sca_enabled) + cfg_appsec_enabled = next( + ( + configuration_by_name.get(config_name) + for config_name in dd_appsec_sca_enabled_names + if config_name in configuration_by_name + ), + None, + ) assert cfg_appsec_enabled is not None, f"Missing telemetry config item for '{dd_appsec_sca_enabled}'" assert cfg_appsec_enabled[0].get("value") is None else: - assert dd_appsec_sca_enabled not in configuration_by_name + assert all(config_name not in configuration_by_name for config_name in dd_appsec_sca_enabled_names) diff --git a/tests/test_telemetry.py b/tests/test_telemetry.py index 17a863be28f..9b7f436b8e5 100644 --- a/tests/test_telemetry.py +++ b/tests/test_telemetry.py @@ -465,9 +465,9 @@ def test_app_started_client_configuration(self): trace_agent_port = scenarios.default.weblog_container.trace_agent_port - test_configuration: dict[str, dict] = { + test_configuration: dict[str, dict[str, object]] = { "dotnet": {}, - "nodejs": {"hostname": "proxy", "port": trace_agent_port, "appsec.enabled": True}, + "nodejs": {"DD_AGENT_HOST": "proxy", "DD_TRACE_AGENT_PORT": trace_agent_port, "DD_APPSEC_ENABLED": True}, # to-do :need to add configuration keys once python bug is fixed "python": {}, "cpp_nginx": {"trace_agent_port": trace_agent_port}, @@ -477,6 +477,11 @@ def test_app_started_client_configuration(self): "golang": {"lambda_mode": False}, } configuration_map = test_configuration[context.library.name] + nodejs_legacy_config_names = { + "DD_AGENT_HOST": ["DD_AGENT_HOST", "hostname"], + "DD_TRACE_AGENT_PORT": ["DD_TRACE_AGENT_PORT", "port"], + "DD_APPSEC_ENABLED": ["DD_APPSEC_ENABLED", "appsec.enabled"], + } def validator(data: dict): if get_request_type(data) == "app-started": @@ -486,10 +491,14 @@ def validator(data: dict): # validator is updated to handle tracers sending configuration chaining data for expected_config_name, expected_value in configuration_map.items(): - config_name_to_check = expected_config_name + config_names_to_check = [expected_config_name] if context.library.name == "java": # support for older versions of Java Tracer - config_name_to_check = expected_config_name.replace(".", "_") + config_names_to_check = [expected_config_name.replace(".", "_")] + elif context.library.name == "nodejs": + config_names_to_check = nodejs_legacy_config_names.get( + expected_config_name, [expected_config_name] + ) expected_value_str = str(expected_value).lower() @@ -497,17 +506,20 @@ def validator(data: dict): config_found = False for cnf in configurations: # Handle different configuration structures - some might not have 'value' key - if cnf.get("name") == config_name_to_check: + if cnf.get("name") in config_names_to_check: config_value = cnf.get("value") # Accept both the expected value and its float version for telemetry_heartbeat_interval - if expected_config_name == "DD_TELEMETRY_HEARTBEAT_INTERVAL": + if expected_config_name == "DD_TELEMETRY_HEARTBEAT_INTERVAL" and isinstance( + expected_value, str | int | float + ): try: expected_float = float(expected_value) - config_float = float(config_value) - if config_float == expected_float: - config_found = True - configurations_present.append(expected_config_name) - break + if isinstance(config_value, str | int | float): + config_float = float(config_value) + if config_float == expected_float: + config_found = True + configurations_present.append(expected_config_name) + break except Exception as e: logger.debug( f"Could not compare as float for config '{expected_config_name}': {e}" @@ -519,7 +531,7 @@ def validator(data: dict): if not config_found: # For debugging, show all entries with this config name - matching_entries = [cnf for cnf in configurations if cnf.get("name") == config_name_to_check] + matching_entries = [cnf for cnf in configurations if cnf.get("name") in config_names_to_check] if matching_entries: values_found = [ f"{cnf.get('value', 'NO_VALUE')} (origin: {cnf.get('origin', 'unknown')}, keys: {list(cnf.keys())})" @@ -532,7 +544,7 @@ def validator(data: dict): else: raise Exception( f"Client Configuration information is not accurately reported, " - f"{expected_config_name} is not present in configuration on app-started event" + f"none of {config_names_to_check} are present in configuration on app-started event" ) self.validate_library_telemetry_data(validator) @@ -576,7 +588,7 @@ class Test_TelemetryEnhancedConfigReporting: # Expected configuration precedence: default -> env_var -> code EXPECTED_CONFIGS: dict[str, dict[str, Any]] = { "nodejs": { - "name": "DD_LOG_INJECTION", + "names": ["DD_LOGS_INJECTION", "DD_LOG_INJECTION"], "precedence": [ {"origin": "default", "value": True}, {"origin": "env_var", "value": False}, @@ -624,15 +636,17 @@ def test_telemetry_events_seq_id(self): def test_telemetry_enhanced_config_reporting_precedence(self): """Verify configuration precedence order matches expected sequence.""" expected_config = self.EXPECTED_CONFIGS[context.library.name] - config_name = expected_config["name"] + config_names = expected_config.get("names") + if config_names is None: + config_names = [expected_config["name"]] expected_precedence: list[dict[str, Any]] = expected_config["precedence"] # Get configurations from telemetry events all_configs = interfaces.library.get_telemetry_configurations() assert all_configs, "No configurations found" - matching_configs = [cfg for cfg in all_configs if cfg["name"] == config_name] - assert matching_configs, f"No configurations found for {config_name}" + matching_configs = [cfg for cfg in all_configs if cfg["name"] in config_names] + assert matching_configs, f"No configurations found for any of {config_names}" # Group configurations by origin and keep the latest (highest seq_id) for each origin latest_by_origin: dict[str, dict[str, Any]] = self._get_latest_configs_by_origin(matching_configs) @@ -646,7 +660,7 @@ def test_telemetry_enhanced_config_reporting_precedence(self): # Verify each configuration matches expected precedence for i, expected in enumerate(expected_precedence): actual = sorted_configs[i] - assert actual["name"] == config_name, f"Config: {actual}, Expected Name: {config_name}" + assert actual["name"] in config_names, f"Config: {actual}, Expected Name in: {config_names}" assert actual["origin"] == expected["origin"], f"Config: {actual}, Expected Origin: {expected['origin']}" assert actual["value"] == expected["value"], f" Config: {actual}, Expected Value: {expected['value']}" diff --git a/utils/telemetry_utils.py b/utils/telemetry_utils.py index 033cddc723a..bcc7edc5399 100644 --- a/utils/telemetry_utils.py +++ b/utils/telemetry_utils.py @@ -17,10 +17,11 @@ def get_loaded_dependency(library: str) -> dict[str, bool]: return TelemetryUtils.test_loaded_dependencies[library] @staticmethod - def get_dd_appsec_sca_enabled_str(library: ComponentVersion) -> str: - result = "DD_APPSEC_SCA_ENABLED" + def get_dd_appsec_sca_enabled_names(library: ComponentVersion) -> list[str]: if library == "nodejs": - result = "appsec.sca.enabled" - elif library == "php": - result = "appsec.sca_enabled" - return result + # Temporary compatibility for dd-trace-js PR #7734. + # Remove "appsec.sca.enabled" once Node.js only reports the canonical env name. + return ["DD_APPSEC_SCA_ENABLED", "appsec.sca.enabled"] + if library == "php": + return ["appsec.sca_enabled"] + return ["DD_APPSEC_SCA_ENABLED"]