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 attach_stacktrace: true,
27 before_send: Some(std::sync::Arc::new(before_send)),
28 ..Default::default()
29 }
30}
31
32fn env_environment() -> std::borrow::Cow<'static, str> {
33 match std::env::var("SENTRY_ENVIRONMENT") {
34 Ok(value) if !value.is_empty() => std::borrow::Cow::Owned(value),
35 _ => std::borrow::Cow::Borrowed("production"),
36 }
37}
38
39fn env_sample_rate() -> f32 {
40 super::env_rate("SENTRY_SAMPLE_RATE", 1.0)
41}
42
43fn env_traces_sample_rate() -> f32 {
44 super::env_rate("SENTRY_TRACES_SAMPLE_RATE", 0.0)
45}
46
47fn before_send(
48 mut event: sentry::protocol::Event<'static>,
49) -> Option<sentry::protocol::Event<'static>> {
50 if let Some(request) = event.request.as_mut() {
51 let _ = request.data.take();
52 }
53 event
54 .extra
55 .retain(|extra_key, _| !extra_key.to_lowercase().contains("body"));
56 Some(event)
57}
58
59pub(in crate::telemetry) fn tracing_layer<S>() -> sentry::integrations::tracing::SentryLayer<S>
61where
62 S: tracing::Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
63{
64 sentry::integrations::tracing::layer().event_filter(|metadata| {
65 use sentry::integrations::tracing::EventFilter;
66
67 match *metadata.level() {
68 tracing::Level::ERROR => EventFilter::Event,
69 tracing::Level::WARN | tracing::Level::INFO | tracing::Level::DEBUG => {
70 EventFilter::Breadcrumb
71 }
72 tracing::Level::TRACE => EventFilter::Ignore,
73 }
74 })
75}
76
77pub(in crate::telemetry) fn tower_new_layer(
79) -> sentry::integrations::tower::NewSentryLayer<axum::http::Request<axum::body::Body>> {
80 sentry::integrations::tower::NewSentryLayer::new_from_top()
81}
82
83pub(in crate::telemetry) fn tower_http_layer() -> sentry::integrations::tower::SentryHttpLayer {
85 sentry::integrations::tower::SentryHttpLayer::new()
86}
87
88#[cfg(test)]
89mod tests {
90 use std::sync::Mutex;
91
92 static ENV_MUTEX: Mutex<()> = Mutex::new(());
93
94 fn lock_env() -> std::sync::MutexGuard<'static, ()> {
95 match ENV_MUTEX.lock() {
96 Ok(guard) => guard,
97 Err(poisoned) => poisoned.into_inner(),
98 }
99 }
100
101 #[test]
102 fn before_send_strips_request_data() {
103 let event: sentry::protocol::Event<'static> = sentry::protocol::Event {
104 request: Some(sentry::protocol::Request {
105 data: Some(String::from("# secret document body")),
106 ..sentry::protocol::Request::default()
107 }),
108 ..sentry::protocol::Event::default()
109 };
110
111 let stripped = super::before_send(event);
112
113 assert!(matches!(
114 stripped
115 .as_ref()
116 .and_then(|stripped_event| stripped_event.request.as_ref()),
117 Some(request) if request.data.is_none()
118 ));
119 }
120
121 #[test]
122 fn before_send_strips_body_keys_in_extra() {
123 let mut event: sentry::protocol::Event<'static> = sentry::protocol::Event::default();
124 event
125 .extra
126 .insert(String::from("request_body"), serde_json::json!("secret"));
127 event
128 .extra
129 .insert(String::from("response_body"), serde_json::json!("secret"));
130 event
131 .extra
132 .insert(String::from("other"), serde_json::json!("kept"));
133
134 let stripped = super::before_send(event);
135
136 assert!(
137 matches!(stripped, Some(stripped_event) if stripped_event.extra.len() == 1 && stripped_event.extra.contains_key("other"))
138 );
139 }
140
141 #[test]
142 fn env_sample_rate_clamps_high() {
143 let _env_guard = lock_env();
144 std::env::set_var("SENTRY_SAMPLE_RATE", "5.0");
145
146 assert_eq!(super::env_sample_rate().to_bits(), f32::to_bits(1.0));
147
148 std::env::remove_var("SENTRY_SAMPLE_RATE");
149 }
150
151 #[test]
152 fn env_sample_rate_clamps_low() {
153 let _env_guard = lock_env();
154 std::env::set_var("SENTRY_SAMPLE_RATE", "-0.5");
155
156 assert_eq!(super::env_sample_rate().to_bits(), f32::to_bits(0.0));
157
158 std::env::remove_var("SENTRY_SAMPLE_RATE");
159 }
160
161 #[test]
162 fn env_sample_rate_defaults_to_one() {
163 let _env_guard = lock_env();
164 std::env::remove_var("SENTRY_SAMPLE_RATE");
165
166 assert_eq!(super::env_sample_rate().to_bits(), f32::to_bits(1.0));
167 }
168
169 #[test]
170 fn env_traces_sample_rate_defaults_to_zero() {
171 let _env_guard = lock_env();
172 std::env::remove_var("SENTRY_TRACES_SAMPLE_RATE");
173
174 assert_eq!(super::env_traces_sample_rate().to_bits(), f32::to_bits(0.0));
175 }
176
177 #[test]
178 fn env_environment_defaults_to_production() {
179 let _env_guard = lock_env();
180 std::env::remove_var("SENTRY_ENVIRONMENT");
181
182 assert_eq!(super::env_environment(), "production");
183 }
184
185 #[test]
186 fn init_sentry_returns_none_on_invalid_dsn() {
187 assert!(super::init_sentry("not-a-dsn").is_none());
188 }
189
190 #[test]
191 fn client_options_enables_attach_stacktrace() -> Result<(), Box<dyn core::error::Error>> {
192 let _env_guard = lock_env();
193 let dsn: sentry::types::Dsn = "https://public@example.com/1".parse()?;
194 let opts = super::client_options(dsn);
195 if opts.attach_stacktrace {
196 Ok(())
197 } else {
198 Err("attach_stacktrace should be true".into())
199 }
200 }
201}