diff --git a/.gitlab/build-and-test-fast.yml b/.gitlab/build-and-test-fast.yml index 35017d93..a5e05054 100644 --- a/.gitlab/build-and-test-fast.yml +++ b/.gitlab/build-and-test-fast.yml @@ -40,7 +40,7 @@ lint: shellcheck: extends: .build-and-test-fast - image: $CI_REGISTRY/nginx_musl_toolchain + image: $CI_REGISTRY/nginx_musl_toolchain:rust-1.85 tags: ["arch:amd64"] script: - find bin/ test/ example/ .gitlab/ -type f \( \( -executable -not -name '*.py' \) -o -name '*.sh' \) | xargs shellcheck --exclude SC1071,SC1091,SC2317 @@ -99,6 +99,7 @@ build-nginx-rum-fast: - "1.26.3" - "1.27.5" - "1.28.2" + - "1.29.5" - "1.29.6" WAF: ["OFF"] @@ -191,6 +192,10 @@ test-nginx-rum-fast: BASE_IMAGE: ["nginx:1.28.2"] NGINX_VERSION: ["1.28.2"] WAF: ["OFF"] + - ARCH: ["amd64", "arm64"] + BASE_IMAGE: ["nginx:1.29.5"] + NGINX_VERSION: ["1.29.5"] + WAF: ["OFF"] - ARCH: ["amd64", "arm64"] BASE_IMAGE: ["nginx:1.29.6"] NGINX_VERSION: ["1.29.6"] diff --git a/.gitlab/common.yml b/.gitlab/common.yml index 3bcc1a56..c7a6b910 100644 --- a/.gitlab/common.yml +++ b/.gitlab/common.yml @@ -15,7 +15,7 @@ variables: .build: extends: .git-config - image: $CI_REGISTRY/nginx_musl_toolchain + image: $CI_REGISTRY/nginx_musl_toolchain:rust-1.85 tags: ["arch:$ARCH"] variables: MAKE_JOB_COUNT: "8" diff --git a/.gitlab/ssi-package.yml b/.gitlab/ssi-package.yml index eb4f02a5..558f5b16 100644 --- a/.gitlab/ssi-package.yml +++ b/.gitlab/ssi-package.yml @@ -10,7 +10,7 @@ .ssi-assemble: extends: .git-config - image: registry.ddbuild.io/ci/nginx-datadog/nginx_musl_toolchain + image: registry.ddbuild.io/ci/nginx-datadog/nginx_musl_toolchain:rust-1.85 stage: build parallel: matrix: diff --git a/build_env/Dockerfile b/build_env/Dockerfile index 53d0b190..b845e99b 100644 --- a/build_env/Dockerfile +++ b/build_env/Dockerfile @@ -76,7 +76,7 @@ RUN apk add --no-cache openssl-dev pcre-dev pcre2-dev perl zlib-dev # Rust toolchain RUN apk add --no-cache curl -RUN curl --proto '=https' –tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -yq --default-toolchain 1.73.0 && \ +RUN curl --proto '=https' –tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -yq --default-toolchain 1.85.0 && \ ln -s ~/.cargo/bin/cargo /usr/bin/cargo RUN cargo install --locked cbindgen --version 0.26.0 && \ diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 87fc5e4d..038cd9d8 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -1,8 +1,11 @@ include(FetchContent) -set(INJECT_BROWSER_SDK_GIT_REF "49245e1c6ed8275990bb910125187e0aaad09e3d" CACHE STRING +set(INJECT_BROWSER_SDK_GIT_REF "f509450" CACHE STRING "Git tag/branch for inject-browser-sdk") +set(INJECT_BROWSER_SDK_NO_DEFAULT_FEATURES OFF CACHE BOOL + "Build inject-browser-sdk without default features") + FetchContent_Declare( InjectBrowserSDK GIT_REPOSITORY https://github.com/DataDog/inject-browser-sdk.git @@ -10,6 +13,16 @@ FetchContent_Declare( ) FetchContent_MakeAvailable(InjectBrowserSDK) +# Pass cross-compilation sysroot/target flags to Rust cc crate builds +# (e.g. aws-lc-sys) that invoke the C compiler via CFLAGS. +if(CMAKE_SYSROOT AND CMAKE_C_COMPILER_TARGET AND COMMAND corrosion_set_env_vars) + set(_cross_flags "--sysroot=${CMAKE_SYSROOT} --target=${CMAKE_C_COMPILER_TARGET} -fuse-ld=lld -rtlib=compiler-rt") + corrosion_set_env_vars(inject_browser_sdk_ffi + "CFLAGS_x86_64_unknown_linux_musl=${_cross_flags}" + "CFLAGS_aarch64_unknown_linux_musl=${_cross_flags}" + ) +endif() + if(NOT TARGET inject_browser_sdk) add_library(inject_browser_sdk ALIAS inject_browser_sdk_ffi) endif() diff --git a/src/rum/config.cpp b/src/rum/config.cpp index d109722e..bc50ff8f 100644 --- a/src/rum/config.cpp +++ b/src/rum/config.cpp @@ -41,6 +41,35 @@ rum_config_map get_rum_config_from_env() { return config; } +rum_config_map get_rum_config_from_stable_config() { + rum_config_map config; + + auto entries = std::unique_ptr( + stable_config_get_entries("browser-sdk", false), + stable_config_entries_cleanup); + + if (entries == nullptr || entries->error_code) { + return config; + } + + for (uint32_t i = 0; i < entries->count; ++i) { + const char* name = entries->entries[i].name; + const char* value = entries->entries[i].value; + if (name == nullptr || value == nullptr || value[0] == '\0') continue; + + std::string_view name_sv(name); + for (const auto& [env_name, config_key] : rum_env_mappings) { + if (name_sv == env_name) { + config[std::string(config_key)] = {std::string(value)}; + break; + } + } + } + + return config; +} + std::optional get_rum_enabled_from_env() { const char* value = std::getenv("DD_RUM_ENABLED"); if (value == nullptr || value[0] == '\0') { @@ -209,7 +238,7 @@ char* on_datadog_rum_config(ngx_conf_t* cf, ngx_command_t* command, arg1_str.c_str()); } - auto rum_config = get_rum_config_from_env(); + auto rum_config = get_rum_config_from_stable_config(); ngx_conf_t save = *cf; cf->handler = set_config; @@ -244,37 +273,33 @@ char* on_datadog_rum_config(ngx_conf_t* cf, ngx_command_t* command, void try_build_snippet_from_env(ngx_conf_t* cf, datadog::nginx::datadog_loc_conf_t* loc_conf) { try { - auto env_config = get_rum_config_from_env(); - if (env_config.empty()) return; - - auto json = make_rum_json_config(default_rum_config_version, env_config); - if (json.empty()) { - ngx_log_error( - NGX_LOG_WARN, cf->log, 0, - "nginx-datadog: DD_RUM_* environment variables were set but " - "JSON config generation produced an empty result"); - return; - } - auto snippet = std::unique_ptr( - snippet_create_from_json(json.c_str()), snippet_cleanup); + snippet_create_from_stable_config("browser-sdk", false), + snippet_cleanup); + if (snippet == nullptr || snippet->error_code) { ngx_log_error(NGX_LOG_WARN, cf->log, 0, "nginx-datadog: failed to create RUM snippet from " - "environment variables: %s", + "stable config: %s", snippet ? snippet->error_message : "null snippet"); return; } loc_conf->rum_snippet = snippet.release(); - apply_rum_config_tags(loc_conf, env_config); + // Telemetry tags: best-effort from env (snippet is opaque) + const char* app_id = std::getenv("DD_RUM_APPLICATION_ID"); + if (app_id && app_id[0] != '\0') { + loc_conf->rum_application_id_tag = + std::string("application_id:") + app_id; + } + loc_conf->rum_remote_config_tag = "remote_config_used:false"; } catch (const std::bad_alloc&) { throw; - } catch (const std::exception& exception) { - ngx_log_error(NGX_LOG_WARN, cf->log, 0, - "nginx-datadog: failed to build RUM snippet from environment " - "variables: %s", - exception.what()); + } catch (const std::exception& e) { + ngx_log_error( + NGX_LOG_WARN, cf->log, 0, + "nginx-datadog: failed to build RUM snippet from stable config: %s", + e.what()); } } @@ -346,11 +371,8 @@ char* datadog_rum_merge_loc_config(ngx_conf_t* cf, std::vector get_environment_variable_names() { std::vector names; - names.reserve(rum_env_mappings.size() + 1); names.push_back("DD_RUM_ENABLED"sv); - for (const auto& [env_name, config_key] : rum_env_mappings) { - names.push_back(env_name); - } + names.push_back("DD_RUM_APPLICATION_ID"sv); // for telemetry tags return names; } diff --git a/src/rum/config_internal.h b/src/rum/config_internal.h index 76ad854a..87c499cc 100644 --- a/src/rum/config_internal.h +++ b/src/rum/config_internal.h @@ -41,6 +41,8 @@ using rum_config_map = rum_config_map get_rum_config_from_env(); +rum_config_map get_rum_config_from_stable_config(); + std::optional get_rum_enabled_from_env(); std::string make_rum_json_config(int config_version, diff --git a/test/cases/rum/test_injection.py b/test/cases/rum/test_injection.py index 2280115e..39ad2616 100644 --- a/test/cases/rum/test_injection.py +++ b/test/cases/rum/test_injection.py @@ -373,22 +373,54 @@ def test_injection_disabled(self): self.assertIsNone(headers.get("x-datadog-rum-injected")) self.assertNotIn("datadog-rum.js", body) + def _write_stable_config(self, config_vars): + """Write an application_monitoring.yaml into the nginx container + at the stable config path used by snippet_create_from_stable_config.""" + import subprocess + from ..orchestration import docker_compose_command, child_env + yaml_lines = ["apm_configuration_default:"] + for key, value in config_vars.items(): + yaml_lines.append(f" {key}: {value}") + yaml_content = "\n".join(yaml_lines) + "\n" + + config_path = "/etc/datadog-agent/application_monitoring.yaml" + command = docker_compose_command( + "exec", "-T", "--", "nginx", "/bin/sh", "-c", + f"mkdir -p /etc/datadog-agent && cat >'{config_path}'") + subprocess.run(command, + input=yaml_content, + encoding="utf8", + env=child_env(), + check=True, + capture_output=True) + + def _cleanup_stable_config(self): + """Remove the stable config file from the nginx container.""" + import subprocess + from ..orchestration import docker_compose_command, child_env + command = docker_compose_command( + "exec", "-T", "--", "nginx", "rm", "-f", + "/etc/datadog-agent/application_monitoring.yaml") + subprocess.run(command, + env=child_env(), + check=True, + capture_output=True) + def test_env_only_config(self): """ - Verify RUM injection works when configuration comes entirely from - DD_RUM_* environment variables, with no datadog_rum_config directive - in the nginx config. + Verify RUM injection works when configuration comes from the + stable config file (application_monitoring.yaml), with no + datadog_rum_config directive in the nginx config. """ conf_path = Path(__file__).parent / "conf" / "rum_env_only.conf" nginx_conf = conf_path.read_text() - env = { + stable_config = { "DD_RUM_APPLICATION_ID": "", "DD_RUM_CLIENT_TOKEN": "", "DD_RUM_SITE": "datadoghq.com", "DD_RUM_SERVICE": "env-only-service", "DD_RUM_ENVIRONMENT": "env-test", - "DD_RUM_MAJOR_VERSION": "3.0.0", "DD_RUM_SESSION_SAMPLE_RATE": "100", "DD_RUM_SESSION_REPLAY_SAMPLE_RATE": "50", "DD_RUM_TRACK_RESOURCES": "true", @@ -396,29 +428,31 @@ def test_env_only_config(self): "DD_RUM_TRACK_USER_INTERACTIONS": "true", } - with self.orch.custom_nginx(nginx_conf, - extra_env=env, - healthcheck_port=80) as nginx: - status, headers, body = self.orch.send_nginx_http_request("/") - self.assertEqual(200, status) - self.assertInjection(headers, body) - - self.assertIn('"applicationId":""', body) - self.assertIn('"clientToken":""', body) - self.assertIn('"service":"env-only-service"', body) - self.assertIn('"env":"env-test"', body) - self.assertIn('"version":"3.0.0"', body) + self._write_stable_config(stable_config) + try: + with self.orch.custom_nginx(nginx_conf, + healthcheck_port=80) as nginx: + status, headers, body = self.orch.send_nginx_http_request("/") + self.assertEqual(200, status) + self.assertInjection(headers, body) + + self.assertIn('"applicationId":""', body) + self.assertIn('"clientToken":""', body) + self.assertIn('"service":"env-only-service"', body) + self.assertIn('"env":"env-test"', body) + finally: + self._cleanup_stable_config() def test_partial_env_config(self): """ - Verify that nginx config fields override corresponding DD_RUM_* env - vars (per-field merging). The datadog_rum_config block sets service - and env; the remaining fields come from env vars. + Verify that nginx config fields override corresponding stable config + values (per-field merging). The datadog_rum_config block sets service + and env; the remaining fields come from stable config. """ conf_path = Path(__file__).parent / "conf" / "rum_partial_env.conf" nginx_conf = conf_path.read_text() - env = { + stable_config = { "DD_RUM_APPLICATION_ID": "", "DD_RUM_CLIENT_TOKEN": "", "DD_RUM_SITE": "datadoghq.eu", @@ -431,21 +465,24 @@ def test_partial_env_config(self): "DD_RUM_TRACK_USER_INTERACTIONS": "true", } - with self.orch.custom_nginx(nginx_conf, - extra_env=env, - healthcheck_port=80) as nginx: - status, headers, body = self.orch.send_nginx_http_request("/") - self.assertEqual(200, status) - self.assertInjection(headers, body) - - # nginx config values take precedence - self.assertIn('"service":"nginx-partial-env"', body) - self.assertIn('"env":"staging"', body) - - # env var values used for fields not in nginx config - self.assertIn('"applicationId":""', body) - self.assertIn('"clientToken":""', body) - self.assertIn('"site":"datadoghq.eu"', body) + self._write_stable_config(stable_config) + try: + with self.orch.custom_nginx(nginx_conf, + healthcheck_port=80) as nginx: + status, headers, body = self.orch.send_nginx_http_request("/") + self.assertEqual(200, status) + self.assertInjection(headers, body) + + # nginx config values take precedence + self.assertIn('"service":"nginx-partial-env"', body) + self.assertIn('"env":"staging"', body) + + # stable config values used for fields not in nginx config + self.assertIn('"applicationId":""', body) + self.assertIn('"clientToken":""', body) + self.assertIn('"site":"datadoghq.eu"', body) + finally: + self._cleanup_stable_config() def test_nginx_config_takes_full_precedence(self): """ @@ -474,7 +511,7 @@ def test_env_remote_configuration_id(self): conf_path = Path(__file__).parent / "conf" / "rum_env_only.conf" nginx_conf = conf_path.read_text() - env = { + stable_config = { "DD_RUM_APPLICATION_ID": "", "DD_RUM_CLIENT_TOKEN": "", "DD_RUM_SITE": "datadoghq.com", @@ -482,64 +519,76 @@ def test_env_remote_configuration_id(self): "DD_RUM_REMOTE_CONFIGURATION_ID": "abc-123-remote-cfg", } - with self.orch.custom_nginx(nginx_conf, - extra_env=env, - healthcheck_port=80) as nginx: - status, headers, body = self.orch.send_nginx_http_request("/") - self.assertEqual(200, status) - self.assertInjection(headers, body) - - self.assertIn('"remoteConfigurationId":"abc-123-remote-cfg"', body) + self._write_stable_config(stable_config) + try: + with self.orch.custom_nginx(nginx_conf, + healthcheck_port=80) as nginx: + status, headers, body = self.orch.send_nginx_http_request("/") + self.assertEqual(200, status) + self.assertInjection(headers, body) + + self.assertIn('"remoteConfigurationId":"abc-123-remote-cfg"', + body) + finally: + self._cleanup_stable_config() def test_env_disabled_overrides_env_config(self): """ - Verify DD_RUM_ENABLED=false disables RUM even when DD_RUM_* config - env vars are present. + Verify DD_RUM_ENABLED=false disables RUM even when stable config + provides a complete RUM configuration. """ conf_path = Path(__file__).parent / "conf" / "rum_env_only.conf" nginx_conf = conf_path.read_text() - env = { - "DD_RUM_ENABLED": "false", + stable_config = { "DD_RUM_APPLICATION_ID": "", "DD_RUM_CLIENT_TOKEN": "", "DD_RUM_SITE": "datadoghq.com", "DD_RUM_SERVICE": "should-not-inject", } - with self.orch.custom_nginx(nginx_conf, - extra_env=env, - healthcheck_port=80) as nginx: - status, headers, body = self.orch.send_nginx_http_request("/") - self.assertEqual(200, status) - headers = self.make_dict_headers(headers) - self.assertIsNone(headers.get("x-datadog-rum-injected")) - self.assertNotIn("datadog-rum.js", body) + env = {"DD_RUM_ENABLED": "false"} + + self._write_stable_config(stable_config) + try: + with self.orch.custom_nginx(nginx_conf, + extra_env=env, + healthcheck_port=80) as nginx: + status, headers, body = self.orch.send_nginx_http_request("/") + self.assertEqual(200, status) + headers = self.make_dict_headers(headers) + self.assertIsNone(headers.get("x-datadog-rum-injected")) + self.assertNotIn("datadog-rum.js", body) + finally: + self._cleanup_stable_config() def test_env_disabled_location_override(self): """ Verify that datadog_rum off in a location still disables injection - even when env vars provide a complete RUM config. + even when stable config provides a complete RUM configuration. """ conf_path = Path(__file__).parent / "conf" / "rum_env_only.conf" nginx_conf = conf_path.read_text() - env = { + stable_config = { "DD_RUM_APPLICATION_ID": "", "DD_RUM_CLIENT_TOKEN": "", "DD_RUM_SITE": "datadoghq.com", "DD_RUM_SERVICE": "env-service", } - with self.orch.custom_nginx(nginx_conf, - extra_env=env, - healthcheck_port=80) as nginx: - status, headers, body = self.orch.send_nginx_http_request( - "/disable-rum") - self.assertEqual(200, status) - headers = self.make_dict_headers(headers) - self.assertIsNone(headers.get("x-datadog-rum-injected")) - self.assertNotIn("datadog-rum.js", body) + self._write_stable_config(stable_config) + try: + with self.orch.custom_nginx(nginx_conf, + healthcheck_port=80) as nginx: + status, headers, body = self.orch.send_nginx_http_request( + "/disable-rum") + self.assertEqual(200, status) + headers = self.make_dict_headers(headers) + self.assertIsNone(headers.get("x-datadog-rum-injected")) + self.assertNotIn("datadog-rum.js", body) + finally: + self._cleanup_stable_config() def test_skip_injection_when_already_injected(self): """