sentry_core/
client.rs

1use std::any::TypeId;
2use std::borrow::Cow;
3#[cfg(feature = "logs")]
4use std::collections::BTreeMap;
5use std::fmt;
6use std::panic::RefUnwindSafe;
7use std::sync::{Arc, RwLock};
8use std::time::Duration;
9
10#[cfg(feature = "release-health")]
11use crate::protocol::SessionUpdate;
12use rand::random;
13use sentry_types::random_uuid;
14
15use crate::constants::SDK_INFO;
16#[cfg(feature = "logs")]
17use crate::logs::LogsBatcher;
18use crate::protocol::{ClientSdkInfo, Event};
19#[cfg(feature = "release-health")]
20use crate::session::SessionFlusher;
21use crate::types::{Dsn, Uuid};
22#[cfg(feature = "release-health")]
23use crate::SessionMode;
24use crate::{ClientOptions, Envelope, Hub, Integration, Scope, Transport};
25#[cfg(feature = "logs")]
26use sentry_types::protocol::v7::Context;
27#[cfg(feature = "logs")]
28use sentry_types::protocol::v7::{Log, LogAttribute};
29
30impl<T: Into<ClientOptions>> From<T> for Client {
31    fn from(o: T) -> Client {
32        Client::with_options(o.into())
33    }
34}
35
36pub(crate) type TransportArc = Arc<RwLock<Option<Arc<dyn Transport>>>>;
37
38/// The Sentry Client.
39///
40/// The Client is responsible for event processing and sending events to the
41/// sentry server via the configured [`Transport`]. It can be created from a
42/// [`ClientOptions`].
43///
44/// See the [Unified API] document for more details.
45///
46/// # Examples
47///
48/// ```
49/// sentry::Client::from(sentry::ClientOptions::default());
50/// ```
51///
52/// [`ClientOptions`]: struct.ClientOptions.html
53/// [`Transport`]: trait.Transport.html
54/// [Unified API]: https://develop.sentry.dev/sdk/unified-api/
55pub struct Client {
56    options: ClientOptions,
57    transport: TransportArc,
58    #[cfg(feature = "release-health")]
59    session_flusher: RwLock<Option<SessionFlusher>>,
60    #[cfg(feature = "logs")]
61    logs_batcher: RwLock<Option<LogsBatcher>>,
62    #[cfg(feature = "logs")]
63    default_log_attributes: Option<BTreeMap<String, LogAttribute>>,
64    integrations: Vec<(TypeId, Arc<dyn Integration>)>,
65    pub(crate) sdk_info: ClientSdkInfo,
66}
67
68impl fmt::Debug for Client {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        f.debug_struct("Client")
71            .field("dsn", &self.dsn())
72            .field("options", &self.options)
73            .finish()
74    }
75}
76
77impl Clone for Client {
78    fn clone(&self) -> Client {
79        let transport = Arc::new(RwLock::new(self.transport.read().unwrap().clone()));
80
81        #[cfg(feature = "release-health")]
82        let session_flusher = RwLock::new(Some(SessionFlusher::new(
83            transport.clone(),
84            self.options.session_mode,
85        )));
86
87        #[cfg(feature = "logs")]
88        let logs_batcher = RwLock::new(if self.options.enable_logs {
89            Some(LogsBatcher::new(transport.clone()))
90        } else {
91            None
92        });
93
94        Client {
95            options: self.options.clone(),
96            transport,
97            #[cfg(feature = "release-health")]
98            session_flusher,
99            #[cfg(feature = "logs")]
100            logs_batcher,
101            #[cfg(feature = "logs")]
102            default_log_attributes: self.default_log_attributes.clone(),
103            integrations: self.integrations.clone(),
104            sdk_info: self.sdk_info.clone(),
105        }
106    }
107}
108
109impl Client {
110    /// Creates a new Sentry client from a config.
111    ///
112    /// # Supported Configs
113    ///
114    /// The following common values are supported for the client config:
115    ///
116    /// * `ClientOptions`: configure the client with the given client options.
117    /// * `()` or empty string: Disable the client.
118    /// * `&str` / `String` / `&OsStr` / `String`: configure the client with the given DSN.
119    /// * `Dsn` / `&Dsn`: configure the client with a given DSN.
120    /// * `(Dsn, ClientOptions)`: configure the client from the given DSN and optional options.
121    ///
122    /// The `Default` implementation of `ClientOptions` pulls in the DSN from the
123    /// `SENTRY_DSN` environment variable.
124    ///
125    /// # Panics
126    ///
127    /// The `Into<ClientOptions>` implementations can panic for the forms where a DSN needs to be
128    /// parsed.  If you want to handle invalid DSNs you need to parse them manually by calling
129    /// parse on it and handle the error.
130    pub fn from_config<O: Into<ClientOptions>>(opts: O) -> Client {
131        Client::with_options(opts.into())
132    }
133
134    /// Creates a new sentry client for the given options.
135    ///
136    /// If the DSN on the options is set to `None` the client will be entirely
137    /// disabled.
138    pub fn with_options(mut options: ClientOptions) -> Client {
139        // Create the main hub eagerly to avoid problems with the background thread
140        // See https://github.com/getsentry/sentry-rust/issues/237
141        Hub::with(|_| {});
142
143        let create_transport = || {
144            options.dsn.as_ref()?;
145            let factory = options.transport.as_ref()?;
146            Some(factory.create_transport(&options))
147        };
148
149        let transport = Arc::new(RwLock::new(create_transport()));
150
151        let mut sdk_info = SDK_INFO.clone();
152
153        // NOTE: We do not filter out duplicate integrations based on their
154        // TypeId.
155        let integrations: Vec<_> = options
156            .integrations
157            .iter()
158            .map(|integration| (integration.as_ref().type_id(), integration.clone()))
159            .collect();
160
161        for (_, integration) in integrations.iter() {
162            integration.setup(&mut options);
163            sdk_info.integrations.push(integration.name().to_string());
164        }
165
166        #[cfg(feature = "release-health")]
167        let session_flusher = RwLock::new(Some(SessionFlusher::new(
168            transport.clone(),
169            options.session_mode,
170        )));
171
172        #[cfg(feature = "logs")]
173        let logs_batcher = RwLock::new(if options.enable_logs {
174            Some(LogsBatcher::new(transport.clone()))
175        } else {
176            None
177        });
178
179        #[allow(unused_mut)]
180        let mut client = Client {
181            options,
182            transport,
183            #[cfg(feature = "release-health")]
184            session_flusher,
185            #[cfg(feature = "logs")]
186            logs_batcher,
187            #[cfg(feature = "logs")]
188            default_log_attributes: None,
189            integrations,
190            sdk_info,
191        };
192
193        #[cfg(feature = "logs")]
194        client.cache_default_log_attributes();
195
196        client
197    }
198
199    #[cfg(feature = "logs")]
200    fn cache_default_log_attributes(&mut self) {
201        let mut attributes = BTreeMap::new();
202
203        if let Some(environment) = self.options.environment.as_ref() {
204            attributes.insert("sentry.environment".to_owned(), environment.clone().into());
205        }
206
207        if let Some(release) = self.options.release.as_ref() {
208            attributes.insert("sentry.release".to_owned(), release.clone().into());
209        }
210
211        attributes.insert(
212            "sentry.sdk.name".to_owned(),
213            self.sdk_info.name.to_owned().into(),
214        );
215
216        attributes.insert(
217            "sentry.sdk.version".to_owned(),
218            self.sdk_info.version.to_owned().into(),
219        );
220
221        // Process a fake event through integrations, so that `ContextIntegration` (if available)
222        // provides the OS Context.
223        // This is needed as that integration adds the OS Context to events using an event
224        // processor, which logs don't go through.
225        // We cannot get the `ContextIntegration` directly, as its type lives in `sentry-contexts`,
226        // which `sentry-core` doesn't depend on.
227        let mut fake_event = Event::default();
228        for (_, integration) in self.integrations.iter() {
229            if let Some(res) = integration.process_event(fake_event.clone(), &self.options) {
230                fake_event = res;
231            }
232        }
233
234        if let Some(Context::Os(os)) = fake_event.contexts.get("os") {
235            if let Some(name) = os.name.as_ref() {
236                attributes.insert("os.name".to_owned(), name.to_owned().into());
237            }
238            if let Some(version) = os.version.as_ref() {
239                attributes.insert("os.version".to_owned(), version.to_owned().into());
240            }
241        }
242
243        if let Some(server) = &self.options.server_name {
244            attributes.insert("server.address".to_owned(), server.clone().into());
245        }
246
247        self.default_log_attributes = Some(attributes);
248    }
249
250    pub(crate) fn get_integration<I>(&self) -> Option<&I>
251    where
252        I: Integration,
253    {
254        let id = TypeId::of::<I>();
255        let integration = &self.integrations.iter().find(|(iid, _)| *iid == id)?.1;
256        integration.as_ref().as_any().downcast_ref()
257    }
258
259    /// Prepares an event for transmission to sentry.
260    pub fn prepare_event(
261        &self,
262        mut event: Event<'static>,
263        scope: Option<&Scope>,
264    ) -> Option<Event<'static>> {
265        // event_id and sdk_info are set before the processors run so that the
266        // processors can poke around in that data.
267        if event.event_id.is_nil() {
268            event.event_id = random_uuid();
269        }
270
271        if event.sdk.is_none() {
272            // NOTE: we need to clone here because `Event` must be `'static`
273            event.sdk = Some(Cow::Owned(self.sdk_info.clone()));
274        }
275
276        if let Some(scope) = scope {
277            event = scope.apply_to_event(event)?;
278        }
279
280        for (_, integration) in self.integrations.iter() {
281            let id = event.event_id;
282            event = match integration.process_event(event, &self.options) {
283                Some(event) => event,
284                None => {
285                    sentry_debug!("integration dropped event {:?}", id);
286                    return None;
287                }
288            }
289        }
290
291        if event.release.is_none() {
292            event.release.clone_from(&self.options.release);
293        }
294        if event.environment.is_none() {
295            event.environment.clone_from(&self.options.environment);
296        }
297        if event.server_name.is_none() {
298            event.server_name.clone_from(&self.options.server_name);
299        }
300        if &event.platform == "other" {
301            event.platform = "native".into();
302        }
303
304        if let Some(ref func) = self.options.before_send {
305            sentry_debug!("invoking before_send callback");
306            let id = event.event_id;
307            if let Some(processed_event) = func(event) {
308                event = processed_event;
309            } else {
310                sentry_debug!("before_send dropped event {:?}", id);
311                return None;
312            }
313        }
314
315        if let Some(scope) = scope {
316            scope.update_session_from_event(&event);
317        }
318
319        if !self.sample_should_send(self.options.sample_rate) {
320            None
321        } else {
322            Some(event)
323        }
324    }
325
326    /// Returns the options of this client.
327    pub fn options(&self) -> &ClientOptions {
328        &self.options
329    }
330
331    /// Returns the DSN that constructed this client.
332    pub fn dsn(&self) -> Option<&Dsn> {
333        self.options.dsn.as_ref()
334    }
335
336    /// Quick check to see if the client is enabled.
337    ///
338    /// The Client is enabled if it has a valid DSN and Transport configured.
339    ///
340    /// # Examples
341    ///
342    /// ```
343    /// use std::sync::Arc;
344    ///
345    /// let client = sentry::Client::from(sentry::ClientOptions::default());
346    /// assert!(!client.is_enabled());
347    ///
348    /// let dsn = "https://public@example.com/1";
349    /// let transport = sentry::test::TestTransport::new();
350    /// let client = sentry::Client::from((
351    ///     dsn,
352    ///     sentry::ClientOptions {
353    ///         transport: Some(Arc::new(transport)),
354    ///         ..Default::default()
355    ///     },
356    /// ));
357    /// assert!(client.is_enabled());
358    /// ```
359    pub fn is_enabled(&self) -> bool {
360        self.options.dsn.is_some() && self.transport.read().unwrap().is_some()
361    }
362
363    /// Captures an event and sends it to sentry.
364    pub fn capture_event(&self, event: Event<'static>, scope: Option<&Scope>) -> Uuid {
365        if let Some(ref transport) = *self.transport.read().unwrap() {
366            if let Some(event) = self.prepare_event(event, scope) {
367                let event_id = event.event_id;
368                let mut envelope: Envelope = event.into();
369                // For request-mode sessions, we aggregate them all instead of
370                // flushing them out early.
371                #[cfg(feature = "release-health")]
372                if self.options.session_mode == SessionMode::Application {
373                    let session_item = scope.and_then(|scope| {
374                        scope
375                            .session
376                            .lock()
377                            .unwrap()
378                            .as_mut()
379                            .and_then(|session| session.create_envelope_item())
380                    });
381                    if let Some(session_item) = session_item {
382                        envelope.add_item(session_item);
383                    }
384                }
385
386                if let Some(scope) = scope {
387                    for attachment in scope.attachments.iter().cloned() {
388                        envelope.add_item(attachment);
389                    }
390                }
391
392                transport.send_envelope(envelope);
393                return event_id;
394            }
395        }
396        Default::default()
397    }
398
399    /// Sends the specified [`Envelope`] to sentry.
400    pub fn send_envelope(&self, envelope: Envelope) {
401        if let Some(ref transport) = *self.transport.read().unwrap() {
402            transport.send_envelope(envelope);
403        }
404    }
405
406    #[cfg(feature = "release-health")]
407    pub(crate) fn enqueue_session(&self, session_update: SessionUpdate<'static>) {
408        if let Some(ref flusher) = *self.session_flusher.read().unwrap() {
409            flusher.enqueue(session_update);
410        }
411    }
412
413    /// Drains all pending events without shutting down.
414    pub fn flush(&self, timeout: Option<Duration>) -> bool {
415        #[cfg(feature = "release-health")]
416        if let Some(ref flusher) = *self.session_flusher.read().unwrap() {
417            flusher.flush();
418        }
419        #[cfg(feature = "logs")]
420        if let Some(ref batcher) = *self.logs_batcher.read().unwrap() {
421            batcher.flush();
422        }
423        if let Some(ref transport) = *self.transport.read().unwrap() {
424            transport.flush(timeout.unwrap_or(self.options.shutdown_timeout))
425        } else {
426            true
427        }
428    }
429
430    /// Drains all pending events and shuts down the transport behind the
431    /// client.  After shutting down the transport is removed.
432    ///
433    /// This returns `true` if the queue was successfully drained in the
434    /// given time or `false` if not (for instance because of a timeout).
435    /// If no timeout is provided the client will wait for as long a
436    /// `shutdown_timeout` in the client options.
437    pub fn close(&self, timeout: Option<Duration>) -> bool {
438        #[cfg(feature = "release-health")]
439        drop(self.session_flusher.write().unwrap().take());
440        #[cfg(feature = "logs")]
441        drop(self.logs_batcher.write().unwrap().take());
442        let transport_opt = self.transport.write().unwrap().take();
443        if let Some(transport) = transport_opt {
444            sentry_debug!("client close; request transport to shut down");
445            transport.shutdown(timeout.unwrap_or(self.options.shutdown_timeout))
446        } else {
447            sentry_debug!("client close; no transport to shut down");
448            true
449        }
450    }
451
452    /// Returns a random boolean with a probability defined
453    /// by rate
454    pub fn sample_should_send(&self, rate: f32) -> bool {
455        if rate >= 1.0 {
456            true
457        } else if rate <= 0.0 {
458            false
459        } else {
460            random::<f32>() < rate
461        }
462    }
463
464    /// Captures a log and sends it to Sentry.
465    #[cfg(feature = "logs")]
466    pub fn capture_log(&self, log: Log, scope: &Scope) {
467        if !self.options.enable_logs {
468            sentry_debug!("[Client] called capture_log, but options.enable_logs is set to false");
469            return;
470        }
471        if let Some(log) = self.prepare_log(log, scope) {
472            if let Some(ref batcher) = *self.logs_batcher.read().unwrap() {
473                batcher.enqueue(log);
474            }
475        }
476    }
477
478    /// Prepares a log to be sent, setting the `trace_id` and other default attributes, and
479    /// processing it through `before_send_log`.
480    #[cfg(feature = "logs")]
481    fn prepare_log(&self, mut log: Log, scope: &Scope) -> Option<Log> {
482        scope.apply_to_log(&mut log);
483
484        if let Some(default_attributes) = self.default_log_attributes.as_ref() {
485            for (key, val) in default_attributes.iter() {
486                log.attributes.entry(key.to_owned()).or_insert(val.clone());
487            }
488        }
489
490        if let Some(ref func) = self.options.before_send_log {
491            log = func(log)?;
492        }
493
494        Some(log)
495    }
496}
497
498// Make this unwind safe. It's not out of the box because of the
499// `BeforeCallback`s inside `ClientOptions`, and the contained Integrations
500impl RefUnwindSafe for Client {}