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 let mut options = sentry::ClientOptions::new()
20 .maybe_release(sentry::release_name!())
21 .environment(env_environment())
22 .sample_rate(env_sample_rate())
23 .send_default_pii(false)
24 .attach_stacktrace(true)
25 .before_send(before_send);
26
27 let traces_sample_rate = env_traces_sample_rate();
30 if traces_sample_rate > 0.0 {
31 options = options.traces_sample_rate(traces_sample_rate);
32 }
33
34 options.dsn = Some(parsed_data_source_name);
37 options
38}
39
40fn env_environment() -> std::borrow::Cow<'static, str> {
41 match std::env::var("SENTRY_ENVIRONMENT") {
42 Ok(value) if !value.is_empty() => std::borrow::Cow::Owned(value),
43 _ => std::borrow::Cow::Borrowed("production"),
44 }
45}
46
47fn env_sample_rate() -> f32 {
48 super::env_rate("SENTRY_SAMPLE_RATE", 1.0)
49}
50
51fn env_traces_sample_rate() -> f32 {
52 super::env_rate("SENTRY_TRACES_SAMPLE_RATE", 0.0)
53}
54
55fn before_send(
56 mut event: sentry::protocol::Event<'static>,
57) -> Option<sentry::protocol::Event<'static>> {
58 if let Some(request) = event.request.as_mut() {
59 let _ = request.data.take();
60 }
61 event
62 .extra
63 .retain(|extra_key, _| !extra_key.to_lowercase().contains("body"));
64 Some(event)
65}
66
67pub(in crate::telemetry) fn tracing_layer<S>() -> sentry::integrations::tracing::SentryLayer<S>
69where
70 S: tracing::Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
71{
72 sentry::integrations::tracing::layer().event_filter(|metadata| {
73 use sentry::integrations::tracing::EventFilter;
74
75 match *metadata.level() {
76 tracing::Level::ERROR => EventFilter::Event,
77 tracing::Level::WARN | tracing::Level::INFO | tracing::Level::DEBUG => {
78 EventFilter::Breadcrumb
79 }
80 tracing::Level::TRACE => EventFilter::Ignore,
81 }
82 })
83}
84
85pub(in crate::telemetry) fn tower_new_layer(
87) -> sentry::integrations::tower::NewSentryLayer<axum::http::Request<axum::body::Body>> {
88 sentry::integrations::tower::NewSentryLayer::new_from_top()
89}
90
91pub(in crate::telemetry) fn tower_http_layer() -> sentry::integrations::tower::SentryHttpLayer {
93 sentry::integrations::tower::SentryHttpLayer::new()
94}
95
96#[cfg(test)]
97mod tests {
98 #![allow(clippy::panic_in_result_fn)]
101
102 use std::sync::Mutex;
103
104 static ENV_MUTEX: Mutex<()> = Mutex::new(());
105
106 fn lock_env() -> std::sync::MutexGuard<'static, ()> {
107 match ENV_MUTEX.lock() {
108 Ok(guard) => guard,
109 Err(poisoned) => poisoned.into_inner(),
110 }
111 }
112
113 #[test]
114 fn before_send_strips_request_data() {
115 let event: sentry::protocol::Event<'static> = sentry::protocol::Event {
116 request: Some(sentry::protocol::Request {
117 data: Some(String::from("# secret document body")),
118 ..sentry::protocol::Request::default()
119 }),
120 ..sentry::protocol::Event::default()
121 };
122
123 let stripped = super::before_send(event);
124
125 assert!(matches!(
126 stripped
127 .as_ref()
128 .and_then(|stripped_event| stripped_event.request.as_ref()),
129 Some(request) if request.data.is_none()
130 ));
131 }
132
133 #[test]
134 fn before_send_strips_body_keys_in_extra() {
135 let mut event: sentry::protocol::Event<'static> = sentry::protocol::Event::default();
136 event
137 .extra
138 .insert(String::from("request_body"), serde_json::json!("secret"));
139 event
140 .extra
141 .insert(String::from("response_body"), serde_json::json!("secret"));
142 event
143 .extra
144 .insert(String::from("other"), serde_json::json!("kept"));
145
146 let stripped = super::before_send(event);
147
148 assert!(
149 matches!(stripped, Some(stripped_event) if stripped_event.extra.len() == 1 && stripped_event.extra.contains_key("other"))
150 );
151 }
152
153 #[test]
154 fn env_sample_rate_clamps_high() {
155 let _env_guard = lock_env();
156 std::env::set_var("SENTRY_SAMPLE_RATE", "5.0");
157
158 assert_eq!(super::env_sample_rate().to_bits(), f32::to_bits(1.0));
159
160 std::env::remove_var("SENTRY_SAMPLE_RATE");
161 }
162
163 #[test]
164 fn env_sample_rate_clamps_low() {
165 let _env_guard = lock_env();
166 std::env::set_var("SENTRY_SAMPLE_RATE", "-0.5");
167
168 assert_eq!(super::env_sample_rate().to_bits(), f32::to_bits(0.0));
169
170 std::env::remove_var("SENTRY_SAMPLE_RATE");
171 }
172
173 #[test]
174 fn env_sample_rate_defaults_to_one() {
175 let _env_guard = lock_env();
176 std::env::remove_var("SENTRY_SAMPLE_RATE");
177
178 assert_eq!(super::env_sample_rate().to_bits(), f32::to_bits(1.0));
179 }
180
181 #[test]
182 fn env_traces_sample_rate_defaults_to_zero() {
183 let _env_guard = lock_env();
184 std::env::remove_var("SENTRY_TRACES_SAMPLE_RATE");
185
186 assert_eq!(super::env_traces_sample_rate().to_bits(), f32::to_bits(0.0));
187 }
188
189 #[test]
190 fn env_environment_defaults_to_production() {
191 let _env_guard = lock_env();
192 std::env::remove_var("SENTRY_ENVIRONMENT");
193
194 assert_eq!(super::env_environment(), "production");
195 }
196
197 #[test]
198 fn init_sentry_returns_none_on_invalid_dsn() {
199 assert!(super::init_sentry("not-a-dsn").is_none());
200 }
201
202 #[test]
203 fn env_traces_sample_rate_ignores_non_finite() {
204 let _env_guard = lock_env();
205 std::env::set_var("SENTRY_TRACES_SAMPLE_RATE", "NaN");
206
207 assert_eq!(super::env_traces_sample_rate().to_bits(), f32::to_bits(0.0));
208
209 std::env::remove_var("SENTRY_TRACES_SAMPLE_RATE");
210 }
211
212 #[test]
213 fn client_options_enables_attach_stacktrace() -> Result<(), Box<dyn core::error::Error>> {
214 let _env_guard = lock_env();
215 let dsn: sentry::types::Dsn = "https://public@example.com/1".parse()?;
216 let opts = super::client_options(dsn);
217 if opts.attach_stacktrace {
218 Ok(())
219 } else {
220 Err("attach_stacktrace should be true".into())
221 }
222 }
223
224 #[test]
225 fn client_options_sets_dsn_release_environment_and_sample_rate(
226 ) -> Result<(), Box<dyn core::error::Error>> {
227 let _env_guard = lock_env();
228 std::env::set_var("SENTRY_ENVIRONMENT", "staging");
229 std::env::set_var("SENTRY_SAMPLE_RATE", "0.25");
230 let dsn: sentry::types::Dsn = "https://public@example.com/1".parse()?;
231
232 let opts = super::client_options(dsn.clone());
233
234 std::env::remove_var("SENTRY_ENVIRONMENT");
235 std::env::remove_var("SENTRY_SAMPLE_RATE");
236
237 assert_eq!(opts.dsn, Some(dsn));
238 assert_eq!(opts.release, sentry::release_name!());
239 assert_eq!(
240 opts.environment,
241 Some(std::borrow::Cow::Borrowed("staging"))
242 );
243 assert!(!opts.send_default_pii);
244 assert!(opts.before_send.is_some());
245 assert!(matches!(
246 opts.event_sampling_strategy,
247 sentry::EventSamplingStrategy::FixedRate(rate) if rate.to_bits() == f32::to_bits(0.25)
248 ));
249 Ok(())
250 }
251
252 #[test]
253 fn client_options_leaves_traces_disabled_at_zero() -> Result<(), Box<dyn core::error::Error>> {
254 let _env_guard = lock_env();
255 std::env::remove_var("SENTRY_TRACES_SAMPLE_RATE");
256 let dsn: sentry::types::Dsn = "https://public@example.com/1".parse()?;
257
258 let opts = super::client_options(dsn);
259
260 assert!(matches!(
261 opts.traces_sampling_strategy,
262 sentry::TracesSamplingStrategy::Disabled
263 ));
264 Ok(())
265 }
266
267 #[test]
268 fn client_options_sets_fixed_traces_rate_above_zero() -> Result<(), Box<dyn core::error::Error>>
269 {
270 let _env_guard = lock_env();
271 std::env::set_var("SENTRY_TRACES_SAMPLE_RATE", "0.5");
272 let dsn: sentry::types::Dsn = "https://public@example.com/1".parse()?;
273
274 let opts = super::client_options(dsn);
275
276 std::env::remove_var("SENTRY_TRACES_SAMPLE_RATE");
277
278 assert!(matches!(
279 opts.traces_sampling_strategy,
280 sentry::TracesSamplingStrategy::FixedRate(rate) if rate.to_bits() == f32::to_bits(0.5)
281 ));
282 Ok(())
283 }
284}