Skip to main content

mz_adapter/coord/
statement_logging.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use std::collections::BTreeMap;
11use std::sync::{Arc, Mutex};
12use std::time::Duration;
13
14use mz_adapter_types::connection::ConnectionId;
15use mz_compute_client::controller::error::CollectionLookupError;
16use mz_controller_types::ClusterId;
17use mz_ore::now::{EpochMillis, NowFn, epoch_to_uuid_v7, to_datetime};
18use mz_ore::task::spawn;
19use mz_ore::{cast::CastFrom, cast::CastInto};
20use mz_repr::adt::timestamp::TimestampLike;
21use mz_repr::{Datum, Diff, GlobalId, Row, Timestamp};
22use mz_sql::plan::Params;
23use mz_sql::session::metadata::SessionMetadata;
24use mz_storage_client::controller::IntrospectionType;
25use qcell::QCell;
26use rand::SeedableRng;
27use sha2::{Digest, Sha256};
28use tokio::time::MissedTickBehavior;
29use uuid::Uuid;
30
31use crate::coord::{ConnMeta, Coordinator, WatchSetResponse};
32use crate::session::{LifecycleTimestamps, Session};
33use crate::statement_logging::{
34    FrontendStatementLoggingEvent, PreparedStatementEvent, PreparedStatementLoggingInfo,
35    SessionHistoryEvent, StatementBeganExecutionRecord, StatementEndedExecutionReason,
36    StatementEndedExecutionRecord, StatementLifecycleEvent, StatementLoggingFrontend,
37    StatementLoggingId, StatementPreparedRecord, ThrottlingState, WatchSetCreation,
38    create_began_execution_record, effective_sample_rate, pack_statement_began_execution_update,
39    pack_statement_execution_inner, pack_statement_prepared_update, should_sample_statement,
40};
41
42use super::Message;
43
44/// Statement logging state in the Coordinator.
45#[derive(Debug)]
46pub(crate) struct StatementLogging {
47    /// Information about statement executions that have been logged
48    /// but not finished.
49    ///
50    /// This map needs to have enough state left over to later retract
51    /// the system table entries (so that we can update them when the
52    /// execution finished.)
53    executions_begun: BTreeMap<Uuid, StatementBeganExecutionRecord>,
54
55    /// Information about sessions that have been started, but which
56    /// have not yet been logged in `mz_session_history`.
57    /// They may be logged as part of a statement being executed (and chosen for logging).
58    unlogged_sessions: BTreeMap<Uuid, SessionHistoryEvent>,
59
60    /// A reproducible RNG for deciding whether to sample statement executions.
61    /// Only used by tests; otherwise, `rand::rng()` is used.
62    /// Controlled by the system var `statement_logging_use_reproducible_rng`.
63    /// This same instance will be used by all frontend tasks.
64    reproducible_rng: Arc<Mutex<rand_chacha::ChaCha8Rng>>,
65
66    /// Events to be persisted periodically.
67    pending_statement_execution_events: Vec<(Row, Diff)>,
68    pending_prepared_statement_events: Vec<PreparedStatementEvent>,
69    pending_session_events: Vec<Row>,
70    pending_statement_lifecycle_events: Vec<Row>,
71
72    /// Shared throttling state for rate-limiting statement logging.
73    pub(crate) throttling_state: Arc<ThrottlingState>,
74
75    /// Function to get the current time.
76    pub(crate) now: NowFn,
77}
78
79impl StatementLogging {
80    const REPRODUCIBLE_RNG_SEED: u64 = 42;
81
82    pub(crate) fn new(now: NowFn) -> Self {
83        Self {
84            executions_begun: BTreeMap::new(),
85            unlogged_sessions: BTreeMap::new(),
86            reproducible_rng: Arc::new(Mutex::new(rand_chacha::ChaCha8Rng::seed_from_u64(
87                Self::REPRODUCIBLE_RNG_SEED,
88            ))),
89            pending_statement_execution_events: Vec::new(),
90            pending_prepared_statement_events: Vec::new(),
91            pending_session_events: Vec::new(),
92            pending_statement_lifecycle_events: Vec::new(),
93            throttling_state: Arc::new(ThrottlingState::new(&now)),
94            now,
95        }
96    }
97
98    /// Create a `StatementLoggingFrontend` for use by frontend peek sequencing.
99    ///
100    /// This provides the frontend with all the state it needs to perform statement
101    /// logging without direct access to the Coordinator.
102    pub(crate) fn create_frontend(
103        &self,
104        build_info_human_version: String,
105    ) -> StatementLoggingFrontend {
106        StatementLoggingFrontend {
107            throttling_state: Arc::clone(&self.throttling_state),
108            reproducible_rng: Arc::clone(&self.reproducible_rng),
109            build_info_human_version,
110            now: self.now.clone(),
111        }
112    }
113}
114
115impl Coordinator {
116    /// Helper to write began execution events to pending buffers.
117    /// Can be called from both old and new peek sequencing.
118    fn write_began_execution_events(
119        &mut self,
120        record: StatementBeganExecutionRecord,
121        mseh_update: Row,
122        prepared_statement: Option<PreparedStatementEvent>,
123    ) {
124        // `mz_statement_execution_history`
125        self.statement_logging
126            .pending_statement_execution_events
127            .push((mseh_update, Diff::ONE));
128
129        // Track the execution for later updates
130        self.statement_logging
131            .executions_begun
132            .insert(record.id, record);
133
134        // If we have a prepared statement, log it and possibly its session
135        if let Some(ps_event) = prepared_statement {
136            let session_id = ps_event.session_id;
137            self.statement_logging
138                .pending_prepared_statement_events
139                .push(ps_event);
140
141            // Check if we need to log the session for this prepared statement
142            if let Some(sh) = self.statement_logging.unlogged_sessions.remove(&session_id) {
143                let sh_update = Self::pack_session_history_update(&sh);
144                self.statement_logging
145                    .pending_session_events
146                    .push(sh_update);
147            }
148        }
149    }
150
151    /// Handle a statement logging event from frontend peek sequencing.
152    pub(crate) fn handle_frontend_statement_logging_event(
153        &mut self,
154        event: FrontendStatementLoggingEvent,
155    ) {
156        match event {
157            FrontendStatementLoggingEvent::BeganExecution {
158                record,
159                mseh_update,
160                prepared_statement,
161            } => {
162                self.record_statement_lifecycle_event(
163                    &StatementLoggingId(record.id),
164                    &StatementLifecycleEvent::ExecutionBegan,
165                    record.began_at,
166                );
167                self.write_began_execution_events(record, mseh_update, prepared_statement);
168            }
169            FrontendStatementLoggingEvent::EndedExecution(ended_record) => {
170                self.end_statement_execution(
171                    StatementLoggingId(ended_record.id),
172                    ended_record.reason,
173                    ended_record.ended_at,
174                );
175            }
176            FrontendStatementLoggingEvent::SetCluster {
177                id,
178                cluster_id,
179                cluster_name,
180            } => {
181                self.set_statement_execution_cluster(id, cluster_id, cluster_name);
182            }
183            FrontendStatementLoggingEvent::SetTimestamp { id, timestamp } => {
184                self.set_statement_execution_timestamp(id, timestamp);
185            }
186            FrontendStatementLoggingEvent::SetTransientIndex {
187                id,
188                transient_index_id,
189            } => {
190                self.set_transient_index_id(id, transient_index_id);
191            }
192            FrontendStatementLoggingEvent::Lifecycle { id, event, when } => {
193                self.record_statement_lifecycle_event(&id, &event, when);
194            }
195        }
196    }
197
198    // TODO[btv] make this configurable via LD?
199    // Although... Logging every 5 seconds seems like it
200    // should have acceptable cost for now, since we do a
201    // group commit for tables every 1s anyway.
202    const STATEMENT_LOGGING_WRITE_INTERVAL: Duration = Duration::from_secs(5);
203
204    pub(crate) fn spawn_statement_logging_task(&self) {
205        let internal_cmd_tx = self.internal_cmd_tx.clone();
206        spawn(|| "statement_logging", async move {
207            let mut interval = tokio::time::interval(Coordinator::STATEMENT_LOGGING_WRITE_INTERVAL);
208            interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
209            loop {
210                interval.tick().await;
211                let _ = internal_cmd_tx.send(Message::DrainStatementLog);
212            }
213        });
214    }
215
216    #[mz_ore::instrument(level = "debug")]
217    pub(crate) fn drain_statement_log(&mut self) {
218        let session_updates = std::mem::take(&mut self.statement_logging.pending_session_events)
219            .into_iter()
220            .map(|update| (update, Diff::ONE))
221            .collect();
222        let (prepared_statement_updates, sql_text_updates) =
223            std::mem::take(&mut self.statement_logging.pending_prepared_statement_events)
224                .into_iter()
225                .map(
226                    |PreparedStatementEvent {
227                         prepared_statement,
228                         sql_text,
229                         ..
230                     }| {
231                        ((prepared_statement, Diff::ONE), (sql_text, Diff::ONE))
232                    },
233                )
234                .unzip::<_, _, Vec<_>, Vec<_>>();
235        let statement_execution_updates =
236            std::mem::take(&mut self.statement_logging.pending_statement_execution_events);
237        let statement_lifecycle_updates =
238            std::mem::take(&mut self.statement_logging.pending_statement_lifecycle_events)
239                .into_iter()
240                .map(|update| (update, Diff::ONE))
241                .collect();
242
243        use IntrospectionType::*;
244        for (type_, updates) in [
245            (SessionHistory, session_updates),
246            (PreparedStatementHistory, prepared_statement_updates),
247            (StatementExecutionHistory, statement_execution_updates),
248            (StatementLifecycleHistory, statement_lifecycle_updates),
249            (SqlText, sql_text_updates),
250        ] {
251            if !updates.is_empty() && !self.controller.read_only() {
252                self.controller
253                    .storage
254                    .append_introspection_updates(type_, updates);
255            }
256        }
257    }
258
259    /// Check whether we need to do throttling (i.e., whether `STATEMENT_LOGGING_TARGET_DATA_RATE` is set).
260    /// If so, actually do the check.
261    ///
262    /// We expect `rows` to be the list of rows we intend to record and calculate the cost by summing the
263    /// byte lengths of the rows.
264    ///
265    /// Returns `false` if we must throttle this statement, and `true` otherwise.
266    fn statement_logging_throttling_check<'a, I>(&self, rows: I) -> bool
267    where
268        I: IntoIterator<Item = Option<&'a Row>>,
269    {
270        let cost = rows
271            .into_iter()
272            .filter_map(|row_opt| row_opt.map(|row| row.byte_len()))
273            .fold(0_usize, |acc, x| acc.saturating_add(x));
274
275        let Some(target_data_rate) = self
276            .catalog
277            .system_config()
278            .statement_logging_target_data_rate()
279        else {
280            return true;
281        };
282        let max_data_credit = self
283            .catalog
284            .system_config()
285            .statement_logging_max_data_credit();
286
287        self.statement_logging.throttling_state.throttling_check(
288            cost.cast_into(),
289            target_data_rate.cast_into(),
290            max_data_credit.map(CastInto::cast_into),
291            &self.statement_logging.now,
292        )
293    }
294
295    /// Marks a prepared statement as "already logged".
296    /// Mutates the `PreparedStatementLoggingInfo` metadata.
297    fn record_prepared_statement_as_logged(
298        &self,
299        uuid: Uuid,
300        session: &mut Session,
301        logging: &Arc<QCell<PreparedStatementLoggingInfo>>,
302    ) {
303        let logging = session.qcell_rw(&*logging);
304        if let PreparedStatementLoggingInfo::StillToLog { kind, .. } = logging {
305            let kind = *kind;
306            *logging = PreparedStatementLoggingInfo::AlreadyLogged { uuid, kind };
307        }
308    }
309
310    /// Returns any statement logging events needed for a particular
311    /// prepared statement. This is a read-only operation that does not mutate
312    /// the `PreparedStatementLoggingInfo` metadata.
313    ///
314    /// This function does not do a sampling check, and assumes we did so in a higher layer.
315    /// It also does not do a throttling check - that is done separately in `begin_statement_execution`.
316    ///
317    /// Returns a tuple containing:
318    /// - `Option<(StatementPreparedRecord, PreparedStatementEvent)>`: If the prepared statement
319    ///   has not yet been logged, returns the prepared statement record and the packed rows.
320    /// - `Uuid`: The UUID of the prepared statement.
321    pub(crate) fn get_prepared_statement_info(
322        &self,
323        session: &Session,
324        logging: &Arc<QCell<PreparedStatementLoggingInfo>>,
325    ) -> (
326        Option<(StatementPreparedRecord, PreparedStatementEvent)>,
327        Uuid,
328    ) {
329        let logging = session.qcell_ro(&*logging);
330
331        match logging {
332            PreparedStatementLoggingInfo::AlreadyLogged { uuid, .. } => (None, *uuid),
333            PreparedStatementLoggingInfo::StillToLog {
334                sql,
335                redacted_sql,
336                prepared_at,
337                name,
338                session_id,
339                accounted,
340                kind,
341                _sealed: _,
342            } => {
343                assert!(
344                    *accounted,
345                    "accounting for logging should be done in `begin_statement_execution`"
346                );
347                let uuid = epoch_to_uuid_v7(prepared_at);
348                let sql_hash: [u8; 32] = Sha256::digest(sql.as_bytes()).into();
349                let record = StatementPreparedRecord {
350                    id: uuid,
351                    sql_hash,
352                    name: name.to_string(),
353                    session_id: *session_id,
354                    prepared_at: *prepared_at,
355                    kind: *kind,
356                };
357
358                // `mz_prepared_statement_history`
359                let mut mpsh_row = Row::default();
360                let mut mpsh_packer = mpsh_row.packer();
361                pack_statement_prepared_update(&record, &mut mpsh_packer);
362                let throttled_count = self
363                    .statement_logging
364                    .throttling_state
365                    .get_throttled_count();
366                mpsh_packer.push(Datum::UInt64(CastFrom::cast_from(throttled_count)));
367
368                let sql_row = Row::pack([
369                    Datum::TimestampTz(
370                        to_datetime(*prepared_at)
371                            .truncate_day()
372                            .try_into()
373                            .expect("must fit"),
374                    ),
375                    Datum::Bytes(sql_hash.as_slice()),
376                    Datum::String(sql.as_str()),
377                    Datum::String(redacted_sql.as_str()),
378                ]);
379
380                (
381                    Some((
382                        record,
383                        PreparedStatementEvent {
384                            prepared_statement: mpsh_row,
385                            sql_text: sql_row,
386                            session_id: *session_id,
387                        },
388                    )),
389                    uuid,
390                )
391            }
392        }
393    }
394
395    /// Record the end of statement execution for a statement whose beginning
396    /// was logged. Ends are idempotent: the first end wins and later ends for
397    /// the same statement are ignored.
398    ///
399    /// `ended_at` is when execution finished, which is not the same as when this
400    /// runs. A statement sequenced off the coordinator loop finishes in its
401    /// session task and reports the end as a message, so taking the clock here
402    /// would charge it for however long that message sat in the queue.
403    pub(crate) fn end_statement_execution(
404        &mut self,
405        id: StatementLoggingId,
406        reason: StatementEndedExecutionReason,
407        ended_at: EpochMillis,
408    ) {
409        let StatementLoggingId(uuid) = id;
410        let ended_record = StatementEndedExecutionRecord {
411            id: uuid,
412            reason,
413            ended_at,
414        };
415
416        let Some(began_record) = self.statement_logging.executions_begun.remove(&uuid) else {
417            // The statement was already ended; the first end wins.
418            //
419            // A missing entry can only mean a duplicate end, never an end that
420            // overtook its begin: a StatementLoggingId is only minted when a
421            // begin is logged, and begins travel the same FIFO command channel
422            // as the commands that hand statements to the coordinator, so the
423            // coordinator never ends a statement before processing its begin.
424            //
425            // Duplicate ends are legitimate, if rare. Ownership of the end is
426            // handed from the frontend to the coordinator while a statement is
427            // dispatched, and async cancellation can strike mid-handoff: if a
428            // client disconnect drops the frontend future after the
429            // coordinator registered a peek but before the frontend defused
430            // its logging guard, both sides own the end and both emit one.
431            tracing::warn!(
432                statement_uuid = %uuid,
433                reason = ?ended_record.reason,
434                "duplicate end_statement_execution, keeping the first end",
435            );
436            return;
437        };
438        for (row, diff) in
439            Self::pack_statement_ended_execution_updates(&began_record, &ended_record)
440        {
441            self.statement_logging
442                .pending_statement_execution_events
443                .push((row, diff));
444        }
445        self.record_statement_lifecycle_event(
446            &id,
447            &StatementLifecycleEvent::ExecutionFinished,
448            ended_at,
449        );
450    }
451
452    fn pack_session_history_update(event: &SessionHistoryEvent) -> Row {
453        let SessionHistoryEvent {
454            id,
455            connected_at,
456            application_name,
457            authenticated_user,
458        } = event;
459        Row::pack_slice(&[
460            Datum::Uuid(*id),
461            Datum::TimestampTz(to_datetime(*connected_at).try_into().expect("must fit")),
462            Datum::String(&*application_name),
463            Datum::String(&*authenticated_user),
464        ])
465    }
466
467    fn pack_statement_lifecycle_event(
468        StatementLoggingId(uuid): &StatementLoggingId,
469        event: &StatementLifecycleEvent,
470        when: EpochMillis,
471    ) -> Row {
472        Row::pack_slice(&[
473            Datum::Uuid(*uuid),
474            Datum::String(event.as_str()),
475            Datum::TimestampTz(to_datetime(when).try_into().expect("must fit")),
476        ])
477    }
478
479    fn pack_full_statement_execution_update(
480        began_record: &StatementBeganExecutionRecord,
481        ended_record: &StatementEndedExecutionRecord,
482    ) -> Row {
483        let mut row = Row::default();
484        let mut packer = row.packer();
485        pack_statement_execution_inner(began_record, &mut packer);
486        let (status, error_message, result_size, rows_returned, execution_strategy) =
487            match &ended_record.reason {
488                StatementEndedExecutionReason::Success {
489                    result_size,
490                    rows_returned,
491                    execution_strategy,
492                } => (
493                    "success",
494                    None,
495                    result_size.map(|rs| i64::try_from(rs).expect("must fit")),
496                    rows_returned.map(|rr| i64::try_from(rr).expect("must fit")),
497                    execution_strategy.map(|es| es.name()),
498                ),
499                StatementEndedExecutionReason::Canceled => ("canceled", None, None, None, None),
500                StatementEndedExecutionReason::Errored { error } => {
501                    // Backstop for SQL-435: `CREATE SECRET`/`ALTER SECRET` errors
502                    // can embed secret material (the rejected statement text, or a
503                    // value-bearing eval error). The per-site fixes redact the known
504                    // cases, but we never persist an unredacted error for these
505                    // kinds, regardless of which error variant fired. The client
506                    // still receives the real error over pgwire.
507                    let error = if began_record.kind.is_some_and(|kind| kind.is_secret()) {
508                        "<error redacted for secret statement>"
509                    } else {
510                        error.as_str()
511                    };
512                    ("error", Some(error), None, None, None)
513                }
514                StatementEndedExecutionReason::Aborted => ("aborted", None, None, None, None),
515            };
516        packer.extend([
517            Datum::TimestampTz(
518                to_datetime(ended_record.ended_at)
519                    .try_into()
520                    .expect("Sane system time"),
521            ),
522            status.into(),
523            error_message.into(),
524            result_size.into(),
525            rows_returned.into(),
526            execution_strategy.into(),
527        ]);
528        row
529    }
530
531    fn pack_statement_ended_execution_updates(
532        began_record: &StatementBeganExecutionRecord,
533        ended_record: &StatementEndedExecutionRecord,
534    ) -> [(Row, Diff); 2] {
535        let retraction = pack_statement_began_execution_update(began_record);
536        let new = Self::pack_full_statement_execution_update(began_record, ended_record);
537        [(retraction, Diff::MINUS_ONE), (new, Diff::ONE)]
538    }
539
540    /// Mutate a statement execution record via the given function `f`.
541    fn mutate_record<F: FnOnce(&mut StatementBeganExecutionRecord)>(
542        &mut self,
543        StatementLoggingId(id): StatementLoggingId,
544        f: F,
545    ) {
546        let record = self
547            .statement_logging
548            .executions_begun
549            .get_mut(&id)
550            .expect("mutate_record must not be called after execution ends");
551        let retraction = pack_statement_began_execution_update(record);
552        self.statement_logging
553            .pending_statement_execution_events
554            .push((retraction, Diff::MINUS_ONE));
555        f(record);
556        let update = pack_statement_began_execution_update(record);
557        self.statement_logging
558            .pending_statement_execution_events
559            .push((update, Diff::ONE));
560    }
561
562    /// Set the `cluster_id` and `cluster_name` for a statement, once they're known.
563    ///
564    /// The name is resolved by the caller (from the same catalog snapshot that selected the
565    /// cluster), rather than re-resolved here from `cluster_id`. This avoids a panic when the
566    /// cluster was concurrently dropped, and logs the name as it was at selection time.
567    ///
568    /// TODO(peek-seq): We could do the packing in the frontend task, and just send over the rows.
569    pub(crate) fn set_statement_execution_cluster(
570        &mut self,
571        id: StatementLoggingId,
572        cluster_id: ClusterId,
573        cluster_name: String,
574    ) {
575        self.mutate_record(id, |record| {
576            record.cluster_name = Some(cluster_name);
577            record.cluster_id = Some(cluster_id);
578        });
579    }
580
581    /// Sets the execution timestamp if the statement is still active.
582    ///
583    /// A duplicate end can race the asynchronous group-commit update, so an ended statement is
584    /// skipped rather than treated as corruption.
585    pub(crate) fn set_statement_execution_timestamp(
586        &mut self,
587        id: StatementLoggingId,
588        timestamp: Timestamp,
589    ) {
590        let StatementLoggingId(uuid) = id;
591        if !self.statement_logging.executions_begun.contains_key(&uuid) {
592            tracing::warn!(
593                statement_uuid = %uuid,
594                "execution already ended, skipping execution timestamp update",
595            );
596            return;
597        }
598        self.mutate_record(id, |record| {
599            record.execution_timestamp = Some(u64::from(timestamp));
600        });
601    }
602
603    pub(crate) fn set_transient_index_id(
604        &mut self,
605        id: StatementLoggingId,
606        transient_index_id: GlobalId,
607    ) {
608        self.mutate_record(id, |record| {
609            record.transient_index_id = Some(transient_index_id)
610        });
611    }
612
613    /// Possibly record the beginning of statement execution, depending on a randomly-chosen value.
614    /// If the execution beginning was indeed logged, returns a `StatementLoggingId` that must be
615    /// passed to `end_statement_execution` to record when it ends.
616    ///
617    /// `lifecycle_timestamps` has timestamps that come from the Adapter frontend (`mz-pgwire`) part
618    /// of the lifecycle.
619    pub(crate) fn begin_statement_execution(
620        &mut self,
621        session: &mut Session,
622        params: &Params,
623        logging: &Arc<QCell<PreparedStatementLoggingInfo>>,
624        lifecycle_timestamps: Option<LifecycleTimestamps>,
625    ) -> Option<StatementLoggingId> {
626        let enable_internal_statement_logging = self
627            .catalog()
628            .system_config()
629            .enable_internal_statement_logging();
630        if session.user().is_internal() && !enable_internal_statement_logging {
631            return None;
632        }
633
634        let sample_rate = effective_sample_rate(session, self.catalog().system_config());
635        let use_reproducible_rng = self
636            .catalog()
637            .system_config()
638            .statement_logging_use_reproducible_rng();
639        // Only lock the RNG when we actually need reproducible sampling (tests only)
640        let sample = if use_reproducible_rng {
641            let mut rng = self
642                .statement_logging
643                .reproducible_rng
644                .lock()
645                .expect("rng lock poisoned");
646            should_sample_statement(sample_rate, Some(&mut *rng))
647        } else {
648            should_sample_statement(sample_rate, None)
649        };
650
651        // Figure out the cost of everything before we log.
652
653        // Track how many statements we're recording.
654        let sampled_label = sample.then_some("true").unwrap_or("false");
655        self.metrics
656            .statement_logging_records
657            .with_label_values(&[sampled_label])
658            .inc_by(1);
659
660        if let Some((sql, accounted)) = match session.qcell_rw(logging) {
661            PreparedStatementLoggingInfo::AlreadyLogged { .. } => None,
662            PreparedStatementLoggingInfo::StillToLog { sql, accounted, .. } => {
663                Some((sql, accounted))
664            }
665        } {
666            if !*accounted {
667                self.metrics
668                    .statement_logging_unsampled_bytes
669                    .inc_by(u64::cast_from(sql.len()));
670                if sample {
671                    self.metrics
672                        .statement_logging_actual_bytes
673                        .inc_by(u64::cast_from(sql.len()));
674                }
675                *accounted = true;
676            }
677        }
678        if !sample {
679            return None;
680        }
681
682        let (maybe_ps, ps_uuid) = self.get_prepared_statement_info(session, logging);
683
684        let began_at = if let Some(lifecycle_timestamps) = lifecycle_timestamps {
685            lifecycle_timestamps.received
686        } else {
687            self.now()
688        };
689        let now = self.now();
690        let execution_uuid = epoch_to_uuid_v7(&now);
691
692        let build_info_version = self
693            .catalog()
694            .state()
695            .config()
696            .build_info
697            .human_version(None);
698        let kind = session.qcell_ro(logging).kind();
699        let record = create_began_execution_record(
700            execution_uuid,
701            ps_uuid,
702            sample_rate,
703            params,
704            session,
705            began_at,
706            build_info_version,
707            kind,
708        );
709
710        // `mz_statement_execution_history`
711        let mseh_update = pack_statement_began_execution_update(&record);
712
713        let (maybe_ps_event, maybe_sh_event) = if let Some((ps_record, ps_event)) = maybe_ps {
714            if let Some(sh) = self
715                .statement_logging
716                .unlogged_sessions
717                .get(&ps_record.session_id)
718            {
719                (
720                    Some(ps_event),
721                    Some((Self::pack_session_history_update(sh), ps_record.session_id)),
722                )
723            } else {
724                (Some(ps_event), None)
725            }
726        } else {
727            (None, None)
728        };
729
730        let maybe_ps_prepared_statement = maybe_ps_event.as_ref().map(|e| &e.prepared_statement);
731        let maybe_ps_sql_text = maybe_ps_event.as_ref().map(|e| &e.sql_text);
732
733        if !self.statement_logging_throttling_check([
734            Some(&mseh_update),
735            maybe_ps_prepared_statement,
736            maybe_ps_sql_text,
737            maybe_sh_event.as_ref().map(|(row, _)| row),
738        ]) {
739            // Increment throttled_count in shared state
740            self.statement_logging
741                .throttling_state
742                .increment_throttled_count();
743            return None;
744        }
745        // When we successfully log the first instance of a prepared statement
746        // (i.e., it is not throttled), we also capture the number of previously
747        // throttled statement executions in the builtin prepared statement history table above,
748        // and then reset the throttled count for future tracking.
749        else if let PreparedStatementLoggingInfo::StillToLog { .. } = session.qcell_ro(logging) {
750            self.statement_logging
751                .throttling_state
752                .reset_throttled_count();
753        }
754
755        self.record_prepared_statement_as_logged(ps_uuid, session, logging);
756
757        self.record_statement_lifecycle_event(
758            &StatementLoggingId(execution_uuid),
759            &StatementLifecycleEvent::ExecutionBegan,
760            began_at,
761        );
762
763        self.statement_logging
764            .pending_statement_execution_events
765            .push((mseh_update, Diff::ONE));
766        self.statement_logging
767            .executions_begun
768            .insert(execution_uuid, record);
769
770        if let Some((sh_update, session_id)) = maybe_sh_event {
771            self.statement_logging
772                .pending_session_events
773                .push(sh_update);
774            // Mark the session as logged to avoid logging it again in the future
775            self.statement_logging.unlogged_sessions.remove(&session_id);
776        }
777        if let Some(ps_event) = maybe_ps_event {
778            self.statement_logging
779                .pending_prepared_statement_events
780                .push(ps_event);
781        }
782
783        Some(StatementLoggingId(execution_uuid))
784    }
785
786    /// Record a new connection event
787    pub(crate) fn begin_session_for_statement_logging(&mut self, session: &ConnMeta) {
788        let id = session.uuid();
789        let session_role = session.authenticated_role_id();
790        let event = SessionHistoryEvent {
791            id,
792            connected_at: session.connected_at(),
793            application_name: session.application_name().to_owned(),
794            authenticated_user: self.catalog.get_role(session_role).name.clone(),
795        };
796        self.statement_logging.unlogged_sessions.insert(id, event);
797    }
798
799    pub(crate) fn end_session_for_statement_logging(&mut self, uuid: Uuid) {
800        self.statement_logging.unlogged_sessions.remove(&uuid);
801    }
802
803    pub(crate) fn record_statement_lifecycle_event(
804        &mut self,
805        id: &StatementLoggingId,
806        event: &StatementLifecycleEvent,
807        when: EpochMillis,
808    ) {
809        if mz_adapter_types::dyncfgs::ENABLE_STATEMENT_LIFECYCLE_LOGGING
810            .get(self.catalog().system_config().dyncfgs())
811        {
812            let row = Self::pack_statement_lifecycle_event(id, event, when);
813            self.statement_logging
814                .pending_statement_lifecycle_events
815                .push(row);
816        }
817    }
818
819    /// Install watch sets for statement lifecycle logging.
820    ///
821    /// This installs both storage and compute watch sets that will fire
822    /// `StatementLifecycleEvent::StorageDependenciesFinished` and
823    /// `StatementLifecycleEvent::ComputeDependenciesFinished` respectively
824    /// when the dependencies are ready at the given timestamp.
825    pub(crate) fn install_peek_watch_sets(
826        &mut self,
827        conn_id: ConnectionId,
828        watch_set: WatchSetCreation,
829    ) -> Result<(), CollectionLookupError> {
830        let WatchSetCreation {
831            logging_id,
832            timestamp,
833            storage_ids,
834            compute_ids,
835        } = watch_set;
836
837        self.install_storage_watch_set(
838            conn_id.clone(),
839            storage_ids,
840            timestamp,
841            WatchSetResponse::StatementDependenciesReady(
842                logging_id,
843                StatementLifecycleEvent::StorageDependenciesFinished,
844            ),
845        )?;
846        self.install_compute_watch_set(
847            conn_id,
848            compute_ids,
849            timestamp,
850            WatchSetResponse::StatementDependenciesReady(
851                logging_id,
852                StatementLifecycleEvent::ComputeDependenciesFinished,
853            ),
854        )?;
855        Ok(())
856    }
857}