1use anyhow::{Context, Result};
2use arrow_flight::decode::FlightRecordBatchStream;
3use chrono::DateTime;
4use datafusion::arrow::array::{
5 BinaryArray, GenericListArray, Int32Array, Int64Array, StringArray, TimestampNanosecondArray,
6};
7use futures::StreamExt;
8use micromegas_ingestion::data_lake_connection::DataLakeConnection;
9use micromegas_telemetry::blob_storage::PutIfAbsent;
10use micromegas_tracing::prelude::*;
11use std::sync::Arc;
12use uuid::Uuid;
13
14use crate::{
15 dfext::{string_column_accessor::string_column_by_name, typed_column::typed_column_by_name},
16 properties::{
17 properties_column_accessor::properties_column_by_name,
18 utils::extract_properties_from_properties_column,
19 },
20};
21async fn ingest_streams(
22 lake: Arc<DataLakeConnection>,
23 mut rb_stream: FlightRecordBatchStream,
24) -> Result<i64> {
25 let mut tr = lake.db_pool.begin().await?;
26 let mut nb_rows: i64 = 0;
27 while let Some(res) = rb_stream.next().await {
28 let b = res?;
29 nb_rows += b.num_rows() as i64;
30 let stream_id_column = string_column_by_name(&b, "stream_id")?;
31 let process_id_column = string_column_by_name(&b, "process_id")?;
32 let dependencies_metadata_column: &BinaryArray =
33 typed_column_by_name(&b, "dependencies_metadata")?;
34 let objects_metadata_column: &BinaryArray = typed_column_by_name(&b, "objects_metadata")?;
35 let tags_column: &GenericListArray<i32> = typed_column_by_name(&b, "tags")?;
36 let properties_accessor = properties_column_by_name(&b, "properties")?;
37 let insert_time_column: &TimestampNanosecondArray =
38 typed_column_by_name(&b, "insert_time")?;
39 let format_column = string_column_by_name(&b, "format")?;
42 let audience_column = string_column_by_name(&b, "audience")?;
46
47 for row in 0..b.num_rows() {
48 let stream_id = Uuid::parse_str(stream_id_column.value(row)?)?;
49 let process_id = Uuid::parse_str(process_id_column.value(row)?)?;
50 let tags: Vec<String> = tags_column
51 .value(row)
52 .as_any()
53 .downcast_ref::<StringArray>()
54 .with_context(|| "casting tags")?
55 .iter()
56 .map(|item| String::from(item.unwrap_or_default()))
57 .collect();
58 let properties_map =
59 extract_properties_from_properties_column(properties_accessor.as_ref(), row)?;
60 let properties = micromegas_telemetry::property::make_properties(&properties_map);
61
62 instrument_named!(
63 sqlx::query(
64 "INSERT INTO streams (stream_id, process_id, dependencies_metadata, objects_metadata, tags, properties, insert_time, format, audience)
65 VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
66 ON CONFLICT (stream_id) DO NOTHING;",
67 )
68 .bind(stream_id)
69 .bind(process_id)
70 .bind(dependencies_metadata_column.value(row))
71 .bind(objects_metadata_column.value(row))
72 .bind(tags)
73 .bind(properties)
74 .bind(DateTime::from_timestamp_nanos(
75 insert_time_column.value(row),
76 ))
77 .bind(format_column.value(row)?)
78 .bind(audience_column.value(row)?)
79 .execute(&mut *tr),
80 "sql_insert_stream"
81 )
82 .await
83 .with_context(|| "inserting into streams")?;
84 }
85 }
86 tr.commit().await?;
87 info!("ingested {nb_rows} streams");
88 Ok(nb_rows)
89}
90
91async fn ingest_processes(
92 lake: Arc<DataLakeConnection>,
93 mut rb_stream: FlightRecordBatchStream,
94) -> Result<i64> {
95 let mut tr = lake.db_pool.begin().await?;
96 let mut nb_rows: i64 = 0;
97 while let Some(res) = rb_stream.next().await {
98 let b = res?;
99 nb_rows += b.num_rows() as i64;
100 let process_id_column = string_column_by_name(&b, "process_id")?;
101 let exe_column = string_column_by_name(&b, "exe")?;
102 let username_column = string_column_by_name(&b, "username")?;
103 let realname_column = string_column_by_name(&b, "realname")?;
104 let computer_column = string_column_by_name(&b, "computer")?;
105 let distro_column = string_column_by_name(&b, "distro")?;
106 let cpu_brand_column = string_column_by_name(&b, "cpu_brand")?;
107 let process_tsc_freq_column: &Int64Array = typed_column_by_name(&b, "tsc_frequency")?;
108 let start_time_column: &TimestampNanosecondArray = typed_column_by_name(&b, "start_time")?;
109 let start_ticks_column: &Int64Array = typed_column_by_name(&b, "start_ticks")?;
110 let insert_time_column: &TimestampNanosecondArray =
111 typed_column_by_name(&b, "insert_time")?;
112 let parent_process_id_column = string_column_by_name(&b, "parent_process_id")?;
113 let properties_accessor = properties_column_by_name(&b, "properties")?;
114 let audience_column = string_column_by_name(&b, "audience")?;
119 for row in 0..b.num_rows() {
120 let process_id = Uuid::parse_str(process_id_column.value(row)?)?;
121 let parent_process_str = parent_process_id_column.value(row)?;
122 let parent_process_id = if parent_process_str.is_empty() {
123 None
124 } else {
125 Some(Uuid::parse_str(parent_process_str)?)
126 };
127 let properties_map =
128 extract_properties_from_properties_column(properties_accessor.as_ref(), row)?;
129 let properties = micromegas_telemetry::property::make_properties(&properties_map);
130 instrument_named!(
131 sqlx::query(
132 "INSERT INTO processes
133 (process_id, exe, username, realname, computer, distro, cpu_brand,
134 tsc_frequency, start_time, start_ticks, insert_time, parent_process_id,
135 properties, audience)
136 VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)
137 ON CONFLICT (process_id) DO NOTHING;",
138 )
139 .bind(process_id)
140 .bind(exe_column.value(row)?)
141 .bind(username_column.value(row)?)
142 .bind(realname_column.value(row)?)
143 .bind(computer_column.value(row)?)
144 .bind(distro_column.value(row)?)
145 .bind(cpu_brand_column.value(row)?)
146 .bind(process_tsc_freq_column.value(row))
147 .bind(DateTime::from_timestamp_nanos(start_time_column.value(row)))
148 .bind(start_ticks_column.value(row))
149 .bind(DateTime::from_timestamp_nanos(
150 insert_time_column.value(row),
151 ))
152 .bind(parent_process_id)
153 .bind(properties)
154 .bind(audience_column.value(row)?)
155 .execute(&mut *tr),
156 "sql_insert_process"
157 )
158 .await
159 .with_context(|| "executing sql insert into processes")?;
160 }
161 }
162 tr.commit().await?;
163 info!("ingested {nb_rows} processes");
164 Ok(nb_rows)
165}
166
167async fn ingest_payloads(
168 lake: Arc<DataLakeConnection>,
169 mut rb_stream: FlightRecordBatchStream,
170) -> Result<i64> {
171 let mut nb_rows: i64 = 0;
172 while let Some(res) = rb_stream.next().await {
173 let b = res?;
174 nb_rows += b.num_rows() as i64;
175 let process_id_column = string_column_by_name(&b, "process_id")?;
176 let stream_id_column = string_column_by_name(&b, "stream_id")?;
177 let block_id_column = string_column_by_name(&b, "block_id")?;
178 let payload_column: &BinaryArray = typed_column_by_name(&b, "payload")?;
179 for row in 0..b.num_rows() {
180 let process_id = process_id_column.value(row)?;
181 let stream_id = stream_id_column.value(row)?;
182 let block_id = block_id_column.value(row)?;
183 let obj_path = format!("blobs/{process_id}/{stream_id}/{block_id}");
184 let payload = bytes::Bytes::copy_from_slice(payload_column.value(row));
185 let put_outcome = lake
190 .blob_storage
191 .put_if_absent(&obj_path, payload)
192 .await
193 .with_context(|| "Error writing block to blob storage")?;
194 if put_outcome == PutIfAbsent::AlreadyExists {
195 debug!("block payload already exists at {obj_path}, skipping");
196 }
197 }
198 }
199 info!("ingested {nb_rows} payloads");
200 Ok(nb_rows)
201}
202
203async fn ingest_blocks(
204 lake: Arc<DataLakeConnection>,
205 mut rb_stream: FlightRecordBatchStream,
206) -> Result<i64> {
207 let mut tr = lake.db_pool.begin().await?;
208 let mut nb_rows: i64 = 0;
209 while let Some(res) = rb_stream.next().await {
210 let b = res?;
211 nb_rows += b.num_rows() as i64;
212 let block_id_column = string_column_by_name(&b, "block_id")?;
213 let stream_id_column = string_column_by_name(&b, "stream_id")?;
214 let process_id_column = string_column_by_name(&b, "process_id")?;
215 let begin_time_column: &TimestampNanosecondArray = typed_column_by_name(&b, "begin_time")?;
216 let begin_ticks_column: &Int64Array = typed_column_by_name(&b, "begin_ticks")?;
217 let end_time_column: &TimestampNanosecondArray = typed_column_by_name(&b, "end_time")?;
218 let end_ticks_column: &Int64Array = typed_column_by_name(&b, "end_ticks")?;
219 let nb_objects_column: &Int32Array = typed_column_by_name(&b, "nb_objects")?;
220 let object_offset_column: &Int64Array = typed_column_by_name(&b, "object_offset")?;
221 let payload_size_column: &Int64Array = typed_column_by_name(&b, "payload_size")?;
222 let insert_time_column: &TimestampNanosecondArray =
223 typed_column_by_name(&b, "insert_time")?;
224 let audience_column = string_column_by_name(&b, "audience")?;
229 for row in 0..b.num_rows() {
230 let block_id = Uuid::parse_str(block_id_column.value(row)?)?;
231 let stream_id = Uuid::parse_str(stream_id_column.value(row)?)?;
232 let process_id = Uuid::parse_str(process_id_column.value(row)?)?;
233 instrument_named!(
234 sqlx::query(
235 "INSERT INTO blocks (block_id, stream_id, process_id, begin_time, begin_ticks,
236 end_time, end_ticks, nb_objects, object_offset, payload_size, insert_time,
237 audience)
238 VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
239 ON CONFLICT (block_id) DO NOTHING;",
240 )
241 .bind(block_id)
242 .bind(stream_id)
243 .bind(process_id)
244 .bind(DateTime::from_timestamp_nanos(begin_time_column.value(row)))
245 .bind(begin_ticks_column.value(row))
246 .bind(DateTime::from_timestamp_nanos(end_time_column.value(row)))
247 .bind(end_ticks_column.value(row))
248 .bind(nb_objects_column.value(row))
249 .bind(object_offset_column.value(row))
250 .bind(payload_size_column.value(row))
251 .bind(DateTime::from_timestamp_nanos(
252 insert_time_column.value(row),
253 ))
254 .bind(audience_column.value(row)?)
255 .execute(&mut *tr),
256 "sql_insert_block"
257 )
258 .await
259 .with_context(|| "executing sql insert into blocks")?;
260 }
261 }
262 tr.commit().await?;
263 info!("ingested {nb_rows} blocks");
264 Ok(nb_rows)
265}
266
267pub async fn bulk_ingest(
269 lake: Arc<DataLakeConnection>,
270 table_name: &str,
271 rb_stream: FlightRecordBatchStream,
272) -> Result<i64> {
273 match table_name {
274 "processes" => ingest_processes(lake, rb_stream).await,
275 "streams" => ingest_streams(lake, rb_stream).await,
276 "blocks" => ingest_blocks(lake, rb_stream).await,
277 "payloads" => ingest_payloads(lake, rb_stream).await,
278 other => anyhow::bail!("bulk ingest for table {other} not supported"),
279 }
280}