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