1use super::{
2 audience_guard::{AudienceGuard, Authorized, IdKind},
3 lakehouse_context::LakehouseContext,
4 partition_cache::QueryPartitionProvider,
5 process_streams::get_process_thread_list,
6 session_configurator::NoOpSessionConfigurator,
7 view_factory::ViewFactory,
8};
9use crate::{dfext::expressions::exp_to_string, span_table::get_spans_schema, time::TimeRange};
10use async_stream::try_stream;
11use datafusion::{
12 arrow::{
13 array::{ArrayRef, RecordBatch, StringDictionaryBuilder},
14 datatypes::{DataType, Field, Int16Type, Schema, SchemaRef},
15 },
16 catalog::{Session, TableFunctionArgs, TableFunctionImpl, TableProvider},
17 common::{Result as DFResult, plan_err},
18 error::DataFusionError,
19 execution::{SendableRecordBatchStream, TaskContext},
20 logical_expr::{Expr, TableType},
21 physical_expr::EquivalenceProperties,
22 physical_plan::{
23 DisplayAs, DisplayFormatType, ExecutionPlan, Partitioning, PlanProperties,
24 execution_plan::{Boundedness, EmissionType},
25 limit::GlobalLimitExec,
26 projection::ProjectionExec,
27 stream::RecordBatchStreamAdapter,
28 },
29};
30use futures::{StreamExt, TryStreamExt};
31use micromegas_tracing::prelude::*;
32use std::{
33 fmt::{self, Debug, Formatter},
34 sync::Arc,
35};
36use uuid::Uuid;
37
38#[derive(Debug, Clone, Copy)]
40pub enum SpanTypes {
41 Thread,
42 Async,
43 Both,
44}
45
46fn output_schema() -> SchemaRef {
47 let mut fields = vec![
48 Field::new(
49 "stream_id",
50 DataType::Dictionary(Box::new(DataType::Int16), Box::new(DataType::Utf8)),
51 false,
52 ),
53 Field::new(
54 "thread_name",
55 DataType::Dictionary(Box::new(DataType::Int16), Box::new(DataType::Utf8)),
56 false,
57 ),
58 ];
59 fields.extend(get_spans_schema().fields.iter().map(|f| f.as_ref().clone()));
60 Arc::new(Schema::new(fields))
61}
62
63fn augment_batch(
64 batch: &RecordBatch,
65 schema: SchemaRef,
66 stream_id: &str,
67 thread_name: &str,
68) -> DFResult<RecordBatch> {
69 let n = batch.num_rows();
70 let mut stream_id_builder = StringDictionaryBuilder::<Int16Type>::new();
71 let mut thread_name_builder = StringDictionaryBuilder::<Int16Type>::new();
72 stream_id_builder.append_values(stream_id, n);
73 thread_name_builder.append_values(thread_name, n);
74 let mut columns: Vec<ArrayRef> = vec![
75 Arc::new(stream_id_builder.finish()),
76 Arc::new(thread_name_builder.finish()),
77 ];
78 columns.extend(batch.columns().iter().cloned());
79 RecordBatch::try_new(schema, columns).map_err(Into::into)
80}
81
82#[derive(Debug)]
85pub struct ProcessSpansTableFunction {
86 lakehouse: Arc<LakehouseContext>,
87 view_factory: Arc<ViewFactory>,
88 part_provider: Arc<dyn QueryPartitionProvider>,
89 query_range: Option<TimeRange>,
90 guard: Arc<AudienceGuard>,
91}
92
93impl ProcessSpansTableFunction {
94 pub fn new(
95 lakehouse: Arc<LakehouseContext>,
96 view_factory: Arc<ViewFactory>,
97 part_provider: Arc<dyn QueryPartitionProvider>,
98 query_range: Option<TimeRange>,
99 guard: Arc<AudienceGuard>,
100 ) -> Self {
101 Self {
102 lakehouse,
103 view_factory,
104 part_provider,
105 query_range,
106 guard,
107 }
108 }
109}
110
111impl TableFunctionImpl for ProcessSpansTableFunction {
112 #[span_fn]
113 fn call_with_args(
114 &self,
115 args: TableFunctionArgs,
116 ) -> datafusion::error::Result<Arc<dyn TableProvider>> {
117 let exprs = args.exprs();
118 let arg1 = exprs.first().map(exp_to_string);
119 let Some(Ok(process_id_arg)) = arg1 else {
120 return plan_err!(
121 "First argument to process_spans must be a string (the process ID), given {:?}",
122 arg1
123 );
124 };
125 let Ok(process_uuid) = Uuid::parse_str(&process_id_arg) else {
130 return plan_err!(
131 "First argument to process_spans must be a valid UUID (the process ID), given '{process_id_arg}'"
132 );
133 };
134 let process_id = process_uuid.hyphenated().to_string();
135
136 let arg2 = exprs.get(1).map(exp_to_string);
137 let Some(Ok(span_types_str)) = arg2 else {
138 return plan_err!(
139 "Second argument to process_spans must be a string ('thread', 'async', or 'both'), given {:?}",
140 arg2
141 );
142 };
143
144 let span_types = match span_types_str.as_str() {
145 "thread" => SpanTypes::Thread,
146 "async" => SpanTypes::Async,
147 "both" => SpanTypes::Both,
148 _ => {
149 return plan_err!(
150 "span_types must be 'thread', 'async', or 'both', given: {span_types_str}"
151 );
152 }
153 };
154
155 let schema = output_schema();
156 let execution_plan = Arc::new(ProcessSpansExecutionPlan::new(
157 schema,
158 process_id,
159 process_uuid,
160 span_types,
161 self.query_range,
162 self.lakehouse.clone(),
163 self.view_factory.clone(),
164 self.part_provider.clone(),
165 self.guard.clone(),
166 ));
167
168 Ok(Arc::new(ProcessSpansTableProvider { execution_plan }))
169 }
170}
171
172pub struct ProcessSpansExecutionPlan {
175 schema: SchemaRef,
176 process_id: String,
177 process_uuid: Uuid,
178 span_types: SpanTypes,
179 query_range: Option<TimeRange>,
180 lakehouse: Arc<LakehouseContext>,
181 view_factory: Arc<ViewFactory>,
182 part_provider: Arc<dyn QueryPartitionProvider>,
183 guard: Arc<AudienceGuard>,
184 authorized: Option<Authorized>,
189 properties: Arc<PlanProperties>,
190}
191
192impl ProcessSpansExecutionPlan {
193 #[allow(clippy::too_many_arguments)]
194 fn new(
195 schema: SchemaRef,
196 process_id: String,
197 process_uuid: Uuid,
198 span_types: SpanTypes,
199 query_range: Option<TimeRange>,
200 lakehouse: Arc<LakehouseContext>,
201 view_factory: Arc<ViewFactory>,
202 part_provider: Arc<dyn QueryPartitionProvider>,
203 guard: Arc<AudienceGuard>,
204 ) -> Self {
205 let properties = PlanProperties::new(
206 EquivalenceProperties::new(schema.clone()),
207 Partitioning::UnknownPartitioning(1),
208 EmissionType::Final,
209 Boundedness::Bounded,
210 );
211 Self {
212 schema,
213 process_id,
214 process_uuid,
215 span_types,
216 query_range,
217 lakehouse,
218 view_factory,
219 part_provider,
220 guard,
221 authorized: None,
222 properties: Arc::new(properties),
223 }
224 }
225
226 fn with_authorized(&self, authorized: Authorized) -> Self {
231 Self {
232 schema: self.schema.clone(),
233 process_id: self.process_id.clone(),
234 process_uuid: self.process_uuid,
235 span_types: self.span_types,
236 query_range: self.query_range,
237 lakehouse: self.lakehouse.clone(),
238 view_factory: self.view_factory.clone(),
239 part_provider: self.part_provider.clone(),
240 guard: self.guard.clone(),
241 authorized: Some(authorized),
242 properties: self.properties.clone(),
243 }
244 }
245}
246
247impl Debug for ProcessSpansExecutionPlan {
248 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
249 f.debug_struct("ProcessSpansExecutionPlan")
250 .field("process_id", &self.process_id)
251 .field("span_types", &self.span_types)
252 .finish()
253 }
254}
255
256impl DisplayAs for ProcessSpansExecutionPlan {
257 fn fmt_as(&self, _t: DisplayFormatType, f: &mut fmt::Formatter) -> fmt::Result {
258 write!(
259 f,
260 "ProcessSpansExecutionPlan: process_id={}, span_types={:?}",
261 self.process_id, self.span_types
262 )
263 }
264}
265
266impl ExecutionPlan for ProcessSpansExecutionPlan {
267 fn name(&self) -> &str {
268 "ProcessSpansExecutionPlan"
269 }
270
271 fn schema(&self) -> SchemaRef {
272 self.schema.clone()
273 }
274
275 fn properties(&self) -> &Arc<PlanProperties> {
276 &self.properties
277 }
278
279 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
280 vec![]
281 }
282
283 fn with_new_children(
284 self: Arc<Self>,
285 _children: Vec<Arc<dyn ExecutionPlan>>,
286 ) -> DFResult<Arc<dyn ExecutionPlan>> {
287 Ok(self)
288 }
289
290 #[span_fn]
291 fn execute(
292 &self,
293 _partition: usize,
294 _context: Arc<TaskContext>,
295 ) -> DFResult<SendableRecordBatchStream> {
296 let schema = self.schema.clone();
297 let stream_schema = schema.clone();
298 let process_id = self.process_id.clone();
299 let span_types = self.span_types;
300 let query_range = self.query_range;
301 let lakehouse = self.lakehouse.clone();
302 let view_factory = self.view_factory.clone();
303 let part_provider = self.part_provider.clone();
304 let Some(authorized) = &self.authorized else {
307 return Err(DataFusionError::Internal(
308 "process_spans: unauthorized plan (no witness from scan)".into(),
309 ));
310 };
311 let caller = authorized.internal_caller();
312
313 let record_batch_stream = try_stream! {
314 let schema = stream_schema;
315 let ctx = super::query::make_session_context(
321 lakehouse,
322 part_provider,
323 query_range,
324 view_factory,
325 Arc::new(NoOpSessionConfigurator),
326 caller,
327 )
328 .await
329 .map_err(|e| datafusion::error::DataFusionError::Internal(
330 format!("Failed to create session context: {e}"),
331 ))?;
332
333 if matches!(span_types, SpanTypes::Thread | SpanTypes::Both) {
335 let threads = get_process_thread_list(&process_id, &ctx)
336 .await
337 .map_err(|e| datafusion::error::DataFusionError::Internal(
338 format!("Failed to get thread list: {e}"),
339 ))?;
340
341 let max_concurrent = std::thread::available_parallelism()
342 .map(|n| n.get())
343 .unwrap_or(4);
344
345 let queries: Vec<(String, String, String)> = threads
346 .iter()
347 .map(|(stream_id, _thread_id, display_name)| {
348 let sql = format!(
349 "SELECT * FROM view_instance('thread_spans', '{stream_id}')"
350 );
351 (stream_id.clone(), display_name.clone(), sql)
352 })
353 .collect();
354
355 let stream_results: Vec<(String, String, SendableRecordBatchStream)> =
356 futures::stream::iter(queries)
357 .map(|(stream_id, thread_name, sql)| {
358 let ctx = ctx.clone();
359 async move {
360 spawn_with_context(async move {
361 let df = ctx.sql(&sql).await?;
362 let s = df.execute_stream().await?;
363 Ok::<_, anyhow::Error>((stream_id, thread_name, s))
364 })
365 .await?
366 }
367 })
368 .buffered(max_concurrent)
369 .try_collect()
370 .await
371 .map_err(|e| datafusion::error::DataFusionError::Internal(
372 format!("Failed to query thread spans: {e}"),
373 ))?;
374
375 for (stream_id, thread_name, mut data_stream) in stream_results {
376 while let Some(batch) = data_stream.try_next().await? {
377 let augmented = augment_batch(&batch, schema.clone(), &stream_id, &thread_name)?;
378 yield augmented;
379 }
380 }
381 }
382
383 if matches!(span_types, SpanTypes::Async | SpanTypes::Both) {
385 let async_sql = format!(
386 "SELECT \
387 b.span_id as id, \
388 b.parent_span_id as parent, \
389 b.depth, \
390 b.hash, \
391 b.time as \"begin\", \
392 e.time as \"end\", \
393 arrow_cast(e.time, 'Int64') - arrow_cast(b.time, 'Int64') as duration, \
394 b.name, \
395 b.target, \
396 b.filename, \
397 b.line \
398 FROM (SELECT * FROM view_instance('async_events', '{process_id}') \
399 WHERE event_type = 'begin') b \
400 INNER JOIN (SELECT * FROM view_instance('async_events', '{process_id}') \
401 WHERE event_type = 'end') e \
402 ON b.span_id = e.span_id \
403 WHERE b.time < e.time \
404 ORDER BY b.time"
405 );
406
407 let df = ctx.sql(&async_sql).await
408 .map_err(|e| datafusion::error::DataFusionError::Internal(
409 format!("Failed to query async spans: {e}"),
410 ))?;
411 let mut async_stream = df.execute_stream().await
412 .map_err(|e| datafusion::error::DataFusionError::Internal(
413 format!("Failed to execute async spans stream: {e}"),
414 ))?;
415
416 while let Some(batch) = async_stream.try_next().await? {
417 let augmented = augment_batch(&batch, schema.clone(), "", "async")?;
418 yield augmented;
419 }
420 }
421 };
422
423 Ok(Box::pin(RecordBatchStreamAdapter::new(
424 schema,
425 record_batch_stream,
426 )))
427 }
428
429 fn apply_expressions(
430 &self,
431 _f: &mut dyn FnMut(
432 &Arc<dyn datafusion::physical_plan::PhysicalExpr>,
433 ) -> DFResult<datafusion::common::tree_node::TreeNodeRecursion>,
434 ) -> DFResult<datafusion::common::tree_node::TreeNodeRecursion> {
435 Ok(datafusion::common::tree_node::TreeNodeRecursion::Continue)
436 }
437}
438
439#[derive(Debug)]
442struct ProcessSpansTableProvider {
443 execution_plan: Arc<ProcessSpansExecutionPlan>,
444}
445
446#[async_trait::async_trait]
447impl TableProvider for ProcessSpansTableProvider {
448 fn schema(&self) -> SchemaRef {
449 self.execution_plan.schema()
450 }
451
452 fn table_type(&self) -> TableType {
453 TableType::Base
454 }
455
456 async fn scan(
457 &self,
458 _state: &dyn Session,
459 projection: Option<&Vec<usize>>,
460 _filters: &[Expr],
461 limit: Option<usize>,
462 ) -> DFResult<Arc<dyn ExecutionPlan>> {
463 let authorized = self
464 .execution_plan
465 .guard
466 .authorize(
467 self.execution_plan.process_uuid,
468 IdKind::Process,
469 "process_spans",
470 )
471 .await?;
472 let mut plan: Arc<dyn ExecutionPlan> =
473 Arc::new(self.execution_plan.with_authorized(authorized));
474 if let Some(projection) = projection {
475 let schema = plan.schema();
476 let projected_exprs: Vec<(Arc<dyn datafusion::physical_expr::PhysicalExpr>, String)> =
477 projection
478 .iter()
479 .map(|&i| {
480 let name = schema.field(i).name().clone();
481 let expr = Arc::new(datafusion::physical_expr::expressions::Column::new(
482 &name, i,
483 ))
484 as Arc<dyn datafusion::physical_expr::PhysicalExpr>;
485 (expr, name)
486 })
487 .collect();
488 plan = Arc::new(ProjectionExec::try_new(projected_exprs, plan)?);
489 }
490 if let Some(fetch) = limit {
491 plan = Arc::new(GlobalLimitExec::new(plan, 0, Some(fetch)));
492 }
493 Ok(plan)
494 }
495}