Skip to main content

statsig_rust/observability/
observability_client_adapter.rs

1use std::{collections::HashMap, sync::Arc};
2
3use async_trait::async_trait;
4
5use crate::{observability::ops_stats::OpsStatsEventObserver, statsig_metadata::StatsigMetadata};
6
7use super::ops_stats::OpsStatsEvent;
8
9static HIGH_CARDINALITY_TAGS: &[&str] = &["lcut", "prev_lcut"];
10static SDK_EXCEPTION_COUNT_EXTRA_TAGS: &[&str] = &["request_path", "status_code"];
11
12#[derive(Clone)]
13pub enum MetricType {
14    Increment,
15    Gauge,
16    Dist,
17}
18
19#[derive(Clone)]
20pub struct ObservabilityEvent {
21    pub metric_type: MetricType,
22    pub metric_name: String,
23    pub value: f64,
24    pub tags: Option<HashMap<String, String>>,
25}
26
27impl ObservabilityEvent {
28    pub fn new_event(
29        metric_type: MetricType,
30        metric_name: String,
31        value: f64,
32        tags: Option<HashMap<String, String>>,
33    ) -> OpsStatsEvent {
34        OpsStatsEvent::Observability(ObservabilityEvent {
35            metric_type,
36            metric_name,
37            value,
38            tags,
39        })
40    }
41}
42
43fn add_default_metric_tags(mut tags: HashMap<String, String>) -> HashMap<String, String> {
44    let metadata = StatsigMetadata::get_metadata();
45    tags.insert("sdk_type".to_string(), metadata.sdk_type);
46    tags.insert("sdk_version".to_string(), metadata.sdk_version);
47    tags
48}
49
50pub trait ObservabilityClient: Send + Sync + 'static + OpsStatsEventObserver {
51    fn init(&self);
52    fn increment(&self, metric_name: String, value: f64, tags: Option<HashMap<String, String>>);
53    fn gauge(&self, metric_name: String, value: f64, tags: Option<HashMap<String, String>>);
54    fn dist(&self, metric_name: String, value: f64, tags: Option<HashMap<String, String>>);
55    fn error(&self, tag: String, error: String);
56    fn should_enable_high_cardinality_for_this_tag(&self, tag: String) -> Option<bool>;
57    // This is needed since upper casting is not officially supported yet
58    // (WIP to support https://github.com/rust-lang/rust/issues/65991)
59    // For implementation, just return Self; should be sufficient
60    fn to_ops_stats_event_observer(self: Arc<Self>) -> Arc<dyn OpsStatsEventObserver>;
61}
62
63#[async_trait]
64impl<T: ObservabilityClient> OpsStatsEventObserver for T {
65    async fn handle_event(&self, event: OpsStatsEvent) {
66        match event {
67            OpsStatsEvent::Observability(data) => {
68                let tags = data
69                    .tags
70                    .unwrap_or_default()
71                    .into_iter()
72                    .filter(|(key, _)| {
73                        if HIGH_CARDINALITY_TAGS.contains(&key.as_str()) {
74                            self.should_enable_high_cardinality_for_this_tag(key.to_string())
75                                .unwrap_or_default()
76                        } else {
77                            true
78                        }
79                    })
80                    .collect();
81                let tags = Some(add_default_metric_tags(tags));
82                let metric_name = format!("statsig.sdk.{}", data.metric_name.clone());
83                match data.metric_type {
84                    MetricType::Increment => self.increment(metric_name, data.value, tags),
85                    MetricType::Gauge => self.gauge(metric_name, data.value, tags),
86                    MetricType::Dist => self.dist(metric_name, data.value, tags),
87                };
88            }
89            OpsStatsEvent::SDKError(error) => {
90                self.error(error.tag.clone(), error.info.to_string());
91                let mut tags = HashMap::from([
92                    ("tag".to_string(), error.tag),
93                    ("exception".to_string(), error.exception),
94                ]);
95
96                if let Some(extra) = error.extra {
97                    for (key, value) in extra {
98                        if SDK_EXCEPTION_COUNT_EXTRA_TAGS.contains(&key.as_str()) {
99                            tags.insert(key, value);
100                        }
101                    }
102                }
103
104                self.increment(
105                    "statsig.sdk.sdk_exceptions_count".to_string(),
106                    1.0,
107                    Some(add_default_metric_tags(tags)),
108                );
109            }
110            _ => {}
111        }
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn add_default_metric_tags_attaches_sdk_metadata() {
121        let metadata = StatsigMetadata::get_metadata();
122
123        let tags = add_default_metric_tags(HashMap::new());
124
125        assert_eq!(tags.get("sdk_type"), Some(&metadata.sdk_type));
126        assert_eq!(tags.get("sdk_version"), Some(&metadata.sdk_version));
127    }
128
129    #[test]
130    fn add_default_metric_tags_overwrites_call_site_sdk_metadata() {
131        let metadata = StatsigMetadata::get_metadata();
132        let tags = add_default_metric_tags(HashMap::from([
133            ("sdk_type".to_string(), "wrong".to_string()),
134            ("sdk_version".to_string(), "wrong".to_string()),
135        ]));
136
137        assert_eq!(tags.get("sdk_type"), Some(&metadata.sdk_type));
138        assert_eq!(tags.get("sdk_version"), Some(&metadata.sdk_version));
139    }
140}