1use std::sync::atomic::{AtomicU64, Ordering};
4use std::sync::{Arc, Mutex, OnceLock};
5use std::time::{Duration, Instant};
6
7use nametrics_core::atomic::{add, AtomicHistogram, LATENCY_SECONDS};
8use nametrics_core::{
9 LegacyMetricsSource, MetricDescriptor, MetricKind, MetricSample, MetricValue,
10};
11use nanotify_core::{AlertRoute, EventKind, Notification, NotificationFields, Severity};
12
13use crate::{DatabaseDriver, MAX_MANAGED_DATASOURCES};
14
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
17pub enum StatementLogging {
18 #[default]
19 Disabled,
21 Debug,
23 Trace,
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum ConnectionPurpose {
30 Mapper,
32 Migration,
34 Probe,
36 Direct,
38}
39
40impl ConnectionPurpose {
41 pub const fn as_str(self) -> &'static str {
47 match self {
48 Self::Mapper => "mapper",
49 Self::Migration => "migration",
50 Self::Probe => "probe",
51 Self::Direct => "direct",
52 }
53 }
54}
55
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub enum AcquireOutcome {
59 Ok,
61 Timeout,
63 Closed,
65 Worker,
67 Cancelled,
69 Other,
71}
72
73impl AcquireOutcome {
74 const fn status(self) -> usize {
80 match self {
81 Self::Ok => 0,
82 Self::Cancelled => 2,
83 _ => 1,
84 }
85 }
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub enum ConnectionRejection {
91 TxRequired,
93 TxForbidden,
95 CrossDatasource,
97 CrossDriver,
99 UnknownDatasource,
101 RegistryUnavailable,
103 TxConnectionBusy,
105}
106
107#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
109pub enum WaitLogLevel {
110 Info,
112 #[default]
113 Warn,
115}
116
117#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub struct WaitObservationPolicy {
120 pub threshold: Duration,
122 pub log_enabled: bool,
124 pub log_level: WaitLogLevel,
126 pub log_cooldown: Duration,
128}
129
130pub struct AcquireAlertPolicy {
132 pub route: AlertRoute,
134 pub severity: Severity,
136 pub cooldown: Duration,
138 pub purposes: Vec<ConnectionPurpose>,
140 pub service: String,
142 pub instance: String,
144 pub environment: Option<String>,
146 pub cluster: Option<String>,
148}
149
150impl Default for WaitObservationPolicy {
151 fn default() -> Self {
157 Self {
158 threshold: Duration::from_millis(250),
159 log_enabled: false,
160 log_level: WaitLogLevel::Warn,
161 log_cooldown: Duration::from_secs(60),
162 }
163 }
164}
165
166struct WaitCells {
167 outcomes: [AtomicU64; 6],
168 durations: [AtomicHistogram; 3],
169 in_flight: AtomicU64,
170}
171
172impl WaitCells {
173 const fn new() -> Self {
179 Self {
180 outcomes: [const { AtomicU64::new(0) }; 6],
181 durations: [const { AtomicHistogram::new(LATENCY_SECONDS) }; 3],
182 in_flight: AtomicU64::new(0),
183 }
184 }
185}
186
187pub struct ConnectionSlot {
189 driver: DatabaseDriver,
190 datasource: Box<str>,
191 acquire: [WaitCells; 4],
192 transaction: WaitCells,
193 rejections: [AtomicU64; 7],
194 policies: OnceLock<(WaitObservationPolicy, WaitObservationPolicy)>,
195 log_next: [AtomicU64; 2],
196 acquire_alert: OnceLock<AcquireAlertPolicy>,
197 alert_next: AtomicU64,
198 alert_sequence: AtomicU64,
199}
200
201impl ConnectionSlot {
202 fn new(driver: DatabaseDriver, datasource: &str) -> Self {
210 Self {
211 driver,
212 datasource: datasource.into(),
213 acquire: [const { WaitCells::new() }; 4],
214 transaction: WaitCells::new(),
215 rejections: [const { AtomicU64::new(0) }; 7],
216 policies: OnceLock::new(),
217 log_next: [const { AtomicU64::new(0) }; 2],
218 acquire_alert: OnceLock::new(),
219 alert_next: AtomicU64::new(0),
220 alert_sequence: AtomicU64::new(0),
221 }
222 }
223
224 pub fn reject(&self, reason: ConnectionRejection) {
231 add(&self.rejections[reason as usize], 1);
232 }
233
234 pub fn acquire(&'static self, purpose: ConnectionPurpose) -> WaitGuard {
241 WaitGuard::new(self, purpose, false)
242 }
243
244 pub fn transaction_slot(&'static self) -> WaitGuard {
250 WaitGuard::new(self, ConnectionPurpose::Direct, true)
251 }
252}
253
254static SLOTS: [OnceLock<ConnectionSlot>; MAX_MANAGED_DATASOURCES * 2] =
255 [const { OnceLock::new() }; MAX_MANAGED_DATASOURCES * 2];
256static REGISTRATION: Mutex<()> = Mutex::new(());
257static UNKNOWN_MYSQL: OnceLock<ConnectionSlot> = OnceLock::new();
258static UNKNOWN_PGSQL: OnceLock<ConnectionSlot> = OnceLock::new();
259static UNKNOWN_SQLITE: OnceLock<ConnectionSlot> = OnceLock::new();
260static CLOCK: OnceLock<Instant> = OnceLock::new();
261
262pub fn register_datasource(
270 driver: DatabaseDriver,
271 datasource: &str,
272) -> anyhow::Result<&'static ConnectionSlot> {
273 crate::validate_datasource_name(datasource)?;
274 let _registration = REGISTRATION
275 .lock()
276 .unwrap_or_else(|error| error.into_inner());
277 CLOCK.get_or_init(Instant::now);
278 fallback(driver);
279 if let Some(slot) = known_slot(driver, datasource) {
280 return Ok(slot);
281 }
282 let cell = SLOTS
283 .iter()
284 .find(|cell| cell.get().is_none())
285 .ok_or_else(|| anyhow::anyhow!("connection observation datasource capacity exceeded"))?;
286 Ok(cell.get_or_init(|| ConnectionSlot::new(driver, datasource)))
287}
288
289pub fn configure_datasource(
299 driver: DatabaseDriver,
300 datasource: &str,
301 acquire: WaitObservationPolicy,
302 slot: WaitObservationPolicy,
303) -> anyhow::Result<()> {
304 let cells = register_datasource(driver, datasource)?;
305 let policy = (acquire, slot);
306 anyhow::ensure!(
307 *cells.policies.get_or_init(|| policy) == policy,
308 "connection observation policy is frozen; restart is required"
309 );
310 Ok(())
311}
312
313pub fn configure_acquire_alert(
322 driver: DatabaseDriver,
323 datasource: &str,
324 policy: AcquireAlertPolicy,
325) -> anyhow::Result<()> {
326 anyhow::ensure!(
327 !policy.purposes.is_empty() && policy.purposes.len() <= 4,
328 "acquire alert purposes must be nonempty and bounded"
329 );
330 let mut seen = [false; 4];
331 for purpose in &policy.purposes {
332 anyhow::ensure!(!seen[*purpose as usize], "duplicate acquire alert purpose");
333 seen[*purpose as usize] = true;
334 }
335 register_datasource(driver, datasource)?
336 .acquire_alert
337 .set(policy)
338 .map_err(|_| anyhow::anyhow!("acquire alert policy is frozen; restart is required"))
339}
340
341pub fn connection_slot(driver: DatabaseDriver, datasource: &str) -> &'static ConnectionSlot {
349 known_slot(driver, datasource).unwrap_or_else(|| fallback(driver))
350}
351
352fn known_slot(driver: DatabaseDriver, datasource: &str) -> Option<&'static ConnectionSlot> {
360 SLOTS
361 .iter()
362 .filter_map(OnceLock::get)
363 .find(|slot| slot.driver == driver && slot.datasource.as_ref() == datasource)
364}
365
366fn fallback(driver: DatabaseDriver) -> &'static ConnectionSlot {
373 match driver {
374 DatabaseDriver::MySql => &UNKNOWN_MYSQL,
375 DatabaseDriver::PostgreSql => &UNKNOWN_PGSQL,
376 DatabaseDriver::Sqlite => &UNKNOWN_SQLITE,
377 }
378 .get_or_init(|| ConnectionSlot::new(driver, "__unknown__"))
379}
380
381pub struct WaitGuard {
383 slot: &'static ConnectionSlot,
384 purpose: ConnectionPurpose,
385 transaction: bool,
386 started: Instant,
387 finished: bool,
388}
389
390impl WaitGuard {
391 fn new(slot: &'static ConnectionSlot, purpose: ConnectionPurpose, transaction: bool) -> Self {
400 let guard = Self {
401 slot,
402 purpose,
403 transaction,
404 started: Instant::now(),
405 finished: false,
406 };
407 guard.cells().in_flight.fetch_add(1, Ordering::Relaxed);
408 guard
409 }
410
411 fn cells(&self) -> &WaitCells {
417 if self.transaction {
418 &self.slot.transaction
419 } else {
420 &self.slot.acquire[self.purpose as usize]
421 }
422 }
423
424 pub fn finish(&mut self, outcome: AcquireOutcome) {
431 if self.finished {
432 return;
433 }
434 self.finished = true;
435 let elapsed = self.started.elapsed();
436 let cells = self.cells();
437 add(&cells.outcomes[outcome as usize], 1);
438 cells.durations[outcome.status()].observe(elapsed);
439 cells.in_flight.fetch_sub(1, Ordering::Relaxed);
440 if outcome == AcquireOutcome::Ok {
441 self.log_wait(elapsed);
442 }
443 if outcome == AcquireOutcome::Timeout && !self.transaction {
444 self.notify_timeout(elapsed);
445 }
446 }
447
448 fn notify_timeout(&self, elapsed: Duration) {
455 let Some(policy) = self.slot.acquire_alert.get() else {
456 return;
457 };
458 if !policy.purposes.contains(&self.purpose) {
459 return;
460 }
461 let now = CLOCK
462 .get_or_init(Instant::now)
463 .elapsed()
464 .as_millis()
465 .min(u64::MAX as u128) as u64;
466 if !policy.cooldown.is_zero()
467 && self
468 .slot
469 .alert_next
470 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
471 (value <= now).then(|| {
472 now.saturating_add(policy.cooldown.as_millis().min(u64::MAX as u128) as u64)
473 })
474 })
475 .is_err()
476 {
477 return;
478 }
479 let sequence = self.slot.alert_sequence.fetch_add(1, Ordering::Relaxed);
480 let occurred_at_unix_ms = std::time::SystemTime::now()
481 .duration_since(std::time::UNIX_EPOCH)
482 .unwrap_or_default()
483 .as_millis()
484 .min(u64::MAX as u128) as u64;
485 use std::hash::{Hash, Hasher};
486 let mut identity = std::collections::hash_map::DefaultHasher::new();
487 (
488 policy.instance.as_str(),
489 driver_name(self.slot.driver),
490 self.slot.datasource.as_ref(),
491 )
492 .hash(&mut identity);
493 let notification = Notification::new(NotificationFields {
494 id: format!(
496 "acquire-{:x}-{occurred_at_unix_ms}-{sequence}",
497 identity.finish()
498 ),
499 event: EventKind::AcquireTimeout,
500 severity: policy.severity,
501 occurred_at_unix_ms,
502 service: policy.service.clone(),
503 instance: policy.instance.clone(),
504 environment: policy.environment.clone(),
505 cluster: policy.cluster.clone(),
506 driver: driver_name(self.slot.driver).to_owned(),
507 datasource: self.slot.datasource.to_string(),
508 operation: "acquire".to_owned(),
509 purpose: Some(self.purpose.as_str().to_owned()),
510 duration: elapsed,
511 outcome: "timeout".to_owned(),
512 error_kind: Some("timeout".to_owned()),
513 ..NotificationFields::default()
514 });
515 let _ = policy.route.try_send(notification);
516 }
517
518 fn log_wait(&self, elapsed: Duration) {
525 let Some(policies) = self.slot.policies.get() else {
526 return;
527 };
528 let policy = if self.transaction {
529 policies.1
530 } else {
531 policies.0
532 };
533 if !policy.log_enabled || elapsed < policy.threshold {
534 return;
535 }
536 let next = &self.slot.log_next[usize::from(self.transaction)];
537 let now = CLOCK
538 .get_or_init(Instant::now)
539 .elapsed()
540 .as_millis()
541 .min(u64::MAX as u128) as u64;
542 if policy.log_cooldown != Duration::ZERO
543 && next
544 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
545 (value <= now).then(|| {
546 now.saturating_add(
547 policy.log_cooldown.as_millis().min(u64::MAX as u128) as u64
548 )
549 })
550 })
551 .is_err()
552 {
553 return;
554 }
555 let event = if self.transaction {
556 "transaction_slot_wait"
557 } else {
558 "connection_acquire_wait"
559 };
560 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
562 match policy.log_level {
563 WaitLogLevel::Info => {
564 tracing::info!(target: "natx::connection", event, datasource = self.slot.datasource.as_ref(), driver = driver_name(self.slot.driver), purpose = self.purpose.as_str(), duration_seconds = elapsed.as_secs_f64(), "数据库连接等待超过阈值")
565 }
566 WaitLogLevel::Warn => {
567 tracing::warn!(target: "natx::connection", event, datasource = self.slot.datasource.as_ref(), driver = driver_name(self.slot.driver), purpose = self.purpose.as_str(), duration_seconds = elapsed.as_secs_f64(), "数据库连接等待超过阈值")
568 }
569 }
570 }));
571 if let Err(payload) = result {
572 if let Err(nested) =
574 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| drop(payload)))
575 {
576 std::mem::forget(nested);
577 }
578 }
579 }
580}
581
582impl Drop for WaitGuard {
583 fn drop(&mut self) {
589 self.finish(AcquireOutcome::Cancelled);
590 }
591}
592
593fn driver_name(driver: DatabaseDriver) -> &'static str {
600 match driver {
601 DatabaseDriver::MySql => "mysql",
602 DatabaseDriver::PostgreSql => "postgresql",
603 DatabaseDriver::Sqlite => "sqlite",
604 }
605}
606
607const OUTCOMES: [&str; 6] = ["ok", "timeout", "closed", "worker", "cancelled", "other"];
608const STATUSES: [&str; 3] = ["success", "failure", "cancelled"];
609const PURPOSES: [&str; 4] = ["mapper", "migration", "probe", "direct"];
610const REJECTIONS: [&str; 7] = [
611 "tx_required",
612 "tx_forbidden",
613 "cross_datasource",
614 "cross_driver",
615 "unknown_datasource",
616 "registry_unavailable",
617 "tx_connection_busy",
618];
619
620macro_rules! descriptor {
621 ($name:ident, $text:literal, $kind:ident, $labels:expr, $bounds:expr) => {
622 static $name: MetricDescriptor = MetricDescriptor {
623 name: $text,
624 help: "数据库连接与等待状态。",
625 unit: if matches!(MetricKind::$kind, MetricKind::Histogram) {
626 "seconds"
627 } else {
628 ""
629 },
630 kind: MetricKind::$kind,
631 label_names: $labels,
632 histogram_bounds: $bounds,
633 };
634 };
635}
636descriptor!(
637 ACQUIRES,
638 "natx_connection_acquires_total",
639 Counter,
640 &["datasource", "driver", "purpose", "outcome"],
641 &[]
642);
643descriptor!(
644 ACQUIRE_DURATION,
645 "natx_connection_acquire_duration_seconds",
646 Histogram,
647 &["datasource", "driver", "purpose", "status"],
648 LATENCY_SECONDS
649);
650descriptor!(
651 ACQUIRE_IN_FLIGHT,
652 "natx_connection_acquire_in_flight",
653 Gauge,
654 &["datasource", "driver", "purpose"],
655 &[]
656);
657descriptor!(
658 SLOT_WAITS,
659 "natx_transaction_slot_waits_total",
660 Counter,
661 &["datasource", "driver", "outcome"],
662 &[]
663);
664descriptor!(
665 SLOT_DURATION,
666 "natx_transaction_slot_wait_duration_seconds",
667 Histogram,
668 &["datasource", "driver", "status"],
669 LATENCY_SECONDS
670);
671descriptor!(
672 SLOT_IN_FLIGHT,
673 "natx_transaction_slot_wait_in_flight",
674 Gauge,
675 &["datasource", "driver"],
676 &[]
677);
678descriptor!(
679 REJECTION,
680 "natx_connection_rejections_total",
681 Counter,
682 &["datasource", "driver", "reason"],
683 &[]
684);
685descriptor!(
686 POOL_CONNECTIONS,
687 "natx_pool_connections",
688 Gauge,
689 &["datasource", "driver", "state"],
690 &[]
691);
692descriptor!(
693 POOL_MAX,
694 "natx_pool_max_connections",
695 Gauge,
696 &["datasource", "driver"],
697 &[]
698);
699static CONNECTION_DESCRIPTORS: &[&MetricDescriptor] = &[
700 &ACQUIRES,
701 &ACQUIRE_DURATION,
702 &ACQUIRE_IN_FLIGHT,
703 &SLOT_WAITS,
704 &SLOT_DURATION,
705 &SLOT_IN_FLIGHT,
706 &REJECTION,
707];
708static POOL_DESCRIPTORS: &[&MetricDescriptor] = &[&POOL_CONNECTIONS, &POOL_MAX];
709
710pub struct ConnectionMetricsSource {
712 slots: Box<[&'static ConnectionSlot]>,
713}
714
715impl ConnectionMetricsSource {
716 pub fn new(driver: DatabaseDriver) -> Self {
723 let _registration = REGISTRATION
725 .lock()
726 .unwrap_or_else(|error| error.into_inner());
727 let slots = SLOTS
728 .iter()
729 .filter_map(OnceLock::get)
730 .filter(|slot| slot.driver == driver)
731 .chain(std::iter::once(fallback(driver)))
732 .collect::<Vec<_>>()
733 .into_boxed_slice();
734 Self {
735 slots,
736 }
737 }
738
739 pub fn series_budget(&self) -> usize {
745 self.slots.len() * (5 * (6 + 3 * (LATENCY_SECONDS.len() + 3) + 1) + 7)
746 }
747}
748
749impl LegacyMetricsSource for ConnectionMetricsSource {
750 fn descriptors(&self) -> &'static [&'static MetricDescriptor] {
756 CONNECTION_DESCRIPTORS
757 }
758
759 fn snapshot(&self) -> Option<Vec<MetricSample>> {
765 let mut samples = Vec::new();
766 for slot in &self.slots {
767 let base = vec![
768 ("datasource", slot.datasource.to_string()),
769 ("driver", driver_name(slot.driver).to_owned()),
770 ];
771 for (purpose, cells) in PURPOSES.iter().zip(&slot.acquire) {
772 let mut labels = base.clone();
773 labels.push(("purpose", (*purpose).to_owned()));
774 snapshot_wait(
775 &mut samples,
776 cells,
777 labels,
778 &ACQUIRES,
779 &ACQUIRE_DURATION,
780 &ACQUIRE_IN_FLIGHT,
781 );
782 }
783 snapshot_wait(
784 &mut samples,
785 &slot.transaction,
786 base.clone(),
787 &SLOT_WAITS,
788 &SLOT_DURATION,
789 &SLOT_IN_FLIGHT,
790 );
791 for (reason, cell) in REJECTIONS.iter().zip(&slot.rejections) {
792 let mut labels = base.clone();
793 labels.push(("reason", (*reason).to_owned()));
794 samples.push(MetricSample {
795 name: REJECTION.name,
796 labels,
797 value: MetricValue::Counter(cell.load(Ordering::Relaxed)),
798 });
799 }
800 }
801 Some(samples)
802 }
803
804 fn render_prometheus(&self, _output: &mut String) {}
811}
812
813fn snapshot_wait(
825 samples: &mut Vec<MetricSample>,
826 cells: &WaitCells,
827 labels: Vec<(&'static str, String)>,
828 counter: &'static MetricDescriptor,
829 histogram: &'static MetricDescriptor,
830 gauge: &'static MetricDescriptor,
831) {
832 for (outcome, cell) in OUTCOMES.iter().zip(&cells.outcomes) {
833 let mut labels = labels.clone();
834 labels.push(("outcome", (*outcome).to_owned()));
835 samples.push(MetricSample {
836 name: counter.name,
837 labels,
838 value: MetricValue::Counter(cell.load(Ordering::Relaxed)),
839 });
840 }
841 for (status, cell) in STATUSES.iter().zip(&cells.durations) {
842 let mut labels = labels.clone();
843 labels.push(("status", (*status).to_owned()));
844 samples.push(MetricSample {
845 name: histogram.name,
846 labels,
847 value: cell.snapshot(),
848 });
849 }
850 samples.push(MetricSample {
851 name: gauge.name,
852 labels,
853 value: MetricValue::Gauge(cells.in_flight.load(Ordering::Relaxed) as f64),
854 });
855}
856
857pub struct PoolState {
859 pub total: u32,
861 pub idle: u32,
863 pub max: u32,
865}
866
867pub trait PoolSnapshot: Send + Sync {
869 fn snapshot(&self) -> PoolState;
875}
876
877pub struct PoolMetricsSource {
879 driver: DatabaseDriver,
880 pools: Vec<(String, Arc<dyn PoolSnapshot>)>,
881}
882
883impl PoolMetricsSource {
884 pub fn new(
892 driver: DatabaseDriver,
893 pools: Vec<(String, Arc<dyn PoolSnapshot>)>,
894 ) -> anyhow::Result<Self> {
895 anyhow::ensure!(
896 pools.len() <= MAX_MANAGED_DATASOURCES,
897 "pool observation capacity exceeded"
898 );
899 let mut names = std::collections::BTreeSet::new();
900 for (name, _) in &pools {
901 crate::validate_datasource_name(name)?;
902 anyhow::ensure!(names.insert(name), "duplicate pool observation datasource");
903 }
904 Ok(Self {
905 driver,
906 pools,
907 })
908 }
909
910 pub fn series_budget(&self) -> usize {
916 self.pools.len() * 4
917 }
918}
919
920impl LegacyMetricsSource for PoolMetricsSource {
921 fn descriptors(&self) -> &'static [&'static MetricDescriptor] {
927 POOL_DESCRIPTORS
928 }
929
930 fn snapshot(&self) -> Option<Vec<MetricSample>> {
936 let mut samples = Vec::with_capacity(self.series_budget());
937 for (name, pool) in &self.pools {
938 let state = pool.snapshot();
939 let base = vec![
940 ("datasource", name.clone()),
941 ("driver", driver_name(self.driver).to_owned()),
942 ];
943 for (label, value) in [
944 ("total", state.total),
945 ("idle", state.idle),
946 ("in_use", state.total.saturating_sub(state.idle)),
947 ] {
948 let mut labels = base.clone();
949 labels.push(("state", label.to_owned()));
950 samples.push(MetricSample {
951 name: POOL_CONNECTIONS.name,
952 labels,
953 value: MetricValue::Gauge(value as f64),
954 });
955 }
956 samples.push(MetricSample {
957 name: POOL_MAX.name,
958 labels: base,
959 value: MetricValue::Gauge(state.max as f64),
960 });
961 }
962 Some(samples)
963 }
964
965 fn render_prometheus(&self, _output: &mut String) {}
972}