Skip to main content

micromegas_analytics/lakehouse/
log_stats_view.rs

1use super::{
2    session_configurator::NoOpSessionConfigurator,
3    sql_batch_view::SqlBatchView,
4    view_definition::{ViewDefinition, ViewOptions, build_sql_batch_view},
5    view_factory::ViewFactory,
6};
7use anyhow::Result;
8use datafusion::execution::runtime_env::RuntimeEnv;
9use micromegas_ingestion::data_lake_connection::DataLakeConnection;
10use std::sync::Arc;
11
12/// Query to count source rows in the time range by summing nb_objects from log blocks only.
13const COUNT_SRC_QUERY: &str = r#"
14        SELECT sum(nb_objects) as count
15        FROM blocks
16        WHERE array_has("streams.tags", 'log')
17        AND insert_time >= '{begin}'
18        AND insert_time < '{end}'
19        "#;
20
21// Transform query to aggregate logs by time bin, process, level, and target. No ORDER BY is
22// written here -- the extract path applies the sort from the with_merge_sort_order columns
23// below (see sql_partition_spec::plan_sorted_extract) before recording the
24// (time_bin, process_id, level, target) sort_order guarantee, never reaching this SQL text.
25//
26// `audience` joins the GROUP BY: `log_entries.audience` is a per-row stamp, and a single
27// `process_id` can still span two audiences, so grouping on it too keeps those rows separate
28// instead of letting `max(audience)` collapse them into one mislabelled row. It does **not**
29// join the declared `with_merge_sort_order` columns below -- see that builder's doc comment
30// for why an extra, unordered `GROUP BY` key degrades the merge query's `InputOrderMode` to
31// `PartiallySorted` rather than blocking streaming aggregation outright.
32const EXTRACT_QUERY: &str = r#"
33        SELECT date_bin('1 minute', time) as time_bin,
34               process_id,
35               level,
36               target,
37               count(*) as count,
38               arrow_cast(max(audience), 'Dictionary(Int32, Utf8)') as audience
39        FROM log_entries
40        WHERE insert_time >= '{begin}'
41        AND insert_time < '{end}'
42        GROUP BY process_id, level, target, time_bin, audience
43        "#;
44
45// Merge query to combine partitions. No ORDER BY is written here -- QueryMerger applies the
46// sort as a DataFusion logical-plan node from the with_merge_sort_order columns below, never
47// reaching this SQL text. `audience` joins this GROUP BY too, for the same reason as the
48// transform query above.
49const MERGE_QUERY: &str = r#"
50        SELECT time_bin,
51               process_id,
52               level,
53               target,
54               sum(count) as count,
55               arrow_cast(max(audience), 'Dictionary(Int32, Utf8)') as audience
56        FROM {source}
57        GROUP BY process_id, level, target, time_bin, audience
58        "#;
59
60/// `log_stats`'s definition, expressed as a `ViewDefinition` -- the single source of truth shared
61/// by the migration seed (`migration.rs`'s `upgrade_v9_to_v10`), the parser round-trip test, and
62/// [`make_log_stats_view`] below. `log_stats` is a `SqlBatchView` whose only distinction from a
63/// DDL-defined view is that its SQL lives in Rust rather than in a `lakehouse_view_set_definitions`
64/// row -- this function is what lets the migration seed that row.
65pub fn log_stats_view_definition() -> ViewDefinition {
66    ViewDefinition {
67        view_set_name: "log_stats".to_string(),
68        extract_query: EXTRACT_QUERY.to_string(),
69        count_src_query: COUNT_SRC_QUERY.to_string(),
70        merge_partitions_query: MERGE_QUERY.to_string(),
71        update_group: 3000,
72        options: ViewOptions {
73            min_time_column: "time_bin".to_string(),
74            max_time_column: "time_bin".to_string(),
75            source_partition_delta: "1 day".to_string(),
76            merge_partition_delta: "1 day".to_string(),
77            // Time first: keeps merged partitions time-local, preserving row-group pruning on
78            // time_bin for user queries. GROUP BY key order is irrelevant to streaming, so any
79            // prefix of these four columns would stream too -- this is the full declared order.
80            merge_sort_order: Some(vec![
81                "time_bin".to_string(),
82                "process_id".to_string(),
83                "level".to_string(),
84                "target".to_string(),
85            ]),
86        },
87    }
88}
89
90/// Assembles `log_stats_view_definition()`'s equivalent `CREATE MATERIALIZED VIEW log_stats
91/// WITH (...)` DDL text, for the migration seed's `definition_sql` column (kept for display and
92/// audit only -- never re-parsed). Each query is wrapped in `$$...$$`; none of them contains a
93/// `$$`, so no quote-escaping helper is needed.
94pub fn log_stats_ddl_text() -> String {
95    let def = log_stats_view_definition();
96    format!(
97        "CREATE MATERIALIZED VIEW {} WITH (\n\
98         \x20 extract_query = $${}$$,\n\
99         \x20 count_src_query = $${}$$,\n\
100         \x20 merge_partitions_query = $${}$$,\n\
101         \x20 update_group = {},\n\
102         \x20 time_column = '{}',\n\
103         \x20 source_partition_delta = '{}',\n\
104         \x20 merge_partition_delta = '{}',\n\
105         \x20 merge_sort_order = '{}'\n\
106         )",
107        def.view_set_name,
108        def.extract_query,
109        def.count_src_query,
110        def.merge_partitions_query,
111        def.update_group,
112        def.options.min_time_column,
113        def.options.source_partition_delta,
114        def.options.merge_partition_delta,
115        def.options
116            .merge_sort_order
117            .as_ref()
118            .map(|cols| cols.join(", "))
119            .unwrap_or_default(),
120    )
121}
122
123/// Creates a new `SqlBatchView` for log statistics aggregated by process, minute, level, and
124/// target -- `build_sql_batch_view` over [`log_stats_view_definition`], so `log_stats` is built
125/// through the exact same path a DDL-defined view is.
126pub async fn make_log_stats_view(
127    runtime: Arc<RuntimeEnv>,
128    lake: Arc<DataLakeConnection>,
129    view_factory: Arc<ViewFactory>,
130) -> Result<SqlBatchView> {
131    build_sql_batch_view(
132        &log_stats_view_definition(),
133        runtime,
134        lake,
135        view_factory,
136        Arc::new(NoOpSessionConfigurator),
137    )
138    .await
139}