1use crate::model::Loaded;
22use anyhow::{Context, Result};
23use arrow::{
24 array::{ArrayRef, BooleanBuilder, Int64Builder, StringBuilder, StructArray, UInt64Builder},
25 buffer::NullBuffer,
26 datatypes::{DataType, Field, Fields, Schema},
27 record_batch::RecordBatch,
28};
29use backbeat::{
30 schema::{FieldRole, FieldType, Phase},
31 wire::{OwnedField, OwnedSchema},
32};
33use parquet::{
34 arrow::ArrowWriter,
35 file::{metadata::KeyValue, properties::WriterProperties},
36};
37use std::{
38 collections::{HashMap, HashSet},
39 fs::File,
40 path::Path,
41 sync::Arc,
42};
43
44pub fn to_parquet(dumps: &[Loaded], output: &Path, host: &str, zstd_level: i32) -> Result<usize> {
48 let mut schemas: Vec<OwnedSchema> = Vec::new();
51 let mut seen = HashSet::new();
52 for d in dumps {
53 for s in &d.schemas {
54 if seen.insert(s.id.get()) {
55 schemas.push(s.clone());
56 }
57 }
58 }
59 schemas.sort_by(|a, b| a.qualified_name.cmp(&b.qualified_name));
60 let by_id: HashMap<u64, usize> = schemas
61 .iter()
62 .enumerate()
63 .map(|(i, s)| (s.id.get(), i))
64 .collect();
65
66 let mut rows: Vec<Row> = Vec::new();
69 for d in dumps {
70 for r in &d.records {
71 let id = d.schemas[r.schema_idx].id.get();
72 rows.push(Row {
73 ts_nanos: r.ts_nanos,
74 instance_id: d.instance_id,
75 shard_id: r.shard_id,
76 local_seq: r.local_seq,
77 schema_idx: by_id[&id],
78 fields: &r.fields,
79 intern: &d.intern,
80 });
81 }
82 }
83 rows.sort_by_key(|r| (r.ts_nanos, r.instance_id, r.shard_id, r.local_seq));
84
85 let batch = build_batch(&schemas, &rows)?;
86
87 let footer_host = if !host.is_empty() {
90 host
91 } else {
92 dumps.first().map(|d| d.host.as_str()).unwrap_or("")
93 };
94 write_parquet(output, &batch, footer_host, zstd_level)?;
95 Ok(rows.len())
96}
97
98struct Row<'a> {
101 ts_nanos: u64,
102 instance_id: u64,
103 shard_id: u32,
104 local_seq: u64,
105 schema_idx: usize,
107 fields: &'a [u8],
108 intern: &'a HashMap<u32, String>,
109}
110
111enum Value {
113 U64(u64),
114 I64(i64),
115 Bool(bool),
116 Str(String),
117}
118
119fn decode_field(field: &OwnedField, bytes: &[u8], intern: &HashMap<u32, String>) -> Option<Value> {
121 let start = field.offset as usize;
122 let end = start + field.width as usize;
123 let slice = bytes.get(start..end)?;
124 Some(match field.ty {
125 FieldType::U8 => Value::U64(slice[0] as u64),
126 FieldType::U16 => Value::U64(u16::from_le_bytes(slice.try_into().ok()?) as u64),
127 FieldType::U32 => Value::U64(u32::from_le_bytes(slice.try_into().ok()?) as u64),
128 FieldType::U64 => Value::U64(u64::from_le_bytes(slice.try_into().ok()?)),
129 FieldType::I8 => Value::I64(slice[0] as i8 as i64),
130 FieldType::I16 => Value::I64(i16::from_le_bytes(slice.try_into().ok()?) as i64),
131 FieldType::I32 => Value::I64(i32::from_le_bytes(slice.try_into().ok()?) as i64),
132 FieldType::I64 => Value::I64(i64::from_le_bytes(slice.try_into().ok()?)),
133 FieldType::Bool => Value::Bool(slice[0] != 0),
134 FieldType::Bytes => Value::Str(hex(slice)),
135 FieldType::Enum { repr } => {
136 let raw = read_uint(slice, repr as usize)?;
137 let label = field
138 .enum_labels
139 .iter()
140 .find(|l| l.value == raw)
141 .map(|l| l.label.clone())
142 .unwrap_or_else(|| raw.to_string());
143 Value::Str(label)
144 }
145 FieldType::Interned { .. } => {
146 let id = u32::from_le_bytes(slice.get(..4)?.try_into().ok()?);
147 Value::Str(intern.get(&id).cloned().unwrap_or_else(|| format!("#{id}")))
148 }
149 _ => Value::Str(hex(slice)),
151 })
152}
153
154fn read_uint(slice: &[u8], width: usize) -> Option<u64> {
156 let mut buf = [0u8; 8];
157 buf.get_mut(..width)?.copy_from_slice(slice.get(..width)?);
158 Some(u64::from_le_bytes(buf))
159}
160
161fn hex(bytes: &[u8]) -> String {
162 let mut s = String::with_capacity(bytes.len() * 2);
163 for b in bytes {
164 s.push_str(&format!("{b:02x}"));
165 }
166 s
167}
168
169fn arrow_type(ty: &FieldType) -> DataType {
171 match ty {
172 FieldType::U8 | FieldType::U16 | FieldType::U32 | FieldType::U64 => DataType::UInt64,
173 FieldType::I8 | FieldType::I16 | FieldType::I32 | FieldType::I64 => DataType::Int64,
174 FieldType::Bool => DataType::Boolean,
175 FieldType::Bytes | FieldType::Enum { .. } | FieldType::Interned { .. } => DataType::Utf8,
176 _ => DataType::Utf8,
178 }
179}
180
181fn display_names(schemas: &[OwnedSchema]) -> Vec<String> {
186 let mut counts: HashMap<&str, usize> = HashMap::new();
187 for s in schemas {
188 *counts.entry(s.qualified_name.as_str()).or_default() += 1;
189 }
190 schemas
191 .iter()
192 .map(|s| {
193 if counts[s.qualified_name.as_str()] > 1 {
194 format!("{}#{:016x}", s.qualified_name, s.id.get())
195 } else {
196 s.qualified_name.clone()
197 }
198 })
199 .collect()
200}
201
202fn is_promoted(role: FieldRole) -> bool {
205 matches!(
206 role,
207 FieldRole::Key | FieldRole::SpanId | FieldRole::ParentSpanId
208 )
209}
210
211fn field_metadata(f: &OwnedField) -> HashMap<String, String> {
214 let mut m = HashMap::new();
215 let role = match f.role {
216 FieldRole::None => None,
217 FieldRole::Key => Some("key"),
218 FieldRole::SpanId => Some("span_id"),
219 FieldRole::ParentSpanId => Some("parent_span_id"),
220 _ => None,
221 };
222 if let Some(role) = role {
223 m.insert("backbeat.role".to_string(), role.to_string());
224 }
225 if let Some(unit) = &f.unit {
226 m.insert("backbeat.unit".to_string(), unit.clone());
227 }
228 if let Some(desc) = &f.description {
229 m.insert("backbeat.description".to_string(), desc.clone());
230 }
231 m
232}
233
234fn event_metadata(s: &OwnedSchema) -> HashMap<String, String> {
236 let mut m = HashMap::new();
237 let phase = match s.phase {
238 Phase::None => None,
239 Phase::Enter => Some("enter"),
240 Phase::Exit => Some("exit"),
241 _ => None,
242 };
243 if let Some(phase) = phase {
244 m.insert("backbeat.span".to_string(), phase.to_string());
245 }
246 if let Some(desc) = &s.description {
247 m.insert("backbeat.description".to_string(), desc.clone());
248 }
249 m
250}
251
252enum Col {
254 U64(UInt64Builder),
255 I64(Int64Builder),
256 Bool(BooleanBuilder),
257 Str(StringBuilder),
258}
259
260impl Col {
261 fn new(dt: &DataType) -> Self {
262 match dt {
263 DataType::UInt64 => Col::U64(UInt64Builder::new()),
264 DataType::Int64 => Col::I64(Int64Builder::new()),
265 DataType::Boolean => Col::Bool(BooleanBuilder::new()),
266 DataType::Utf8 => Col::Str(StringBuilder::new()),
267 other => unreachable!("unexpected column type {other:?}"),
268 }
269 }
270
271 fn append(&mut self, v: Value) {
272 match (self, v) {
273 (Col::U64(b), Value::U64(x)) => b.append_value(x),
274 (Col::I64(b), Value::I64(x)) => b.append_value(x),
275 (Col::Bool(b), Value::Bool(x)) => b.append_value(x),
276 (Col::Str(b), Value::Str(x)) => b.append_value(x),
277 (c, _) => c.append_null(),
279 }
280 }
281
282 fn append_null(&mut self) {
283 match self {
284 Col::U64(b) => b.append_null(),
285 Col::I64(b) => b.append_null(),
286 Col::Bool(b) => b.append_null(),
287 Col::Str(b) => b.append_null(),
288 }
289 }
290
291 fn finish(&mut self) -> ArrayRef {
292 match self {
293 Col::U64(b) => Arc::new(b.finish()),
294 Col::I64(b) => Arc::new(b.finish()),
295 Col::Bool(b) => Arc::new(b.finish()),
296 Col::Str(b) => Arc::new(b.finish()),
297 }
298 }
299}
300
301fn build_batch(schemas: &[OwnedSchema], rows: &[Row]) -> Result<RecordBatch> {
303 let names = display_names(schemas);
307
308 let mut key_names: Vec<String> = Vec::new();
313 let mut key_type: HashMap<String, DataType> = HashMap::new();
314 for s in schemas {
315 for f in s.fields.iter().filter(|f| is_promoted(f.role)) {
316 if !key_type.contains_key(&f.name) {
317 key_names.push(f.name.clone());
318 key_type.insert(f.name.clone(), arrow_type(&f.ty));
319 }
320 }
321 }
322
323 let mut seq = UInt64Builder::new();
325 let mut ts = UInt64Builder::new();
326 let mut event = StringBuilder::new();
327 let mut event_id = UInt64Builder::new();
328 let mut instance_id = UInt64Builder::new();
329 let mut key_cols: Vec<Col> = key_names.iter().map(|n| Col::new(&key_type[n])).collect();
330
331 struct EventCols {
333 children: Vec<(OwnedField, Col)>,
335 valid: Vec<bool>,
337 }
338 let mut event_cols: Vec<EventCols> = schemas
339 .iter()
340 .map(|s| EventCols {
341 children: s
342 .fields
343 .iter()
344 .filter(|f| !is_promoted(f.role))
345 .map(|f| (f.clone(), Col::new(&arrow_type(&f.ty))))
346 .collect(),
347 valid: Vec::with_capacity(rows.len()),
348 })
349 .collect();
350
351 for (i, row) in rows.iter().enumerate() {
353 let s = &schemas[row.schema_idx];
354 seq.append_value(i as u64);
355 ts.append_value(row.ts_nanos);
356 event.append_value(&names[row.schema_idx]);
357 event_id.append_value(s.id.get());
358 instance_id.append_value(row.instance_id);
359
360 for (name, col) in key_names.iter().zip(key_cols.iter_mut()) {
362 match s
363 .fields
364 .iter()
365 .find(|f| is_promoted(f.role) && &f.name == name)
366 {
367 Some(f) => match decode_field(f, row.fields, row.intern) {
368 Some(v) => col.append(v),
369 None => col.append_null(),
370 },
371 None => col.append_null(),
372 }
373 }
374
375 for (idx, ec) in event_cols.iter_mut().enumerate() {
377 let mine = idx == row.schema_idx;
378 ec.valid.push(mine);
379 for (f, col) in ec.children.iter_mut() {
380 if mine {
381 match decode_field(f, row.fields, row.intern) {
382 Some(v) => col.append(v),
383 None => col.append_null(),
384 }
385 } else {
386 col.append_null();
387 }
388 }
389 }
390 }
391
392 let mut fields: Vec<Field> = Vec::new();
394 let mut arrays: Vec<ArrayRef> = Vec::new();
395
396 fields.push(Field::new("seq", DataType::UInt64, false));
397 arrays.push(Arc::new(seq.finish()));
398 fields.push(Field::new("ts_nanos", DataType::UInt64, false));
399 arrays.push(Arc::new(ts.finish()));
400 fields.push(Field::new("event", DataType::Utf8, false));
401 arrays.push(Arc::new(event.finish()));
402 fields.push(Field::new("event_id", DataType::UInt64, false));
403 arrays.push(Arc::new(event_id.finish()));
404 fields.push(Field::new("instance_id", DataType::UInt64, false));
405 arrays.push(Arc::new(instance_id.finish()));
406
407 for (name, mut col) in key_names.iter().zip(key_cols.into_iter()) {
408 let decl = schemas
410 .iter()
411 .find_map(|s| s.fields.iter().find(|f| &f.name == name));
412 let mut field = Field::new(name, key_type[name].clone(), true);
413 if let Some(f) = decl {
414 field = field.with_metadata(field_metadata(f));
415 }
416 fields.push(field);
417 arrays.push(col.finish());
418 }
419
420 for (idx, mut ec) in event_cols.into_iter().enumerate() {
421 let s = &schemas[idx];
422 if ec.children.is_empty() {
427 continue;
428 }
429 let child_fields: Fields = ec
430 .children
431 .iter()
432 .map(|(f, _)| {
433 Field::new(&f.name, arrow_type(&f.ty), true).with_metadata(field_metadata(f))
434 })
435 .collect::<Vec<_>>()
436 .into();
437 let child_arrays: Vec<ArrayRef> = ec.children.iter_mut().map(|(_, c)| c.finish()).collect();
438 let nulls = NullBuffer::from(ec.valid);
439 let struct_array = StructArray::new(child_fields.clone(), child_arrays, Some(nulls));
440 let dt = DataType::Struct(child_fields);
441 let mut field = Field::new(&names[idx], dt, true);
444 field = field.with_metadata(event_metadata(s));
445 fields.push(field);
446 arrays.push(Arc::new(struct_array));
447 }
448
449 let schema = Arc::new(Schema::new(fields));
450 RecordBatch::try_new(schema, arrays).context("assembling record batch")
451}
452
453fn write_parquet(output: &Path, batch: &RecordBatch, host: &str, zstd_level: i32) -> Result<()> {
461 let mut kv = vec![KeyValue::new(
462 "backbeat.format".to_string(),
463 "1".to_string(),
464 )];
465 if !host.is_empty() {
466 kv.push(KeyValue::new("backbeat.host".to_string(), host.to_string()));
467 }
468 let level = parquet::basic::ZstdLevel::try_new(zstd_level)
471 .with_context(|| format!("invalid zstd level {zstd_level} (valid range is 1–22)"))?;
472 let props = WriterProperties::builder()
473 .set_compression(parquet::basic::Compression::ZSTD(level))
474 .set_key_value_metadata(Some(kv))
475 .build();
476 let file = File::create(output).with_context(|| format!("creating {}", output.display()))?;
477 let mut writer = ArrowWriter::try_new(file, batch.schema(), Some(props))?;
478 writer.write(batch)?;
479 writer.close()?;
480 Ok(())
481}