Skip to content

Commit f78cf31

Browse files
authored
feat(client): set the default user-agent and allow to add product info(s) (#135)
1 parent 6a87d25 commit f78cf31

File tree

6 files changed

+189
-13
lines changed

6 files changed

+189
-13
lines changed

src/headers.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use crate::ProductInfo;
2+
use hyper::header::USER_AGENT;
3+
use hyper::http::request::Builder;
4+
use std::collections::HashMap;
5+
use std::env::consts::OS;
6+
7+
fn get_user_agent(products_info: &[ProductInfo]) -> String {
8+
// See https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates
9+
let pkg_ver = option_env!("CARGO_PKG_VERSION").unwrap_or("unknown");
10+
let rust_ver = option_env!("CARGO_PKG_RUST_VERSION").unwrap_or("unknown");
11+
let default_agent = format!("clickhouse-rs/{pkg_ver} (lv:rust/{rust_ver}, os:{OS})");
12+
if products_info.is_empty() {
13+
default_agent
14+
} else {
15+
let products = products_info
16+
.iter()
17+
.rev()
18+
.map(|product_info| product_info.to_string())
19+
.collect::<Vec<String>>()
20+
.join(" ");
21+
format!("{products} {default_agent}")
22+
}
23+
}
24+
25+
pub(crate) fn with_request_headers(
26+
mut builder: Builder,
27+
headers: &HashMap<String, String>,
28+
products_info: &[ProductInfo],
29+
) -> Builder {
30+
for (name, value) in headers {
31+
builder = builder.header(name, value);
32+
}
33+
builder = builder.header(USER_AGENT.to_string(), get_user_agent(products_info));
34+
builder
35+
}

src/insert.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use tokio::{
1010
};
1111
use url::Url;
1212

13+
use crate::headers::with_request_headers;
1314
use crate::{
1415
error::{Error, Result},
1516
request_body::{ChunkSender, RequestBody},
@@ -351,10 +352,7 @@ impl<T> Insert<T> {
351352
drop(pairs);
352353

353354
let mut builder = Request::post(url.as_str());
354-
355-
for (name, value) in &client.headers {
356-
builder = builder.header(name, value);
357-
}
355+
builder = with_request_headers(builder, &client.headers, &client.products_info);
358356

359357
if let Some(user) = &client.user {
360358
builder = builder.header("X-ClickHouse-User", user);

src/lib.rs

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,18 @@
55
#[macro_use]
66
extern crate static_assertions;
77

8-
use std::{collections::HashMap, sync::Arc, time::Duration};
9-
8+
pub use clickhouse_derive::Row;
109
#[cfg(feature = "tls")]
1110
use hyper_tls::HttpsConnector;
1211
use hyper_util::{
1312
client::legacy::{connect::HttpConnector, Client as HyperClient},
1413
rt::TokioExecutor,
1514
};
16-
17-
use self::{error::Result, http_client::HttpClient};
15+
use std::fmt::Display;
16+
use std::{collections::HashMap, sync::Arc, time::Duration};
1817

1918
pub use self::{compression::Compression, row::Row};
20-
pub use clickhouse_derive::Row;
19+
use self::{error::Result, http_client::HttpClient};
2120

2221
pub mod error;
2322
pub mod insert;
@@ -34,6 +33,7 @@ pub mod watch;
3433
mod buflist;
3534
mod compression;
3635
mod cursor;
36+
mod headers;
3737
mod http_client;
3838
mod request_body;
3939
mod response;
@@ -76,6 +76,19 @@ pub struct Client {
7676
compression: Compression,
7777
options: HashMap<String, String>,
7878
headers: HashMap<String, String>,
79+
products_info: Vec<ProductInfo>,
80+
}
81+
82+
#[derive(Clone)]
83+
struct ProductInfo {
84+
name: String,
85+
version: String,
86+
}
87+
88+
impl Display for ProductInfo {
89+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90+
write!(f, "{}/{}", self.name, self.version)
91+
}
7992
}
8093

8194
impl Default for Client {
@@ -132,6 +145,7 @@ impl Client {
132145
compression: Compression::default(),
133146
options: HashMap::new(),
134147
headers: HashMap::new(),
148+
products_info: Vec::default(),
135149
}
136150
}
137151

@@ -221,6 +235,55 @@ impl Client {
221235
self
222236
}
223237

238+
/// Specifies the product name and version that will be included
239+
/// in the default User-Agent header. Multiple products are supported.
240+
/// This could be useful for the applications built on top of this client.
241+
///
242+
/// # Examples
243+
///
244+
/// Sample default User-Agent header:
245+
///
246+
/// ```plaintext
247+
/// clickhouse-rs/0.12.2 (lv:rust/1.67.0, os:macos)
248+
/// ```
249+
///
250+
/// Sample User-Agent with a single product information:
251+
///
252+
/// ```
253+
/// # use clickhouse::Client;
254+
/// let client = Client::default().with_product_info("MyDataSource", "v1.0.0");
255+
/// ```
256+
///
257+
/// ```plaintext
258+
/// MyDataSource/v1.0.0 clickhouse-rs/0.12.2 (lv:rust/1.67.0, os:macos)
259+
/// ```
260+
///
261+
/// Sample User-Agent with multiple products information
262+
/// (NB: the products are added in the reverse order of [`Client::with_product_info`] calls,
263+
/// which could be useful to add higher abstraction layers first):
264+
///
265+
/// ```
266+
/// # use clickhouse::Client;
267+
/// let client = Client::default()
268+
/// .with_product_info("MyDataSource", "v1.0.0")
269+
/// .with_product_info("MyApp", "0.0.1");
270+
/// ```
271+
///
272+
/// ```plaintext
273+
/// MyApp/0.0.1 MyDataSource/v1.0.0 clickhouse-rs/0.12.2 (lv:rust/1.67.0, os:macos)
274+
/// ```
275+
pub fn with_product_info(
276+
mut self,
277+
product_name: impl Into<String>,
278+
product_version: impl Into<String>,
279+
) -> Self {
280+
self.products_info.push(ProductInfo {
281+
name: product_name.into(),
282+
version: product_version.into(),
283+
});
284+
self
285+
}
286+
224287
/// Starts a new INSERT statement.
225288
///
226289
/// # Panics

src/query.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use hyper::{header::CONTENT_LENGTH, Method, Request};
22
use serde::Deserialize;
33
use url::Url;
44

5+
use crate::headers::with_request_headers;
56
use crate::{
67
cursor::RowBinaryCursor,
78
error::{Error, Result},
@@ -159,10 +160,7 @@ impl Query {
159160
drop(pairs);
160161

161162
let mut builder = Request::builder().method(method).uri(url.as_str());
162-
163-
for (name, value) in &self.client.headers {
164-
builder = builder.header(name, value);
165-
}
163+
builder = with_request_headers(builder, &self.client.headers, &self.client.products_info);
166164

167165
if content_length == 0 {
168166
builder = builder.header(CONTENT_LENGTH, "0");

tests/it/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ mod ip;
6363
mod nested;
6464
mod query;
6565
mod time;
66+
mod user_agent;
6667
mod uuid;
6768
mod watch;
6869

tests/it/user_agent.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
use crate::{create_simple_table, flush_query_log, SimpleRow};
2+
use clickhouse::sql::Identifier;
3+
use clickhouse::Client;
4+
5+
const PKG_VER: &str = env!("CARGO_PKG_VERSION");
6+
const RUST_VER: &str = env!("CARGO_PKG_RUST_VERSION");
7+
const OS: &str = std::env::consts::OS;
8+
9+
#[tokio::test]
10+
async fn default_user_agent() {
11+
let table_name = "chrs_default_user_agent";
12+
let client = prepare_database!();
13+
let expected_user_agent = format!("clickhouse-rs/{PKG_VER} (lv:rust/{RUST_VER}, os:{OS})");
14+
assert_queries_user_agents(&client, table_name, &expected_user_agent).await;
15+
}
16+
17+
#[tokio::test]
18+
async fn user_agent_with_single_product_info() {
19+
let table_name = "chrs_user_agent_with_single_product_info";
20+
let client = prepare_database!().with_product_info("my-app", "0.1.0");
21+
let expected_user_agent =
22+
format!("my-app/0.1.0 clickhouse-rs/{PKG_VER} (lv:rust/{RUST_VER}, os:{OS})");
23+
assert_queries_user_agents(&client, table_name, &expected_user_agent).await;
24+
}
25+
26+
#[tokio::test]
27+
async fn user_agent_with_multiple_product_info() {
28+
let table_name = "chrs_user_agent_with_multiple_product_info";
29+
let client = prepare_database!()
30+
.with_product_info("my-datasource", "2.5.0")
31+
.with_product_info("my-app", "0.1.0");
32+
let expected_user_agent = format!(
33+
"my-app/0.1.0 my-datasource/2.5.0 clickhouse-rs/{PKG_VER} (lv:rust/{RUST_VER}, os:{OS})"
34+
);
35+
assert_queries_user_agents(&client, table_name, &expected_user_agent).await;
36+
}
37+
38+
async fn assert_queries_user_agents(client: &Client, table_name: &str, expected_user_agent: &str) {
39+
let row = SimpleRow::new(42, "foo");
40+
41+
create_simple_table(client, table_name).await;
42+
43+
let mut insert = client.insert(table_name).unwrap();
44+
insert.write(&row).await.unwrap();
45+
insert.end().await.unwrap();
46+
47+
let rows = client
48+
.query("SELECT ?fields FROM ?")
49+
.bind(Identifier(table_name))
50+
.fetch_all::<SimpleRow>()
51+
.await
52+
.unwrap();
53+
54+
assert_eq!(rows.len(), 1);
55+
assert_eq!(rows[0], row);
56+
57+
flush_query_log(client).await;
58+
59+
let recorded_user_agents = client
60+
.query(&format!(
61+
"
62+
SELECT http_user_agent
63+
FROM system.query_log
64+
WHERE type = 'QueryFinish'
65+
AND (
66+
query LIKE 'SELECT%FROM%{table_name}%'
67+
OR
68+
query LIKE 'INSERT%INTO%{table_name}%'
69+
)
70+
ORDER BY event_time_microseconds DESC
71+
LIMIT 2
72+
"
73+
))
74+
.fetch_all::<String>()
75+
.await
76+
.unwrap();
77+
78+
assert_eq!(recorded_user_agents.len(), 2);
79+
assert_eq!(recorded_user_agents[0], expected_user_agent);
80+
assert_eq!(recorded_user_agents[1], expected_user_agent);
81+
}

0 commit comments

Comments
 (0)