binaryninja/
tracing.rs

1//! Integration with the [`tracing`](https://docs.rs/tracing) ecosystem. Send logs to and from Binary Ninja.
2//!
3//! This module allows you to use standard Rust `tracing` macros (like `info!`, `warn!`, `error!`)
4//! and have them get sent to Binary Ninja, as well as the other way around with [`TracingLogListener`].
5//!
6//! ## For Plugins
7//!
8//! When writing a plugin, and you want your Rust logs to appear in the Binary Ninja UI, use the
9//! [`crate::tracing_init`] macro at the start of your plugin's init function.
10//!
11//! ## For Headless
12//!
13//! When running headless (standalone executables), and you want internal Binary Ninja logs to appear
14//! in your terminal's standard output, register the [`TracingLogListener`] after initializing your
15//! tracing subscriber.
16//!
17//! ## Never use both [`BinaryNinjaLayer`] and [`TracingLogListener`] simultaneously
18//!
19//! Enabling both creates an infinite feedback loop where a log triggers a log, deadlocking the application.
20
21use crate::file_metadata::SessionId;
22use crate::logger::{
23    bn_log_with_session, BnLogLevel, LogContext, LogGuard, LogListener, LOGGER_DEFAULT_SESSION_ID,
24};
25use tracing::{Event, Id, Level, Subscriber};
26use tracing_subscriber::layer::Context;
27use tracing_subscriber::prelude::*;
28use tracing_subscriber::registry::LookupSpan;
29use tracing_subscriber::Layer;
30
31/// Helper macro to initialize the [`BinaryNinjaLayer`] tracing layer for plugins.
32///
33/// Maps the current crate name to the provided display name and enables target formatting.
34/// Use [`init_with_layer`] if you intend on registering a [`BinaryNinjaLayer`] with non-default values.
35///
36/// ## Note for Plugins
37///
38/// This should **only** be called once, at the start of plugins.
39///
40/// ```no_run
41/// #[unsafe(no_mangle)]
42/// pub unsafe extern "C" fn CorePluginInit() -> bool {
43///     binaryninja::tracing_init!("MyPlugin");
44///     tracing::info!("Core plugin initialized");
45///     true
46/// }
47/// ```
48///
49/// ## Note for Headless
50///
51/// This should **never** be called if you are running headlessly and as you will likely be
52/// registering a [`LogListener`], which will possibly round-trip back to tracing logs and deadlock
53/// the program.
54#[macro_export]
55macro_rules! tracing_init {
56    () => {
57        let layer = $crate::tracing::BinaryNinjaLayer::default();
58        $crate::tracing::init_with_layer(layer);
59    };
60    ($name:expr) => {
61        let layer = $crate::tracing::BinaryNinjaLayer::default()
62            .with_target_mapping(env!("CARGO_CRATE_NAME"), $name);
63        $crate::tracing::init_with_layer(layer);
64    };
65}
66
67/// Initialize the core tracing subscriber with the given [`BinaryNinjaLayer`]. Collects and sends logs
68/// to the core.
69pub fn init_with_layer(layer: BinaryNinjaLayer) {
70    let subscriber = tracing_subscriber::registry().with(layer);
71    let _ = tracing::subscriber::set_global_default(subscriber);
72}
73
74/// Subscribes to all spans and events emitted by `tracing` and forwards them to the core logging API.
75#[derive(Clone)]
76pub struct BinaryNinjaLayer {
77    /// Rewrite mappings for the default target.
78    ///
79    /// # Example
80    ///
81    /// Given the target "my_crate::commands::analyze" and the mapping ("my_crate", "MyPlugin") the
82    /// target will be rewritten to "MyPlugin::commands::analyze", assuming no other rewrites occur.
83    pub target_mappings: Vec<(String, String)>,
84    /// Whether the default target should be formatted to be displayed in the "common" logger name
85    /// format for Binary Ninja.
86    ///
87    /// This formatting will only be applied when the target name is implicit. Explicitly provided
88    /// target names will be preserved as-is.
89    ///
90    /// # Example
91    ///
92    /// Given the target "my_crate::commands::analyze" the target will be rewritten to "MyCrate.Commands.Analyze".
93    pub format_target: bool,
94}
95
96impl BinaryNinjaLayer {
97    pub fn new() -> Self {
98        Self::default()
99    }
100
101    /// Add a target mapping which will rewrite the `old` in the default target to `new`.
102    ///
103    /// This is typically done when you have a plugin name that is verbose, and you wish to display
104    /// it in the logs as something else.
105    pub fn with_target_mapping(mut self, old: &str, new: &str) -> Self {
106        self.target_mappings
107            .push((old.to_string(), new.to_string()));
108        self
109    }
110
111    /// Whether formatting of the default target should be applied when sending logs to the core.
112    pub fn with_format_target(mut self, formatted: bool) -> Self {
113        self.format_target = formatted;
114        self
115    }
116}
117
118impl BinaryNinjaLayer {
119    /// Rewrite the target so that the logger name will be displayed in the common Binary Ninja format.
120    pub fn rewrite_target(&self, mut target: String) -> String {
121        // Formats the target such that "my_crate::commands::analyze" becomes "MyPlugin.Commands.Analyze".
122        let format_target = |target: &str| -> String {
123            let mut result = String::with_capacity(target.len());
124            let mut capitalize_next = true;
125            let mut chars = target.chars().peekable();
126
127            while let Some(c) = chars.next() {
128                match c {
129                    ':' if chars.peek() == Some(&':') => {
130                        // Found "::", consume the second ':' and treat as a dot separator
131                        chars.next();
132                        result.push('.');
133                        capitalize_next = true;
134                    }
135                    '.' => {
136                        result.push('.');
137                        capitalize_next = true;
138                    }
139                    '_' => {
140                        // Treat underscore as a separator: strip it and capitalize next
141                        capitalize_next = true;
142                    }
143                    _ if capitalize_next => {
144                        for upper in c.to_uppercase() {
145                            result.push(upper);
146                        }
147                        capitalize_next = false;
148                    }
149                    _ => {
150                        result.push(c);
151                    }
152                }
153            }
154
155            result
156        };
157
158        // Perform "my_crate" -> "MyPlugin" rewrite rules.
159        for (old, new) in &self.target_mappings {
160            target = target.replace(old, new);
161        }
162
163        if self.format_target {
164            target = format_target(&target);
165        }
166
167        target
168    }
169}
170
171impl Default for BinaryNinjaLayer {
172    fn default() -> Self {
173        Self {
174            target_mappings: vec![("binaryninja".to_string(), "Binary Ninja".to_string())],
175            format_target: true,
176        }
177    }
178}
179
180impl<S> Layer<S> for BinaryNinjaLayer
181where
182    S: Subscriber + for<'a> LookupSpan<'a>,
183{
184    fn on_new_span(&self, attrs: &tracing::span::Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
185        let span = ctx.span(id).expect("Span not found in registry");
186
187        let mut visitor = BnFieldVisitor::default();
188        attrs.record(&mut visitor);
189
190        // If this span has a session_id, store it in the span's extensions
191        if let Some(session_id) = visitor.session_id {
192            span.extensions_mut().insert(session_id);
193        }
194    }
195
196    fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
197        let level = match *event.metadata().level() {
198            Level::ERROR => BnLogLevel::ErrorLog,
199            Level::WARN => BnLogLevel::WarningLog,
200            Level::INFO => BnLogLevel::InfoLog,
201            Level::DEBUG | Level::TRACE => BnLogLevel::DebugLog,
202        };
203
204        let mut visitor = BnFieldVisitor::default();
205        event.record(&mut visitor);
206
207        // Walk up the span tree to find the session id.
208        // NOTE: Inserted by `BinaryNinjaLayer::on_new_span`.
209        let session_from_scope = || {
210            ctx.event_scope(event).and_then(|scope| {
211                scope
212                    .from_root()
213                    .find_map(|span| span.extensions().get::<SessionId>().copied())
214            })
215        };
216
217        // First we check the log event itself, then we check the scope context.
218        let session_id = visitor
219            .session_id
220            .or_else(session_from_scope)
221            .unwrap_or(LOGGER_DEFAULT_SESSION_ID);
222
223        let mut logger_name = event.metadata().target().to_string();
224        // Target is not overridden, we should try and apply mapping.
225        if let Some(module_path) = event.metadata().module_path() {
226            if module_path == logger_name {
227                // Target is the default module path, rewrite it into a more friendly format.
228                logger_name = self.rewrite_target(logger_name);
229            }
230        }
231
232        bn_log_with_session(session_id, &logger_name, level, &visitor.message);
233    }
234}
235
236#[derive(Default)]
237struct BnFieldVisitor {
238    message: String,
239    session_id: Option<SessionId>,
240}
241
242impl tracing::field::Visit for BnFieldVisitor {
243    fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
244        if field.name() == "session_id" {
245            self.session_id = Some(SessionId(value as usize));
246        }
247    }
248
249    fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
250        if field.name() == "session_id" {
251            self.session_id = Some(SessionId(value as usize));
252        }
253    }
254
255    fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
256        if field.name() == "message" {
257            use std::fmt::Write;
258            let _ = write!(self.message, "{:?}", value);
259        }
260    }
261}
262
263/// A [`LogListener`] that forwards logs to the registered [`Subscriber`].
264///
265/// This should **never** be registered if the [`BinaryNinjaLayer`] is active. The [`BinaryNinjaLayer`]
266/// will consume our events we send in this log listener and send them back to the core, causing a
267/// never-ending cycle of sending and receiving the same logs.
268///
269/// Typically, you will register this listener for headless applications. You can technically use this
270/// in a plugin, but it is likely that you are sending tracing logs to the core, in which case you will
271/// run into the problem above.
272///
273/// ```no_run
274/// use binaryninja::tracing::TracingLogListener;
275/// use binaryninja::logger::{register_log_listener, BnLogLevel, bn_log};
276/// use binaryninja::headless::Session;
277///
278/// pub fn main() {
279///     // Register our tracing subscriber, this will send tracing events to stdout.
280///     tracing_subscriber::fmt::init();
281///     // Register our log listener, this will send logs from the core to our tracing subscriber.
282///     let _listener = TracingLogListener::new().register();
283///     // Should see logs from the core in regard to initialization show up.
284///     let _session = Session::new().expect("Failed to create session");
285///     bn_log("Test", BnLogLevel::DebugLog, "Hello, world!");
286/// }
287/// ```
288pub struct TracingLogListener {
289    minimum_level: BnLogLevel,
290}
291
292impl TracingLogListener {
293    /// Create a [`TracingLogListener`] with the minimum log level set to [`BnLogLevel::InfoLog`].
294    pub fn new() -> Self {
295        Self::default()
296    }
297
298    pub fn new_with_lvl(minimum_level: BnLogLevel) -> Self {
299        Self { minimum_level }
300    }
301
302    /// Register the [`TracingLogListener`] and send logs to the registered tracing subscriber until
303    /// the [`LogGuard`] is dropped, make sure to register your tracing subscriber before registering.
304    #[must_use]
305    pub fn register(self) -> LogGuard<Self> {
306        crate::logger::register_log_listener(self)
307    }
308}
309
310impl Default for TracingLogListener {
311    fn default() -> Self {
312        Self {
313            minimum_level: BnLogLevel::InfoLog,
314        }
315    }
316}
317
318impl LogListener for TracingLogListener {
319    fn log(&self, ctx: &LogContext, level: BnLogLevel, message: &str) {
320        let session = ctx.session_id.map(|s| s.0);
321        match level {
322            BnLogLevel::ErrorLog | BnLogLevel::AlertLog => {
323                tracing::error!(
324                    target: "binaryninja",
325                    session_id = session,
326                    logger = %ctx.logger_name,
327                    "{}",
328                    message
329                )
330            }
331            BnLogLevel::WarningLog => {
332                tracing::warn!(
333                    target: "binaryninja",
334                    session_id = session,
335                    logger = %ctx.logger_name,
336                    "{}",
337                    message
338                )
339            }
340            BnLogLevel::InfoLog => {
341                tracing::info!(
342                    target: "binaryninja",
343                    session_id = session,
344                    logger = %ctx.logger_name,
345                    "{}",
346                    message
347                )
348            }
349            BnLogLevel::DebugLog => {
350                tracing::debug!(
351                    target: "binaryninja",
352                    session_id = session,
353                    logger = %ctx.logger_name,
354                    "{}",
355                    message
356                )
357            }
358        };
359    }
360
361    fn level(&self) -> BnLogLevel {
362        self.minimum_level
363    }
364}