diff --git a/Cargo.lock b/Cargo.lock index 92642ab076..c965da783a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1557,6 +1557,7 @@ dependencies = [ "mime_guess", "percent-encoding", "prost 0.12.6", + "reqwest 0.12.15", "rustc-hash 1.1.0", "serde", "serde_json", @@ -2089,7 +2090,7 @@ dependencies = [ "hashbrown 0.14.5", "protobuf", "regex", - "reqwest", + "reqwest 0.12.9", "serde", "serde_json", "thiserror", @@ -2307,6 +2308,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -2977,11 +2993,27 @@ dependencies = [ "tokio-io-timeout", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" dependencies = [ "bytes", "futures-channel", @@ -2989,6 +3021,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "hyper 1.6.0", + "libc", "pin-project-lite", "socket2", "tokio", @@ -3306,10 +3339,11 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.72" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -3383,9 +3417,9 @@ checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" [[package]] name = "libc" -version = "0.2.167" +version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "libloading" @@ -3394,7 +3428,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -3618,6 +3652,23 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -3764,12 +3815,50 @@ version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" +[[package]] +name = "openssl" +version = "0.10.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-sys" +version = "0.9.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "opentelemetry" version = "0.17.0" @@ -4727,7 +4816,50 @@ dependencies = [ "wasm-bindgen-futures", "web-sys", "webpki-roots", - "windows-registry", + "windows-registry 0.2.0", +] + +[[package]] +name = "reqwest" +version = "0.12.15" +source = "git+https://github.com/seanmonstar/reqwest.git?branch=uds#c50e9d3df14c8b63697b865d77dcfa73b09f0884" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.6", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower 0.5.2", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry 0.4.0", ] [[package]] @@ -5297,9 +5429,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", @@ -5508,6 +5640,27 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tar" version = "0.4.43" @@ -5654,7 +5807,7 @@ dependencies = [ "memchr", "parse-display", "pin-project-lite", - "reqwest", + "reqwest 0.12.9", "serde", "serde_json", "serde_with", @@ -5846,6 +5999,16 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.0" @@ -6045,6 +6208,7 @@ dependencies = [ "futures-util", "pin-project-lite", "sync_wrapper 1.0.1", + "tokio", "tower-layer", "tower-service", ] @@ -6291,6 +6455,12 @@ dependencies = [ "ryu", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -6333,24 +6503,24 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn 2.0.87", @@ -6371,9 +6541,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6381,9 +6551,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -6394,9 +6564,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" @@ -6572,6 +6745,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result 0.3.0", + "windows-strings 0.3.0", + "windows-targets 0.53.0", +] + [[package]] name = "windows-result" version = "0.2.0" diff --git a/ddcommon/Cargo.toml b/ddcommon/Cargo.toml index 708517fc49..867ae397e4 100644 --- a/ddcommon/Cargo.toml +++ b/ddcommon/Cargo.toml @@ -19,7 +19,7 @@ futures-core = { version = "0.3.0", default-features = false } futures-util = { version = "0.3.0", default-features = false } hex = "0.4" hyper = { version = "1.6", features = ["http1", "client"] } -hyper-util = { version = "0.1.10", features = [ +hyper-util = { version = "0.1", features = [ "http1", "client", "client-legacy", diff --git a/profiling/Cargo.toml b/profiling/Cargo.toml index db79e5576e..e47a746c11 100644 --- a/profiling/Cargo.toml +++ b/profiling/Cargo.toml @@ -48,6 +48,7 @@ serde_json = {version = "1.0"} target-triple = "0.1.4" tokio = {version = "1.23", features = ["rt", "macros"]} tokio-util = "0.7.1" +reqwest = { git = "https://github.com/seanmonstar/reqwest.git", branch = "uds" } [dev-dependencies] bolero = "0.13" diff --git a/profiling/src/lib.rs b/profiling/src/lib.rs index be90833ee6..2d5906588b 100644 --- a/profiling/src/lib.rs +++ b/profiling/src/lib.rs @@ -13,3 +13,4 @@ pub mod internal; pub mod iter; pub mod pprof; pub mod serializer; +pub mod reqwest_exporter; \ No newline at end of file diff --git a/profiling/src/reqwest_exporter/config.rs b/profiling/src/reqwest_exporter/config.rs new file mode 100644 index 0000000000..affb49d2aa --- /dev/null +++ b/profiling/src/reqwest_exporter/config.rs @@ -0,0 +1,76 @@ +// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(unix)] +use ddcommon::connector::uds; +use ddcommon::Endpoint; + +#[cfg(windows)] +use ddcommon::connector::named_pipe; + +use http::Uri; +use std::borrow::Cow; +use std::str::FromStr; + +/// Creates an Endpoint for talking to the Datadog agent. +/// +/// # Arguments +/// * `base_url` - has protocol, host, and port e.g. http://localhost:8126/ +pub fn agent(base_url: Uri) -> anyhow::Result { + let mut parts = base_url.into_parts(); + let p_q = match parts.path_and_query { + None => None, + Some(pq) => { + let path = pq.path(); + let path = path.strip_suffix('/').unwrap_or(path); + Some(format!("{path}/profiling/v1/input").parse()?) + } + }; + parts.path_and_query = p_q; + let url = Uri::from_parts(parts)?; + Ok(Endpoint::from_url(url)) +} + +/// Creates an Endpoint for talking to the Datadog agent though a unix socket. +/// +/// # Arguments +/// * `socket_path` - file system path to the socket +#[cfg(unix)] +pub fn agent_uds(path: &std::path::Path) -> anyhow::Result { + let base_url = uds::socket_path_to_uri(path)?; + agent(base_url) +} + +/// Creates an Endpoint for talking to the Datadog agent though a windows named pipe. +/// +/// # Arguments +/// * `path` - file system path to the named pipe +#[cfg(windows)] +pub fn agent_named_pipe(path: &std::path::Path) -> anyhow::Result { + let base_url = named_pipe::named_pipe_path_to_uri(path)?; + agent(base_url) +} + +/// Creates an Endpoint for talking to Datadog intake without using the agent. +/// This is an experimental feature. +/// +/// # Arguments +/// * `site` - e.g. "datadoghq.com". +/// * `api_key` +pub fn agentless, IntoCow: Into>>( + site: AsStrRef, + api_key: IntoCow, +) -> anyhow::Result { + let intake_url: String = format!("https://intake.profile.{}/api/v2/profile", site.as_ref()); + + Ok(Endpoint { + url: Uri::from_str(intake_url.as_str())?, + api_key: Some(api_key.into()), + ..Default::default() + }) +} + +pub fn file(path: impl AsRef) -> anyhow::Result { + let url: String = format!("file://{}", path.as_ref()); + Ok(Endpoint::from_slice(&url)) +} diff --git a/profiling/src/reqwest_exporter/errors.rs b/profiling/src/reqwest_exporter/errors.rs new file mode 100644 index 0000000000..063459a94a --- /dev/null +++ b/profiling/src/reqwest_exporter/errors.rs @@ -0,0 +1,25 @@ +// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use std::error; +use std::fmt; + +#[derive(Clone, Debug, Eq, PartialEq)] +#[allow(dead_code)] +pub(crate) enum Error { + InvalidUrl, + OperationTimedOut, + UserRequestedCancellation, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::InvalidUrl => "invalid url", + Self::OperationTimedOut => "operation timed out", + Self::UserRequestedCancellation => "operation cancelled by user", + }) + } +} + +impl error::Error for Error {} diff --git a/profiling/src/reqwest_exporter/mod.rs b/profiling/src/reqwest_exporter/mod.rs new file mode 100644 index 0000000000..daa94a6c77 --- /dev/null +++ b/profiling/src/reqwest_exporter/mod.rs @@ -0,0 +1,377 @@ +// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use bytes::Bytes; +pub use chrono::{DateTime, Utc}; +pub use ddcommon::tag::Tag; +pub use hyper::Uri; +use hyper_multipart_rfc7578::client::multipart; +use lz4_flex::frame::FrameEncoder; +use serde_json::json; +use std::borrow::Cow; +use std::fmt::Debug; +use std::io::{Cursor, Write}; +use std::{future, iter}; +use tokio::runtime::Runtime; +use tokio_util::sync::CancellationToken; + +use ddcommon::{ + azure_app_services, connector, hyper_migration, tag, Endpoint, HttpClient, HttpResponse, +}; + +pub mod config; +mod errors; + +#[cfg(unix)] +pub use connector::uds::{socket_path_from_uri, socket_path_to_uri}; + +#[cfg(windows)] +pub use connector::named_pipe::{named_pipe_path_from_uri, named_pipe_path_to_uri}; + +use crate::internal::EncodedProfile; + +const DURATION_ZERO: std::time::Duration = std::time::Duration::from_millis(0); + +pub struct Exporter { + client: HttpClient, + runtime: Runtime, +} + +pub struct Fields { + pub start: DateTime, + pub end: DateTime, +} + +pub struct ProfileExporter { + exporter: Exporter, + endpoint: Endpoint, + family: Cow<'static, str>, + profiling_library_name: Cow<'static, str>, + profiling_library_version: Cow<'static, str>, + tags: Option>, +} + +pub struct File<'a> { + pub name: &'a str, + pub bytes: &'a [u8], +} + +#[derive(Debug)] +pub struct Request { + timeout: Option, + req: hyper_migration::HttpRequest, +} + +impl From for Request { + fn from(req: hyper_migration::HttpRequest) -> Self { + Self { req, timeout: None } + } +} + +impl Request { + fn with_timeout(mut self, timeout: std::time::Duration) -> Self { + self.timeout = if timeout != DURATION_ZERO { + Some(timeout) + } else { + None + }; + self + } + + pub fn timeout(&self) -> &Option { + &self.timeout + } + + pub fn uri(&self) -> &hyper::Uri { + self.req.uri() + } + + pub fn headers(&self) -> &hyper::HeaderMap { + self.req.headers() + } + + pub fn body(self) -> hyper_migration::Body { + self.req.into_body() + } + + async fn send( + self, + client: &HttpClient, + cancel: Option<&CancellationToken>, + ) -> anyhow::Result { + tokio::select! { + _ = async { match cancel { + Some(cancellation_token) => cancellation_token.cancelled().await, + // If no token is provided, future::pending() provides a no-op future that never resolves + None => future::pending().await, + }} + => Err(crate::exporter::errors::Error::UserRequestedCancellation.into()), + result = async { + match self.timeout { + Some(t) => { + let res = tokio::time::timeout(t, client.request(self.req)).await; + res + .map_err(|_| anyhow::Error::from(crate::exporter::errors::Error::OperationTimedOut)) + }, + None => Ok(client.request(self.req).await), + } + } + => Ok(hyper_migration::into_response(result??)), + } + } +} + + + +impl ProfileExporter { + /// Creates a new exporter to be used to report profiling data. + /// # Arguments + /// * `profiling_library_name` - Profiling library name, usually dd-trace-something, e.g. "dd-trace-rb". See + /// https://datadoghq.atlassian.net/wiki/spaces/PROF/pages/1538884229/Client#Header-values (Datadog internal link) + /// for a list of common values. + /// * `profiling_library_version` - Version used when publishing the profiling library to a + /// package manager + /// * `family` - Profile family, e.g. "ruby" + /// * `tags` - Tags to include with every profile reported by this exporter. It's also possible + /// to include profile-specific tags, see `additional_tags` on `build`. + /// * `endpoint` - Configuration for reporting data + pub fn new( + profiling_library_name: N, + profiling_library_version: V, + family: F, + tags: Option>, + endpoint: Endpoint, + ) -> anyhow::Result + where + F: Into>, + N: Into>, + V: Into>, + { + Ok(Self { + exporter: Exporter::new()?, + endpoint, + family: family.into(), + profiling_library_name: profiling_library_name.into(), + profiling_library_version: profiling_library_version.into(), + tags, + }) + } + + /// The target triple. This is a string like: + /// - aarch64-apple-darwin + /// - x86_64-unknown-linux-gnu + /// + /// The name is which is a misnomer, it traditionally had 3 pieces, but + /// it's commonly 4+ fragments today. + const TARGET_TRIPLE: &'static str = target_triple::TARGET; + + #[inline] + fn runtime_platform_tag(&self) -> Tag { + tag!("runtime_platform", ProfileExporter::TARGET_TRIPLE) + } + + #[allow(clippy::too_many_arguments)] + /// Build a Request object representing the profile information provided. + /// + /// Consumes the `EncodedProfile`, which is unavailable for use after. + /// + /// For details on the `internal_metadata` parameter, please reference the Datadog-internal + /// "RFC: Attaching internal metadata to pprof profiles". + /// If you use this parameter, please update the RFC with your use-case, so we can keep track of + /// how this is getting used. + /// + /// For details on the `info` parameter, please reference the Datadog-internal + /// "RFC: Pprof System Info Support". + pub fn build( + &self, + profile: EncodedProfile, + files_to_compress_and_export: &[File], + files_to_export_unmodified: &[File], + additional_tags: Option<&Vec>, + internal_metadata: Option, + info: Option, + ) -> anyhow::Result { + let mut form = multipart::Form::default(); + + // combine tags and additional_tags + let mut tags_profiler = String::new(); + let other_tags = additional_tags.into_iter(); + for tag in self.tags.iter().chain(other_tags).flatten() { + tags_profiler.push_str(tag.as_ref()); + tags_profiler.push(','); + } + + if let Some(aas_metadata) = azure_app_services::get_metadata() { + let aas_tags = [ + ("aas.resource.id", aas_metadata.get_resource_id()), + ( + "aas.environment.extension_version", + aas_metadata.get_extension_version(), + ), + ( + "aas.environment.instance_id", + aas_metadata.get_instance_id(), + ), + ( + "aas.environment.instance_name", + aas_metadata.get_instance_name(), + ), + ("aas.environment.os", aas_metadata.get_operating_system()), + ("aas.resource.group", aas_metadata.get_resource_group()), + ("aas.site.name", aas_metadata.get_site_name()), + ("aas.site.kind", aas_metadata.get_site_kind()), + ("aas.site.type", aas_metadata.get_site_type()), + ("aas.subscription.id", aas_metadata.get_subscription_id()), + ]; + aas_tags.into_iter().for_each(|(name, value)| { + if let Ok(tag) = Tag::new(name, value) { + tags_profiler.push_str(tag.as_ref()); + tags_profiler.push(','); + } + }); + } + + // Since this is the last tag, we add it without a comma afterward. If + // any tags get added after this one, you'll need to add the comma + // between them. + tags_profiler.push_str(self.runtime_platform_tag().as_ref()); + + let attachments: Vec = files_to_compress_and_export + .iter() + .chain(files_to_export_unmodified.iter()) + .map(|file| file.name.to_owned()) + .chain(iter::once("profile.pprof".to_string())) + .collect(); + + let endpoint_counts = if profile.endpoints_stats.is_empty() { + None + } else { + Some(profile.endpoints_stats) + }; + let mut internal: serde_json::value::Value = internal_metadata.unwrap_or_else(|| json!({})); + internal["libdatadog_version"] = json!(env!("CARGO_PKG_VERSION")); + + let event = json!({ + "attachments": attachments, + "tags_profiler": tags_profiler, + "start": DateTime::::from(profile.start).format("%Y-%m-%dT%H:%M:%S%.9fZ").to_string(), + "end": DateTime::::from(profile.end).format("%Y-%m-%dT%H:%M:%S%.9fZ").to_string(), + "family": self.family.as_ref(), + "version": "4", + "endpoint_counts" : endpoint_counts, + "internal": internal, + "info": info.unwrap_or_else(|| json!({})), + }) + .to_string(); + + form.add_reader_file_with_mime( + // Intake does not look for filename=event.json, it looks for name=event. + "event", + // this one shouldn't be compressed + Cursor::new(event), + "event.json", + mime::APPLICATION_JSON, + ); + + for file in files_to_compress_and_export { + // We tend to have good compression ratios for the pprof files, + // especially with timeline enabled. Not all files compress this + // well, but these are just initial Vec sizes, not a hard-bound. + // Using 1/10 gives us a better start than starting at zero, while + // not reserving too much for things that compress really well, and + // power-of-two capacities are almost always the best performing. + let capacity = (file.bytes.len() / 10).next_power_of_two(); + let buffer = Vec::with_capacity(capacity); + let mut encoder = FrameEncoder::new(buffer); + encoder.write_all(file.bytes)?; + let encoded = encoder.finish()?; + /* The Datadog RFC examples strip off the file extension, but the exact behavior + * isn't specified. This does the simple thing of using the filename + * without modification for the form name because intake does not care + * about these name of the form field for these attachments. + */ + form.add_reader_file(file.name, Cursor::new(encoded), file.name); + } + + for file in files_to_export_unmodified { + let encoded = file.bytes.to_vec(); + /* The Datadog RFC examples strip off the file extension, but the exact behavior + * isn't specified. This does the simple thing of using the filename + * without modification for the form name because intake does not care + * about these name of the form field for these attachments. + */ + form.add_reader_file(file.name, Cursor::new(encoded), file.name) + } + // Add the actual pprof + form.add_reader_file( + "profile.pprof", + Cursor::new(profile.buffer), + "profile.pprof", + ); + + let builder = self + .endpoint + .to_request_builder(concat!("DDProf/", env!("CARGO_PKG_VERSION")))? + .method(http::Method::POST) + .header("Connection", "close") + .header("DD-EVP-ORIGIN", self.profiling_library_name.as_ref()) + .header( + "DD-EVP-ORIGIN-VERSION", + self.profiling_library_version.as_ref(), + ); + + Ok(Request::from( + form.set_body::(builder)? + .map(hyper_migration::Body::boxed), + ) + .with_timeout(std::time::Duration::from_millis(self.endpoint.timeout_ms))) + } + + pub fn send( + &self, + request: Request, + cancel: Option<&CancellationToken>, + ) -> anyhow::Result { + self.exporter + .runtime + .block_on(request.send(&self.exporter.client, cancel)) + } + + pub fn set_timeout(&mut self, timeout_ms: u64) { + self.endpoint.timeout_ms = timeout_ms; + } +} + +impl Exporter { + /// Creates a new Exporter, initializing the TLS stack. + pub fn new() -> anyhow::Result { + // Set idle to 0, which prevents the pipe being broken every 2nd request + let client = hyper_migration::new_client_periodic(); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + Ok(Self { client, runtime }) + } + + pub fn send( + &self, + http_method: http::Method, + url: &str, + mut headers: hyper::header::HeaderMap, + body: &[u8], + timeout: std::time::Duration, + ) -> anyhow::Result { + self.runtime.block_on(async { + let mut request = hyper::Request::builder() + .method(http_method) + .uri(url) + .body(hyper_migration::Body::from_bytes(Bytes::copy_from_slice( + body, + )))?; + std::mem::swap(request.headers_mut(), &mut headers); + + let request: Request = request.into(); + request.with_timeout(timeout).send(&self.client, None).await + }) + } +}