1use anyhow::Result;
11use backbeat::{
12 record::RecordView,
13 ring::walk,
14 schema::{FieldRole, Phase},
15 wire::{DumpReader, OwnedSchema},
16};
17use bytes::Bytes;
18use std::{collections::BTreeMap, io::Write};
19
20pub fn inspect(bytes: impl Into<Bytes>, out: &mut impl Write) -> Result<()> {
22 let bytes = bytes.into();
23 let total_len = bytes.len();
24 let reader = DumpReader::new(bytes).map_err(|e| anyhow::anyhow!("{e}"))?;
25 let schemas = reader.schemas().map_err(|e| anyhow::anyhow!("{e}"))?;
26 let intern = reader.intern_tables().map_err(|e| anyhow::anyhow!("{e}"))?;
27 let metas = reader.metas().map_err(|e| anyhow::anyhow!("{e}"))?;
28 let views = reader.views().map_err(|e| anyhow::anyhow!("{e}"))?;
29 let shards = reader.shards().map_err(|e| anyhow::anyhow!("{e}"))?;
30
31 let by_id: BTreeMap<u64, &OwnedSchema> = schemas.iter().map(|s| (s.id.get(), s)).collect();
33
34 writeln!(out, "envelope")?;
35 writeln!(out, " size: {total_len} bytes")?;
36 writeln!(out, " flags: {:#06x}", reader.flags())?;
37 writeln!(out, " sections: {}", reader.section_count())?;
38
39 writeln!(out, "\nschema registry ({} events)", schemas.len())?;
40 for s in &schemas {
41 let phase = match s.phase {
42 Phase::Enter => " [span enter]",
43 Phase::Exit => " [span exit]",
44 _ => "",
45 };
46 writeln!(
47 out,
48 " {:#018x} {} ({} bytes, {} fields){}{}",
49 s.id.get(),
50 s.qualified_name,
51 s.record_size,
52 s.fields.len(),
53 phase,
54 s.description
55 .as_deref()
56 .map(|d| format!(" — {d}"))
57 .unwrap_or_default()
58 )?;
59 for f in &s.fields {
60 let marker = match f.role {
62 FieldRole::Key => "*",
63 FieldRole::SpanId => "$",
64 FieldRole::ParentSpanId => "^",
65 _ => " ",
66 };
67 writeln!(
68 out,
69 " {}{}: {:?}{}{}",
70 marker,
71 f.name,
72 f.ty,
73 f.unit
74 .as_deref()
75 .map(|u| format!(" [{u}]"))
76 .unwrap_or_default(),
77 f.sentinel
78 .map(|s| format!(" (sentinel {s})"))
79 .unwrap_or_default()
80 )?;
81 }
82 }
83 writeln!(out, " (* = key, $ = span id, ^ = parent span id)")?;
84
85 writeln!(out, "\ninstances ({})", metas.len())?;
87 for m in &metas {
88 let n_intern: usize = intern
89 .iter()
90 .filter(|t| t.instance_id == m.instance_id)
91 .map(|t| t.entries.len())
92 .sum();
93 let host = if m.host.is_empty() {
94 "(no host)"
95 } else {
96 m.host.as_str()
97 };
98 writeln!(
99 out,
100 " {:#018x} {host} ({n_intern} intern entries)",
101 m.instance_id
102 )?;
103 }
104
105 let total_intern: usize = intern.iter().map(|t| t.entries.len()).sum();
106 writeln!(
107 out,
108 "\nintern table: {total_intern} entries across {} instance(s)",
109 intern.len()
110 )?;
111
112 if !views.is_empty() {
115 let total_lines: usize = views.iter().map(|v| v.lines().count()).sum();
116 writeln!(
117 out,
118 "\nview sets ({}, {total_lines} lines of DDL)",
119 views.len()
120 )?;
121 for (i, v) in views.iter().enumerate() {
122 writeln!(
123 out,
124 " #{}: {} bytes, {} lines",
125 i + 1,
126 v.len(),
127 v.lines().count()
128 )?;
129 }
130 }
131
132 writeln!(out, "\nshards ({})", shards.len())?;
136 let mut total = 0usize;
137 let mut per_event: BTreeMap<String, usize> = BTreeMap::new();
138
139 for shard in &shards {
140 let mut count = 0usize;
141 walk(
142 &shard.region,
143 shard.head as usize,
144 shard.capacity as usize,
145 |payload| match RecordView::parse(&payload[..]) {
146 Some(rec) => match by_id.get(&rec.event_id.get()) {
147 Some(schema) if rec.fields.len() == schema.record_size as usize => {
148 *per_event.entry(schema.qualified_name.clone()).or_default() += 1;
149 count += 1;
150 true
151 }
152 _ => false,
153 },
154 None => false,
155 },
156 );
157 total += count;
158 writeln!(
159 out,
160 " instance {:#018x} shard {:>3}: head {:>10} capacity {:>10} {} records",
161 shard.instance_id, shard.shard_id, shard.head, shard.capacity, count
162 )?;
163 }
164
165 writeln!(out, "\nrecords: {total} total")?;
166 for (name, n) in &per_event {
167 writeln!(out, " {name}: {n}")?;
168 }
169
170 Ok(())
171}