1use std::any::TypeId;
2use std::borrow::Cow;
3#[cfg(any(feature = "logs", feature = "metrics"))]
4use std::collections::BTreeMap;
5use std::fmt;
6use std::panic::RefUnwindSafe;
7use std::sync::{Arc, RwLock};
8use std::time::Duration;
9
10#[cfg(feature = "metrics")]
11use crate::metrics::IntoProtocolMetric;
12#[cfg(feature = "release-health")]
13use crate::protocol::SessionUpdate;
14use rand::random;
15use sentry_types::random_uuid;
16
17#[cfg(any(feature = "logs", feature = "metrics"))]
18use crate::batcher::Batcher;
19use crate::constants::SDK_INFO;
20use crate::protocol::{ClientSdkInfo, Event};
21#[cfg(feature = "release-health")]
22use crate::session::SessionFlusher;
23use crate::types::{Dsn, Uuid};
24#[cfg(feature = "release-health")]
25use crate::SessionMode;
26use crate::{ClientOptions, Envelope, Hub, Integration, Scope, Transport};
27#[cfg(feature = "logs")]
28use sentry_types::protocol::v7::Context;
29#[cfg(feature = "logs")]
30use sentry_types::protocol::v7::Log;
31#[cfg(any(feature = "logs", feature = "metrics"))]
32use sentry_types::protocol::v7::LogAttribute;
33#[cfg(feature = "metrics")]
34use sentry_types::protocol::v7::Metric;
35
36impl<T: Into<ClientOptions>> From<T> for Client {
37 fn from(o: T) -> Client {
38 Client::with_options(o.into())
39 }
40}
41
42pub(crate) type TransportArc = Arc<RwLock<Option<Arc<dyn Transport>>>>;
43
44pub struct Client {
62 options: ClientOptions,
63 transport: TransportArc,
64 #[cfg(feature = "release-health")]
65 session_flusher: RwLock<Option<SessionFlusher>>,
66 #[cfg(feature = "logs")]
67 logs_batcher: RwLock<Option<Batcher<Log>>>,
68 #[cfg(feature = "metrics")]
69 metrics_batcher: RwLock<Option<Batcher<Metric>>>,
70 #[cfg(feature = "logs")]
71 default_log_attributes: Option<BTreeMap<String, LogAttribute>>,
72 #[cfg(feature = "metrics")]
73 default_metric_attributes: BTreeMap<Cow<'static, str>, LogAttribute>,
74 integrations: Vec<(TypeId, Arc<dyn Integration>)>,
75 pub(crate) sdk_info: ClientSdkInfo,
76}
77
78impl fmt::Debug for Client {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 f.debug_struct("Client")
81 .field("dsn", &self.dsn())
82 .field("options", &self.options)
83 .finish()
84 }
85}
86
87impl Clone for Client {
88 fn clone(&self) -> Client {
89 let transport = Arc::new(RwLock::new(self.transport.read().unwrap().clone()));
90
91 #[cfg(feature = "release-health")]
92 let session_flusher = RwLock::new(Some(SessionFlusher::new(
93 transport.clone(),
94 self.options.session_mode,
95 )));
96
97 #[cfg(feature = "logs")]
98 let logs_batcher = RwLock::new(if self.options.enable_logs {
99 Some(Batcher::new(transport.clone()))
100 } else {
101 None
102 });
103
104 #[cfg(feature = "metrics")]
105 let metrics_batcher = RwLock::new(
106 self.options
107 .enable_metrics
108 .then(|| Batcher::new(transport.clone())),
109 );
110
111 Client {
112 options: self.options.clone(),
113 transport,
114 #[cfg(feature = "release-health")]
115 session_flusher,
116 #[cfg(feature = "logs")]
117 logs_batcher,
118 #[cfg(feature = "metrics")]
119 metrics_batcher,
120 #[cfg(feature = "logs")]
121 default_log_attributes: self.default_log_attributes.clone(),
122 #[cfg(feature = "metrics")]
123 default_metric_attributes: self.default_metric_attributes.clone(),
124 integrations: self.integrations.clone(),
125 sdk_info: self.sdk_info.clone(),
126 }
127 }
128}
129
130impl Client {
131 pub fn from_config<O: Into<ClientOptions>>(opts: O) -> Client {
152 Client::with_options(opts.into())
153 }
154
155 pub fn with_options(mut options: ClientOptions) -> Client {
160 Hub::with(|_| {});
163
164 let create_transport = || {
165 options.dsn.as_ref()?;
166 let factory = options.transport.as_ref()?;
167 Some(factory.create_transport(&options))
168 };
169
170 let transport = Arc::new(RwLock::new(create_transport()));
171
172 let mut sdk_info = SDK_INFO.clone();
173
174 let integrations: Vec<_> = options
177 .integrations
178 .iter()
179 .map(|integration| (integration.as_ref().type_id(), integration.clone()))
180 .collect();
181
182 for (_, integration) in integrations.iter() {
183 integration.setup(&mut options);
184 sdk_info.integrations.push(integration.name().to_string());
185 }
186
187 #[cfg(feature = "release-health")]
188 let session_flusher = RwLock::new(Some(SessionFlusher::new(
189 transport.clone(),
190 options.session_mode,
191 )));
192
193 #[cfg(feature = "logs")]
194 let logs_batcher = RwLock::new(if options.enable_logs {
195 Some(Batcher::new(transport.clone()))
196 } else {
197 None
198 });
199
200 #[cfg(feature = "metrics")]
201 let metrics_batcher = RwLock::new(
202 options
203 .enable_metrics
204 .then(|| Batcher::new(transport.clone())),
205 );
206
207 #[allow(unused_mut)]
208 let mut client = Client {
209 options,
210 transport,
211 #[cfg(feature = "release-health")]
212 session_flusher,
213 #[cfg(feature = "logs")]
214 logs_batcher,
215 #[cfg(feature = "metrics")]
216 metrics_batcher,
217 #[cfg(feature = "logs")]
218 default_log_attributes: None,
219 #[cfg(feature = "metrics")]
220 default_metric_attributes: Default::default(),
221 integrations,
222 sdk_info,
223 };
224
225 #[cfg(feature = "logs")]
226 client.cache_default_log_attributes();
227
228 #[cfg(feature = "metrics")]
229 client.cache_default_metric_attributes();
230
231 client
232 }
233
234 #[cfg(feature = "logs")]
235 fn cache_default_log_attributes(&mut self) {
236 let mut attributes = BTreeMap::new();
237
238 if let Some(environment) = self.options.environment.as_ref() {
239 attributes.insert("sentry.environment".to_owned(), environment.clone().into());
240 }
241
242 if let Some(release) = self.options.release.as_ref() {
243 attributes.insert("sentry.release".to_owned(), release.clone().into());
244 }
245
246 attributes.insert(
247 "sentry.sdk.name".to_owned(),
248 self.sdk_info.name.to_owned().into(),
249 );
250
251 attributes.insert(
252 "sentry.sdk.version".to_owned(),
253 self.sdk_info.version.to_owned().into(),
254 );
255
256 let mut fake_event = Event::default();
263 for (_, integration) in self.integrations.iter() {
264 if let Some(res) = integration.process_event(fake_event.clone(), &self.options) {
265 fake_event = res;
266 }
267 }
268
269 if let Some(Context::Os(os)) = fake_event.contexts.get("os") {
270 if let Some(name) = os.name.as_ref() {
271 attributes.insert("os.name".to_owned(), name.to_owned().into());
272 }
273 if let Some(version) = os.version.as_ref() {
274 attributes.insert("os.version".to_owned(), version.to_owned().into());
275 }
276 }
277
278 if let Some(server) = &self.options.server_name {
279 attributes.insert("server.address".to_owned(), server.clone().into());
280 }
281
282 self.default_log_attributes = Some(attributes);
283 }
284
285 #[cfg(feature = "metrics")]
286 fn cache_default_metric_attributes(&mut self) {
287 let always_present_attributes = [
288 ("sentry.sdk.name", &self.sdk_info.name),
289 ("sentry.sdk.version", &self.sdk_info.version),
290 ]
291 .into_iter()
292 .map(|(name, value)| (name.into(), value.as_str().into()));
293
294 let maybe_present_attributes = [
295 ("sentry.environment", &self.options.environment),
296 ("sentry.release", &self.options.release),
297 ("server.address", &self.options.server_name),
298 ]
299 .into_iter()
300 .filter_map(|(name, value)| value.clone().map(|value| (name.into(), value.into())));
301
302 self.default_metric_attributes = maybe_present_attributes
303 .chain(always_present_attributes)
304 .collect();
305 }
306
307 pub(crate) fn get_integration<I>(&self) -> Option<&I>
308 where
309 I: Integration,
310 {
311 let id = TypeId::of::<I>();
312 let integration = &self.integrations.iter().find(|(iid, _)| *iid == id)?.1;
313 integration.as_ref().as_any().downcast_ref()
314 }
315
316 pub fn prepare_event(
318 &self,
319 mut event: Event<'static>,
320 scope: Option<&Scope>,
321 ) -> Option<Event<'static>> {
322 if event.event_id.is_nil() {
325 event.event_id = random_uuid();
326 }
327
328 if event.sdk.is_none() {
329 event.sdk = Some(Cow::Owned(self.sdk_info.clone()));
331 }
332
333 if let Some(scope) = scope {
334 event = scope.apply_to_event(event)?;
335 }
336
337 for (_, integration) in self.integrations.iter() {
338 let id = event.event_id;
339 event = match integration.process_event(event, &self.options) {
340 Some(event) => event,
341 None => {
342 sentry_debug!("integration dropped event {:?}", id);
343 return None;
344 }
345 }
346 }
347
348 if event.release.is_none() {
349 event.release.clone_from(&self.options.release);
350 }
351 if event.environment.is_none() {
352 event.environment.clone_from(&self.options.environment);
353 }
354 if event.server_name.is_none() {
355 event.server_name.clone_from(&self.options.server_name);
356 }
357 if &event.platform == "other" {
358 event.platform = "native".into();
359 }
360
361 if let Some(ref func) = self.options.before_send {
362 sentry_debug!("invoking before_send callback");
363 let id = event.event_id;
364 if let Some(processed_event) = func(event) {
365 event = processed_event;
366 } else {
367 sentry_debug!("before_send dropped event {:?}", id);
368 return None;
369 }
370 }
371
372 if let Some(scope) = scope {
373 scope.update_session_from_event(&event);
374 }
375
376 if !self.sample_should_send(self.options.sample_rate) {
377 None
378 } else {
379 Some(event)
380 }
381 }
382
383 pub fn options(&self) -> &ClientOptions {
385 &self.options
386 }
387
388 pub fn dsn(&self) -> Option<&Dsn> {
390 self.options.dsn.as_ref()
391 }
392
393 pub fn is_enabled(&self) -> bool {
417 self.options.dsn.is_some() && self.transport.read().unwrap().is_some()
418 }
419
420 pub fn capture_event(&self, event: Event<'static>, scope: Option<&Scope>) -> Uuid {
422 if let Some(ref transport) = *self.transport.read().unwrap() {
423 if let Some(event) = self.prepare_event(event, scope) {
424 let event_id = event.event_id;
425 let mut envelope: Envelope = event.into();
426 #[cfg(feature = "release-health")]
429 if self.options.session_mode == SessionMode::Application {
430 let session_item = scope.and_then(|scope| {
431 scope
432 .session
433 .lock()
434 .unwrap()
435 .as_mut()
436 .and_then(|session| session.create_envelope_item())
437 });
438 if let Some(session_item) = session_item {
439 envelope.add_item(session_item);
440 }
441 }
442
443 if let Some(scope) = scope {
444 for attachment in scope.attachments.iter().cloned() {
445 envelope.add_item(attachment);
446 }
447 }
448
449 transport.send_envelope(envelope);
450 return event_id;
451 }
452 }
453 Default::default()
454 }
455
456 pub fn send_envelope(&self, envelope: Envelope) {
458 if let Some(ref transport) = *self.transport.read().unwrap() {
459 transport.send_envelope(envelope);
460 }
461 }
462
463 #[cfg(feature = "release-health")]
464 pub(crate) fn enqueue_session(&self, session_update: SessionUpdate<'static>) {
465 if let Some(ref flusher) = *self.session_flusher.read().unwrap() {
466 flusher.enqueue(session_update);
467 }
468 }
469
470 pub fn flush(&self, timeout: Option<Duration>) -> bool {
472 #[cfg(feature = "release-health")]
473 if let Some(ref flusher) = *self.session_flusher.read().unwrap() {
474 flusher.flush();
475 }
476 #[cfg(feature = "logs")]
477 if let Some(ref batcher) = *self.logs_batcher.read().unwrap() {
478 batcher.flush();
479 }
480 #[cfg(feature = "metrics")]
481 if let Some(ref batcher) = *self.metrics_batcher.read().unwrap() {
482 batcher.flush();
483 }
484 if let Some(ref transport) = *self.transport.read().unwrap() {
485 transport.flush(timeout.unwrap_or(self.options.shutdown_timeout))
486 } else {
487 true
488 }
489 }
490
491 pub fn close(&self, timeout: Option<Duration>) -> bool {
499 #[cfg(feature = "release-health")]
500 drop(self.session_flusher.write().unwrap().take());
501 #[cfg(feature = "logs")]
502 drop(self.logs_batcher.write().unwrap().take());
503 #[cfg(feature = "metrics")]
504 drop(self.metrics_batcher.write().unwrap().take());
505 let transport_opt = self.transport.write().unwrap().take();
506 if let Some(transport) = transport_opt {
507 sentry_debug!("client close; request transport to shut down");
508 transport.shutdown(timeout.unwrap_or(self.options.shutdown_timeout))
509 } else {
510 sentry_debug!("client close; no transport to shut down");
511 true
512 }
513 }
514
515 pub fn sample_should_send(&self, rate: f32) -> bool {
518 if rate >= 1.0 {
519 true
520 } else if rate <= 0.0 {
521 false
522 } else {
523 random::<f32>() < rate
524 }
525 }
526
527 #[cfg(feature = "logs")]
529 pub fn capture_log(&self, log: Log, scope: &Scope) {
530 if !self.options.enable_logs {
531 sentry_debug!("[Client] called capture_log, but options.enable_logs is set to false");
532 return;
533 }
534 if let Some(log) = self.prepare_log(log, scope) {
535 if let Some(ref batcher) = *self.logs_batcher.read().unwrap() {
536 batcher.enqueue(log);
537 }
538 }
539 }
540
541 #[cfg(feature = "logs")]
544 fn prepare_log(&self, mut log: Log, scope: &Scope) -> Option<Log> {
545 scope.apply_to_log(&mut log);
546
547 if let Some(default_attributes) = self.default_log_attributes.as_ref() {
548 for (key, val) in default_attributes.iter() {
549 log.attributes.entry(key.to_owned()).or_insert(val.clone());
550 }
551 }
552
553 if let Some(ref func) = self.options.before_send_log {
554 log = func(log)?;
555 }
556
557 Some(log)
558 }
559
560 #[cfg(feature = "metrics")]
562 pub fn capture_metric<M: IntoProtocolMetric>(&self, metric: M, scope: &Scope) {
563 if !self.options.enable_metrics {
564 return;
566 }
567
568 if let Some(metric) = self.prepare_metric(metric, scope) {
569 if let Some(batcher) = self
570 .metrics_batcher
571 .read()
572 .expect("metrics batcher lock could not be acquired")
573 .as_ref()
574 {
575 batcher.enqueue(metric);
576 }
577 }
578 }
579
580 #[cfg(feature = "metrics")]
583 fn prepare_metric<M: IntoProtocolMetric>(&self, metric: M, scope: &Scope) -> Option<Metric> {
584 let mut metric = scope.apply_to_metric(metric, self.options().send_default_pii);
585
586 for (key, val) in &self.default_metric_attributes {
587 metric.attributes.entry(key.clone()).or_insert(val.clone());
588 }
589
590 if let Some(ref func) = self.options.before_send_metric {
591 metric = func(metric)?;
592 }
593
594 Some(metric)
595 }
596}
597
598impl RefUnwindSafe for Client {}