Skip to content

Commit 536a7f1

Browse files
authored
feat(clickhouse sink): make database and table templateable (vectordotdev#18005)
Closes: vectordotdev#16329
1 parent fbc0308 commit 536a7f1

File tree

7 files changed

+193
-49
lines changed

7 files changed

+193
-49
lines changed

src/sinks/clickhouse/config.rs

+11-7
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ pub struct ClickhouseConfig {
2525

2626
/// The table that data is inserted into.
2727
#[configurable(metadata(docs::examples = "mytable"))]
28-
pub table: String,
28+
pub table: Template,
2929

3030
/// The database that contains the table that data is inserted into.
3131
#[configurable(metadata(docs::examples = "mydatabase"))]
32-
pub database: Option<String>,
32+
pub database: Option<Template>,
3333

3434
/// Sets `input_format_skip_unknown_fields`, allowing ClickHouse to discard fields not present in the table schema.
3535
#[serde(default)]
@@ -90,25 +90,30 @@ impl SinkConfig for ClickhouseConfig {
9090
let service = ClickhouseService::new(
9191
client.clone(),
9292
auth.clone(),
93-
&endpoint,
94-
self.database.as_deref(),
95-
self.table.as_str(),
93+
endpoint.clone(),
9694
self.skip_unknown_fields,
9795
self.date_time_best_effort,
98-
)?;
96+
);
9997

10098
let request_limits = self.request.unwrap_with(&Default::default());
10199
let service = ServiceBuilder::new()
102100
.settings(request_limits, ClickhouseRetryLogic::default())
103101
.service(service);
104102

105103
let batch_settings = self.batch.into_batcher_settings()?;
104+
let database = self.database.clone().unwrap_or_else(|| {
105+
"default"
106+
.try_into()
107+
.expect("'default' should be a valid template")
108+
});
106109
let sink = ClickhouseSink::new(
107110
batch_settings,
108111
self.compression,
109112
self.encoding.clone(),
110113
service,
111114
protocol,
115+
database,
116+
self.table.clone(),
112117
);
113118

114119
let healthcheck = Box::pin(healthcheck(client, endpoint, auth));
@@ -126,7 +131,6 @@ impl SinkConfig for ClickhouseConfig {
126131
}
127132

128133
async fn healthcheck(client: HttpClient, endpoint: Uri, auth: Option<Auth>) -> crate::Result<()> {
129-
// TODO: check if table exists?
130134
let uri = format!("{}/?query=SELECT%201", endpoint);
131135
let mut request = Request::get(uri).body(Body::empty()).unwrap();
132136

src/sinks/clickhouse/integration_tests.rs

+69-5
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ async fn insert_events() {
4545

4646
let config = ClickhouseConfig {
4747
endpoint: host.parse().unwrap(),
48-
table: table.clone(),
48+
table: table.clone().try_into().unwrap(),
4949
compression: Compression::None,
5050
batch,
5151
request: TowerRequestConfig {
@@ -94,7 +94,7 @@ async fn skip_unknown_fields() {
9494

9595
let config = ClickhouseConfig {
9696
endpoint: host.parse().unwrap(),
97-
table: table.clone(),
97+
table: table.clone().try_into().unwrap(),
9898
skip_unknown_fields: true,
9999
compression: Compression::None,
100100
batch,
@@ -140,7 +140,7 @@ async fn insert_events_unix_timestamps() {
140140

141141
let config = ClickhouseConfig {
142142
endpoint: host.parse().unwrap(),
143-
table: table.clone(),
143+
table: table.clone().try_into().unwrap(),
144144
compression: Compression::None,
145145
encoding: Transformer::new(None, None, Some(TimestampFormat::Unix)).unwrap(),
146146
batch,
@@ -269,7 +269,7 @@ async fn no_retry_on_incorrect_data() {
269269

270270
let config = ClickhouseConfig {
271271
endpoint: host.parse().unwrap(),
272-
table: table.clone(),
272+
table: table.clone().try_into().unwrap(),
273273
compression: Compression::None,
274274
batch,
275275
..Default::default()
@@ -320,7 +320,7 @@ async fn no_retry_on_incorrect_data_warp() {
320320

321321
let config = ClickhouseConfig {
322322
endpoint: host.parse().unwrap(),
323-
table: gen_table(),
323+
table: gen_table().try_into().unwrap(),
324324
batch,
325325
..Default::default()
326326
};
@@ -338,6 +338,70 @@ async fn no_retry_on_incorrect_data_warp() {
338338
assert_eq!(receiver.try_recv(), Ok(BatchStatus::Rejected));
339339
}
340340

341+
#[tokio::test]
342+
async fn templated_table() {
343+
trace_init();
344+
345+
let n_tables = 2;
346+
let table_events: Vec<(String, Event, BatchStatusReceiver)> = (0..n_tables)
347+
.map(|_| {
348+
let table = gen_table();
349+
let (mut event, receiver) = make_event();
350+
event.as_mut_log().insert("table", table.as_str());
351+
(table, event, receiver)
352+
})
353+
.collect();
354+
355+
let host = clickhouse_address();
356+
357+
let mut batch = BatchConfig::default();
358+
batch.max_events = Some(1);
359+
360+
let config = ClickhouseConfig {
361+
endpoint: host.parse().unwrap(),
362+
table: "{{ .table }}".try_into().unwrap(),
363+
batch,
364+
..Default::default()
365+
};
366+
367+
let client = ClickhouseClient::new(host);
368+
for (table, _, _) in &table_events {
369+
client
370+
.create_table(
371+
table,
372+
"host String, timestamp String, message String, table String",
373+
)
374+
.await;
375+
}
376+
377+
let (sink, _hc) = config.build(SinkContext::default()).await.unwrap();
378+
379+
let events: Vec<Event> = table_events
380+
.iter()
381+
.map(|(_, event, _)| event.clone())
382+
.collect();
383+
run_and_assert_sink_compliance(sink, stream::iter(events), &SINK_TAGS).await;
384+
385+
for (table, event, mut receiver) in table_events {
386+
let output = client.select_all(&table).await;
387+
assert_eq!(1, output.rows, "table {} should have 1 row", table);
388+
389+
let expected = serde_json::to_value(event.into_log()).unwrap();
390+
assert_eq!(
391+
expected, output.data[0],
392+
"table \"{}\"'s one row should have the correct data",
393+
table
394+
);
395+
396+
assert_eq!(
397+
receiver.try_recv(),
398+
Ok(BatchStatus::Delivered),
399+
"table \"{}\"'s event should have been delivered",
400+
table
401+
);
402+
}
403+
}
404+
341405
fn make_event() -> (Event, BatchStatusReceiver) {
342406
let (batch, receiver) = BatchNotifier::new_with_receiver();
343407
let mut event = LogEvent::from("raw log line").with_batch_notifier(&batch);

src/sinks/clickhouse/service.rs

+35-26
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ use crate::{
1919

2020
#[derive(Debug, Clone)]
2121
pub struct ClickhouseRequest {
22+
pub database: String,
23+
pub table: String,
2224
pub body: Bytes,
2325
pub compression: Compression,
2426
pub finalizers: EventFinalizers,
@@ -107,31 +109,28 @@ impl RetryLogic for ClickhouseRetryLogic {
107109
#[derive(Debug, Clone)]
108110
pub struct ClickhouseService {
109111
client: HttpClient,
110-
uri: Uri,
111112
auth: Option<Auth>,
113+
endpoint: Uri,
114+
skip_unknown_fields: bool,
115+
date_time_best_effort: bool,
112116
}
113117

114118
impl ClickhouseService {
115119
/// Creates a new `ClickhouseService`.
116-
pub fn new(
120+
pub const fn new(
117121
client: HttpClient,
118122
auth: Option<Auth>,
119-
endpoint: &Uri,
120-
database: Option<&str>,
121-
table: &str,
123+
endpoint: Uri,
122124
skip_unknown_fields: bool,
123125
date_time_best_effort: bool,
124-
) -> crate::Result<Self> {
125-
// Set the URI query once during initialization, as it won't change throughout the lifecycle
126-
// of the service.
127-
let uri = set_uri_query(
126+
) -> Self {
127+
Self {
128+
client,
129+
auth,
128130
endpoint,
129-
database.unwrap_or("default"),
130-
table,
131131
skip_unknown_fields,
132132
date_time_best_effort,
133-
)?;
134-
Ok(Self { client, auth, uri })
133+
}
135134
}
136135
}
137136

@@ -148,22 +147,32 @@ impl Service<ClickhouseRequest> for ClickhouseService {
148147
// Emission of Error internal event is handled upstream by the caller.
149148
fn call(&mut self, request: ClickhouseRequest) -> Self::Future {
150149
let mut client = self.client.clone();
150+
let auth = self.auth.clone();
151151

152-
let mut builder = Request::post(&self.uri)
153-
.header(CONTENT_TYPE, "application/x-ndjson")
154-
.header(CONTENT_LENGTH, request.body.len());
155-
if let Some(ce) = request.compression.content_encoding() {
156-
builder = builder.header(CONTENT_ENCODING, ce);
157-
}
158-
if let Some(auth) = &self.auth {
159-
builder = auth.apply_builder(builder);
160-
}
161-
162-
let http_request = builder
163-
.body(Body::from(request.body))
164-
.expect("building HTTP request failed unexpectedly");
152+
// Build the URI outside of the boxed future to avoid unnecessary clones.
153+
let uri = set_uri_query(
154+
&self.endpoint,
155+
&request.database,
156+
&request.table,
157+
self.skip_unknown_fields,
158+
self.date_time_best_effort,
159+
);
165160

166161
Box::pin(async move {
162+
let mut builder = Request::post(&uri?)
163+
.header(CONTENT_TYPE, "application/x-ndjson")
164+
.header(CONTENT_LENGTH, request.body.len());
165+
if let Some(ce) = request.compression.content_encoding() {
166+
builder = builder.header(CONTENT_ENCODING, ce);
167+
}
168+
if let Some(auth) = auth {
169+
builder = auth.apply_builder(builder);
170+
}
171+
172+
let http_request = builder
173+
.body(Body::from(request.body))
174+
.expect("building HTTP request failed unexpectedly");
175+
167176
let response = client.call(http_request).in_current_span().await?;
168177
let (parts, body) = response.into_parts();
169178
let body = body::to_bytes(body).await?;

0 commit comments

Comments
 (0)