docspec_http/telemetry/
sentry.rs1use core::str::FromStr as _;
4
5pub(in crate::telemetry) fn init_sentry(
7 data_source_name: &str,
8) -> Option<::sentry::ClientInitGuard> {
9 sentry::types::Dsn::from_str(data_source_name).map_or_else(
10 |_| {
11 eprintln!("warning: invalid Sentry DSN format; Sentry disabled");
12 None
13 },
14 |parsed_data_source_name| Some(sentry::init(client_options(parsed_data_source_name))),
15 )
16}
17
18fn client_options(parsed_data_source_name: sentry::types::Dsn) -> sentry::ClientOptions {
19 sentry::ClientOptions {
20 dsn: Some(parsed_data_source_name),
21 release: sentry::release_name!(),
22 environment: Some(env_environment()),
23 sample_rate: env_sample_rate(),
24 traces_sample_rate: env_traces_sample_rate(),
25 send_default_pii: false,
26 before_send: Some(std::sync::Arc::new(before_send)),
27 ..Default::default()
28 }
29}
30
31fn env_environment() -> std::borrow::Cow<'static, str> {
32 match std::env::var("SENTRY_ENVIRONMENT") {
33 Ok(value) if !value.is_empty() => std::borrow::Cow::Owned(value),
34 _ => std::borrow::Cow::Borrowed("production"),
35 }
36}
37
38fn env_sample_rate() -> f32 {
39 super::env_rate("SENTRY_SAMPLE_RATE", 1.0)
40}
41
42fn env_traces_sample_rate() -> f32 {
43 super::env_rate("SENTRY_TRACES_SAMPLE_RATE", 0.0)
44}
45
46fn before_send(
47 mut event: sentry::protocol::Event<'static>,
48) -> Option<sentry::protocol::Event<'static>> {
49 if let Some(request) = event.request.as_mut() {
50 let _ = request.data.take();
51 }
52 event
53 .extra
54 .retain(|extra_key, _| !extra_key.to_lowercase().contains("body"));
55 Some(event)
56}
57
58pub(in crate::telemetry) fn tracing_layer<S>() -> sentry::integrations::tracing::SentryLayer<S>
60where
61 S: tracing::Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
62{
63 sentry::integrations::tracing::layer().event_filter(|metadata| {
64 use sentry::integrations::tracing::EventFilter;
65
66 match *metadata.level() {
67 tracing::Level::ERROR => EventFilter::Event,
68 tracing::Level::WARN | tracing::Level::INFO | tracing::Level::DEBUG => {
69 EventFilter::Breadcrumb
70 }
71 tracing::Level::TRACE => EventFilter::Ignore,
72 }
73 })
74}
75
76pub(in crate::telemetry) fn tower_new_layer(
78) -> sentry::integrations::tower::NewSentryLayer<axum::http::Request<axum::body::Body>> {
79 sentry::integrations::tower::NewSentryLayer::new_from_top()
80}
81
82pub(in crate::telemetry) fn tower_http_layer() -> sentry::integrations::tower::SentryHttpLayer {
84 sentry::integrations::tower::SentryHttpLayer::new()
85}
86
87#[cfg(test)]
88mod tests {
89 use std::sync::Mutex;
90
91 static ENV_MUTEX: Mutex<()> = Mutex::new(());
92
93 fn lock_env() -> std::sync::MutexGuard<'static, ()> {
94 match ENV_MUTEX.lock() {
95 Ok(guard) => guard,
96 Err(poisoned) => poisoned.into_inner(),
97 }
98 }
99
100 #[test]
101 fn before_send_strips_request_data() {
102 let event: sentry::protocol::Event<'static> = sentry::protocol::Event {
103 request: Some(sentry::protocol::Request {
104 data: Some(String::from("# secret document body")),
105 ..sentry::protocol::Request::default()
106 }),
107 ..sentry::protocol::Event::default()
108 };
109
110 let stripped = super::before_send(event);
111
112 assert!(matches!(
113 stripped
114 .as_ref()
115 .and_then(|stripped_event| stripped_event.request.as_ref()),
116 Some(request) if request.data.is_none()
117 ));
118 }
119
120 #[test]
121 fn before_send_strips_body_keys_in_extra() {
122 let mut event: sentry::protocol::Event<'static> = sentry::protocol::Event::default();
123 event
124 .extra
125 .insert(String::from("request_body"), serde_json::json!("secret"));
126 event
127 .extra
128 .insert(String::from("response_body"), serde_json::json!("secret"));
129 event
130 .extra
131 .insert(String::from("other"), serde_json::json!("kept"));
132
133 let stripped = super::before_send(event);
134
135 assert!(
136 matches!(stripped, Some(stripped_event) if stripped_event.extra.len() == 1 && stripped_event.extra.contains_key("other"))
137 );
138 }
139
140 #[test]
141 fn env_sample_rate_clamps_high() {
142 let _env_guard = lock_env();
143 std::env::set_var("SENTRY_SAMPLE_RATE", "5.0");
144
145 assert_eq!(super::env_sample_rate().to_bits(), f32::to_bits(1.0));
146
147 std::env::remove_var("SENTRY_SAMPLE_RATE");
148 }
149
150 #[test]
151 fn env_sample_rate_clamps_low() {
152 let _env_guard = lock_env();
153 std::env::set_var("SENTRY_SAMPLE_RATE", "-0.5");
154
155 assert_eq!(super::env_sample_rate().to_bits(), f32::to_bits(0.0));
156
157 std::env::remove_var("SENTRY_SAMPLE_RATE");
158 }
159
160 #[test]
161 fn env_sample_rate_defaults_to_one() {
162 let _env_guard = lock_env();
163 std::env::remove_var("SENTRY_SAMPLE_RATE");
164
165 assert_eq!(super::env_sample_rate().to_bits(), f32::to_bits(1.0));
166 }
167
168 #[test]
169 fn env_traces_sample_rate_defaults_to_zero() {
170 let _env_guard = lock_env();
171 std::env::remove_var("SENTRY_TRACES_SAMPLE_RATE");
172
173 assert_eq!(super::env_traces_sample_rate().to_bits(), f32::to_bits(0.0));
174 }
175
176 #[test]
177 fn env_environment_defaults_to_production() {
178 let _env_guard = lock_env();
179 std::env::remove_var("SENTRY_ENVIRONMENT");
180
181 assert_eq!(super::env_environment(), "production");
182 }
183
184 #[test]
185 fn init_sentry_returns_none_on_invalid_dsn() {
186 assert!(super::init_sentry("not-a-dsn").is_none());
187 }
188}