1use std::collections::HashMap;
16use std::sync::Arc;
17use std::time::{Duration, Instant};
18
19use axum::extract::rejection::FormRejection;
20use axum::extract::{Json, Query, State};
21use axum::response::sse::{Event, KeepAlive, Sse};
22use axum::response::{IntoResponse, Response};
23use axum::{Extension, Form};
24use common_catalog::parse_catalog_and_schema_from_db_string;
25use common_error::ext::ErrorExt;
26use common_error::status_code::StatusCode;
27use common_plugins::GREPTIME_EXEC_WRITE_COST;
28use common_query::{Output, OutputData};
29use common_recordbatch::{RecordBatch, SendableRecordBatchStream, util};
30use common_telemetry::tracing;
31use datafusion::physical_plan::ExecutionPlan;
32use datatypes::schema::SchemaRef;
33use futures::StreamExt;
34use query::parser::{DEFAULT_LOOKBACK_STRING, PromQuery};
35use serde::{Deserialize, Serialize};
36use serde_json::Value;
37use session::context::{Channel, QueryContext, QueryContextRef};
38use snafu::ResultExt;
39use sql::dialect::GreptimeDbDialect;
40use sql::parser::{ParseOptions, ParserContext};
41use sql::statements::statement::Statement;
42
43use crate::error::{FailedToParseQuerySnafu, InvalidQuerySnafu, Result};
44use crate::http::header::collect_plan_metrics;
45use crate::http::result::arrow_result::ArrowResponse;
46use crate::http::result::csv_result::CsvResponse;
47use crate::http::result::error_result::ErrorResponse;
48use crate::http::result::greptime_result_v1::GreptimedbV1Response;
49use crate::http::result::influxdb_result_v1::InfluxdbV1Response;
50use crate::http::result::json_result::JsonResponse;
51use crate::http::result::null_result::NullResponse;
52use crate::http::result::table_result::TableResponse;
53use crate::http::{
54 ApiState, Epoch, GreptimeOptionsConfigState, GreptimeQueryOutput, HttpRecordsOutput,
55 HttpResponse, ResponseFormat,
56};
57use crate::metrics_handler::MetricsHandler;
58use crate::query_handler::sql::ServerSqlQueryHandlerRef;
59
60#[derive(Debug, Default, Serialize, Deserialize)]
61pub struct SqlQuery {
62 pub db: Option<String>,
63 pub sql: Option<String>,
64 pub format: Option<String>,
68 pub epoch: Option<String>,
77 pub limit: Option<usize>,
78 pub compression: Option<String>,
80 pub snapshot_interval_ms: Option<u64>,
81}
82
83const DEFAULT_ANALYZE_SNAPSHOT_INTERVAL_MS: u64 = 5000;
84const MIN_ANALYZE_SNAPSHOT_INTERVAL_MS: u64 = 1000;
85const MAX_ANALYZE_SNAPSHOT_INTERVAL_MS: u64 = 60000;
86
87#[derive(Serialize)]
88struct AnalyzeStreamPayload {
89 seq: u64,
90 state: &'static str,
91 partial: bool,
92 elapsed_ms: u64,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 metrics: Option<Value>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 output: Option<GreptimeQueryOutput>,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 reason: Option<String>,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 code: Option<u32>,
101}
102
103struct AnalyzeStreamState {
104 stream: SendableRecordBatchStream,
105 schema: SchemaRef,
106 plan: Option<Arc<dyn ExecutionPlan>>,
107 batches: Vec<RecordBatch>,
108 seq: u64,
109 start: Instant,
110 requested_interval_ms: u64,
111 current_interval_ms: u64,
112 done: bool,
113}
114
115#[axum_macros::debug_handler]
117#[tracing::instrument(skip_all, fields(protocol = "http", request_type = "sql"))]
118pub async fn sql(
119 State(state): State<ApiState>,
120 Query(query_params): Query<SqlQuery>,
121 Extension(mut query_ctx): Extension<QueryContext>,
122 Form(form_params): Form<SqlQuery>,
123) -> HttpResponse {
124 let start = Instant::now();
125 let sql_handler = &state.sql_handler;
126 if let Some(db) = &query_params.db.or(form_params.db) {
127 let (catalog, schema) = parse_catalog_and_schema_from_db_string(db);
128 query_ctx.set_current_catalog(&catalog);
129 query_ctx.set_current_schema(&schema);
130 }
131 let db = query_ctx.get_db_string();
132
133 query_ctx.set_channel(Channel::HttpSql);
134 let query_ctx = Arc::new(query_ctx);
135
136 let _timer = crate::metrics::METRIC_HTTP_SQL_ELAPSED
137 .with_label_values(&[db.as_str()])
138 .start_timer();
139
140 let sql = query_params.sql.or(form_params.sql);
141 let format = query_params
142 .format
143 .or(form_params.format)
144 .map(|s| s.to_lowercase())
145 .map(|s| ResponseFormat::parse(s.as_str()).unwrap_or(ResponseFormat::GreptimedbV1))
146 .unwrap_or(ResponseFormat::GreptimedbV1);
147 let epoch = query_params
148 .epoch
149 .or(form_params.epoch)
150 .map(|s| s.to_lowercase())
151 .map(|s| Epoch::parse(s.as_str()).unwrap_or(Epoch::Millisecond));
152
153 let result = if let Some(sql) = &sql {
154 if let Some((status, msg)) = validate_schema(sql_handler.clone(), query_ctx.clone()).await {
155 Err((status, msg))
156 } else {
157 Ok(sql_handler.do_query(sql, query_ctx.clone()).await)
158 }
159 } else {
160 Err((
161 StatusCode::InvalidArguments,
162 "sql parameter is required.".to_string(),
163 ))
164 };
165
166 let outputs = match result {
167 Err((status, msg)) => {
168 return HttpResponse::Error(
169 ErrorResponse::from_error_message(status, msg)
170 .with_execution_time(start.elapsed().as_millis() as u64),
171 );
172 }
173 Ok(outputs) => outputs,
174 };
175
176 let mut resp = match format {
177 ResponseFormat::Arrow => {
178 ArrowResponse::from_output(outputs, query_params.compression).await
179 }
180 ResponseFormat::Csv(with_names, with_types) => {
181 CsvResponse::from_output(outputs, with_names, with_types).await
182 }
183 ResponseFormat::Table => TableResponse::from_output(outputs).await,
184 ResponseFormat::GreptimedbV1 => GreptimedbV1Response::from_output(outputs).await,
185 ResponseFormat::InfluxdbV1 => InfluxdbV1Response::from_output(outputs, epoch).await,
186 ResponseFormat::Json => JsonResponse::from_output(outputs).await,
187 ResponseFormat::Null => NullResponse::from_output(outputs).await,
188 };
189
190 if let Some(limit) = query_params.limit {
191 resp = resp.with_limit(limit);
192 }
193 resp.with_execution_time(start.elapsed().as_millis() as u64)
194}
195
196#[axum_macros::debug_handler]
205#[tracing::instrument(
206 skip_all,
207 fields(protocol = "http", request_type = "sql_analyze_stream")
208)]
209pub async fn sql_analyze_stream(
210 State(state): State<ApiState>,
211 Query(query_params): Query<SqlQuery>,
212 Extension(mut query_ctx): Extension<QueryContext>,
213 form_params: std::result::Result<Form<SqlQuery>, FormRejection>,
214) -> Response {
215 let start = Instant::now();
216 let form_params = match form_params {
217 Ok(Form(params)) => params,
218 Err(err) => {
219 if err.status() != axum::http::StatusCode::UNSUPPORTED_MEDIA_TYPE {
220 return ErrorResponse::from_error_message(
221 StatusCode::InvalidArguments,
222 err.body_text(),
223 )
224 .with_execution_time(start.elapsed().as_millis() as u64)
225 .into_response();
226 }
227 SqlQuery::default()
228 }
229 };
230 let sql_handler = &state.sql_handler;
231 if let Some(db) = &query_params.db.or(form_params.db) {
232 let (catalog, schema) = parse_catalog_and_schema_from_db_string(db);
233 query_ctx.set_current_catalog(&catalog);
234 query_ctx.set_current_schema(&schema);
235 }
236 query_ctx.set_channel(Channel::HttpSql);
237 query_ctx.enable_live_analyze_metrics();
238 let query_ctx = Arc::new(query_ctx);
239
240 let Some(sql) = query_params.sql.or(form_params.sql) else {
241 return ErrorResponse::from_error_message(
242 StatusCode::InvalidArguments,
243 "sql parameter is required.".to_string(),
244 )
245 .with_execution_time(start.elapsed().as_millis() as u64)
246 .into_response();
247 };
248 if let Some((status, msg)) = validate_schema(sql_handler.clone(), query_ctx.clone()).await {
249 return ErrorResponse::from_error_message(status, msg)
250 .with_execution_time(start.elapsed().as_millis() as u64)
251 .into_response();
252 }
253
254 let interval_ms = query_params
255 .snapshot_interval_ms
256 .or(form_params.snapshot_interval_ms)
257 .unwrap_or(DEFAULT_ANALYZE_SNAPSHOT_INTERVAL_MS)
258 .clamp(
259 MIN_ANALYZE_SNAPSHOT_INTERVAL_MS,
260 MAX_ANALYZE_SNAPSHOT_INTERVAL_MS,
261 );
262
263 let output = match state
264 .sql_handler
265 .do_analyze_stream_query(&sql, query_ctx.clone())
266 .await
267 {
268 Ok(output) => output,
269 Err(err) => {
270 return ErrorResponse::from_error(err)
271 .with_execution_time(start.elapsed().as_millis() as u64)
272 .into_response();
273 }
274 };
275
276 let plan = output.meta.plan.clone();
277 let OutputData::Stream(stream) = output.data else {
278 return ErrorResponse::from_error_message(
279 StatusCode::InvalidArguments,
280 "analyze stream query must return a stream".to_string(),
281 )
282 .with_execution_time(start.elapsed().as_millis() as u64)
283 .into_response();
284 };
285 let schema = stream.schema();
286
287 let sse_stream = futures::stream::unfold(
288 AnalyzeStreamState {
289 stream,
290 schema,
291 plan,
292 batches: Vec::new(),
293 seq: 0,
294 start,
295 requested_interval_ms: interval_ms,
296 current_interval_ms: interval_ms,
297 done: false,
298 },
299 |mut state| async move {
300 if state.done {
301 return None;
302 }
303 let tick = tokio::time::sleep(Duration::from_millis(state.current_interval_ms));
304 tokio::pin!(tick);
305 loop {
306 tokio::select! {
307 item = state.stream.next() => {
308 match item {
309 Some(Ok(batch)) => state.batches.push(batch),
310 Some(Err(err)) => {
311 let status = err.status_code();
312 let event_name = if status == StatusCode::Cancelled { "canceled" } else { "error" };
313 let (payload, _) = make_analyze_payload(AnalyzePayloadArgs {
314 seq: state.seq,
315 state: event_name,
316 partial: false,
317 start: state.start,
318 plan: state.plan.as_ref(),
319 output: None,
320 reason: Some(err.output_msg()),
321 code: Some(status as u32),
322 });
323 state.seq += 1;
324 state.done = true;
325 return Some((Ok::<Event, std::convert::Infallible>(Event::default().event(event_name).data(payload)), state));
326 }
327 None => {
328 let batches = std::mem::take(&mut state.batches);
329 let output = HttpRecordsOutput::try_new(state.schema.clone(), batches)
330 .map(GreptimeQueryOutput::Records);
331 let (event_name, payload) = make_final_analyze_event(
332 output.map_err(|err| (err.output_msg(), err.status_code() as u32)),
333 state.seq,
334 state.start,
335 state.plan.as_ref(),
336 );
337 state.seq += 1;
338 state.done = true;
339 return Some((Ok::<Event, std::convert::Infallible>(Event::default().event(event_name).data(payload)), state));
340 }
341 }
342 }
343 _ = &mut tick => {
344 if state.plan.is_some() {
345 let (payload, payload_bytes) = make_analyze_payload(AnalyzePayloadArgs {
346 seq: state.seq,
347 state: "metrics",
348 partial: true,
349 start: state.start,
350 plan: state.plan.as_ref(),
351 output: None,
352 reason: None,
353 code: None,
354 });
355 state.current_interval_ms = adaptive_interval_ms(payload_bytes, state.requested_interval_ms);
356 state.seq += 1;
357 return Some((Ok::<Event, std::convert::Infallible>(Event::default().event("metrics").data(payload)), state));
358 }
359 tick.as_mut().reset(tokio::time::Instant::now() + Duration::from_millis(state.current_interval_ms));
360 }
361 }
362 }
363 },
364 );
365
366 Sse::new(sse_stream)
367 .keep_alive(KeepAlive::new().interval(Duration::from_secs(15)))
368 .into_response()
369}
370
371fn adaptive_interval_ms(payload_bytes: usize, requested_ms: u64) -> u64 {
372 if payload_bytes >= 10 * 1024 * 1024 {
373 requested_ms.max(30_000)
374 } else if payload_bytes >= 1024 * 1024 {
375 requested_ms.max(10_000)
376 } else {
377 requested_ms
378 }
379}
380
381fn make_final_analyze_event(
382 output: std::result::Result<GreptimeQueryOutput, (String, u32)>,
383 seq: u64,
384 start: Instant,
385 plan: Option<&Arc<dyn ExecutionPlan>>,
386) -> (&'static str, String) {
387 match output {
388 Ok(output) => (
389 "final",
390 make_analyze_payload(AnalyzePayloadArgs {
391 seq,
392 state: "final",
393 partial: false,
394 start,
395 plan,
396 output: Some(output),
397 reason: None,
398 code: None,
399 })
400 .0,
401 ),
402 Err((reason, code)) => (
403 "error",
404 make_analyze_payload(AnalyzePayloadArgs {
405 seq,
406 state: "error",
407 partial: false,
408 start,
409 plan,
410 output: None,
411 reason: Some(reason),
412 code: Some(code),
413 })
414 .0,
415 ),
416 }
417}
418
419struct AnalyzePayloadArgs<'a> {
420 seq: u64,
421 state: &'static str,
422 partial: bool,
423 start: Instant,
424 plan: Option<&'a Arc<dyn ExecutionPlan>>,
425 output: Option<GreptimeQueryOutput>,
426 reason: Option<String>,
427 code: Option<u32>,
428}
429
430fn make_analyze_payload(args: AnalyzePayloadArgs<'_>) -> (String, usize) {
431 let AnalyzePayloadArgs {
432 seq,
433 state,
434 partial,
435 start,
436 plan,
437 output,
438 reason,
439 code,
440 } = args;
441 let metrics = plan.and_then(|plan| query::analyze_plan_metrics_to_json_value(plan, true).ok());
442 let payload = AnalyzeStreamPayload {
443 seq,
444 state,
445 partial,
446 elapsed_ms: start.elapsed().as_millis() as u64,
447 metrics,
448 output,
449 reason,
450 code,
451 };
452 let payload_string = serde_json::to_string(&payload).unwrap_or_else(|e| {
453 serde_json::json!({
454 "seq": seq,
455 "state": "error",
456 "partial": false,
457 "reason": format!("Failed to serialize SSE payload: {e}"),
458 })
459 .to_string()
460 });
461 let payload_bytes = payload_string.len();
462 (payload_string, payload_bytes)
463}
464
465#[axum_macros::debug_handler]
467#[tracing::instrument(skip_all, fields(protocol = "http", request_type = "sql"))]
468pub async fn sql_parse(
469 Query(query_params): Query<SqlQuery>,
470 Form(form_params): Form<SqlQuery>,
471) -> Result<Json<Vec<Statement>>> {
472 let Some(sql) = query_params.sql.or(form_params.sql) else {
473 return InvalidQuerySnafu {
474 reason: "sql parameter is required.",
475 }
476 .fail();
477 };
478
479 let stmts =
480 ParserContext::create_with_dialect(&sql, &GreptimeDbDialect {}, ParseOptions::default())
481 .context(FailedToParseQuerySnafu)?;
482
483 Ok(stmts.into())
484}
485
486#[derive(Debug, Serialize, Deserialize)]
487pub struct SqlFormatResponse {
488 pub formatted: String,
489}
490
491#[axum_macros::debug_handler]
493#[tracing::instrument(skip_all, fields(protocol = "http", request_type = "sql_format"))]
494pub async fn sql_format(
495 Query(query_params): Query<SqlQuery>,
496 Form(form_params): Form<SqlQuery>,
497) -> axum::response::Response {
498 let Some(sql) = query_params.sql.or(form_params.sql) else {
499 let resp = ErrorResponse::from_error_message(
500 StatusCode::InvalidArguments,
501 "sql parameter is required.".to_string(),
502 );
503 return HttpResponse::Error(resp).into_response();
504 };
505
506 let stmts = match ParserContext::create_with_dialect(
508 &sql,
509 &GreptimeDbDialect {},
510 ParseOptions::default(),
511 ) {
512 Ok(v) => v,
513 Err(e) => return HttpResponse::Error(ErrorResponse::from_error(e)).into_response(),
514 };
515
516 let mut parts: Vec<String> = Vec::with_capacity(stmts.len());
517 for stmt in stmts {
518 let mut s = format!("{stmt}");
519 if !s.trim_end().ends_with(';') {
520 s.push(';');
521 }
522 parts.push(s);
523 }
524
525 let formatted = parts.join("\n");
526 Json(SqlFormatResponse { formatted }).into_response()
527}
528
529pub async fn from_output(
531 outputs: Vec<crate::error::Result<Output>>,
532) -> std::result::Result<(Vec<GreptimeQueryOutput>, HashMap<String, Value>), ErrorResponse> {
533 let mut results = Vec::with_capacity(outputs.len());
536 let mut merge_map = HashMap::new();
537
538 for out in outputs {
539 match out {
540 Ok(o) => match o.data {
541 OutputData::AffectedRows(rows) => {
542 results.push(GreptimeQueryOutput::AffectedRows(rows));
543 if o.meta.cost > 0 {
544 merge_map.insert(GREPTIME_EXEC_WRITE_COST.to_string(), o.meta.cost as u64);
545 }
546 }
547 OutputData::Stream(stream) => {
548 let schema = stream.schema().clone();
549 let mut http_record_output = match util::collect(stream).await {
551 Ok(rows) => match HttpRecordsOutput::try_new(schema, rows) {
552 Ok(rows) => rows,
553 Err(err) => {
554 return Err(ErrorResponse::from_error(err));
555 }
556 },
557 Err(err) => {
558 return Err(ErrorResponse::from_error(err));
559 }
560 };
561 if let Some(physical_plan) = o.meta.plan {
562 let mut result_map = HashMap::new();
563
564 let mut tmp = vec![&mut merge_map, &mut result_map];
565 collect_plan_metrics(&physical_plan, &mut tmp);
566 let re = result_map
567 .into_iter()
568 .map(|(k, v)| (k, Value::from(v)))
569 .collect::<HashMap<String, Value>>();
570 http_record_output.metrics.extend(re);
571 }
572 results.push(GreptimeQueryOutput::Records(http_record_output))
573 }
574 OutputData::RecordBatches(rbs) => {
575 match HttpRecordsOutput::try_new(rbs.schema(), rbs.take()) {
576 Ok(rows) => {
577 results.push(GreptimeQueryOutput::Records(rows));
578 }
579 Err(err) => {
580 return Err(ErrorResponse::from_error(err));
581 }
582 }
583 }
584 },
585
586 Err(err) => {
587 return Err(ErrorResponse::from_error(err));
588 }
589 }
590 }
591
592 let merge_map = merge_map
593 .into_iter()
594 .map(|(k, v)| (k, Value::from(v)))
595 .collect();
596
597 Ok((results, merge_map))
598}
599
600#[derive(Debug, Default, Serialize, Deserialize)]
601pub struct PromqlQuery {
602 pub query: String,
603 pub start: String,
604 pub end: String,
605 pub step: String,
606 pub lookback: Option<String>,
607 pub db: Option<String>,
608 pub format: Option<String>,
612 pub compression: Option<String>,
614 pub epoch: Option<String>,
625}
626
627impl From<PromqlQuery> for PromQuery {
628 fn from(query: PromqlQuery) -> Self {
629 PromQuery {
630 query: query.query,
631 start: query.start,
632 end: query.end,
633 step: query.step,
634 lookback: query
635 .lookback
636 .unwrap_or_else(|| DEFAULT_LOOKBACK_STRING.to_string()),
637 alias: None,
639 }
640 }
641}
642
643#[axum_macros::debug_handler]
645#[tracing::instrument(skip_all, fields(protocol = "http", request_type = "promql"))]
646pub async fn promql(
647 State(state): State<ApiState>,
648 Query(params): Query<PromqlQuery>,
649 Extension(mut query_ctx): Extension<QueryContext>,
650) -> Response {
651 let sql_handler = &state.sql_handler;
652 let exec_start = Instant::now();
653 let db = query_ctx.get_db_string();
654
655 query_ctx.set_channel(Channel::Promql);
656 let query_ctx = Arc::new(query_ctx);
657
658 let _timer = crate::metrics::METRIC_HTTP_PROMQL_ELAPSED
659 .with_label_values(&[db.as_str()])
660 .start_timer();
661
662 let resp = if let Some((status, msg)) =
663 validate_schema(sql_handler.clone(), query_ctx.clone()).await
664 {
665 let resp = ErrorResponse::from_error_message(status, msg);
666 HttpResponse::Error(resp)
667 } else {
668 let format = params
669 .format
670 .as_ref()
671 .map(|s| s.to_lowercase())
672 .map(|s| ResponseFormat::parse(s.as_str()).unwrap_or(ResponseFormat::GreptimedbV1))
673 .unwrap_or(ResponseFormat::GreptimedbV1);
674 let epoch = params
675 .epoch
676 .as_ref()
677 .map(|s| s.to_lowercase())
678 .map(|s| Epoch::parse(s.as_str()).unwrap_or(Epoch::Millisecond));
679 let compression = params.compression.clone();
680
681 let prom_query = params.into();
682 let outputs = sql_handler.do_promql_query(&prom_query, query_ctx).await;
683
684 match format {
685 ResponseFormat::Arrow => ArrowResponse::from_output(outputs, compression).await,
686 ResponseFormat::Csv(with_names, with_types) => {
687 CsvResponse::from_output(outputs, with_names, with_types).await
688 }
689 ResponseFormat::Table => TableResponse::from_output(outputs).await,
690 ResponseFormat::GreptimedbV1 => GreptimedbV1Response::from_output(outputs).await,
691 ResponseFormat::InfluxdbV1 => InfluxdbV1Response::from_output(outputs, epoch).await,
692 ResponseFormat::Json => JsonResponse::from_output(outputs).await,
693 ResponseFormat::Null => NullResponse::from_output(outputs).await,
694 }
695 };
696
697 resp.with_execution_time(exec_start.elapsed().as_millis() as u64)
698 .into_response()
699}
700
701#[axum_macros::debug_handler]
703pub async fn metrics(
704 State(state): State<MetricsHandler>,
705 Query(_params): Query<HashMap<String, String>>,
706) -> String {
707 #[cfg(not(windows))]
712 if let Some(c) = crate::metrics::jemalloc::JEMALLOC_COLLECTOR.as_ref()
713 && let Err(e) = c.update()
714 {
715 common_telemetry::error!(e; "Failed to update jemalloc metrics");
716 }
717 state.render()
718}
719
720#[derive(Debug, Serialize, Deserialize)]
721pub struct HealthQuery {}
722
723#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
724pub struct HealthResponse {}
725
726#[axum_macros::debug_handler]
730pub async fn health(Query(_params): Query<HealthQuery>) -> Json<HealthResponse> {
731 Json(HealthResponse {})
732}
733
734#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
735pub struct StatusResponse<'a> {
736 pub commit: &'a str,
737 pub branch: &'a str,
738 pub rustc_version: &'a str,
739 pub hostname: String,
740 pub version: &'a str,
741}
742
743#[axum_macros::debug_handler]
745pub async fn status() -> Json<StatusResponse<'static>> {
746 let hostname = hostname::get()
747 .map(|s| s.to_string_lossy().to_string())
748 .unwrap_or_else(|_| "unknown".to_string());
749 let build_info = common_version::build_info();
750 Json(StatusResponse {
751 commit: build_info.commit,
752 branch: build_info.branch,
753 rustc_version: build_info.rustc,
754 hostname,
755 version: build_info.version,
756 })
757}
758
759#[axum_macros::debug_handler]
761pub async fn config(State(state): State<GreptimeOptionsConfigState>) -> Response {
762 (axum::http::StatusCode::OK, state.greptime_config_options).into_response()
763}
764
765async fn validate_schema(
766 sql_handler: ServerSqlQueryHandlerRef,
767 query_ctx: QueryContextRef,
768) -> Option<(StatusCode, String)> {
769 match sql_handler
770 .is_valid_schema(query_ctx.current_catalog(), &query_ctx.current_schema())
771 .await
772 {
773 Ok(true) => None,
774 Ok(false) => Some((
775 StatusCode::DatabaseNotFound,
776 format!("Database not found: {}", query_ctx.get_db_string()),
777 )),
778 Err(e) => Some((
779 StatusCode::Internal,
780 format!(
781 "Error checking database: {}, {}",
782 query_ctx.get_db_string(),
783 e.output_msg(),
784 ),
785 )),
786 }
787}
788
789pub async fn index() -> axum::response::Html<String> {
790 let name = common_version::product_name();
791 let version = common_version::version();
792 axum::response::Html(format!(
793 r#"<!DOCTYPE html>
794<html>
795<head><title>{name}</title></head>
796<body>
797<h1>{name}</h1>
798<p>Version: {version}</p>
799<ul>
800<li><a href="/dashboard">Dashboard UI</a></li>
801<li><a href="/v1/health">Health</a> (JSON)</li>
802<li><a href="/status">Status</a> (JSON)</li>
803<li><a href="/metrics">Metrics</a> (For Prometheus Scrape)</li>
804<li><a href="/config">Config</a> (TXT)</li>
805</ul>
806</body>
807</html>"#,
808 ))
809}
810
811#[cfg(test)]
812mod tests {
813 use super::*;
814
815 #[test]
816 fn test_final_analyze_event_uses_error_event_for_conversion_error() {
817 let (event_name, payload) = make_final_analyze_event(
818 Err((
819 "conversion failed".to_string(),
820 StatusCode::InvalidArguments as u32,
821 )),
822 7,
823 Instant::now(),
824 None,
825 );
826
827 assert_eq!(event_name, "error");
828 let value: Value = serde_json::from_str(&payload).unwrap();
829 assert_eq!(value["state"], "error");
830 assert_eq!(value["reason"], "conversion failed");
831 }
832}