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