Skip to main content

sentry_core/
client.rs

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
44/// The Sentry Client.
45///
46/// The Client is responsible for event processing and sending events to the
47/// sentry server via the configured [`Transport`]. It can be created from a
48/// [`ClientOptions`].
49///
50/// See the [Unified API] document for more details.
51///
52/// # Examples
53///
54/// ```
55/// sentry::Client::from(sentry::ClientOptions::default());
56/// ```
57///
58/// [`ClientOptions`]: struct.ClientOptions.html
59/// [`Transport`]: trait.Transport.html
60/// [Unified API]: https://develop.sentry.dev/sdk/unified-api/
61pub 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    /// Creates a new Sentry client from a config.
132    ///
133    /// # Supported Configs
134    ///
135    /// The following common values are supported for the client config:
136    ///
137    /// * `ClientOptions`: configure the client with the given client options.
138    /// * `()` or empty string: Disable the client.
139    /// * `&str` / `String` / `&OsStr` / `String`: configure the client with the given DSN.
140    /// * `Dsn` / `&Dsn`: configure the client with a given DSN.
141    /// * `(Dsn, ClientOptions)`: configure the client from the given DSN and optional options.
142    ///
143    /// The `Default` implementation of `ClientOptions` pulls in the DSN from the
144    /// `SENTRY_DSN` environment variable.
145    ///
146    /// # Panics
147    ///
148    /// The `Into<ClientOptions>` implementations can panic for the forms where a DSN needs to be
149    /// parsed.  If you want to handle invalid DSNs you need to parse them manually by calling
150    /// parse on it and handle the error.
151    pub fn from_config<O: Into<ClientOptions>>(opts: O) -> Client {
152        Client::with_options(opts.into())
153    }
154
155    /// Creates a new sentry client for the given options.
156    ///
157    /// If the DSN on the options is set to `None` the client will be entirely
158    /// disabled.
159    pub fn with_options(mut options: ClientOptions) -> Client {
160        // Create the main hub eagerly to avoid problems with the background thread
161        // See https://github.com/getsentry/sentry-rust/issues/237
162        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        // NOTE: We do not filter out duplicate integrations based on their
175        // TypeId.
176        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        // Process a fake event through integrations, so that `ContextIntegration` (if available)
257        // provides the OS Context.
258        // This is needed as that integration adds the OS Context to events using an event
259        // processor, which logs don't go through.
260        // We cannot get the `ContextIntegration` directly, as its type lives in `sentry-contexts`,
261        // which `sentry-core` doesn't depend on.
262        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    /// Prepares an event for transmission to sentry.
317    pub fn prepare_event(
318        &self,
319        mut event: Event<'static>,
320        scope: Option<&Scope>,
321    ) -> Option<Event<'static>> {
322        // event_id and sdk_info are set before the processors run so that the
323        // processors can poke around in that data.
324        if event.event_id.is_nil() {
325            event.event_id = random_uuid();
326        }
327
328        if event.sdk.is_none() {
329            // NOTE: we need to clone here because `Event` must be `'static`
330            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    /// Returns the options of this client.
384    pub fn options(&self) -> &ClientOptions {
385        &self.options
386    }
387
388    /// Returns the DSN that constructed this client.
389    pub fn dsn(&self) -> Option<&Dsn> {
390        self.options.dsn.as_ref()
391    }
392
393    /// Quick check to see if the client is enabled.
394    ///
395    /// The Client is enabled if it has a valid DSN and Transport configured.
396    ///
397    /// # Examples
398    ///
399    /// ```
400    /// use std::sync::Arc;
401    ///
402    /// let client = sentry::Client::from(sentry::ClientOptions::default());
403    /// assert!(!client.is_enabled());
404    ///
405    /// let dsn = "https://public@example.com/1";
406    /// let transport = sentry::test::TestTransport::new();
407    /// let client = sentry::Client::from((
408    ///     dsn,
409    ///     sentry::ClientOptions {
410    ///         transport: Some(Arc::new(transport)),
411    ///         ..Default::default()
412    ///     },
413    /// ));
414    /// assert!(client.is_enabled());
415    /// ```
416    pub fn is_enabled(&self) -> bool {
417        self.options.dsn.is_some() && self.transport.read().unwrap().is_some()
418    }
419
420    /// Captures an event and sends it to sentry.
421    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                // For request-mode sessions, we aggregate them all instead of
427                // flushing them out early.
428                #[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    /// Sends the specified [`Envelope`] to sentry.
457    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    /// Drains all pending events without shutting down.
471    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    /// Drains all pending events and shuts down the transport behind the
492    /// client.  After shutting down the transport is removed.
493    ///
494    /// This returns `true` if the queue was successfully drained in the
495    /// given time or `false` if not (for instance because of a timeout).
496    /// If no timeout is provided the client will wait for as long a
497    /// `shutdown_timeout` in the client options.
498    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    /// Returns a random boolean with a probability defined
516    /// by rate
517    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    /// Captures a log and sends it to Sentry.
528    #[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    /// Prepares a log to be sent, setting the `trace_id` and other default attributes, and
542    /// processing it through `before_send_log`.
543    #[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    /// Captures a metric and sends it to Sentry.
561    #[cfg(feature = "metrics")]
562    pub fn capture_metric<M: IntoProtocolMetric>(&self, metric: M, scope: &Scope) {
563        if !self.options.enable_metrics {
564            // Skip preparing the metric if we don't send it anyways.
565            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    /// Prepares a metric to be sent, setting the `trace_id` and other default attributes, and
581    /// processing it through `before_send_metric`.
582    #[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
598// Make this unwind safe. It's not out of the box because of the
599// `BeforeCallback`s inside `ClientOptions`, and the contained Integrations
600impl RefUnwindSafe for Client {}