diff --git a/elasticsearch/Cargo.toml b/elasticsearch/Cargo.toml index 05416c5f..e306af45 100644 --- a/elasticsearch/Cargo.toml +++ b/elasticsearch/Cargo.toml @@ -29,6 +29,7 @@ rustls-tls = ["reqwest/rustls-tls"] base64 = "^0.11" bytes = "^1.0" dyn-clone = "~1" +lazy_static = "1.4" percent-encoding = "2.1.0" reqwest = { version = "~0.11", default-features = false, features = ["gzip", "json"] } url = "^2.1" @@ -45,6 +46,7 @@ futures = "0.3.1" http = "0.2" hyper = { version = "0.14", default-features = false, features = ["tcp", "stream", "server"] } os_type = "2.2" +regex="1.4" sysinfo = "0.12.0" textwrap = "^0.11" tokio = { version = "1.0", default-features = false, features = ["macros", "net", "time", "rt-multi-thread"] } diff --git a/elasticsearch/build.rs b/elasticsearch/build.rs index 1a213aa3..53983a99 100644 --- a/elasticsearch/build.rs +++ b/elasticsearch/build.rs @@ -20,7 +20,8 @@ extern crate rustc_version; use rustc_version::{version_meta, Channel}; fn main() { - match version_meta().unwrap().channel { + let version = version_meta().unwrap(); + match version.channel { Channel::Stable => { println!("cargo:rustc-cfg=RUSTC_IS_STABLE"); } @@ -34,4 +35,11 @@ fn main() { println!("cargo:rustc-cfg=RUSTC_IS_DEV"); } } + + let semver = version.semver; + let mut rustcv: String = format!("{}.{}.{}", semver.major, semver.minor, semver.patch); + if version.channel != Channel::Stable { + rustcv.push('p'); + } + println!("cargo:rustc-env=RUSTC_VERSION={}", rustcv); } diff --git a/elasticsearch/src/http/transport.rs b/elasticsearch/src/http/transport.rs index 14a2cdea..ead0c82a 100644 --- a/elasticsearch/src/http/transport.rs +++ b/elasticsearch/src/http/transport.rs @@ -37,6 +37,7 @@ use crate::{ }; use base64::write::EncoderWriter as Base64Encoder; use bytes::BytesMut; +use lazy_static::lazy_static; use serde::Serialize; use std::{ error, fmt, @@ -97,6 +98,37 @@ impl fmt::Display for BuildError { /// Default address to Elasticsearch running on `http://localhost:9200` pub static DEFAULT_ADDRESS: &str = "http://localhost:9200"; +lazy_static! { + /// Client metadata header: service, language, transport, followed by additional information + static ref CLIENT_META: String = build_meta(); +} + +fn build_meta() -> String { + let mut version_parts = env!("CARGO_PKG_VERSION").split(&['.', '-'][..]); + let mut version = String::new(); + + // major.minor.patch followed with an optional 'p' for preliminary versions + version.push_str(version_parts.next().unwrap()); + version.push('.'); + version.push_str(version_parts.next().unwrap()); + version.push('.'); + version.push_str(version_parts.next().unwrap()); + if version_parts.next().is_some() { + version.push('p'); + } + + let rustc = env!("RUSTC_VERSION"); + let mut meta = format!("es={},rs={},t={}", version, rustc, version); + + if cfg!(feature = "native-tls") { + meta.push_str(",tls=n"); + } else if cfg!(feature = "rustls-tls") { + meta.push_str(",tls=r"); + } + + meta +} + /// Builds a HTTP transport to make API calls to Elasticsearch pub struct TransportBuilder { client_builder: reqwest::ClientBuilder, @@ -108,6 +140,7 @@ pub struct TransportBuilder { proxy_credentials: Option, disable_proxy: bool, headers: HeaderMap, + meta_header: bool, timeout: Option, } @@ -128,6 +161,7 @@ impl TransportBuilder { proxy_credentials: None, disable_proxy: false, headers: HeaderMap::new(), + meta_header: true, timeout: None, } } @@ -187,6 +221,16 @@ impl TransportBuilder { self } + /// Whether to send a `x-elastic-client-meta` header that describes the runtime environment. + /// + /// This header contains information that is similar to what could be found in `User-Agent`. Using a separate + /// header allows applications to use `User-Agent` for their own needs, e.g. to identify application version + /// or other environment information. Defaults to `true`. + pub fn enable_meta_header(mut self, enable: bool) -> Self { + self.meta_header = enable; + self + } + /// Sets a global request timeout for the client. /// /// The timeout is applied from when the request starts connecting until the response body has finished. @@ -270,6 +314,7 @@ impl TransportBuilder { client, conn_pool: self.conn_pool, credentials: self.credentials, + send_meta: self.meta_header, }) } } @@ -309,6 +354,7 @@ pub struct Transport { client: reqwest::Client, credentials: Option, conn_pool: Box, + send_meta: bool, } impl Transport { @@ -396,13 +442,20 @@ impl Transport { } // default headers first, overwrite with any provided - let mut request_headers = HeaderMap::with_capacity(3 + headers.len()); + let mut request_headers = HeaderMap::with_capacity(4 + headers.len()); request_headers.insert(CONTENT_TYPE, HeaderValue::from_static(DEFAULT_CONTENT_TYPE)); request_headers.insert(ACCEPT, HeaderValue::from_static(DEFAULT_ACCEPT)); request_headers.insert(USER_AGENT, HeaderValue::from_static(DEFAULT_USER_AGENT)); for (name, value) in headers { request_headers.insert(name.unwrap(), value); } + // if meta header enabled, send it last so that it's not overridden. + if self.send_meta { + request_headers.insert( + "x-elastic-client-meta", + HeaderValue::from_static(CLIENT_META.as_str()), + ); + } request_builder = request_builder.headers(request_headers); @@ -601,9 +654,10 @@ impl ConnectionPool for CloudConnectionPool { #[cfg(test)] pub mod tests { + use super::*; #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] use crate::auth::ClientCertificate; - use crate::http::transport::{CloudId, Connection, SingleNodeConnectionPool, TransportBuilder}; + use regex::Regex; use url::Url; #[test] @@ -742,4 +796,12 @@ pub mod tests { let conn = Connection::new(url); assert_eq!(conn.url.as_str(), "http://10.1.2.3/"); } + + #[test] + pub fn test_meta_header() { + let re = Regex::new(r"^es=[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,3}p?,rs=[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,3}p?,t=[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,3}p?(,tls=[rn])?$").unwrap(); + let x: &str = CLIENT_META.as_str(); + println!("{}", x); + assert!(re.is_match(x)); + } }