1use super::transport::{query as percent, Origin};
18use anyhow::{bail, ensure, Context, Result};
19use bytes::Bytes;
20use http_body_util::{BodyExt, Full, Limited};
21use hyper::{
22 client::conn::http1,
23 header::{ACCEPT, AUTHORIZATION, CONNECTION, CONTENT_ENCODING, CONTENT_TYPE, HOST},
24 Request,
25};
26use hyper_util::rt::TokioIo;
27use serde::{Deserialize, Serialize};
28use serde_json::{json, Map, Value};
29use std::sync::Arc;
30use std::time::Duration;
31
32pub const QUERY_TIMEOUT: Duration = Duration::from_secs(30);
34pub const MAX_QUERY_RESPONSE_BYTES: usize = 4 * 1024 * 1024;
36pub const MAX_QUERY_BYTES: usize = 16 * 1024;
38pub const MAX_REPORTED_ROWS: usize = 1000;
40pub const MAX_REPORTED_TABLES: usize = 64;
42pub const MAX_COLUMNS: usize = 256;
44pub const MAX_HEADER_BYTES: usize = 16 * 1024;
48pub const MAX_ERROR_TEXT_BYTES: usize = 1024;
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(rename_all = "lowercase")]
54pub enum Language {
55 Flux,
56 Influxql,
57}
58
59#[derive(Clone, Debug, Serialize, Deserialize)]
61pub struct QuerySpec {
62 pub language: Language,
63 pub query: String,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub org: Option<String>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub db: Option<String>,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub rp: Option<String>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub epoch: Option<String>,
72}
73
74impl QuerySpec {
75 pub fn validate(&self) -> Result<()> {
76 ensure!(
77 !self.query.trim().is_empty() && self.query.len() <= MAX_QUERY_BYTES,
78 "query must be 1 to {MAX_QUERY_BYTES} bytes"
79 );
80 match self.language {
81 Language::Flux => {
82 let org = self.org.as_deref().context("a Flux query needs 'org'")?;
83 ensure!(
84 !org.is_empty() && org.len() <= 1024,
85 "org must be 1 to 1024 bytes"
86 );
87 }
88 Language::Influxql => {
89 for value in [&self.db, &self.rp].into_iter().flatten() {
90 ensure!(value.len() <= 1024, "db and rp are at most 1024 bytes");
91 }
92 if let Some(e) = &self.epoch {
93 ensure!(
94 ["ns", "u", "ms", "s", "m", "h"].contains(&e.as_str()),
95 "epoch must be ns, u, ms, s, m or h"
96 );
97 }
98 }
99 }
100 Ok(())
101 }
102
103 fn request(&self, origin: &Origin, token: &Option<String>) -> Result<Request<Full<Bytes>>> {
104 let mut builder = Request::builder()
105 .header(HOST, &origin.authority)
106 .header(CONNECTION, "close");
107 if let Some(token) = token {
108 builder = builder.header(AUTHORIZATION, format!("Token {token}"));
109 }
110 Ok(match self.language {
111 Language::Flux => builder
112 .method("POST")
113 .uri(format!(
114 "/api/v2/query?org={}",
115 percent(self.org.as_deref().unwrap_or_default())
116 ))
117 .header(CONTENT_TYPE, "application/json")
118 .header(ACCEPT, "application/csv")
119 .body(Full::new(Bytes::from(serde_json::to_vec(&json!({
120 "query": self.query,
121 "type": "flux",
122 "dialect": {
123 "header": true,
124 "delimiter": ",",
125 "annotations": ["datatype", "group", "default"],
126 "commentPrefix": "#",
127 "dateTimeFormat": "RFC3339",
128 },
129 }))?)))?,
130 Language::Influxql => {
131 let mut target = format!("/query?q={}", percent(&self.query));
132 for (key, value) in [("db", &self.db), ("rp", &self.rp), ("epoch", &self.epoch)] {
133 if let Some(value) = value {
134 target.push_str(&format!("&{key}={}", percent(value)));
135 }
136 }
137 builder
138 .method("GET")
139 .uri(target)
140 .header(ACCEPT, "application/json")
141 .body(Full::new(Bytes::new()))?
142 }
143 })
144 }
145}
146
147#[derive(Clone, Debug, Serialize)]
149pub struct QueryResponse {
150 pub language: Language,
151 pub query: String,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 pub org: Option<String>,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub db: Option<String>,
156 pub status: u16,
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub tables: Option<Vec<Value>>,
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub statements: Option<Vec<Value>>,
163 pub row_count: usize,
165 pub reported_rows: usize,
166 pub truncated: bool,
167 pub error: Option<Value>,
169}
170
171pub async fn query(
172 origin: Origin,
173 token: Option<String>,
174 spec: QuerySpec,
175) -> Result<QueryResponse> {
176 let request = spec.request(&origin, &token)?;
177 let (status, content_type, body) = tokio::time::timeout(QUERY_TIMEOUT, async move {
178 let socket = tokio::net::TcpStream::connect(&origin.connect_addr).await?;
179 let (mut sender, connection) = http1::Builder::new()
180 .max_headers(64)
181 .max_buf_size(32 * 1024)
182 .handshake(TokioIo::new(socket))
183 .await?;
184 let exchange = async {
185 let response = sender.send_request(request).await?;
186 let (parts, body) = response.into_parts();
187 ensure!(
188 parts
189 .headers
190 .get(CONTENT_ENCODING)
191 .map(|v| v.as_bytes() == b"identity")
192 .unwrap_or(true),
193 "compressed HTTP responses unsupported"
194 );
195 let content_type = parts
196 .headers
197 .get(CONTENT_TYPE)
198 .and_then(|v| v.to_str().ok())
199 .and_then(|v| v.split(';').next())
200 .map(|v| v.trim().to_ascii_lowercase())
201 .unwrap_or_default();
202 let bytes = Limited::new(body, MAX_QUERY_RESPONSE_BYTES)
203 .collect()
204 .await
205 .map_err(|_| {
206 anyhow::anyhow!(
207 "query answer passes {MAX_QUERY_RESPONSE_BYTES} bytes or is cut short"
208 )
209 })?
210 .to_bytes();
211 Ok::<_, anyhow::Error>((parts.status.as_u16(), content_type, bytes))
212 };
213 tokio::pin!(connection);
214 tokio::pin!(exchange);
215 tokio::select! {
216 result = &mut exchange => result,
217 result = &mut connection => {
218 result.context("HTTP driver failed")?;
219 exchange.await
220 }
221 }
222 })
223 .await
224 .context("InfluxDB query deadline exceeded")??;
225 let mut response = QueryResponse {
226 language: spec.language,
227 query: spec.query,
228 org: spec.org,
229 db: spec.db,
230 status,
231 tables: None,
232 statements: None,
233 row_count: 0,
234 reported_rows: 0,
235 truncated: false,
236 error: None,
237 };
238 if !(200..300).contains(&status) {
239 response.error = Some(error_body(&body));
240 return Ok(response);
241 }
242 if let Err(e) = read_answer(&mut response, spec.language, &content_type, &body) {
246 response.error = Some(json!({
247 "code": "unreadable_answer",
248 "message": crate::utils::truncate_for_log(&e.to_string(), MAX_ERROR_TEXT_BYTES),
249 }));
250 }
251 Ok(response)
252}
253
254fn read_answer(
255 response: &mut QueryResponse,
256 language: Language,
257 content_type: &str,
258 body: &[u8],
259) -> Result<()> {
260 let text = std::str::from_utf8(body).context("the answer is not UTF-8")?;
261 let mut budget = Budget::default();
262 match language {
263 Language::Flux => {
264 ensure!(
265 content_type.is_empty()
266 || content_type == "text/csv"
267 || content_type == "application/csv",
268 "a Flux answer must be CSV, not {content_type}"
269 );
270 let answer = read_flux(text)?;
271 response.tables = Some(budget.flux(answer));
272 }
273 Language::Influxql => {
274 let value: Value =
275 serde_json::from_str(text).context("the InfluxQL answer is not JSON")?;
276 response.statements = Some(budget.statements(&value)?);
277 }
278 }
279 budget.finish(response);
280 Ok(())
281}
282
283fn error_body(body: &[u8]) -> Value {
286 let text = String::from_utf8_lossy(body);
287 match serde_json::from_str::<Value>(&text) {
288 Ok(v) if v.get("message").is_some() => json!({
289 "code": v.get("code").map_or(Value::Null, bounded),
290 "message": bounded(&v["message"]),
291 }),
292 Ok(v) if v.get("error").is_some() => {
293 json!({"code": Value::Null, "message": bounded(&v["error"])})
294 }
295 _ => json!({
296 "code": Value::Null,
297 "message": crate::utils::truncate_for_log(text.trim(), MAX_ERROR_TEXT_BYTES),
298 }),
299 }
300}
301
302fn bounded(value: &Value) -> Value {
306 match value {
307 Value::String(s) => Value::String(crate::utils::truncate_for_log(s, MAX_ERROR_TEXT_BYTES)),
308 Value::Array(_) | Value::Object(_) => {
309 let text = value.to_string();
310 if text.len() <= MAX_ERROR_TEXT_BYTES {
311 value.clone()
312 } else {
313 Value::String(crate::utils::truncate_for_log(&text, MAX_ERROR_TEXT_BYTES))
314 }
315 }
316 _ => value.clone(),
317 }
318}
319
320#[derive(Default)]
321struct Budget {
322 rows: usize,
323 reported: usize,
324 tables: usize,
325 truncated: bool,
326}
327
328impl Budget {
329 fn take(&mut self, rows: Vec<Value>) -> Vec<Value> {
330 self.rows += rows.len();
331 let room = MAX_REPORTED_ROWS - self.reported;
332 if rows.len() > room {
333 self.truncated = true;
334 }
335 let kept: Vec<Value> = rows.into_iter().take(room).collect();
336 self.reported += kept.len();
337 kept
338 }
339
340 fn table_room(&mut self) -> bool {
341 self.tables += 1;
342 if self.tables > MAX_REPORTED_TABLES {
343 self.truncated = true;
344 return false;
345 }
346 true
347 }
348
349 fn flux(&mut self, answer: FluxAnswer) -> Vec<Value> {
351 let mut out = Vec::new();
352 for t in answer.tables {
353 if !self.table_room() {
354 self.rows += t.rows.len();
355 continue;
356 }
357 let rows = self.take(t.rows);
358 out.push(json!({
359 "result": t.result,
360 "table": t.table,
361 "columns": t.columns,
362 "rows": rows,
363 }));
364 }
365 self.rows = answer.row_count;
367 if answer.table_count > MAX_REPORTED_TABLES || answer.row_count > self.reported {
368 self.truncated = true;
369 }
370 out
371 }
372
373 fn statements(&mut self, value: &Value) -> Result<Vec<Value>> {
374 let results = value
375 .get("results")
376 .and_then(|r| r.as_array())
377 .context("InfluxQL answer has no 'results'")?;
378 let mut out = Vec::new();
379 for r in results {
380 let id = r.get("statement_id").cloned().unwrap_or(Value::Null);
381 if let Some(e) = r.get("error") {
382 out.push(json!({"statement_id": id, "error": bounded(e)}));
383 continue;
384 }
385 let mut series = Vec::new();
386 for s in r
387 .get("series")
388 .and_then(|s| s.as_array())
389 .into_iter()
390 .flatten()
391 {
392 let values = s
393 .get("values")
394 .and_then(|v| v.as_array())
395 .cloned()
396 .unwrap_or_default();
397 if !self.table_room() {
398 self.rows += values.len();
399 continue;
400 }
401 let columns = s
402 .get("columns")
403 .and_then(|c| c.as_array())
404 .context("a series has no columns")?;
405 ensure!(
406 columns.len() <= MAX_COLUMNS,
407 "a series has more than {MAX_COLUMNS} columns"
408 );
409 let mut entry = Map::new();
410 entry.insert("name".into(), s.get("name").cloned().unwrap_or(Value::Null));
411 if let Some(tags) = s.get("tags") {
412 entry.insert("tags".into(), tags.clone());
413 }
414 entry.insert("columns".into(), Value::Array(columns.clone()));
415 entry.insert("values".into(), Value::Array(self.take(values)));
416 series.push(Value::Object(entry));
417 }
418 out.push(json!({"statement_id": id, "series": series}));
419 }
420 Ok(out)
421 }
422
423 fn finish(self, response: &mut QueryResponse) {
424 response.row_count = self.rows;
425 response.reported_rows = self.reported;
426 response.truncated = self.truncated;
427 }
428}
429
430#[derive(Debug)]
432pub struct FluxTable {
433 pub result: String,
434 pub table: i64,
435 pub columns: Vec<Value>,
436 pub rows: Vec<Value>,
437}
438
439#[derive(Debug)]
442pub struct FluxAnswer {
443 pub tables: Vec<FluxTable>,
444 pub table_count: usize,
446 pub row_count: usize,
448}
449
450pub fn csv_records(text: &str) -> Result<Vec<Vec<String>>> {
453 CsvRecords::new(text, usize::MAX).collect()
454}
455
456pub struct CsvRecords<'a> {
460 chars: std::iter::Peekable<std::str::Chars<'a>>,
461 max_fields: usize,
462 done: bool,
463}
464
465impl<'a> CsvRecords<'a> {
466 pub fn new(text: &'a str, max_fields: usize) -> Self {
467 CsvRecords {
468 chars: text.chars().peekable(),
469 max_fields,
470 done: false,
471 }
472 }
473}
474
475impl Iterator for CsvRecords<'_> {
476 type Item = Result<Vec<String>>;
477
478 fn next(&mut self) -> Option<Self::Item> {
479 if self.done {
480 return None;
481 }
482 let mut record = Vec::new();
483 let mut field = String::new();
484 let mut quoted = false;
485 let mut empty_line = true;
486 while let Some(c) = self.chars.next() {
487 if quoted {
488 if c == '"' {
489 if self.chars.peek() == Some(&'"') {
490 field.push('"');
491 self.chars.next();
492 } else {
493 quoted = false;
494 }
495 } else {
496 field.push(c);
497 }
498 continue;
499 }
500 match c {
501 '"' => {
502 quoted = true;
503 empty_line = false;
504 }
505 ',' => {
506 record.push(std::mem::take(&mut field));
507 empty_line = false;
508 if record.len() >= self.max_fields {
510 self.done = true;
511 return Some(Err(anyhow::anyhow!(
512 "a CSV record has more than {} fields",
513 self.max_fields
514 )));
515 }
516 }
517 '\r' if self.chars.peek() == Some(&'\n') => {}
518 '\n' => {
519 if empty_line && record.is_empty() && field.is_empty() {
520 return Some(Ok(Vec::new()));
521 }
522 record.push(field);
523 return Some(Ok(record));
524 }
525 c => {
526 field.push(c);
527 empty_line = false;
528 }
529 }
530 }
531 self.done = true;
532 if quoted {
533 return Some(Err(anyhow::anyhow!("unterminated quoted CSV field")));
534 }
535 if !empty_line || !record.is_empty() || !field.is_empty() {
536 record.push(field);
537 return Some(Ok(record));
538 }
539 None
540 }
541}
542
543fn typed(text: &str, datatype: &str) -> Result<Value> {
544 Ok(match datatype {
545 "long" => json!(text
546 .parse::<i64>()
547 .with_context(|| format!("{text:?} is not a long"))?),
548 "unsignedLong" => json!(text
549 .parse::<u64>()
550 .with_context(|| format!("{text:?} is not an unsignedLong"))?),
551 "double" => match text {
552 "+Inf" | "Inf" => json!("+Inf"),
553 "-Inf" => json!("-Inf"),
554 "NaN" => json!("NaN"),
555 _ => json!(text
556 .parse::<f64>()
557 .with_context(|| format!("{text:?} is not a double"))?),
558 },
559 "boolean" => match text {
560 "true" => json!(true),
561 "false" => json!(false),
562 _ => bail!("{text:?} is not a boolean"),
563 },
564 _ => json!(text),
566 })
567}
568
569struct Header {
571 names: Vec<String>,
572 columns: Arc<Vec<Value>>,
575 generation: usize,
577 error_at: Option<usize>,
579}
580
581impl Header {
582 fn new(names: Vec<String>, datatypes: &[String], groups: &[String], generation: usize) -> Self {
583 let error_at = if names.iter().any(|n| n == "table") {
584 None
585 } else {
586 names.iter().position(|n| n == "error")
587 };
588 let columns = Arc::new(Self::columns(&names, datatypes, groups));
589 Header {
590 names,
591 columns,
592 generation,
593 error_at,
594 }
595 }
596
597 fn columns(names: &[String], datatypes: &[String], groups: &[String]) -> Vec<Value> {
598 names
599 .iter()
600 .enumerate()
601 .skip(1)
602 .filter(|(_, name)| *name != "result" && *name != "table")
603 .map(|(i, name)| {
604 json!({
605 "name": name,
606 "type": datatypes.get(i).map(String::as_str).unwrap_or("string"),
607 "group": groups.get(i).is_some_and(|g| g == "true"),
608 })
609 })
610 .collect()
611 }
612
613 fn last(&self, name: &str) -> Option<usize> {
615 self.names
616 .iter()
617 .enumerate()
618 .skip(1)
619 .filter(|(_, n)| *n == name)
620 .map(|(i, _)| i)
621 .last()
622 }
623}
624
625struct Current {
627 result: String,
628 table: i64,
629 generation: usize,
630 columns: Arc<Vec<Value>>,
631 kept: bool,
632}
633
634fn record_bytes(record: &[String]) -> usize {
635 record.iter().map(String::len).sum()
636}
637
638fn cell<'a>(record: &'a [String], defaults: &'a [String], i: usize) -> &'a str {
641 if record[i].is_empty() {
642 defaults.get(i).map_or("", String::as_str)
643 } else {
644 &record[i]
645 }
646}
647
648pub fn read_flux(text: &str) -> Result<FluxAnswer> {
656 let mut answer = FluxAnswer {
657 tables: Vec::new(),
658 table_count: 0,
659 row_count: 0,
660 };
661 let mut kept_rows = 0usize;
662 let mut datatypes: Vec<String> = Vec::new();
663 let mut groups: Vec<String> = Vec::new();
664 let mut defaults: Vec<String> = Vec::new();
665 let mut header: Option<Header> = None;
666 let mut generation = 0usize;
667 let mut current: Option<Current> = None;
668 for record in CsvRecords::new(text, MAX_COLUMNS + 1) {
669 let record = record?;
670 if record.is_empty() || record.iter().all(|f| f.is_empty()) {
671 header = None;
672 continue;
673 }
674 if matches!(record[0].as_str(), "#datatype" | "#group" | "#default") {
675 ensure!(
676 record_bytes(&record) <= MAX_HEADER_BYTES,
677 "a Flux annotation row passes {MAX_HEADER_BYTES} bytes"
678 );
679 }
680 match record[0].as_str() {
681 "#datatype" => {
682 header = None;
683 datatypes = record;
684 continue;
685 }
686 "#group" => {
687 groups = record;
688 if let Some(h) = header.as_mut() {
690 generation += 1;
691 h.columns = Arc::new(Header::columns(&h.names, &datatypes, &groups));
692 h.generation = generation;
693 }
694 continue;
695 }
696 "#default" => {
697 defaults = record;
698 continue;
699 }
700 _ => {}
701 }
702 let Some(h) = &header else {
703 ensure!(
704 record.len() <= MAX_COLUMNS + 1,
705 "more than {MAX_COLUMNS} columns"
706 );
707 ensure!(
708 record_bytes(&record) <= MAX_HEADER_BYTES,
709 "a Flux header passes {MAX_HEADER_BYTES} bytes of column names"
710 );
711 let reports_error =
712 record.iter().any(|n| n == "error") && !record.iter().any(|n| n == "table");
713 if !reports_error {
714 ensure!(
715 datatypes.len() == record.len(),
716 "a Flux header without a matching #datatype annotation"
717 );
718 }
719 generation += 1;
720 header = Some(Header::new(record, &datatypes, &groups, generation));
721 continue;
722 };
723 ensure!(
724 record.len() == h.names.len(),
725 "a Flux row has {} fields for {} columns",
726 record.len(),
727 h.names.len()
728 );
729 if let Some(at) = h.error_at {
730 bail!(
731 "the query failed: {}",
732 crate::utils::truncate_for_log(&record[at], MAX_ERROR_TEXT_BYTES)
733 );
734 }
735 let result = h
736 .last("result")
737 .map_or("_result", |i| cell(&record, &defaults, i));
738 let table: i64 = match h.last("table") {
739 Some(i) => cell(&record, &defaults, i)
740 .parse()
741 .context("the table column is not a long")?,
742 None => 0,
743 };
744
745 let same = current.as_ref().is_some_and(|c| {
746 c.table == table
747 && c.result == result
748 && (c.generation == h.generation || c.columns == h.columns)
749 });
750 if same {
751 if let Some(c) = current.as_mut() {
752 c.generation = h.generation;
753 }
754 } else {
755 answer.table_count += 1;
756 let kept = answer.tables.len() < MAX_REPORTED_TABLES;
757 if kept {
758 answer.tables.push(FluxTable {
759 result: result.to_string(),
760 table,
761 columns: h.columns.as_ref().clone(),
762 rows: Vec::new(),
763 });
764 }
765 current = Some(Current {
766 result: result.to_string(),
767 table,
768 generation: h.generation,
769 columns: h.columns.clone(),
770 kept,
771 });
772 }
773 answer.row_count += 1;
774 let keep = current.as_ref().is_some_and(|c| c.kept) && kept_rows < MAX_REPORTED_ROWS;
775
776 let mut row = Vec::new();
777 for (i, name) in h.names.iter().enumerate().skip(1) {
778 if name == "result" || name == "table" {
779 continue;
780 }
781 let datatype = datatypes.get(i).map(String::as_str).unwrap_or("string");
782 let text = cell(&record, &defaults, i);
783 let value = if text.is_empty() && datatype != "string" {
784 Value::Null
785 } else {
786 typed(text, datatype)?
787 };
788 if keep {
789 row.push(value);
790 }
791 }
792 if keep {
793 if let Some(t) = answer.tables.last_mut() {
794 t.rows.push(Value::Array(row));
795 kept_rows += 1;
796 }
797 }
798 }
799 Ok(answer)
800}