micromegas_analytics/lakehouse/view_factory.rs
1//! [`default_view_factory`] makes the default [`ViewFactory`], giving users access to view instances, grouped in sets.
2//!
3//! # View sets
4//!
5//! A ViewFactory defines the available view sets and can instanciate view instances.
6//! All view instances in a set have the same schema.
7//! Some view instances are global (their view_instance_id is 'global').
8//! Global view instances are implicitly accessible to SQL queries.
9//! Non-global view instances are accessible using the table function `view_instance`. See [ViewInstanceTableFunction](super::view_instance_table_function::ViewInstanceTableFunction).
10//!
11//! ## log_entries
12//!
13//! | field | type | description |
14//! |------------- |-----------------------------|-----------------------------------------------------------|
15//! |process_id |Utf8 | unique id of the process, references the processes table |
16//! |exe |Utf8 | filename of the process |
17//! |username |Utf8 | username of the process |
18//! |computer |Utf8 | computer name of the process |
19//! |time |UTC Timestamp (nanoseconds) | time of the log entry event |
20//! |target |Utf8 | category or module name of the log entry |
21//! |level |int32 | verbosity level (Fatal=1, Error=2, Warning=3, Info=4, Debug=5, Trace=6)| |
22//! |msg |Utf8 | message |
23//! |audience |Dictionary(Int32, Utf8) | the audience of the block this log entry came from (the block's own stamp), never `NULL` |
24//!
25//! ### log_entries view instances
26//!
27//! The implicit use of the `log_entries` table corresponds to the 'global' instance, which contains the log entries of all the processes.
28//!
29//! Except the 'global' instance, the instance_id refers to any process_id. `view_instance('log_entries', process_id)` contains that process's log. Process-specific views are materialized just-in-time and can provide much better query performance compared to the 'global' instance.
30//!
31//! ## measures
32//!
33//! | field | type | description |
34//! |------------- |-----------------------------|-----------------------------------------------------------|
35//! |process_id |Utf8 | unique id of the process, references the processes table |
36//! |exe |Utf8 | filename of the process |
37//! |username |Utf8 | username of the process |
38//! |computer |Utf8 | computer name of the process |
39//! |time |UTC Timestamp (nanoseconds) | time of the measure event |
40//! |target |Utf8 | category or module name of the measure |
41//! |name |Utf8 | name of the measure |
42//! |unit |Utf8 | unit of measure |
43//! |value |Float64 | value measured |
44//! |audience |Dictionary(Int32, Utf8) | the audience of the block this measure came from (the block's own stamp), never `NULL` |
45//!
46//!
47//! ### measures view instances
48//!
49//! The implicit use of the `measures` table corresponds to the 'global' instance, which contains the metrics of all the processes.
50//!
51//! Except the 'global' instance, the instance_id refers to any process_id. `view_instance('measures', process_id)` contains that process's metrics. Process-specific views are materialized just-in-time and can provide much better query performance compared to the 'global' instance.
52//!
53//! ## log_stats
54//!
55//! Materialized aggregate of `log_entries` by process, minute, level, and target -- one global
56//! instance, implicitly available (no `view_instance(...)` form). `log_stats` is
57//! a seeded row in `lakehouse_view_set_definitions`, not one `default_view_factory` builds -- it
58//! is droppable and replaceable by an operator like any other DDL-defined view, and the schema
59//! below is its shipped default, not a guarantee.
60//!
61//! | field | type | description |
62//! |------------- |-----------------------------|------------------------------------------------------------|
63//! |time_bin |UTC Timestamp (nanoseconds) | 1-minute time bucket for aggregation |
64//! |process_id |Dictionary(Int32, Utf8) | process unique id |
65//! |level |Int32 | log level (see `log_entries` above) |
66//! |target |Dictionary(Int32, Utf8) | category or module name of the aggregated logs |
67//! |count |Int64 | number of log entries in this aggregation |
68//! |audience |Dictionary(Int32, Utf8) | the audience of the blocks aggregated into this row (the blocks' own stamp), never `NULL` |
69//!
70//! ## thread_spans
71//!
72//! | field | type | description |
73//! |------------- |-----------------------------|------------------------------------------------------------|
74//! |id |Int64 | span id, unique within this thread |
75//! |parent |Int64 | span id of the calling span |
76//! |depth |UInt32 | call stack depth |
77//! |hash |UInt32 | identifies a call site (name, filename, line) |
78//! |begin |UTC Timestamp (nanoseconds) | when the span started its execution |
79//! |end |UTC Timestamp (nanoseconds) | when the span finished its execution |
80//! |duration |Int64 (nanoseconds) | end-begin |
81//! |name |Utf8 | name of the span, usually a function name |
82//! |target |Utf8 | category or module name |
83//! |filename |Utf8 | name or path of the source file where the span is coded |
84//! |line |UInt32 | line number in the file where the span can be found |
85//!
86//! ### thread_spans view instances
87//!
88//! There is no 'global' instance in the 'thread_spans' view set, there is therefore no implicit thread_spans table availble.
89//! Users can call the table function `view_instance('thread_spans', stream_id)` to query the spans in the thread associated with the specified stream_id.
90//!
91//! ## async_events
92//!
93//! | field | type | description |
94//! |------------- |-----------------------------|-----------------------------------------------------------|
95//! |stream_id |Dictionary(Int32, Utf8) | identifier of the thread stream that emitted the event |
96//! |block_id |Dictionary(Int32, Utf8) | unique identifier of the event block |
97//! |time |UTC Timestamp (nanoseconds) | time when the async event occurred |
98//! |event_type |Dictionary(Int32, Utf8) | type of event: "begin" or "end" |
99//! |span_id |Int64 | unique async span identifier |
100//! |parent_span_id|Int64 | span id of the parent async span |
101//! |name |Dictionary(Int32, Utf8) | name of the async span, usually a function name |
102//! |filename |Dictionary(Int32, Utf8) | name or path of the source file where the span is coded |
103//! |target |Dictionary(Int32, Utf8) | category or module name |
104//! |line |UInt32 | line number in the file where the span can be found |
105//!
106//! ### async_events view instances
107//!
108//! There is no 'global' instance in the 'async_events' view set, there is therefore no implicit async_events table available.
109//! Users can call the table function `view_instance('async_events', process_id)` to query the async events in all thread streams associated with the specified process_id.
110//! Process-specific views are materialized just-in-time and can provide good query performance.
111//!
112//! The schema is optimized for high-frequency data. Process information (exe, username, computer, etc.) can be joined when needed:
113//! ```sql
114//! SELECT ae.*, p.exe, p.username, p.computer
115//! FROM view_instance('async_events', process_id) ae
116//! JOIN streams s ON ae.stream_id = s.stream_id
117//! JOIN processes p ON s.process_id = p.process_id
118//! ```
119//!
120//! ## net_spans
121//!
122//! | field | type | description |
123//! |------------------|-----------------------------|-------------------------------------------------------------------------|
124//! |process_id |Dictionary(Int32, Utf8) | process unique id |
125//! |stream_id |Dictionary(Int32, Utf8) | net stream unique id |
126//! |span_id |Int64 | unique span id within the stream |
127//! |parent_span_id |Int64 | span id of the enclosing span (-1 sentinel at Connection roots) |
128//! |depth |UInt32 | tree depth (0 = Connection) |
129//! |kind |Dictionary(Int32, Utf8) | one of `connection` / `object` / `property` / `rpc` |
130//! |name |Dictionary(Int32, Utf8) | span name (connection / object / property / function name) |
131//! |connection_name |Dictionary(Int32, Utf8) | enclosing connection name (denormalized onto every row) |
132//! |is_outgoing |Boolean | direction inherited from the enclosing connection |
133//! |begin_bits |Int64 | cumulative bit offset within the parent span (0 at the Connection root) |
134//! |end_bits |Int64 | `begin_bits + bit_size` |
135//! |bit_size |Int64 | inclusive number of bits attributed to this span |
136//! |begin_time |UTC Timestamp (nanoseconds) | timestamp of the span's Begin event |
137//! |end_time |UTC Timestamp (nanoseconds) | timestamp of the span's End event (equals `begin_time` for properties) |
138//!
139//! ### net_spans view instances
140//!
141//! There is no 'global' instance in the 'net_spans' view set. Users call
142//! `view_instance('net_spans', process_id)` to query the network bandwidth spans emitted by a
143//! given process's net stream. Partitions are materialized just-in-time.
144//!
145//! ## processes
146//!
147//! | field | type | description |
148//! |------------- |-----------------------------|------------------------------------------------------------|
149//! |process_id |Utf8 | process unique id |
150//! |exe |Utf8 | filename of the process |
151//! |username |Utf8 | username of the process |
152//! |realname |Utf8 | real name of the user launching the process |
153//! |computer |Utf8 | name of the computer or vm |
154//! |distro |Utf8 | name of operating system |
155//! |cpu_brand |Utf8 | identifies the cpu |
156//! |tsc frequency |Int64 | number of ticks per second |
157//! |start_time |UTC Timestamp (nanoseconds) | when the process started (as reported by the instrumented process) |
158//! |start_ticks |Int64 | tick count associated with start_time |
159//! |insert_time |UTC Timestamp (nanoseconds) | server-side timestamp when the process metedata was received |
160//! |parent_process_id |Utf8 | unique id of the parent process |
161//! |properties | Array of {key: utf8, value: utf8} | self-reported metadata by the process |
162//! |last_update_time |UTC Timestamp (nanoseconds) | when this process's data was last updated |
163//! |last_block_end_ticks |Int64 | tick count when the last block ended |
164//! |last_block_end_time |UTC Timestamp (nanoseconds) | timestamp when the last block ended |
165//! |audience |Dictionary(Int32, Utf8) | this process's write audience, resolved from its blocks' own stamps via `max(audience)`; never `NULL` |
166//!
167//! There is only one instance in this view set and it is implicitly available.
168//!
169//! ## streams
170//!
171//! | field | type | description |
172//! |------------- |-----------------------------|------------------------------------------------------------|
173//! |stream_id |Utf8 | stream unique id |
174//! |process_id |Utf8 | process unique id |
175//! |dependencies_metadata|Binary | memory layout of the event dependencies |
176//! |objects_metadata|Binary | memory layout of the events |
177//! |tags | Array of utf8 | Purpose of the stream, can contain "log", "metrics" or "cpu" |
178//! |properties | Array of {key: utf8, value: utf8} | self-reported stream metadata by the process |
179//! |insert_time |UTC Timestamp (nanoseconds) | server-side timestamp when the stream metedata was received |
180//! |format |Utf8 | stream payload format (e.g. for OTLP support) |
181//! |last_update_time |UTC Timestamp (nanoseconds) | when this stream's data was last updated |
182//! |audience |Dictionary(Int32, Utf8) | this stream's write audience, resolved from its blocks' own stamps via `max(audience)`; never `NULL` |
183//!
184//! There is only one instance in this view set and it is implicitly available.
185//!
186//! ## blocks
187//!
188//! | field | type | description |
189//! |------------- |-----------------------------|------------------------------------------------------------|
190//! |block_id |Utf8 | block unique id |
191//! |stream_id |Utf8 | stream unique id |
192//! |process_id |Utf8 | process unique id |
193//! |begin_time |UTC Timestamp (nanoseconds) | system time marking the beginning of this event batch |
194//! |begin_ticks |Int64 | tick count associated with begin_time |
195//! |end_time |UTC Timestamp (nanoseconds) | system time marking the ending of this event batch |
196//! |end_ticks |Int64 | tick count associated with end_time |
197//! |nb_objects |Int32 | number of events in this batch |
198//! |object_offset |Int64 | number of events preceding this batch |
199//! |payload_size |Int64 | number of bytes of the binary payload |
200//! |insert_time |UTC Timestamp (nanoseconds) | server-side timestamp when the block was received |
201//! |streams.dependencies_metadata|Binary | memory layout of the event dependencies |
202//! |streams.objects_metadata|Binary | memory layout of the events |
203//! |streams.tags | Array of utf8 | Purpose of the stream, can contain "log", "metrics" or "cpu" |
204//! |streams.properties | Array of {key: utf8, value: utf8} | self-reported stream metadata by the process |
205//! |streams.insert_time |UTC Timestamp (nanoseconds) | server-side timestamp when the stream metedata was received |
206//! |streams.format |Utf8 | stream payload format (e.g. for OTLP support) |
207//! |processes.start_time |UTC Timestamp (nanoseconds) | when the process started (as reported by the instrumented process) |
208//! |processes.start_ticks |Int64 | tick count associated with start_time |
209//! |processes.tsc frequency |Int64 | number of ticks per second |
210//! |processes.exe |Utf8 | filename of the process |
211//! |processes.username |Utf8 | username of the process |
212//! |processes.realname |Utf8 | real name of the user launching the process |
213//! |processes.computer |Utf8 | name of the computer or vm |
214//! |processes.distro |Utf8 | name of operating system |
215//! |processes.cpu_brand |Utf8 | identifies the cpu |
216//! |processes.insert_time |UTC Timestamp (nanoseconds) | server-side timestamp when the process metedata was received |
217//! |processes.parent_process_id |Utf8 | unique id of the parent process (nullable in practice) |
218//! |processes.properties | Array of {key: utf8, value: utf8} | self-reported metadata by the process |
219//! |audience |Dictionary(Int32, Utf8) | this block's own write audience, never derived from its process_id/stream_id -- server-written, never `NULL` |
220//!
221//! There is only one instance in this view set and it is implicitly available.
222//!
223//!
224//!
225use super::blocks_view::BlocksView;
226use super::processes_view::make_processes_view;
227use super::streams_view::make_streams_view;
228use super::{
229 async_events_view::AsyncEventsViewMaker, images_view::ImagesViewMaker, log_view::LogViewMaker,
230 metrics_view::MetricsViewMaker, net_spans_view::NetSpansViewMaker,
231 otel::spans_view::OtelSpansViewMaker, thread_spans_view::ThreadSpansViewMaker, view::View,
232};
233use anyhow::Result;
234use datafusion::arrow::datatypes::Schema;
235use datafusion::execution::runtime_env::RuntimeEnv;
236use micromegas_ingestion::data_lake_connection::DataLakeConnection;
237use std::fmt::Debug;
238use std::{collections::HashMap, sync::Arc};
239
240/// A trait for creating views.
241pub trait ViewMaker: Send + Sync + Debug {
242 /// Creates a new view with the given instance ID.
243 fn make_view(&self, view_instance_id: &str) -> Result<Arc<dyn View>>;
244
245 /// Returns the schema hash for views created by this maker.
246 fn get_schema_hash(&self) -> Vec<u8>;
247
248 /// Returns the schema for views created by this maker.
249 fn get_schema(&self) -> Arc<Schema>;
250}
251
252/// A factory for creating and managing views.
253#[derive(Debug, Clone)]
254pub struct ViewFactory {
255 view_sets: HashMap<String, Arc<dyn ViewMaker>>,
256 global_views: Vec<Arc<dyn View>>,
257}
258
259impl ViewFactory {
260 pub fn new(global_views: Vec<Arc<dyn View>>) -> Self {
261 Self {
262 view_sets: HashMap::new(),
263 global_views,
264 }
265 }
266
267 pub fn get_global_views(&self) -> &[Arc<dyn View>] {
268 &self.global_views
269 }
270
271 pub fn get_global_view(&self, view_name: &str) -> Option<Arc<dyn View>> {
272 self.global_views
273 .iter()
274 .find(|v| *(v.get_view_set_name()) == view_name)
275 .cloned()
276 }
277
278 pub fn add_global_view(&mut self, view: Arc<dyn View>) {
279 self.global_views.push(view);
280 }
281
282 pub fn add_view_set(&mut self, view_set_name: String, maker: Arc<dyn ViewMaker>) {
283 self.view_sets.insert(view_set_name, maker);
284 }
285
286 pub fn get_view_sets(&self) -> &HashMap<String, Arc<dyn ViewMaker>> {
287 &self.view_sets
288 }
289
290 pub fn make_view(&self, view_set_name: &str, view_instance_id: &str) -> Result<Arc<dyn View>> {
291 if let Some(maker) = self.view_sets.get(view_set_name) {
292 maker.make_view(view_instance_id)
293 } else {
294 anyhow::bail!("view set {view_set_name} not found");
295 }
296 }
297}
298
299/// Creates the default `ViewFactory` with all built-in views.
300///
301/// `default_audience` is the deployment's `MICROMEGAS_DEFAULT_AUDIENCE`, which the
302/// `blocks` view binds when it materializes. Callers source it from
303/// `LakehouseContext::default_audience` so every role that builds a factory bakes the same value
304/// into partitions -- notably the maintenance role, which is the one that actually materializes
305/// the five global views. `log_stats` is no longer one of them: it is a seeded row in
306/// `lakehouse_view_set_definitions`, folded in by `ViewRegistry` rather than by this function.
307pub async fn default_view_factory(
308 runtime: Arc<RuntimeEnv>,
309 lake: Arc<DataLakeConnection>,
310 default_audience: Arc<str>,
311) -> Result<ViewFactory> {
312 let blocks_view = Arc::new(BlocksView::new(default_audience)?);
313 let processes_view = Arc::new(
314 make_processes_view(
315 runtime.clone(),
316 lake.clone(),
317 Arc::new(ViewFactory::new(vec![blocks_view.clone()])),
318 )
319 .await?,
320 );
321 let streams_view = Arc::new(
322 make_streams_view(
323 runtime.clone(),
324 lake.clone(),
325 Arc::new(ViewFactory::new(vec![blocks_view.clone()])),
326 )
327 .await?,
328 );
329 let images_view_maker = Arc::new(ImagesViewMaker {});
330 let log_view_maker = Arc::new(LogViewMaker {});
331 let metrics_view_maker = Arc::new(MetricsViewMaker {});
332
333 // Create base views first
334 let global_views = vec![
335 log_view_maker.make_view("global")?,
336 metrics_view_maker.make_view("global")?,
337 processes_view,
338 streams_view,
339 blocks_view,
340 ];
341 let mut factory = ViewFactory::new(global_views);
342 factory.add_view_set(String::from("images"), images_view_maker);
343 factory.add_view_set(String::from("log_entries"), log_view_maker.clone());
344 factory.add_view_set(String::from("measures"), metrics_view_maker);
345
346 // `log_stats` no longer joins it here -- it is seeded into `lakehouse_view_set_definitions`
347 // and folded in by `ViewRegistry` instead (see the module doc comment).
348 let mut updated_factory = factory;
349
350 // Add async_events view maker
351 updated_factory.add_view_set(
352 String::from("async_events"),
353 Arc::new(AsyncEventsViewMaker::new(Arc::new(updated_factory.clone()))),
354 );
355
356 // Add net_spans view maker
357 updated_factory.add_view_set(
358 String::from("net_spans"),
359 Arc::new(NetSpansViewMaker::new(Arc::new(updated_factory.clone()))),
360 );
361
362 // Add thread_spans view maker
363 updated_factory.add_view_set(
364 String::from("thread_spans"),
365 Arc::new(ThreadSpansViewMaker::new(Arc::new(updated_factory.clone()))),
366 );
367
368 // Add otel_spans view maker (per-process JIT only — no global instance)
369 updated_factory.add_view_set(String::from("otel_spans"), Arc::new(OtelSpansViewMaker {}));
370
371 Ok(updated_factory)
372}