diff --git a/Cargo.lock b/Cargo.lock index f2eeb0b..1d00bf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,9 +34,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arc-swap" @@ -898,6 +898,7 @@ dependencies = [ "http", "js-sys", "libdd-capabilities", + "libdd-common", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", @@ -905,8 +906,8 @@ dependencies = [ [[package]] name = "libdd-capabilities" -version = "2.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v37.0.0#86b7f5700d3db4e58076794b5e8073a40d780083" +version = "2.1.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=jwiriath%2FAPMSP-3780-inject-proc-env-stuff#02cb1c1e48515f38847f6e9218548be161219d73" dependencies = [ "anyhow", "bytes", @@ -916,8 +917,8 @@ dependencies = [ [[package]] name = "libdd-capabilities-impl" -version = "2.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v37.0.0#86b7f5700d3db4e58076794b5e8073a40d780083" +version = "3.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=jwiriath%2FAPMSP-3780-inject-proc-env-stuff#02cb1c1e48515f38847f6e9218548be161219d73" dependencies = [ "bytes", "http", @@ -929,8 +930,8 @@ dependencies = [ [[package]] name = "libdd-common" -version = "5.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v37.0.0#86b7f5700d3db4e58076794b5e8073a40d780083" +version = "5.1.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=jwiriath%2FAPMSP-3780-inject-proc-env-stuff#02cb1c1e48515f38847f6e9218548be161219d73" dependencies = [ "anyhow", "bytes", @@ -965,7 +966,7 @@ dependencies = [ [[package]] name = "libdd-crashtracker" version = "1.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v37.0.0#86b7f5700d3db4e58076794b5e8073a40d780083" +source = "git+https://github.com/DataDog/libdatadog.git?branch=jwiriath%2FAPMSP-3780-inject-proc-env-stuff#02cb1c1e48515f38847f6e9218548be161219d73" dependencies = [ "anyhow", "blazesym", @@ -997,8 +998,8 @@ dependencies = [ [[package]] name = "libdd-data-pipeline" -version = "6.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v37.0.0#86b7f5700d3db4e58076794b5e8073a40d780083" +version = "7.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=jwiriath%2FAPMSP-3780-inject-proc-env-stuff#02cb1c1e48515f38847f6e9218548be161219d73" dependencies = [ "anyhow", "arc-swap", @@ -1016,7 +1017,7 @@ dependencies = [ "libdd-shared-runtime", "libdd-tinybytes", "libdd-trace-normalization", - "libdd-trace-protobuf 3.0.2 (git+https://github.com/DataDog/libdatadog.git?tag=v37.0.0)", + "libdd-trace-protobuf", "libdd-trace-stats", "libdd-trace-utils", "rmp-serde", @@ -1031,16 +1032,16 @@ dependencies = [ [[package]] name = "libdd-ddsketch" -version = "1.0.1" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v37.0.0#86b7f5700d3db4e58076794b5e8073a40d780083" +version = "1.1.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=jwiriath%2FAPMSP-3780-inject-proc-env-stuff#02cb1c1e48515f38847f6e9218548be161219d73" dependencies = [ "prost", ] [[package]] name = "libdd-dogstatsd-client" -version = "3.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v37.0.0#86b7f5700d3db4e58076794b5e8073a40d780083" +version = "4.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=jwiriath%2FAPMSP-3780-inject-proc-env-stuff#02cb1c1e48515f38847f6e9218548be161219d73" dependencies = [ "anyhow", "cadence", @@ -1052,26 +1053,12 @@ dependencies = [ [[package]] name = "libdd-library-config" -version = "1.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?rev=353134770b312b7ccd2df6afabc253090b948e5f#353134770b312b7ccd2df6afabc253090b948e5f" -dependencies = [ - "anyhow", - "memfd", - "rand", - "rmp", - "rmp-serde", - "serde", - "serde_yaml", -] - -[[package]] -name = "libdd-library-config" -version = "2.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?rev=7cdeb7896e92d1ba38bde495934e112dac2eda25#7cdeb7896e92d1ba38bde495934e112dac2eda25" +version = "3.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=jwiriath%2FAPMSP-3780-inject-proc-env-stuff#02cb1c1e48515f38847f6e9218548be161219d73" dependencies = [ "anyhow", "libc", - "libdd-trace-protobuf 3.0.2 (git+https://github.com/DataDog/libdatadog.git?rev=7cdeb7896e92d1ba38bde495934e112dac2eda25)", + "libdd-trace-protobuf", "memfd", "prost", "rand", @@ -1094,8 +1081,8 @@ dependencies = [ [[package]] name = "libdd-shared-runtime" -version = "1.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v37.0.0#86b7f5700d3db4e58076794b5e8073a40d780083" +version = "2.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=jwiriath%2FAPMSP-3780-inject-proc-env-stuff#02cb1c1e48515f38847f6e9218548be161219d73" dependencies = [ "async-trait", "futures", @@ -1111,8 +1098,8 @@ dependencies = [ [[package]] name = "libdd-telemetry" -version = "5.0.1" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v37.0.0#86b7f5700d3db4e58076794b5e8073a40d780083" +version = "6.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=jwiriath%2FAPMSP-3780-inject-proc-env-stuff#02cb1c1e48515f38847f6e9218548be161219d73" dependencies = [ "anyhow", "async-trait", @@ -1139,29 +1126,29 @@ dependencies = [ [[package]] name = "libdd-tinybytes" version = "1.1.1" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v37.0.0#86b7f5700d3db4e58076794b5e8073a40d780083" +source = "git+https://github.com/DataDog/libdatadog.git?branch=jwiriath%2FAPMSP-3780-inject-proc-env-stuff#02cb1c1e48515f38847f6e9218548be161219d73" dependencies = [ "serde", ] [[package]] name = "libdd-trace-normalization" -version = "2.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v37.0.0#86b7f5700d3db4e58076794b5e8073a40d780083" +version = "3.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=jwiriath%2FAPMSP-3780-inject-proc-env-stuff#02cb1c1e48515f38847f6e9218548be161219d73" dependencies = [ "anyhow", - "libdd-trace-protobuf 3.0.2 (git+https://github.com/DataDog/libdatadog.git?tag=v37.0.0)", + "libdd-trace-protobuf", ] [[package]] name = "libdd-trace-obfuscation" -version = "4.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v37.0.0#86b7f5700d3db4e58076794b5e8073a40d780083" +version = "5.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=jwiriath%2FAPMSP-3780-inject-proc-env-stuff#02cb1c1e48515f38847f6e9218548be161219d73" dependencies = [ "anyhow", "fluent-uri", "libdd-common", - "libdd-trace-protobuf 3.0.2 (git+https://github.com/DataDog/libdatadog.git?tag=v37.0.0)", + "libdd-trace-protobuf", "libdd-trace-utils", "log", "percent-encoding", @@ -1171,18 +1158,8 @@ dependencies = [ [[package]] name = "libdd-trace-protobuf" -version = "3.0.2" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v37.0.0#86b7f5700d3db4e58076794b5e8073a40d780083" -dependencies = [ - "prost", - "serde", - "serde_bytes", -] - -[[package]] -name = "libdd-trace-protobuf" -version = "3.0.2" -source = "git+https://github.com/DataDog/libdatadog.git?rev=7cdeb7896e92d1ba38bde495934e112dac2eda25#7cdeb7896e92d1ba38bde495934e112dac2eda25" +version = "4.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=jwiriath%2FAPMSP-3780-inject-proc-env-stuff#02cb1c1e48515f38847f6e9218548be161219d73" dependencies = [ "prost", "serde", @@ -1191,8 +1168,8 @@ dependencies = [ [[package]] name = "libdd-trace-stats" -version = "5.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v37.0.0#86b7f5700d3db4e58076794b5e8073a40d780083" +version = "6.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=jwiriath%2FAPMSP-3780-inject-proc-env-stuff#02cb1c1e48515f38847f6e9218548be161219d73" dependencies = [ "anyhow", "arc-swap", @@ -1203,9 +1180,10 @@ dependencies = [ "libdd-capabilities-impl", "libdd-common", "libdd-ddsketch", + "libdd-dogstatsd-client", "libdd-shared-runtime", "libdd-trace-obfuscation", - "libdd-trace-protobuf 3.0.2 (git+https://github.com/DataDog/libdatadog.git?tag=v37.0.0)", + "libdd-trace-protobuf", "libdd-trace-utils", "rmp-serde", "serde", @@ -1216,8 +1194,8 @@ dependencies = [ [[package]] name = "libdd-trace-utils" -version = "8.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v37.0.0#86b7f5700d3db4e58076794b5e8073a40d780083" +version = "9.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=jwiriath%2FAPMSP-3780-inject-proc-env-stuff#02cb1c1e48515f38847f6e9218548be161219d73" dependencies = [ "anyhow", "base64", @@ -1234,7 +1212,7 @@ dependencies = [ "libdd-common", "libdd-tinybytes", "libdd-trace-normalization", - "libdd-trace-protobuf 3.0.2 (git+https://github.com/DataDog/libdatadog.git?tag=v37.0.0)", + "libdd-trace-protobuf", "prost", "rand", "rmp", @@ -1242,6 +1220,7 @@ dependencies = [ "rmpv", "rustc-hash", "serde", + "serde-transcode", "serde_json", "thin-vec", "tokio", @@ -1270,7 +1249,7 @@ version = "0.2.0" dependencies = [ "anyhow", "getrandom 0.2.17", - "libdd-library-config 1.0.0", + "libdd-library-config", "serde", "serde-wasm-bindgen", "wasm-bindgen", @@ -1724,7 +1703,7 @@ dependencies = [ "libdd-common", "libdd-data-pipeline", "libdd-shared-runtime", - "libdd-trace-protobuf 3.0.2 (git+https://github.com/DataDog/libdatadog.git?tag=v37.0.0)", + "libdd-trace-protobuf", "libdd-trace-stats", "libdd-trace-utils", "rmp-serde", @@ -1784,8 +1763,8 @@ name = "process-discovery" version = "0.1.0" dependencies = [ "anyhow", - "libdd-library-config 2.0.0", - "libdd-trace-protobuf 3.0.2 (git+https://github.com/DataDog/libdatadog.git?rev=7cdeb7896e92d1ba38bde495934e112dac2eda25)", + "libdd-library-config", + "libdd-trace-protobuf", "napi", "napi-derive", ] @@ -2148,6 +2127,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-transcode" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "590c0e25c2a5bb6e85bf5c1bce768ceb86b316e7a01bdf07d2cb4ec2271990e2" +dependencies = [ + "serde", +] + [[package]] name = "serde-wasm-bindgen" version = "0.4.5" diff --git a/crates/capabilities/Cargo.toml b/crates/capabilities/Cargo.toml index 888f566..f702725 100644 --- a/crates/capabilities/Cargo.toml +++ b/crates/capabilities/Cargo.toml @@ -15,7 +15,8 @@ http = "1" bytes = "1.4" futures-core = "0.3" anyhow = "1" -libdd-capabilities = { git = "https://github.com/DataDog/libdatadog.git", tag = "v37.0.0" } +libdd-capabilities = { git = "https://github.com/DataDog/libdatadog.git", branch = "jwiriath/APMSP-3780-inject-proc-env-stuff" } +libdd-common = { git = "https://github.com/DataDog/libdatadog.git", branch = "jwiriath/APMSP-3780-inject-proc-env-stuff", default-features = false } [dev-dependencies] wasm-bindgen-test = "0.3" diff --git a/crates/capabilities/src/entity.rs b/crates/capabilities/src/entity.rs new file mode 100644 index 0000000..7cdcc6d --- /dev/null +++ b/crates/capabilities/src/entity.rs @@ -0,0 +1,50 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Seed libdatadog's entity-header store from Node. +//! +//! libdd-common's automatic detection reads `/proc/self/cgroup` and +//! `DD_EXTERNAL_ENV` directly; neither is reachable from the +//! `wasm32-unknown-unknown` sandbox. Node reads them itself and hands the raw +//! values through this module *at wasm-module startup*, before any HTTP +//! send. libdatadog then renders `datadog-container-id`, `datadog-entity-id` +//! and `datadog-external-env` as part of the normal request head — no more +//! post-render header rewrite in JS. + +use js_sys::Reflect; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(module = "/src/http_transport.js")] +extern "C" { + // Returns an object `{ cgroupContent?: string, cgroupInode?: number, externalEnv?: string }`. + // Absent / null / undefined fields signal "not available" (not linux, not in a cgroup, + // env var unset). `cgroupInode` is a JS number so an inode past 2^53 would lose + // precision, but real inodes never approach that. + #[wasm_bindgen(js_name = "getEntityInputs")] + fn get_entity_inputs() -> JsValue; +} + +/// Pull entity ingredients from Node and seed the libdd-common entity-header +/// store. Call once, before the first HTTP send — libdd-common caches +/// first-call-wins, so a later call (or a getter that latched to `None` +/// before this ran) is a permanent no-op. +pub fn init_from_js() { + let inputs = get_entity_inputs(); + + let cgroup_content = Reflect::get(&inputs, &JsValue::from_str("cgroupContent")) + .ok() + .and_then(|v| v.as_string()); + let cgroup_inode = Reflect::get(&inputs, &JsValue::from_str("cgroupInode")) + .ok() + .and_then(|v| v.as_f64()) + .map(|n| n as u64); + let external_env = Reflect::get(&inputs, &JsValue::from_str("externalEnv")) + .ok() + .and_then(|v| v.as_string()); + + libdd_common::entity_id::init_entity_inputs( + cgroup_content.as_deref(), + cgroup_inode, + external_env.as_deref(), + ); +} diff --git a/crates/capabilities/src/http_transport.js b/crates/capabilities/src/http_transport.js index 962b6ba..e6e782d 100644 --- a/crates/capabilities/src/http_transport.js +++ b/crates/capabilities/src/http_transport.js @@ -9,102 +9,53 @@ let storage = f => f() -// libdatadog's automatic container-id / entity-id detection (libdd-common's -// entity_id module) is gated `#[cfg(unix)]` and therefore inert on the -// `wasm32-unknown-unknown` target we build for, and `DD_EXTERNAL_ENV` is also -// unreachable from wasm. Node, however, can read `/proc` and `process.env`, so -// we detect the same values here and add them as the standard Datadog exporter -// headers (`datadog-container-id`, `datadog-entity-id`, `datadog-external-env`) -// — the headers native libdatadog adds via `Endpoint::set_standard_headers`. -// -// The detection mirrors dd-trace-js's `exporters/common/docker.js` (the proven -// legacy-exporter path) and libdd-common's `compute_entity_id` -// (`ci-` else `in-`). - -// The second alternative is the PCF / Garden regexp; no suffix ($) to avoid -// matching pod UIDs. See -// https://github.com/DataDog/datadog-agent/blob/7.40.x/pkg/util/cgroups/reader.go#L50 -const uuidSource = String.raw`[0-9a-f]{8}[-_][0-9a-f]{4}[-_][0-9a-f]{4}[-_][0-9a-f]{4}[-_][0-9a-f]{12}|[0-9a-f]{8}(?:-[0-9a-f]{4}){4}$` -const containerSource = '[0-9a-f]{64}' -const taskSource = String.raw`[0-9a-f]{32}-\d+` +// libdatadog's automatic container-id / entity-id / external-env detection +// (libdd-common's `entity_id` module) is gated `#[cfg(unix)]` and inert on the +// `wasm32-unknown-unknown` target we build for, and `process.env` is also +// unreachable from wasm. Node reads `/proc/self/cgroup`, stats the cgroup +// mount subpath for an inode, and reads `DD_EXTERNAL_ENV`, then hands the raw +// values to Rust via `getEntityInputs`. libdatadog does the regex, `ci-`/`in-` +// composition, external-env validation and header rendering — see +// `libdd-common/src/entity_id/parse.rs`. + +// Cgroup line format `::`. We just need the +// path (group 3) to derive the inode-lookup subpath under `/sys/fs/cgroup`. +// `m` flag matches the first line that fits; we do not attempt controller +// preference (memory-first) because libdd-common's unix path handles that and +// this is only used in the wasm sandbox, where callers accept a best-effort +// inode. const lineReg = /^(\d+):([^:]*):(.+)$/m -const entityReg = new RegExp(String.raw`.*(${uuidSource}|${containerSource}|${taskSource})(?:\.scope)?$`, 'm') -// Detect the entity headers. Parameterized for unit testing; production callers -// use the cached `getEntityHeaders()` with the real cgroup paths / environment. -function detectEntityHeaders (opts = {}) { +// Path arguments are parameterized for unit testing; production callers use +// the defaults, which resolve to Linux's /proc + cgroup mount. +function collectEntityInputs (opts = {}) { const fs = require('node:fs') const cgroupPath = opts.cgroupPath ?? '/proc/self/cgroup' const cgroupMount = opts.cgroupMount ?? '/sys/fs/cgroup' const externalEnv = 'externalEnv' in opts ? opts.externalEnv : process.env.DD_EXTERNAL_ENV - const headers = {} - - let cgroup = '' - let containerId + let cgroupContent, cgroupInode try { - cgroup = fs.readFileSync(cgroupPath, 'utf8').trim() - containerId = cgroup.match(entityReg)?.[1] - } catch { /* not in a cgroup, or not Linux */ } + cgroupContent = fs.readFileSync(cgroupPath, 'utf8').trim() + } catch { /* not linux / not in a cgroup */ } - let inode = 0 - const inodePath = cgroup.match(lineReg)?.[3] + const inodePath = cgroupContent && cgroupContent.match(lineReg)?.[3] if (inodePath) { - const strippedPath = inodePath.replaceAll(/^\/|\/$/g, '') + const stripped = inodePath.replaceAll(/^\/|\/$/g, '') try { - inode = fs.statSync(`${cgroupMount}/${strippedPath}`).ino + cgroupInode = fs.statSync(`${cgroupMount}/${stripped}`).ino } catch { /* mount not present */ } } - // `ci-` when a container id is found, else `in-` - // — matching libdd-common's `compute_entity_id`. - const entityId = containerId ? `ci-${containerId}` : (inode ? `in-${inode}` : undefined) - - if (containerId) headers['datadog-container-id'] = containerId - if (entityId) headers['datadog-entity-id'] = entityId - // Only emit external-env if it is a clean header value (visible ASCII + space/ - // tab). This rejects CR/LF (header-injection / request-smuggling vector) and - // non-latin1 that the latin1-encoded head rewrite couldn't represent — native - // libdatadog likewise rejects invalid bytes via the http crate's HeaderValue. - if (externalEnv && /^[\t\u0020-\u007E]*$/.test(externalEnv)) { - headers['datadog-external-env'] = externalEnv - } - - return headers + return { cgroupContent, cgroupInode, externalEnv } } -let cachedEntityHeaders -function getEntityHeaders () { - if (cachedEntityHeaders === undefined) { - cachedEntityHeaders = detectEntityHeaders() +let cachedEntityInputs +module.exports.getEntityInputs = function () { + if (cachedEntityInputs === undefined) { + cachedEntityInputs = collectEntityInputs() } - return cachedEntityHeaders -} - -// Rewrite the Rust-rendered HTTP/1.1 request head (a `\r\n`-delimited byte -// buffer terminated by a blank line) to carry the detected entity headers. -// Any pre-existing line for a name we set is dropped first, so libdatadog's -// empty `datadog-container-id` is replaced rather than duplicated. Header -// names/values are ASCII, so latin1 is a lossless round-trip. -function applyEntityHeaders (headView, entity = getEntityHeaders()) { - const names = Object.keys(entity) - if (names.length === 0) return Buffer.from(headView) - - const head = Buffer.from(headView).toString('latin1') - const term = head.indexOf('\r\n\r\n') - if (term === -1) return Buffer.from(headView) // malformed; leave untouched - - const drop = new Set(names) - const lines = head.slice(0, term).split('\r\n') - const kept = lines.filter((line, i) => { - if (i === 0) return true // request line - const colon = line.indexOf(':') - const name = (colon === -1 ? line : line.slice(0, colon)).trim().toLowerCase() - return !drop.has(name) - }) - for (const name of names) kept.push(`${name}: ${entity[name]}`) - - return Buffer.from(`${kept.join('\r\n')}\r\n\r\n`, 'latin1') + return cachedEntityInputs } // Parse the Rust-rendered (+ entity-merged) HTTP/1.1 request head into Node @@ -165,10 +116,9 @@ module.exports.setResponseHeaderObserver = function (new_observer) { } // Exposed for unit tests. -module.exports.detectEntityHeaders = detectEntityHeaders -module.exports.applyEntityHeaders = applyEntityHeaders -module.exports._resetEntityHeadersCache = () => { - cachedEntityHeaders = undefined +module.exports.collectEntityInputs = collectEntityInputs +module.exports._resetEntityInputsCache = () => { + cachedEntityInputs = undefined } module.exports.httpRequest = function (host, port, isHttps, socketPath, head_ptr, head_len, body_ptr, body_len, wasm_memory) { @@ -196,11 +146,11 @@ module.exports.httpRequest = function (host, port, isHttps, socketPath, head_ptr const bodyView = new Uint8Array(wasm_memory.buffer, body_ptr, body_len) // The Rust side already rendered the full HTTP/1.1 request head (real - // method, `/v0.4/traces` path, Content-Type/Length, datadog-meta-*); - // applyEntityHeaders merges in the detected entity headers. Parse it into + // method, `/v0.4/traces` path, Content-Type/Length, datadog-meta-*, + // plus entity headers seeded via `getEntityInputs`). Parse it into // request options so the connection uses the correct method/path/headers // on both Node and Bun (host/port or socketPath drive the connection). - const { method, path, headers } = parseRequestHead(applyEntityHeaders(headView)) + const { method, path, headers } = parseRequestHead(headView) const requestOptions = useSocket ? { socketPath, method, path, headers } : { host, port, method, path, headers } diff --git a/crates/capabilities/src/lib.rs b/crates/capabilities/src/lib.rs index f7cb4b0..73c6acf 100644 --- a/crates/capabilities/src/lib.rs +++ b/crates/capabilities/src/lib.rs @@ -15,6 +15,7 @@ use std::time::Duration; use libdd_capabilities::http::HttpError; use libdd_capabilities::{HttpClientCapability, LogWriterCapability, MaybeSend, SleepCapability}; +pub mod entity; pub mod http; pub mod sleep; diff --git a/crates/crashtracker/Cargo.toml b/crates/crashtracker/Cargo.toml index 8bce697..344e828 100644 --- a/crates/crashtracker/Cargo.toml +++ b/crates/crashtracker/Cargo.toml @@ -14,7 +14,7 @@ path = "src/bin/receiver.rs" [dependencies] anyhow = "1" -libdd-crashtracker = { git = "https://github.com/DataDog/libdatadog.git", tag = "v37.0.0" } +libdd-crashtracker = { git = "https://github.com/DataDog/libdatadog.git", branch = "jwiriath/APMSP-3780-inject-proc-env-stuff" } napi = { version = "2", features = ["serde-json"] } napi-derive = { version = "2", default-features = false } rustls = { version = "*", default-features = false, features = ["aws-lc-rs"] } diff --git a/crates/library_config/Cargo.toml b/crates/library_config/Cargo.toml index be76bd4..3634e06 100644 --- a/crates/library_config/Cargo.toml +++ b/crates/library_config/Cargo.toml @@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] anyhow = "1" -libdd-library-config = { git = "https://github.com/DataDog/libdatadog.git", rev = "353134770b312b7ccd2df6afabc253090b948e5f" } +libdd-library-config = { git = "https://github.com/DataDog/libdatadog.git", branch = "jwiriath/APMSP-3780-inject-proc-env-stuff" } wasm-bindgen = "0.2.100" serde = { version = "1.0", features = ["derive"] } diff --git a/crates/pipeline/Cargo.toml b/crates/pipeline/Cargo.toml index 7501ad6..674bcfe 100644 --- a/crates/pipeline/Cargo.toml +++ b/crates/pipeline/Cargo.toml @@ -14,13 +14,13 @@ js-sys = "0.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1" libdatadog-nodejs-capabilities = { path = "../capabilities" } -libdd-capabilities = { git = "https://github.com/DataDog/libdatadog.git", tag = "v37.0.0" } -libdd-common = { git = "https://github.com/DataDog/libdatadog.git", tag = "v37.0.0", default-features = false } -libdd-data-pipeline = { git = "https://github.com/DataDog/libdatadog.git", tag = "v37.0.0", default-features = false } -libdd-trace-utils = { git = "https://github.com/DataDog/libdatadog.git", tag = "v37.0.0", default-features = false, features = ["change-buffer"] } -libdd-trace-stats = { git = "https://github.com/DataDog/libdatadog.git", tag = "v37.0.0", default-features = false } -libdd-trace-protobuf = { git = "https://github.com/DataDog/libdatadog.git", tag = "v37.0.0", default-features = false } -libdd-shared-runtime = { git = "https://github.com/DataDog/libdatadog.git", tag = "v37.0.0", default-features = false } +libdd-capabilities = { git = "https://github.com/DataDog/libdatadog.git", branch = "jwiriath/APMSP-3780-inject-proc-env-stuff" } +libdd-common = { git = "https://github.com/DataDog/libdatadog.git", branch = "jwiriath/APMSP-3780-inject-proc-env-stuff", default-features = false } +libdd-data-pipeline = { git = "https://github.com/DataDog/libdatadog.git", branch = "jwiriath/APMSP-3780-inject-proc-env-stuff", default-features = false } +libdd-trace-utils = { git = "https://github.com/DataDog/libdatadog.git", branch = "jwiriath/APMSP-3780-inject-proc-env-stuff", default-features = false, features = ["change-buffer"] } +libdd-trace-stats = { git = "https://github.com/DataDog/libdatadog.git", branch = "jwiriath/APMSP-3780-inject-proc-env-stuff", default-features = false } +libdd-trace-protobuf = { git = "https://github.com/DataDog/libdatadog.git", branch = "jwiriath/APMSP-3780-inject-proc-env-stuff", default-features = false } +libdd-shared-runtime = { git = "https://github.com/DataDog/libdatadog.git", branch = "jwiriath/APMSP-3780-inject-proc-env-stuff", default-features = false } rmp-serde = "1" bytes = "1" http = "1" diff --git a/crates/pipeline/src/lib.rs b/crates/pipeline/src/lib.rs index 181589a..6bce425 100644 --- a/crates/pipeline/src/lib.rs +++ b/crates/pipeline/src/lib.rs @@ -31,6 +31,9 @@ use utils::*; #[wasm_bindgen(start)] fn init() { console_error_panic_hook::set_once(); + // Seed libdd-common's entity-header store from Node before any HTTP send. + // See libdatadog_nodejs_capabilities::entity for details. + libdatadog_nodejs_capabilities::entity::init_from_js(); } // --- span event attribute decoding --- diff --git a/crates/pipeline/src/stats.rs b/crates/pipeline/src/stats.rs index 3141036..2213836 100644 --- a/crates/pipeline/src/stats.rs +++ b/crates/pipeline/src/stats.rs @@ -60,6 +60,7 @@ impl StatsCollector { "consumer".to_string(), ], Vec::new(), + None, ), meta, agent_url, @@ -86,7 +87,7 @@ impl StatsCollector { /// release the collector *before* the async send — leaving it available for /// `add_spans` while the stats request is in flight. pub fn prepare_request(&mut self, force: bool) -> Result>, String> { - let buckets = self.concentrator.flush(now(), force); + let (buckets, _) = self.concentrator.flush(now(), force); if buckets.is_empty() { return Ok(None); } diff --git a/crates/process_discovery/Cargo.toml b/crates/process_discovery/Cargo.toml index 4404228..d13daf2 100644 --- a/crates/process_discovery/Cargo.toml +++ b/crates/process_discovery/Cargo.toml @@ -8,11 +8,8 @@ crate-type = ["cdylib", "rlib"] [dependencies] anyhow = "1" -# Pointed at the merge commit that introduced ThreadLocalMetadata (caller-supplied -# schema version + extra process-context attributes). Swap back to a tagged release -# once one that includes 7cdeb7896e92d1ba38bde495934e112dac2eda25 is published. -libdd-library-config = { git = "https://github.com/DataDog/libdatadog.git", rev = "7cdeb7896e92d1ba38bde495934e112dac2eda25", features = ["otel-thread-ctx"] } -libdd-trace-protobuf = { git = "https://github.com/DataDog/libdatadog.git", rev = "7cdeb7896e92d1ba38bde495934e112dac2eda25" } +libdd-library-config = { git = "https://github.com/DataDog/libdatadog.git", branch = "jwiriath/APMSP-3780-inject-proc-env-stuff", features = ["otel-thread-ctx"] } +libdd-trace-protobuf = { git = "https://github.com/DataDog/libdatadog.git", branch = "jwiriath/APMSP-3780-inject-proc-env-stuff" } napi = { version = "2" } napi-derive = { version = "2", default-features = false } diff --git a/test/http_transport.js b/test/http_transport.js index 5a97a58..9d5d7e5 100644 --- a/test/http_transport.js +++ b/test/http_transport.js @@ -242,13 +242,14 @@ describe('http_transport unix socket', { skip: process.platform === 'win32' }, ( }) }) -// Entity-header injection: container-id / entity-id / external-env detection -// (Node reads /proc + env; libdatadog's own detection is inert on wasm) and the -// rewrite of the Rust-rendered request head that carries them. -const { detectEntityHeaders, applyEntityHeaders } = transport +// Node's side of entity-input collection. The values gathered here are pushed +// to libdatadog (in Rust) via `getEntityInputs`, and libdatadog does the +// container-id regex, `ci-`/`in-` composition, external-env sanitization and +// header rendering. Coverage of that Rust logic lives in +// libdd-common's entity_id::parse tests. +const { collectEntityInputs } = transport const DOCKER_CGROUP = '12:memory:/docker/3726184226f5d3147c25fdeab5b60097e378e8a720503a5e19ecfdf29f869860' -const DOCKER_ID = '3726184226f5d3147c25fdeab5b60097e378e8a720503a5e19ecfdf29f869860' function writeTmpCgroup (contents) { const p = path.join(os.tmpdir(), `ldn-cgroup-${process.pid}-${Math.random().toString(36).slice(2)}`) @@ -256,156 +257,66 @@ function writeTmpCgroup (contents) { return p } -function headBytes (lines) { - return Buffer.from(lines.join('\r\n') + '\r\n\r\n', 'latin1') -} - -describe('http_transport entity headers', () => { - describe('detectEntityHeaders', () => { - it('extracts a docker container-id and derives ci- entity-id', () => { - const cgroupPath = writeTmpCgroup(DOCKER_CGROUP) - try { - const h = detectEntityHeaders({ cgroupPath, cgroupMount: '/nonexistent', externalEnv: undefined }) - assert.strictEqual(h['datadog-container-id'], DOCKER_ID) - assert.strictEqual(h['datadog-entity-id'], `ci-${DOCKER_ID}`) - assert.strictEqual('datadog-external-env' in h, false) - } finally { - fs.rmSync(cgroupPath, { force: true }) - } - }) - - it('falls back to in- entity-id when no container-id is present', () => { - const cgroupPath = writeTmpCgroup('0::/') - try { - const h = detectEntityHeaders({ cgroupPath, cgroupMount: os.tmpdir(), externalEnv: undefined }) - assert.strictEqual('datadog-container-id' in h, false) - assert.match(h['datadog-entity-id'], /^in-\d+$/) - } finally { - fs.rmSync(cgroupPath, { force: true }) - } - }) - - it('emits datadog-external-env from the provided value', () => { - const h = detectEntityHeaders({ cgroupPath: '/nonexistent', cgroupMount: '/nonexistent', externalEnv: 'it-false,cn-svc,pu-x' }) - assert.strictEqual(h['datadog-external-env'], 'it-false,cn-svc,pu-x') - }) - - it('emits nothing without cgroup, mount, or external-env', () => { - const h = detectEntityHeaders({ cgroupPath: '/nonexistent', cgroupMount: '/nonexistent', externalEnv: undefined }) - assert.deepStrictEqual(h, {}) - }) - - it('rejects an external-env containing CR/LF (header-injection guard)', () => { - const h = detectEntityHeaders({ - cgroupPath: '/nonexistent', +describe('http_transport collectEntityInputs', () => { + it('returns the raw cgroup contents when the file exists', () => { + const cgroupPath = writeTmpCgroup(DOCKER_CGROUP) + try { + const inputs = collectEntityInputs({ + cgroupPath, cgroupMount: '/nonexistent', - externalEnv: 'ok\r\nx-evil: 1', + externalEnv: undefined, }) - assert.strictEqual('datadog-external-env' in h, false) - }) + assert.strictEqual(inputs.cgroupContent, DOCKER_CGROUP) + // The stat target didn't exist, so inode should be missing. + assert.strictEqual(inputs.cgroupInode, undefined) + assert.strictEqual(inputs.externalEnv, undefined) + } finally { + fs.rmSync(cgroupPath, { force: true }) + } }) - describe('applyEntityHeaders (head rewrite)', () => { - const entity = { - 'datadog-container-id': DOCKER_ID, - 'datadog-entity-id': `ci-${DOCKER_ID}`, - 'datadog-external-env': 'it-false,cn-svc,pu-x', + it('returns an inode when the mount subpath is statable', () => { + // Point cgroupMount at an existing directory (os.tmpdir()) whose entry + // for a subdirectory we control is the /docker/... suffix of the fake + // cgroup line. Building the "mount" directory is easier than mocking fs. + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ldn-mount-')) + const sub = path.join(dir, 'docker', '3726184226f5d3147c25fdeab5b60097e378e8a720503a5e19ecfdf29f869860') + fs.mkdirSync(sub, { recursive: true }) + const cgroupPath = writeTmpCgroup(DOCKER_CGROUP) + try { + const inputs = collectEntityInputs({ + cgroupPath, + cgroupMount: dir, + externalEnv: undefined, + }) + assert.strictEqual(inputs.cgroupContent, DOCKER_CGROUP) + assert.strictEqual(typeof inputs.cgroupInode, 'number') + assert.strictEqual(inputs.cgroupInode, fs.statSync(sub).ino) + } finally { + fs.rmSync(cgroupPath, { force: true }) + fs.rmSync(dir, { recursive: true, force: true }) } - - it('appends entity headers and preserves the request line + framing headers', () => { - const head = headBytes([ - 'POST /v0.4/traces HTTP/1.1', - 'Host: localhost:8126', - 'Content-Length: 42', - 'datadog-meta-lang: nodejs', - ]) - const out = applyEntityHeaders(head, entity).toString('latin1') - const lines = out.split('\r\n') - assert.strictEqual(lines[0], 'POST /v0.4/traces HTTP/1.1') - assert.ok(lines.includes('Host: localhost:8126')) - assert.ok(lines.includes('Content-Length: 42')) - assert.ok(lines.includes('datadog-meta-lang: nodejs')) - assert.ok(lines.includes(`datadog-container-id: ${DOCKER_ID}`)) - assert.ok(lines.includes(`datadog-entity-id: ci-${DOCKER_ID}`)) - assert.ok(lines.includes('datadog-external-env: it-false,cn-svc,pu-x')) - assert.ok(out.endsWith('\r\n\r\n')) - }) - - it('replaces libdatadog\'s empty datadog-container-id instead of duplicating it', () => { - const head = headBytes([ - 'POST /v0.4/traces HTTP/1.1', - 'Host: localhost', - 'Content-Length: 0', - 'datadog-container-id: ', - ]) - const out = applyEntityHeaders(head, entity).toString('latin1') - const count = out.split('\r\n').filter(l => l.toLowerCase().startsWith('datadog-container-id:')).length - assert.strictEqual(count, 1) - assert.ok(out.includes(`datadog-container-id: ${DOCKER_ID}`)) - }) - - it('returns the head unchanged when no entity headers are detected', () => { - const head = headBytes(['POST / HTTP/1.1', 'Host: x', 'Content-Length: 0']) - const out = applyEntityHeaders(head, {}) - assert.deepStrictEqual(out, Buffer.from(head)) - }) - - it('leaves a malformed head (no terminator) untouched', () => { - const bad = Buffer.from('POST / HTTP/1.1\r\nHost: x', 'latin1') - const out = applyEntityHeaders(bad, entity) - assert.deepStrictEqual(out, Buffer.from(bad)) - }) }) - describe('httpRequest end-to-end (real transport)', () => { - let server - let port - let received - const prevExternalEnv = process.env.DD_EXTERNAL_ENV - - before(async () => { - // Set the env then clear the memoized detection so this request re-reads - // it (earlier tests may have already populated the cache). - process.env.DD_EXTERNAL_ENV = 'it-false,cn-e2e,pu-1' - transport._resetEntityHeadersCache() - server = http.createServer((req, res) => { - received = req.headers - res.writeHead(200, { 'content-type': 'application/json' }) - res.end('{}') - }) - await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) - port = server.address().port + it('returns undefined fields when nothing is readable', () => { + const inputs = collectEntityInputs({ + cgroupPath: '/nonexistent', + cgroupMount: '/nonexistent', + externalEnv: undefined, }) + assert.strictEqual(inputs.cgroupContent, undefined) + assert.strictEqual(inputs.cgroupInode, undefined) + assert.strictEqual(inputs.externalEnv, undefined) + }) - after(() => new Promise(resolve => server.close(() => { - if (prevExternalEnv === undefined) delete process.env.DD_EXTERNAL_ENV - else process.env.DD_EXTERNAL_ENV = prevExternalEnv - transport._resetEntityHeadersCache() - resolve() - }))) - - it('sends the detected entity headers on the wire (via the Rust-rendered head)', async () => { - const body = Buffer.from('[]', 'latin1') - const head = Buffer.from( - `POST /v0.4/traces HTTP/1.1\r\nHost: 127.0.0.1:${port}\r\nContent-Length: ${body.length}\r\n` - + 'datadog-meta-lang: nodejs\r\ndatadog-container-id: \r\n\r\n', - 'latin1', - ) - const mem = new ArrayBuffer(head.length + body.length) - const view = new Uint8Array(mem) - view.set(head, 0) - view.set(body, head.length) - - const [status] = await transport.httpRequest( - '127.0.0.1', port, false, '', 0, head.length, head.length, body.length, { buffer: mem }, - ) - assert.strictEqual(status, 200) - assert.strictEqual(received['datadog-meta-lang'], 'nodejs') - assert.strictEqual(received['datadog-external-env'], 'it-false,cn-e2e,pu-1') - const detected = detectEntityHeaders() - if (detected['datadog-container-id']) { - assert.strictEqual(received['datadog-container-id'], detected['datadog-container-id']) - } + it('passes DD_EXTERNAL_ENV through verbatim — sanitization happens in Rust', () => { + // Node forwards the raw value; libdd-common's sanitize_external_env + // decides whether to accept or drop it. + const inputs = collectEntityInputs({ + cgroupPath: '/nonexistent', + cgroupMount: '/nonexistent', + externalEnv: 'ok\r\nx-evil: 1', }) + assert.strictEqual(inputs.externalEnv, 'ok\r\nx-evil: 1') }) })