1use 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#[derive(Debug)]
46pub(crate) struct StatementLogging {
47 executions_begun: BTreeMap<Uuid, StatementBeganExecutionRecord>,
54
55 unlogged_sessions: BTreeMap<Uuid, SessionHistoryEvent>,
59
60 reproducible_rng: Arc<Mutex<rand_chacha::ChaCha8Rng>>,
65
66 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 pub(crate) throttling_state: Arc<ThrottlingState>,
74
75 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 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 fn write_began_execution_events(
119 &mut self,
120 record: StatementBeganExecutionRecord,
121 mseh_update: Row,
122 prepared_statement: Option<PreparedStatementEvent>,
123 ) {
124 self.statement_logging
126 .pending_statement_execution_events
127 .push((mseh_update, Diff::ONE));
128
129 self.statement_logging
131 .executions_begun
132 .insert(record.id, record);
133
134 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 self.statement_logging
741 .throttling_state
742 .increment_throttled_count();
743 return None;
744 }
745 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 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 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 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}