sentry_core/hub.rs
1use std::sync::{Arc, RwLock};
2
3#[cfg(feature = "metrics")]
4use crate::metrics::IntoProtocolMetric;
5#[cfg(feature = "logs")]
6use crate::protocol::Log;
7#[cfg(feature = "release-health")]
8use crate::protocol::SessionStatus;
9use crate::protocol::{Event, Level};
10use crate::types::Uuid;
11use crate::{Integration, IntoBreadcrumbs, Scope, ScopeGuard};
12
13/// Marks values as used in minimal builds where `with_client_impl!` turns many
14/// method bodies into no-ops and would otherwise leave their parameters unused.
15macro_rules! use_without_client {
16 ($($value:expr),+ $(,)?) => {
17 #[cfg(not(feature = "client"))]
18 let _ = ($( &$value ),+,);
19 };
20}
21
22/// The central object that can manage scopes and clients.
23///
24/// This can be used to capture events and manage the scope. This object is [`Send`] and
25/// [`Sync`] so it can be used from multiple threads if needed.
26///
27/// Each thread has its own thread-local ( see [`Hub::current`]) hub, which is
28/// automatically derived from the main hub ([`Hub::main`]).
29///
30/// In most situations, developers do not need to interface with the hub directly. Instead
31/// toplevel convenience functions are exposed that will automatically dispatch
32/// to the thread-local ([`Hub::current`]) hub. In some situations, this might not be
33/// possible, in which case it might become necessary to manually work with the
34/// hub. See the main [`crate`] docs for some common use-cases and pitfalls
35/// related to parallel, concurrent or async code.
36///
37/// Hubs that are wrapped in [`Arc`]s can be bound to the current thread with
38/// the `run` static method.
39///
40/// Most common operations:
41///
42/// * [`Hub::new`]: creates a brand new hub
43/// * [`Hub::current`]: returns the thread local hub
44/// * [`Hub::with`]: invoke a callback with the thread local hub
45/// * [`Hub::with_active`]: like `Hub::with` but does not invoke the callback if
46/// the client is not in a supported state or not bound
47/// * [`Hub::new_from_top`]: creates a new hub with just the top scope of another hub.
48#[derive(Debug)]
49pub struct Hub {
50 #[cfg(feature = "client")]
51 pub(crate) inner: crate::hub_impl::HubImpl,
52 pub(crate) last_event_id: RwLock<Option<Uuid>>,
53}
54
55impl Hub {
56 /// Like [`Hub::with`] but only calls the function if a client is bound.
57 ///
58 /// This is useful for integrations that want to do efficiently nothing if there is no
59 /// client bound. Additionally this internally ensures that the client can be safely
60 /// synchronized. This prevents accidental recursive calls into the client.
61 pub fn with_active<F, R>(f: F) -> R
62 where
63 F: FnOnce(&Arc<Hub>) -> R,
64 R: Default,
65 {
66 use_without_client!(f);
67 with_client_impl! {{
68 Hub::with(|hub| {
69 if hub.is_active_and_usage_safe() {
70 f(hub)
71 } else {
72 Default::default()
73 }
74 })
75 }}
76 }
77
78 /// Looks up an integration on the hub.
79 ///
80 /// Calls the given function with the requested integration instance when it
81 /// is active on the currently active client.
82 ///
83 /// See the global [`capture_event`](fn.capture_event.html)
84 /// for more documentation.
85 pub fn with_integration<I, F, R>(&self, f: F) -> R
86 where
87 I: Integration,
88 F: FnOnce(&I) -> R,
89 R: Default,
90 {
91 use_without_client!(f);
92 with_client_impl! {{
93 if let Some(client) = self.client() {
94 if let Some(integration) = client.get_integration::<I>() {
95 return f(integration);
96 }
97 }
98 Default::default()
99 }}
100 }
101
102 /// Returns the last event id.
103 pub fn last_event_id(&self) -> Option<Uuid> {
104 *self.last_event_id.read().unwrap()
105 }
106
107 /// Sends the event to the current client with the current scope.
108 ///
109 /// In case no client is bound this does nothing instead.
110 ///
111 /// See the global [`capture_event`](fn.capture_event.html)
112 /// for more documentation.
113 pub fn capture_event(&self, event: Event<'static>) -> Uuid {
114 use_without_client!(event);
115 with_client_impl! {{
116 let top = self.inner.with(|stack| stack.top().clone());
117 let Some(ref client) = top.client else { return Default::default() };
118 let event_id = client.capture_event(event, Some(&top.scope));
119 *self.last_event_id.write().unwrap() = Some(event_id);
120 event_id
121 }}
122 }
123
124 /// Captures an arbitrary message.
125 ///
126 /// See the global [`capture_message`](fn.capture_message.html)
127 /// for more documentation.
128 pub fn capture_message(&self, msg: &str, level: Level) -> Uuid {
129 use_without_client!(msg, level);
130 with_client_impl! {{
131 let event = Event {
132 message: Some(msg.to_string()),
133 level,
134 ..Default::default()
135 };
136 self.capture_event(event)
137 }}
138 }
139
140 /// Start a new session for Release Health.
141 ///
142 /// See the global [`start_session`](fn.start_session.html)
143 /// for more documentation.
144 #[cfg(feature = "release-health")]
145 pub fn start_session(&self) {
146 with_client_impl! {{
147 self.inner.with_mut(|stack| {
148 let top = stack.top_mut();
149 if let Some(session) = crate::session::Session::from_stack(top) {
150 // When creating a *new* session, we make sure it is unique,
151 // as to no inherit *backwards* to any parents.
152 let scope = Arc::make_mut(&mut top.scope);
153 scope.session = Arc::new(std::sync::Mutex::new(Some(session)));
154 }
155 })
156 }}
157 }
158
159 /// End the current Release Health Session.
160 ///
161 /// See the global [`sentry::end_session`](crate::end_session) for more documentation.
162 #[cfg(feature = "release-health")]
163 pub fn end_session(&self) {
164 self.end_session_with_status(SessionStatus::Exited)
165 }
166
167 /// End the current Release Health Session with the given [`SessionStatus`].
168 ///
169 /// See the global [`end_session_with_status`](crate::end_session_with_status)
170 /// for more documentation.
171 #[cfg(feature = "release-health")]
172 pub fn end_session_with_status(&self, status: SessionStatus) {
173 use_without_client!(status);
174 with_client_impl! {{
175 self.inner.with_mut(|stack| {
176 let top = stack.top_mut();
177 // drop will close and enqueue the session
178 if let Some(mut session) = top.scope.session.lock().unwrap().take() {
179 session.close(status);
180 }
181 })
182 }}
183 }
184
185 /// Pushes a new scope.
186 ///
187 /// This returns a guard that when dropped will pop the scope again.
188 pub fn push_scope(&self) -> ScopeGuard {
189 with_client_impl! {{
190 self.inner.with_mut(|stack| {
191 stack.push();
192 ScopeGuard(Some((self.inner.stack.clone(), stack.depth())))
193 })
194 }}
195 }
196
197 /// Temporarily pushes a scope for a single call optionally reconfiguring it.
198 ///
199 /// See the global [`with_scope`](fn.with_scope.html)
200 /// for more documentation.
201 pub fn with_scope<C, F, R>(&self, scope_config: C, callback: F) -> R
202 where
203 C: FnOnce(&mut Scope),
204 F: FnOnce() -> R,
205 {
206 use_without_client!(scope_config);
207 #[cfg(feature = "client")]
208 {
209 let _guard = self.push_scope();
210 self.configure_scope(scope_config);
211 callback()
212 }
213 #[cfg(not(feature = "client"))]
214 {
215 callback()
216 }
217 }
218
219 /// Invokes a function that can modify the current scope.
220 ///
221 /// See the global [`configure_scope`](fn.configure_scope.html)
222 /// for more documentation.
223 pub fn configure_scope<F, R>(&self, f: F) -> R
224 where
225 R: Default,
226 F: FnOnce(&mut Scope) -> R,
227 {
228 use_without_client!(f);
229 with_client_impl! {{
230 let mut new_scope = self.with_current_scope(|scope| scope.clone());
231 let rv = f(&mut new_scope);
232 self.with_current_scope_mut(|ptr| *ptr = new_scope);
233 rv
234 }}
235 }
236
237 /// Adds a new breadcrumb to the current scope.
238 ///
239 /// See the global [`add_breadcrumb`](fn.add_breadcrumb.html)
240 /// for more documentation.
241 pub fn add_breadcrumb<B: IntoBreadcrumbs>(&self, breadcrumb: B) {
242 use_without_client!(breadcrumb);
243 with_client_impl! {{
244 self.inner.with_mut(|stack| {
245 let top = stack.top_mut();
246 if let Some(ref client) = top.client {
247 let scope = Arc::make_mut(&mut top.scope);
248 let options = client.options();
249 let breadcrumbs = Arc::make_mut(&mut scope.breadcrumbs);
250 for breadcrumb in breadcrumb.into_breadcrumbs() {
251 let breadcrumb_opt = match options.before_breadcrumb {
252 Some(ref callback) => callback(breadcrumb),
253 None => Some(breadcrumb)
254 };
255 if let Some(breadcrumb) = breadcrumb_opt {
256 breadcrumbs.push_back(breadcrumb);
257 }
258 while breadcrumbs.len() > options.max_breadcrumbs {
259 breadcrumbs.pop_front();
260 }
261 }
262 }
263 })
264 }}
265 }
266
267 /// Captures a structured log.
268 #[cfg(feature = "logs")]
269 pub fn capture_log(&self, log: Log) {
270 use_without_client!(log);
271 with_client_impl! {{
272 let top = self.inner.with(|stack| stack.top().clone());
273 let Some(ref client) = top.client else { return };
274 client.capture_log(log, &top.scope);
275 }}
276 }
277
278 /// Captures a metric on this hub, sending it to Sentry.
279 ///
280 /// If this hub has no client, the metric is dropped.
281 #[cfg(feature = "metrics")]
282 pub fn capture_metric<M: IntoProtocolMetric>(&self, metric: M) {
283 use_without_client!(metric);
284 with_client_impl! {{
285 let top = self.inner.with(|stack| stack.top().clone());
286 let Some(ref client) = top.client else { return };
287 client.capture_metric(metric, &top.scope);
288 }}
289 }
290}